From 5810ca8f8136ce7cf306b5968773919f1379488c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Wed, 31 Jul 2024 13:36:56 -0500 Subject: [PATCH] feat: moving components --- .../SpacedArmorSceneComponent/index.tsx | 195 +-------------- .../components/StaticArmorSceneComponent.tsx | 215 +++++++++++++++++ .../components/StaticArmorScene.tsx | 224 ------------------ src/components/StaticArmor/index.tsx | 219 ++++++++++++++++- 4 files changed, 430 insertions(+), 423 deletions(-) create mode 100644 src/components/Armor/components/StaticArmorSceneComponent.tsx delete mode 100644 src/components/StaticArmor/components/StaticArmorScene.tsx diff --git a/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx b/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx index ea01afec1..c532bf6ba 100644 --- a/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx +++ b/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx @@ -1,16 +1,8 @@ import { useThree } from '@react-three/fiber'; -import { clamp } from 'lodash'; -import { useCallback, useEffect, useMemo } from 'react'; +import { useCallback } from 'react'; import { - Box3, - Color, - DoubleSide, - FrontSide, Intersection, - LineBasicMaterial, - Mesh, MeshBasicMaterial, - MeshStandardMaterial, Object3D, Plane, Quaternion, @@ -27,7 +19,6 @@ import { jsxTree } from '../../../../core/blitzkit/jsxTree'; import { ShellType } from '../../../../core/blitzkit/tankDefinitions'; import * as Duel from '../../../../stores/duel'; import * as TankopediaEphemeral from '../../../../stores/tankopediaEphemeral'; -import { ThicknessRange } from '../../../StaticArmor/components/StaticArmorScene'; import { ArmorType } from '../SpacedArmorScene'; import { SpacedArmorSubExternal } from './components/SpacedArmorSubExternal'; import { SpacedArmorSubSpaced } from './components/SpacedArmorSubSpaced'; @@ -36,6 +27,7 @@ type SpacedArmorSceneComponentProps = { node: Object3D; thickness: number; clip?: Plane; + scene: Scene; } & ( | { type: Exclude; @@ -44,18 +36,7 @@ type SpacedArmorSceneComponentProps = { type: ArmorType.External; variant: ExternalModuleVariant; } -) & - ( - | { - static?: false; - scene: Scene; - } - | { - static: true; - name: string; - thicknessRange: ThicknessRange; - } - ); +); export type ExternalModuleVariant = 'gun' | 'track'; export type ArmorUserData = { @@ -74,8 +55,6 @@ const omitMaterial = new MeshBasicMaterial({ colorWrite: false, depthWrite: true, }); -const unselectedColor = new Color(0x404040); -const externalModuleColor = new Color(0, 1 / 4, 1 / 2); /** * Render orders allocations: @@ -92,176 +71,8 @@ export function SpacedArmorSceneComponent({ }: SpacedArmorSceneComponentProps) { const tankopediaEphemeralStore = TankopediaEphemeral.useStore(); const duelStore = Duel.useStore(); - const mutateTankopediaEphemeralStore = TankopediaEphemeral.useMutation(); const camera = useThree((state) => state.camera); - if (props.static) { - const x = thickness / props.thicknessRange.value; - - let color: Color; - let opacity: number; - let depthWrite = true; - - switch (props.type) { - case ArmorType.Primary: - if (x > 1) { - color = new Color(Math.max(2 - x, 0.5), 0, 0); - } else { - color = new Color(-((1 - x) ** 2) + 1, -(x ** 2) + 1, 0); - } - opacity = 1; - break; - - case ArmorType.Spaced: - color = new Color( - clamp(1 - (7 / 8) * x, 0, 1), - 0, - clamp(1 - (1 / 8) * x, 0, 1), - ); - opacity = clamp(x + 1 / 2, 0, 1); - break; - - case ArmorType.External: - color = externalModuleColor; - opacity = 1 / 8; - depthWrite = false; - break; - } - - opacity = clamp(opacity, 0, 1); - - const surfaceMaterial = useMemo( - () => - new MeshStandardMaterial({ - color, - opacity, - transparent: true, - depthWrite, - ...(clip ? { clippingPlanes: [clip] } : {}), - }), - [thickness], - ); - const outlineMaterial = useMemo( - () => - new LineBasicMaterial({ - color: color.clone().multiplyScalar(2 ** 2), - }), - [thickness], - ); - - /** - * hook inside an if statement?? don't panic! I assure you the static prop - * never mutates :) - */ - useEffect(() => { - function handleHighlightArmor(selectedName?: string) { - if (!props.static) return; - - if (selectedName === undefined) { - // nothing selected, go back to defaults - surfaceMaterial.opacity = opacity; - surfaceMaterial.color = color; - surfaceMaterial.depthWrite = props.type !== ArmorType.External; - surfaceMaterial.side = FrontSide; - outlineMaterial.visible = true; - } else if ( - selectedName === props.name || - (props.name.startsWith('chassis_') && - selectedName.startsWith('chassis_')) || - (props.name.startsWith('gun_') && - selectedName.startsWith('gun_') && - !props.name.includes('_armor_') && - !selectedName.includes('_armor_')) - ) { - // this selected, stand out! - surfaceMaterial.opacity = 1; - surfaceMaterial.color = color; - surfaceMaterial.depthWrite = true; - surfaceMaterial.side = DoubleSide; - outlineMaterial.visible = true; - } else { - // something else selected, become background - surfaceMaterial.opacity = 1 / 4; - surfaceMaterial.color = unselectedColor; - surfaceMaterial.depthWrite = props.type !== ArmorType.External; - surfaceMaterial.side = FrontSide; - outlineMaterial.visible = false; - } - - surfaceMaterial.transparent = surfaceMaterial.opacity < 1; - } - - const unsubscribe = tankopediaEphemeralStore.subscribe( - (state) => state.highlightArmor?.name, - handleHighlightArmor, - ); - - return unsubscribe; - }, [thickness]); - - return ( - <> - {jsxTree(node, { - material: surfaceMaterial, - userData: { - type: props.type, - variant: props.type === ArmorType.External ? props.variant : 'gun', - thickness, - } satisfies ArmorUserData, - - onClick(event) { - event.stopPropagation(); - - const { editStatic } = tankopediaEphemeralStore.getState(); - - if (editStatic) { - mutateTankopediaEphemeralStore((draft) => { - draft.editingPlate = { - name: props.name, - default: thickness, - }; - }); - } else { - const bounds = new Box3().setFromObject(event.object); - const point = bounds.min - .clone() - .add(bounds.max) - .divideScalar(2) - .setY(bounds.max.y); - const cameraNormal = camera.position - .clone() - .sub(point) - .normalize(); - const surfaceNormal = event - .normal!.clone() - .applyQuaternion( - event.object.getWorldQuaternion(new Quaternion()), - ); - const angle = surfaceNormal.angleTo(cameraNormal); - const thicknessAngled = thickness / Math.sin(Math.PI / 2 - angle); - - mutateTankopediaEphemeralStore((draft) => { - draft.highlightArmor = { - type: props.type, - name: props.name, - point, - thickness, - thicknessAngled, - angle, - color: `#${color.getHexString()}`, - }; - }); - } - }, - })} - - - - - - ); - } - const shoot = useCallback( async ( point: Vector3, diff --git a/src/components/Armor/components/StaticArmorSceneComponent.tsx b/src/components/Armor/components/StaticArmorSceneComponent.tsx new file mode 100644 index 000000000..c9ab21fb7 --- /dev/null +++ b/src/components/Armor/components/StaticArmorSceneComponent.tsx @@ -0,0 +1,215 @@ +import { useThree } from '@react-three/fiber'; +import { clamp } from 'lodash'; +import { useEffect, useMemo } from 'react'; +import { + Box3, + Color, + DoubleSide, + FrontSide, + LineBasicMaterial, + Mesh, + MeshStandardMaterial, + Object3D, + Plane, + Quaternion, +} from 'three'; +import { jsxTree } from '../../../core/blitzkit/jsxTree'; +import * as Duel from '../../../stores/duel'; +import * as TankopediaEphemeral from '../../../stores/tankopediaEphemeral'; +import { ThicknessRange } from '../../StaticArmor'; +import { ArmorType } from './SpacedArmorScene'; +import { + ArmorUserData, + ExternalModuleVariant, +} from './SpacedArmorSceneComponent'; + +type StaticArmorSceneComponentProps = { + name: string; + clip?: Plane; + thickness: number; + thicknessRange: ThicknessRange; + node: Object3D; +} & ( + | { + type: Exclude; + } + | { + type: ArmorType.External; + variant: ExternalModuleVariant; + } +); + +const unselectedColor = new Color(0x404040); +const externalModuleColor = new Color(0, 1 / 4, 1 / 2); + +export function StaticArmorSceneComponent({ + name, + thickness, + thicknessRange, + node, + clip, + ...props +}: StaticArmorSceneComponentProps) { + const mutateTankopediaEphemeralStore = TankopediaEphemeral.useMutation(); + const tankopediaEphemeralStore = TankopediaEphemeral.useStore(); + const duelStore = Duel.useStore(); + const camera = useThree((state) => state.camera); + + const x = thickness / thicknessRange.value; + let color: Color; + let opacity: number; + let depthWrite = true; + + switch (props.type) { + case ArmorType.Primary: + if (x > 1) { + color = new Color(Math.max(2 - x, 0.5), 0, 0); + } else { + color = new Color(-((1 - x) ** 2) + 1, -(x ** 2) + 1, 0); + } + opacity = 1; + break; + + case ArmorType.Spaced: + color = new Color( + clamp(1 - (7 / 8) * x, 0, 1), + 0, + clamp(1 - (1 / 8) * x, 0, 1), + ); + opacity = clamp(x + 1 / 2, 0, 1); + break; + + case ArmorType.External: + color = externalModuleColor; + opacity = 1 / 8; + depthWrite = false; + break; + } + + opacity = clamp(opacity, 0, 1); + + const surfaceMaterial = useMemo( + () => + new MeshStandardMaterial({ + color, + opacity, + transparent: true, + depthWrite, + ...(clip ? { clippingPlanes: [clip] } : {}), + }), + [thickness], + ); + const outlineMaterial = useMemo( + () => + new LineBasicMaterial({ + color: color.clone().multiplyScalar(2 ** 2), + }), + [thickness], + ); + + /** + * hook inside an if statement?? don't panic! I assure you the static prop + * never mutates :) + */ + useEffect(() => { + function handleHighlightArmor(selectedName?: string) { + if (selectedName === undefined) { + // nothing selected, go back to defaults + surfaceMaterial.opacity = opacity; + surfaceMaterial.color = color; + surfaceMaterial.depthWrite = props.type !== ArmorType.External; + surfaceMaterial.side = FrontSide; + outlineMaterial.visible = true; + } else if ( + selectedName === name || + (name.startsWith('chassis_') && selectedName.startsWith('chassis_')) || + (name.startsWith('gun_') && + selectedName.startsWith('gun_') && + !name.includes('_armor_') && + !selectedName.includes('_armor_')) + ) { + // this selected, stand out! + surfaceMaterial.opacity = 1; + surfaceMaterial.color = color; + surfaceMaterial.depthWrite = true; + surfaceMaterial.side = DoubleSide; + outlineMaterial.visible = true; + } else { + // something else selected, become background + surfaceMaterial.opacity = 1 / 4; + surfaceMaterial.color = unselectedColor; + surfaceMaterial.depthWrite = props.type !== ArmorType.External; + surfaceMaterial.side = FrontSide; + outlineMaterial.visible = false; + } + + surfaceMaterial.transparent = surfaceMaterial.opacity < 1; + } + + const unsubscribe = tankopediaEphemeralStore.subscribe( + (state) => state.highlightArmor?.name, + handleHighlightArmor, + ); + + return unsubscribe; + }, [thickness]); + + return ( + <> + {jsxTree(node, { + material: surfaceMaterial, + userData: { + type: props.type, + variant: props.type === ArmorType.External ? props.variant : 'gun', + thickness, + } satisfies ArmorUserData, + + onClick(event) { + event.stopPropagation(); + + const { editStatic } = tankopediaEphemeralStore.getState(); + + if (editStatic) { + mutateTankopediaEphemeralStore((draft) => { + draft.editingPlate = { + name: name, + default: thickness, + }; + }); + } else { + const bounds = new Box3().setFromObject(event.object); + const point = bounds.min + .clone() + .add(bounds.max) + .divideScalar(2) + .setY(bounds.max.y); + const cameraNormal = camera.position.clone().sub(point).normalize(); + const surfaceNormal = event + .normal!.clone() + .applyQuaternion( + event.object.getWorldQuaternion(new Quaternion()), + ); + const angle = surfaceNormal.angleTo(cameraNormal); + const thicknessAngled = thickness / Math.sin(Math.PI / 2 - angle); + + mutateTankopediaEphemeralStore((draft) => { + draft.highlightArmor = { + type: props.type, + name, + point, + thickness, + thicknessAngled, + angle, + color: `#${color.getHexString()}`, + }; + }); + } + }, + })} + + + + + + ); +} diff --git a/src/components/StaticArmor/components/StaticArmorScene.tsx b/src/components/StaticArmor/components/StaticArmorScene.tsx deleted file mode 100644 index f8788b339..000000000 --- a/src/components/StaticArmor/components/StaticArmorScene.tsx +++ /dev/null @@ -1,224 +0,0 @@ -import { memo, useRef } from 'react'; -import { Group, Plane, Vector3 } from 'three'; -import { correctZYTuple } from '../../../core/blitz/correctZYTuple'; -import { nameToArmorId } from '../../../core/blitzkit/nameToArmorId'; -import { resolveArmor } from '../../../core/blitzkit/resolveThickness'; -import { useArmor } from '../../../hooks/useArmor'; -import { useModel } from '../../../hooks/useModel'; -import { useTankModelDefinition } from '../../../hooks/useTankModelDefinition'; -import { useTankTransform } from '../../../hooks/useTankTransform'; -import * as Duel from '../../../stores/duel'; -import * as TankopediaPersistent from '../../../stores/tankopediaPersistent'; -import { ModelTankWrapper } from '../../Armor/components/ModelTankWrapper'; -import { ArmorType } from '../../Armor/components/SpacedArmorScene'; -import { SpacedArmorSceneComponent } from '../../Armor/components/SpacedArmorSceneComponent'; - -interface StaticArmorSceneProps { - thicknessRange: ThicknessRange; -} - -export interface ThicknessRange { - value: number; -} - -export const StaticArmorScene = memo( - ({ thicknessRange }) => { - const wrapper = useRef(null); - const turretContainer = useRef(null); - const gunContainer = useRef(null); - const protagonist = Duel.use((draft) => draft.protagonist); - const tank = Duel.use((state) => state.protagonist.tank); - const track = Duel.use((state) => state.protagonist.track); - const turret = Duel.use((state) => state.protagonist.turret); - const gun = Duel.use((state) => state.protagonist.gun); - const armorGltf = useArmor(tank.id); - const { gltf: modelGltf } = useModel(tank.id); - const armorNodes = Object.values(armorGltf.nodes); - const modelNodes = Object.values(modelGltf.nodes); - const tankModelDefinition = useTankModelDefinition(); - const trackModelDefinition = tankModelDefinition.tracks[track.id]; - const turretModelDefinition = tankModelDefinition.turrets[turret.id]; - const gunModelDefinition = turretModelDefinition.guns[gun.id]; - const hullOrigin = correctZYTuple(trackModelDefinition.origin); - const turretOrigin = correctZYTuple(tankModelDefinition.turretOrigin); - const gunOrigin = correctZYTuple(turretModelDefinition.gunOrigin); - const maskOrigin = - gunModelDefinition.mask === undefined - ? undefined - : gunModelDefinition.mask + hullOrigin.y + turretOrigin.y + gunOrigin.y; - const showPrimaryArmor = TankopediaPersistent.use( - (state) => state.model.visual.showPrimaryArmor, - ); - const showSpacedArmor = TankopediaPersistent.use( - (state) => state.model.visual.showSpacedArmor, - ); - const showExternalModules = TankopediaPersistent.use( - (state) => state.model.visual.showExternalModules, - ); - - useTankTransform(protagonist, turretContainer, gunContainer); - - return ( - - - {armorNodes.map((node) => { - const isHull = node.name.startsWith('hull_'); - const isVisible = isHull; - const armorId = nameToArmorId(node.name); - const { spaced, thickness } = resolveArmor( - tankModelDefinition.armor, - armorId, - ); - - if ( - !isVisible || - thickness === undefined || - (!showPrimaryArmor && !spaced) || - (!showSpacedArmor && spaced) - ) - return null; - - return ( - - ); - })} - - - {modelNodes.map((node) => { - const isWheel = node.name.startsWith('chassis_wheel_'); - const isTrack = node.name.startsWith('chassis_track_'); - const isVisible = isWheel || isTrack; - - if (!isVisible || !showExternalModules) return null; - - return ( - - ); - })} - - - {armorNodes.map((node) => { - const isCurrentTurret = node.name.startsWith( - `turret_${turretModelDefinition.model.toString().padStart(2, '0')}`, - ); - const isVisible = isCurrentTurret; - const armorId = nameToArmorId(node.name); - const { spaced, thickness } = resolveArmor( - turretModelDefinition.armor, - armorId, - ); - - if ( - !isVisible || - thickness === undefined || - (!showPrimaryArmor && !spaced) || - (!showSpacedArmor && spaced) - ) - return null; - - return ( - - - - - - ); - })} - - - {armorNodes.map((node) => { - const isCurrentGun = node.name.startsWith( - `gun_${gunModelDefinition.model.toString().padStart(2, '0')}`, - ); - const isVisible = isCurrentGun; - const armorId = nameToArmorId(node.name); - const { spaced, thickness } = resolveArmor( - gunModelDefinition.armor, - armorId, - ); - - if ( - !isVisible || - thickness === undefined || - (!showPrimaryArmor && !spaced) || - (!showSpacedArmor && spaced) - ) - return null; - - return ( - - - - - - ); - })} - - {modelNodes.map((node) => { - const gunString = `gun_${gunModelDefinition.model.toString().padStart(2, '0')}`; - const isCurrentGun = gunModelDefinition.mask - ? node.name.startsWith(gunString) - : node.name === gunString; - const isVisible = isCurrentGun; - - if (!isVisible || !showExternalModules) return null; - - return ( - - ); - })} - - - - ); - }, -); diff --git a/src/components/StaticArmor/index.tsx b/src/components/StaticArmor/index.tsx index fbb719bb6..5b312f65d 100644 --- a/src/components/StaticArmor/index.tsx +++ b/src/components/StaticArmor/index.tsx @@ -1,12 +1,217 @@ -import { - StaticArmorScene, - ThicknessRange, -} from './components/StaticArmorScene'; +import { memo, useRef } from 'react'; +import { Group, Plane, Vector3 } from 'three'; +import { correctZYTuple } from '../../core/blitz/correctZYTuple'; +import { nameToArmorId } from '../../core/blitzkit/nameToArmorId'; +import { resolveArmor } from '../../core/blitzkit/resolveThickness'; +import { useArmor } from '../../hooks/useArmor'; +import { useModel } from '../../hooks/useModel'; +import { useTankModelDefinition } from '../../hooks/useTankModelDefinition'; +import { useTankTransform } from '../../hooks/useTankTransform'; +import * as Duel from '../../stores/duel'; +import * as TankopediaPersistent from '../../stores/tankopediaPersistent'; +import { ModelTankWrapper } from '../Armor/components/ModelTankWrapper'; +import { ArmorType } from '../Armor/components/SpacedArmorScene'; +import { StaticArmorSceneComponent } from '../Armor/components/StaticArmorSceneComponent'; -interface StaticArmorProps { +interface ArmorSceneProps { thicknessRange: ThicknessRange; } -export function StaticArmor({ thicknessRange }: StaticArmorProps) { - return ; +export interface ThicknessRange { + value: number; } + +export const StaticArmor = memo(({ thicknessRange }) => { + const wrapper = useRef(null); + const turretContainer = useRef(null); + const gunContainer = useRef(null); + const protagonist = Duel.use((draft) => draft.protagonist); + const tank = Duel.use((state) => state.protagonist.tank); + const track = Duel.use((state) => state.protagonist.track); + const turret = Duel.use((state) => state.protagonist.turret); + const gun = Duel.use((state) => state.protagonist.gun); + const armorGltf = useArmor(tank.id); + const { gltf: modelGltf } = useModel(tank.id); + const armorNodes = Object.values(armorGltf.nodes); + const modelNodes = Object.values(modelGltf.nodes); + const tankModelDefinition = useTankModelDefinition(); + const trackModelDefinition = tankModelDefinition.tracks[track.id]; + const turretModelDefinition = tankModelDefinition.turrets[turret.id]; + const gunModelDefinition = turretModelDefinition.guns[gun.id]; + const hullOrigin = correctZYTuple(trackModelDefinition.origin); + const turretOrigin = correctZYTuple(tankModelDefinition.turretOrigin); + const gunOrigin = correctZYTuple(turretModelDefinition.gunOrigin); + const maskOrigin = + gunModelDefinition.mask === undefined + ? undefined + : gunModelDefinition.mask + hullOrigin.y + turretOrigin.y + gunOrigin.y; + const showPrimaryArmor = TankopediaPersistent.use( + (state) => state.model.visual.showPrimaryArmor, + ); + const showSpacedArmor = TankopediaPersistent.use( + (state) => state.model.visual.showSpacedArmor, + ); + const showExternalModules = TankopediaPersistent.use( + (state) => state.model.visual.showExternalModules, + ); + + useTankTransform(protagonist, turretContainer, gunContainer); + + return ( + + + {armorNodes.map((node) => { + const isHull = node.name.startsWith('hull_'); + const isVisible = isHull; + const armorId = nameToArmorId(node.name); + const { spaced, thickness } = resolveArmor( + tankModelDefinition.armor, + armorId, + ); + + if ( + !isVisible || + thickness === undefined || + (!showPrimaryArmor && !spaced) || + (!showSpacedArmor && spaced) + ) + return null; + + return ( + + ); + })} + + + {modelNodes.map((node) => { + const isWheel = node.name.startsWith('chassis_wheel_'); + const isTrack = node.name.startsWith('chassis_track_'); + const isVisible = isWheel || isTrack; + + if (!isVisible || !showExternalModules) return null; + + return ( + + ); + })} + + + {armorNodes.map((node) => { + const isCurrentTurret = node.name.startsWith( + `turret_${turretModelDefinition.model.toString().padStart(2, '0')}`, + ); + const isVisible = isCurrentTurret; + const armorId = nameToArmorId(node.name); + const { spaced, thickness } = resolveArmor( + turretModelDefinition.armor, + armorId, + ); + + if ( + !isVisible || + thickness === undefined || + (!showPrimaryArmor && !spaced) || + (!showSpacedArmor && spaced) + ) + return null; + + return ( + + + + + + ); + })} + + + {armorNodes.map((node) => { + const isCurrentGun = node.name.startsWith( + `gun_${gunModelDefinition.model.toString().padStart(2, '0')}`, + ); + const isVisible = isCurrentGun; + const armorId = nameToArmorId(node.name); + const { spaced, thickness } = resolveArmor( + gunModelDefinition.armor, + armorId, + ); + + if ( + !isVisible || + thickness === undefined || + (!showPrimaryArmor && !spaced) || + (!showSpacedArmor && spaced) + ) + return null; + + return ( + + + + + + ); + })} + + {modelNodes.map((node) => { + const gunString = `gun_${gunModelDefinition.model.toString().padStart(2, '0')}`; + const isCurrentGun = gunModelDefinition.mask + ? node.name.startsWith(gunString) + : node.name === gunString; + const isVisible = isCurrentGun; + + if (!isVisible || !showExternalModules) return null; + + return ( + + ); + })} + + + + ); +});