diff --git a/src/app/tools/tankopedia/[id]/components/Model/TankDisplay.tsx b/src/app/tools/tankopedia/[id]/components/Model/TankDisplay.tsx index 6396db4e6..04c20a320 100644 --- a/src/app/tools/tankopedia/[id]/components/Model/TankDisplay.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/TankDisplay.tsx @@ -20,6 +20,7 @@ import { Controls } from '../Control'; import { Lighting } from '../Lighting'; import { RotationInputs } from '../RotationInputs'; import { SceneProps } from '../SceneProps'; +import { TankArmor } from './components/TankArmor'; import { TankModel } from './components/TankModel'; export function TankDisplay() { @@ -134,6 +135,7 @@ export function TankDisplay() { } > + diff --git a/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor.tsx b/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor.tsx index d237b93e6..f1b67c521 100644 --- a/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor.tsx @@ -1,82 +1,65 @@ -import { useLoader } from '@react-three/fiber'; -import { - forwardRef, - useEffect, - useImperativeHandle, - useRef, - useState, -} from 'react'; -import { Group, Mesh, Vector3 } from 'three'; -import { GLTFLoader } from 'three-stdlib'; -import { X_AXIS } from '../../../../../../../constants/axis'; -import { asset } from '../../../../../../../core/blitzkrieg/asset'; -import { - ModelDefinitions, - modelDefinitions, -} from '../../../../../../../core/blitzkrieg/modelDefinitions'; -import { useTankopedia } from '../../../../../../../stores/tankopedia'; +import { forwardRef } from 'react'; +import { Group } from 'three'; +import { HeadsUpDisplay } from '../../../../../../../components/HeadsUpDisplay'; -export const TankArmor = forwardRef((_props, ref) => { - const hullContainer = useRef(null); - const protagonist = useTankopedia((state) => { - if (!state.areTanksAssigned) return; - return state.protagonist; - }); - const [awaitedModelDefinitions, setAwaitedModelDefinitions] = useState< - ModelDefinitions | undefined - >(undefined); - - useEffect(() => { - (async () => { - setAwaitedModelDefinitions(await modelDefinitions); - })(); - }, []); - - useImperativeHandle(ref, () => hullContainer.current!); - - if (!awaitedModelDefinitions) return null; - if (!protagonist) return null; - - const tankModelDefinition = awaitedModelDefinitions[protagonist.tank.id]; - const turretModelDefinition = - tankModelDefinition.turrets[protagonist.turret.id]; - const turretOrigin = new Vector3( - tankModelDefinition.turretOrigin[0], - tankModelDefinition.turretOrigin[1], - -tankModelDefinition.turretOrigin[2], - ).applyAxisAngle(X_AXIS, Math.PI / 2); - const gunOrigin = new Vector3( - turretModelDefinition.gunOrigin[0], - turretModelDefinition.gunOrigin[1], - -turretModelDefinition.gunOrigin[2], - ).applyAxisAngle(X_AXIS, Math.PI / 2); - const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id]; - const gltf = useLoader( - GLTFLoader, - asset(`3d/tanks/armor/${protagonist.tank.id}.glb`), - ); +interface TankArmorProps {} +export const TankArmor = forwardRef((_props, ref) => { return ( - - {Object.values(gltf.nodes).map((node) => { - const armor = Number(node.name.match(/.+_armor_(\d+)/)?.[1]); - const position = new Vector3(); + + + + + - if (node.name.startsWith('turret_')) position.add(turretOrigin); - if (node.name.startsWith('gun_')) - position.add(turretOrigin).add(gunOrigin); + {/* {armorObjects.map((object) => { + const isHull = object.name.startsWith('hull_'); + const isVisible = isHull; + + if (!isVisible) return null; return ( - - - + ); - })} - + })} */} + + {/* {armorObjects.map((object) => { + const isCurrentTurret = object.name.startsWith( + `turret_${model.toString().padStart(2, '0')}`, + ); + const isVisible = isCurrentTurret; + + if (!isVisible) return null; + + return ( + + + + ); + })} */} + + {/* {armorObjects.map((object) => { + const isCurrentGun = object.name.startsWith( + `gun_${model.toString().padStart(2, '0')}`, + ); + const isVisible = isCurrentGun; + + if (!isVisible) return null; + + return ( + + + + ); + })} */} + ); }); diff --git a/src/app/tools/tankopedia/[id]/components/Model/components/TankModel.tsx b/src/app/tools/tankopedia/[id]/components/Model/components/TankModel.tsx new file mode 100644 index 000000000..53a437d02 --- /dev/null +++ b/src/app/tools/tankopedia/[id]/components/Model/components/TankModel.tsx @@ -0,0 +1,414 @@ +import { ThreeEvent, useLoader, useThree } from '@react-three/fiber'; +import { + forwardRef, + useEffect, + useImperativeHandle, + useRef, + useState, +} from 'react'; +import { + Euler, + Group, + Mesh, + MeshStandardMaterial, + Vector2, + Vector3, +} from 'three'; +import { GLTFLoader } from 'three-stdlib'; +import { X_AXIS } from '../../../../../../../constants/axis'; +import { applyPitchYawLimits } from '../../../../../../../core/blitz/applyPitchYawLimits'; +import { asset } from '../../../../../../../core/blitzkrieg/asset'; +import { + ModelDefinitions, + modelDefinitions, +} from '../../../../../../../core/blitzkrieg/modelDefinitions'; +import { resolveJsxTree } from '../../../../../../../core/blitzkrieg/resolveJsxTree'; +import { normalizeAnglePI } from '../../../../../../../core/math/normalizeAngle180'; +import mutateTankopedia, { + useTankopedia, +} from '../../../../../../../stores/tankopedia'; + +export const TankModel = forwardRef((_props, ref) => { + // "TypeError: dispatcher.use is not a function" + // const awaitedModelDefinitions = use(modelDefinitions); + const [awaitedModelDefinitions, setAwaitedModelDefinitions] = useState< + ModelDefinitions | undefined + >(undefined); + const protagonist = useTankopedia((state) => { + if (!state.areTanksAssigned) return; + return state.protagonist; + }); + const canvas = useThree((state) => state.gl.domElement); + + if (!protagonist) return null; + + const modelGltf = useLoader( + GLTFLoader, + asset(`3d/tanks/models/${protagonist.tank.id}.glb`), + ); + const armorGltf = useLoader( + GLTFLoader, + asset(`3d/tanks/armor/${protagonist.tank.id}.glb`), + ); + const model = useTankopedia((state) => state.model); + const hullContainer = useRef(null); + const turretContainer = useRef(null); + const gunContainer = useRef(null); + const nodes = Object.values(modelGltf.nodes); + + useImperativeHandle(ref, () => hullContainer.current!); + useEffect(() => { + (async () => { + setAwaitedModelDefinitions(await modelDefinitions); + })(); + }, []); + + if (!awaitedModelDefinitions) return null; + + const tankModelDefinition = awaitedModelDefinitions[protagonist.tank.id]; + const turretModelDefinition = + tankModelDefinition.turrets[protagonist.turret.id]; + const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id]; + const turretOrigin = new Vector3( + tankModelDefinition.turretOrigin[0], + tankModelDefinition.turretOrigin[1], + -tankModelDefinition.turretOrigin[2], + ).applyAxisAngle(X_AXIS, Math.PI / 2); + const gunOrigin = new Vector3( + turretModelDefinition.gunOrigin[0], + turretModelDefinition.gunOrigin[1], + -turretModelDefinition.gunOrigin[2], + ).applyAxisAngle(X_AXIS, Math.PI / 2); + const turretPosition = new Vector3() + .sub(turretOrigin) + .applyAxisAngle(new Vector3(0, 0, 1), model.turretYaw); + const turretRotation = new Euler(0, 0, model.turretYaw); + + if (tankModelDefinition.turretRotation) { + const pitch = -tankModelDefinition.turretRotation.pitch * (Math.PI / 180); + const yaw = -tankModelDefinition.turretRotation.yaw * (Math.PI / 180); + const roll = -tankModelDefinition.turretRotation.roll * (Math.PI / 180); + + turretPosition + .applyAxisAngle(new Vector3(1, 0, 0), pitch) + .applyAxisAngle(new Vector3(0, 1, 0), roll) + .applyAxisAngle(new Vector3(0, 0, 1), yaw); + turretRotation.x += pitch; + turretRotation.y += roll; + turretRotation.z += yaw; + } + + turretPosition.add(turretOrigin); + + return ( + + {nodes.map((node) => { + const isHull = node.name === 'hull'; + const isWheel = node.name.startsWith('chassis_wheel_'); + const isTrack = node.name.startsWith('chassis_track_'); + const isVisible = isHull || isWheel || isTrack; + let draftHullYaw = 0; + + function handlePointerDown(event: ThreeEvent) { + event.stopPropagation(); + mutateTankopedia((draft) => { + draft.model.controlsEnabled = false; + }); + + if (isHull) { + draftHullYaw = model.hullYaw; + + window.addEventListener('pointermove', handlePointerMoveHull); + window.addEventListener('pointerup', handlePointerUpHull); + } else if (isTrack) { + window.addEventListener('pointermove', handlePointerMoveTrack); + window.addEventListener('pointerup', handlePointerUpTrack); + } + } + function handlePointerMoveHull(event: PointerEvent) { + draftHullYaw += event.movementX * ((2 * Math.PI) / canvas.width); + if (hullContainer.current) { + hullContainer.current.rotation.z = draftHullYaw; + } + } + function handlePointerUpHull() { + mutateTankopedia((draft) => { + draft.model.controlsEnabled = true; + draft.model.hullYaw = normalizeAnglePI(draftHullYaw); + }); + window.removeEventListener('pointermove', handlePointerMoveHull); + window.removeEventListener('pointerup', handlePointerUpHull); + } + function handlePointerMoveTrack(event: PointerEvent) { + const mesh = node as Mesh; + const material = mesh.material as MeshStandardMaterial; + const offset = new Vector2( + 0, + event.movementY * (6 / canvas.height) + + event.movementX * (6 / canvas.width), + ); + + material.map?.offset.add(offset); + material.roughnessMap?.offset.add(offset); + material.normalMap?.offset.add(offset); + } + function handlePointerUpTrack() { + mutateTankopedia((draft) => { + draft.model.controlsEnabled = true; + }); + window.removeEventListener('pointermove', handlePointerMoveTrack); + window.removeEventListener('pointerup', handlePointerUpTrack); + } + + if (!isVisible) return null; + return ( + + ); + })} + + + {nodes.map((node) => { + const isTurret = node.name.startsWith('turret_'); + const isCurrentTurret = + node.name === + `turret_${turretModelDefinition.model.toString().padStart(2, '0')}`; + const isVisible = isCurrentTurret; + let draftPitch = 0; + let draftYaw = 0; + + function handlePointerDown(event: ThreeEvent) { + event.stopPropagation(); + + if (isTurret) { + draftYaw = model.turretYaw; + draftPitch = model.gunPitch; + + mutateTankopedia((draft) => { + draft.model.controlsEnabled = false; + }); + window.addEventListener('pointermove', handlePointerMove); + window.addEventListener('pointerup', handlePointerUp); + } + } + function handlePointerMove(event: PointerEvent) { + [draftPitch, draftYaw] = applyPitchYawLimits( + draftPitch, + draftYaw + event.movementX * (Math.PI / canvas.width), + gunModelDefinition.pitch, + turretModelDefinition.yaw, + ); + + if (gunContainer.current) { + gunContainer.current.position + .set(0, 0, 0) + .sub(turretOrigin) + .sub(gunOrigin) + .applyAxisAngle(new Vector3(1, 0, 0), draftPitch) + .add(turretOrigin) + .add(gunOrigin); + gunContainer.current.rotation.x = draftPitch; + } + + if (turretContainer.current) { + turretContainer.current.position + .set(0, 0, 0) + .sub(turretOrigin) + .applyAxisAngle(new Vector3(0, 0, 1), draftYaw); + turretContainer.current.rotation.z = draftYaw; + + if (tankModelDefinition.turretRotation) { + const pitch = + -tankModelDefinition.turretRotation.pitch * (Math.PI / 180); + const yaw = + -tankModelDefinition.turretRotation.yaw * (Math.PI / 180); + const roll = + -tankModelDefinition.turretRotation.roll * (Math.PI / 180); + + turretContainer.current.position + .applyAxisAngle(new Vector3(1, 0, 0), pitch) + .applyAxisAngle(new Vector3(0, 1, 0), roll) + .applyAxisAngle(new Vector3(0, 0, 1), yaw); + } + + turretContainer.current.position.add(turretOrigin); + } + } + function handlePointerUp() { + mutateTankopedia((state) => { + state.model.controlsEnabled = true; + state.model.gunPitch = normalizeAnglePI(draftPitch); + state.model.turretYaw = normalizeAnglePI(draftYaw); + }); + window.removeEventListener('pointermove', handlePointerMove); + window.removeEventListener('pointerup', handlePointerUp); + } + + if (!isVisible) return null; + return ( + + ); + })} + + + {nodes.map((node) => { + const isCurrentMantlet = + node.name === + `gun_${gunModelDefinition.model + .toString() + .padStart(2, '0')}_mask`; + const isCurrentGun = + node.name === + `gun_${gunModelDefinition.model.toString().padStart(2, '0')}`; + const isVisible = isCurrentGun || isCurrentMantlet; + let draftPitch = 0; + let draftYaw = 0; + + function handlePointerDown(event: ThreeEvent) { + event.stopPropagation(); + + mutateTankopedia((draft) => { + draft.model.controlsEnabled = false; + }); + draftPitch = model.gunPitch; + draftYaw = model.turretYaw; + window.addEventListener('pointermove', handlePointerMove); + window.addEventListener('pointerup', handlePointerUp); + } + function handlePointerMove(event: PointerEvent) { + [draftPitch, draftYaw] = applyPitchYawLimits( + draftPitch - event.movementY * (Math.PI / canvas.height), + draftYaw + event.movementX * (Math.PI / canvas.width), + gunModelDefinition.pitch, + turretModelDefinition.yaw, + ); + + if (gunContainer.current) { + gunContainer.current.position + .set(0, 0, 0) + .sub(turretOrigin) + .sub(gunOrigin) + .applyAxisAngle(new Vector3(1, 0, 0), draftPitch) + .add(turretOrigin) + .add(gunOrigin); + gunContainer.current.rotation.x = draftPitch; + } + + if (turretContainer.current) { + turretContainer.current.position + .set(0, 0, 0) + .sub(turretOrigin) + .applyAxisAngle(new Vector3(0, 0, 1), draftYaw); + turretContainer.current.rotation.z = draftYaw; + + if (tankModelDefinition.turretRotation) { + const pitch = + -tankModelDefinition.turretRotation.pitch * (Math.PI / 180); + const yaw = + -tankModelDefinition.turretRotation.yaw * (Math.PI / 180); + const roll = + -tankModelDefinition.turretRotation.roll * (Math.PI / 180); + + turretContainer.current.position + .applyAxisAngle(new Vector3(1, 0, 0), pitch) + .applyAxisAngle(new Vector3(0, 1, 0), roll) + .applyAxisAngle(new Vector3(0, 0, 1), yaw); + } + + turretContainer.current.position.add(turretOrigin); + } + } + function handlePointerUp() { + mutateTankopedia((state) => { + state.model.controlsEnabled = true; + state.model.gunPitch = normalizeAnglePI(draftPitch); + state.model.turretYaw = normalizeAnglePI(draftYaw); + }); + window.removeEventListener('pointermove', handlePointerMove); + window.removeEventListener('pointerup', handlePointerUp); + } + + if (!isVisible) return null; + return ( + + ); + })} + + + {/* { + mutateTankopedia((state) => { + state.model.controlsEnabled = false; + }); + }} + pitchLimits={gunModelDefinition.pitch} + turretContainer={turretContainer} + yaw={model.turretYaw} + onPitchEnd={(pitch, yaw) => { + mutateTankopedia((state) => { + state.model.controlsEnabled = true; + state.model.gunPitch = pitch; + state.model.turretYaw = yaw; + }); + }} + yawLimits={turretModelDefinition.yaw} + gunOrigin={gunOrigin} + pitch={model.gunPitch} + model={gunModelDefinition.model} + modelObjects={Object.values(modelGltf.nodes)} + turretOrigin={turretOrigin} + /> */} + + + ); +}); diff --git a/src/app/tools/tankopedia/[id]/components/Model/components/TankModel/components/GunContainer.tsx b/src/app/tools/tankopedia/[id]/components/Model/components/TankModel/components/GunContainer.tsx deleted file mode 100644 index d045e5907..000000000 --- a/src/app/tools/tankopedia/[id]/components/Model/components/TankModel/components/GunContainer.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import { ThreeEvent, useThree } from '@react-three/fiber'; -import { RefObject, forwardRef, useImperativeHandle, useRef } from 'react'; -import { Group, Mesh, Object3D, Vector3 } from 'three'; -import { ArmorHighlightMaterial } from '../../../../../../../../../components/ArmorHighlightMaterial'; -import { applyPitchYawLimits } from '../../../../../../../../../core/blitz/applyPitchYawLimits'; -import { - InitialTurretRotation, - PitchLimits, - YawLimits, -} from '../../../../../../../../../core/blitzkrieg/modelDefinitions'; -import { resolveJsxTree } from '../../../../../../../../../core/blitzkrieg/resolveJsxTree'; -import { normalizeAnglePI } from '../../../../../../../../../core/math/normalizeAngle180'; - -interface GunContainerProps { - turretOrigin: Vector3; - gunOrigin: Vector3; - pitch: number; - yaw: number; - modelObjects: Object3D[]; - armorObjects: Object3D[]; - model: number; - yawLimits?: YawLimits; - pitchLimits: PitchLimits; - onPitchStart: () => void; - onPitchEnd: (pitch: number, yaw: number) => void; - initialTurretRotation?: InitialTurretRotation; - turretContainer: RefObject; -} - -export const GunContainer = forwardRef( - ( - { - gunOrigin, - turretOrigin, - pitch, - initialTurretRotation, - yaw, - modelObjects, - yawLimits, - onPitchStart, - turretContainer, - armorObjects, - pitchLimits, - model, - onPitchEnd, - }: GunContainerProps, - ref, - ) => { - const canvas = useThree((state) => state.gl.domElement); - const gunContainer = useRef(null); - - useImperativeHandle(ref, () => gunContainer.current!); - - return ( - - {modelObjects.map((object) => { - const isCurrentMantlet = - object.name === `gun_${model.toString().padStart(2, '0')}_mask`; - const isCurrentGun = - object.name === `gun_${model.toString().padStart(2, '0')}`; - const isVisible = isCurrentGun || isCurrentMantlet; - let draftPitch = 0; - let draftYaw = 0; - - function handlePointerDown(event: ThreeEvent) { - event.stopPropagation(); - - onPitchStart(); - draftPitch = pitch; - draftYaw = yaw; - window.addEventListener('pointermove', handlePointerMove); - window.addEventListener('pointerup', handlePointerUp); - } - function handlePointerMove(event: PointerEvent) { - [draftPitch, draftYaw] = applyPitchYawLimits( - draftPitch - event.movementY * (Math.PI / canvas.height), - draftYaw + event.movementX * (Math.PI / canvas.width), - pitchLimits, - yawLimits, - ); - - if (gunContainer.current) { - gunContainer.current.position - .set(0, 0, 0) - .sub(turretOrigin) - .sub(gunOrigin) - .applyAxisAngle(new Vector3(1, 0, 0), draftPitch) - .add(turretOrigin) - .add(gunOrigin); - gunContainer.current.rotation.x = draftPitch; - } - - if (turretContainer.current) { - turretContainer.current.position - .set(0, 0, 0) - .sub(turretOrigin) - .applyAxisAngle(new Vector3(0, 0, 1), draftYaw); - turretContainer.current.rotation.z = draftYaw; - - if (initialTurretRotation) { - const pitch = -initialTurretRotation.pitch * (Math.PI / 180); - const yaw = -initialTurretRotation.yaw * (Math.PI / 180); - const roll = -initialTurretRotation.roll * (Math.PI / 180); - - turretContainer.current.position - .applyAxisAngle(new Vector3(1, 0, 0), pitch) - .applyAxisAngle(new Vector3(0, 1, 0), roll) - .applyAxisAngle(new Vector3(0, 0, 1), yaw); - } - - turretContainer.current.position.add(turretOrigin); - } - } - function handlePointerUp() { - onPitchEnd( - normalizeAnglePI(draftPitch), - normalizeAnglePI(draftYaw), - ); - window.removeEventListener('pointermove', handlePointerMove); - window.removeEventListener('pointerup', handlePointerUp); - } - - if (!isVisible) return null; - return ( - - ); - })} - - {armorObjects.map((object) => { - const isCurrentGun = object.name.startsWith( - `gun_${model.toString().padStart(2, '0')}`, - ); - const isVisible = isCurrentGun; - - if (!isVisible) return null; - - return ( - - - - ); - })} - - ); - }, -); diff --git a/src/app/tools/tankopedia/[id]/components/Model/components/TankModel/components/HullContainer.tsx b/src/app/tools/tankopedia/[id]/components/Model/components/TankModel/components/HullContainer.tsx deleted file mode 100644 index e3ab0f071..000000000 --- a/src/app/tools/tankopedia/[id]/components/Model/components/TankModel/components/HullContainer.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import { ThreeEvent, useThree } from '@react-three/fiber'; -import { ReactNode, forwardRef, useImperativeHandle, useRef } from 'react'; -import { - Euler, - Group, - Mesh, - MeshStandardMaterial, - Object3D, - Vector2, -} from 'three'; -import { ArmorHighlightMaterial } from '../../../../../../../../../components/ArmorHighlightMaterial'; -import { resolveJsxTree } from '../../../../../../../../../core/blitzkrieg/resolveJsxTree'; -import { normalizeAnglePI } from '../../../../../../../../../core/math/normalizeAngle180'; - -interface HullContainerProps { - children: ReactNode; - yaw: number; - modelObjects: Object3D[]; - armorObjects: Object3D[]; - onYawStart: () => void; - onYawEnd: (yaw: number) => void; - onTrackStart: () => void; - onTrackEnd: () => void; -} - -export const HullContainer = forwardRef( - ( - { - children, - yaw, - armorObjects, - modelObjects, - onYawStart, - onYawEnd, - onTrackEnd, - onTrackStart, - }, - ref, - ) => { - const canvas = useThree((state) => state.gl.domElement); - const hullContainer = useRef(null); - const rotation = [-Math.PI / 2, 0, yaw] as unknown as Euler; - - useImperativeHandle(ref, () => hullContainer.current!); - - return ( - - {modelObjects.map((object) => { - const isHull = object.name === 'hull'; - const isWheel = object.name.startsWith('chassis_wheel_'); - const isTrack = object.name.startsWith('chassis_track_'); - const isVisible = isHull || isWheel || isTrack; - let draftHullYaw = 0; - - function handlePointerDown(event: ThreeEvent) { - event.stopPropagation(); - - if (isHull) { - draftHullYaw = yaw; - - onYawStart(); - window.addEventListener('pointermove', handlePointerMoveHull); - window.addEventListener('pointerup', handlePointerUpHull); - } else if (isTrack) { - onTrackStart(); - window.addEventListener('pointermove', handlePointerMoveTrack); - window.addEventListener('pointerup', handlePointerUpTrack); - } - } - function handlePointerMoveHull(event: PointerEvent) { - draftHullYaw += event.movementX * ((2 * Math.PI) / canvas.width); - - if (hullContainer.current) { - hullContainer.current.rotation.z = draftHullYaw; - } - } - function handlePointerUpHull() { - onYawEnd(normalizeAnglePI(draftHullYaw)); - window.removeEventListener('pointermove', handlePointerMoveHull); - window.removeEventListener('pointerup', handlePointerUpHull); - } - function handlePointerMoveTrack(event: PointerEvent) { - const mesh = object as Mesh; - const material = mesh.material as MeshStandardMaterial; - const offset = new Vector2( - 0, - event.movementY * (6 / canvas.height) + - event.movementX * (6 / canvas.width), - ); - - material.map?.offset.add(offset); - material.roughnessMap?.offset.add(offset); - material.normalMap?.offset.add(offset); - } - function handlePointerUpTrack() { - onTrackEnd(); - window.removeEventListener('pointermove', handlePointerMoveTrack); - window.removeEventListener('pointerup', handlePointerUpTrack); - } - - if (!isVisible) return null; - return ( - - ); - })} - - {armorObjects.map((object) => { - const isHull = object.name.startsWith('hull_'); - const isVisible = isHull; - - if (!isVisible) return null; - - return ( - - - - ); - })} - - {children} - - ); - }, -); diff --git a/src/app/tools/tankopedia/[id]/components/Model/components/TankModel/components/TurretContainer.tsx b/src/app/tools/tankopedia/[id]/components/Model/components/TankModel/components/TurretContainer.tsx deleted file mode 100644 index 41e43ba57..000000000 --- a/src/app/tools/tankopedia/[id]/components/Model/components/TankModel/components/TurretContainer.tsx +++ /dev/null @@ -1,191 +0,0 @@ -import { ThreeEvent, useThree } from '@react-three/fiber'; -import { - ReactNode, - RefObject, - forwardRef, - useImperativeHandle, - useRef, -} from 'react'; -import { Euler, Group, Mesh, Object3D, Vector3 } from 'three'; -import { ArmorHighlightMaterial } from '../../../../../../../../../components/ArmorHighlightMaterial'; -import { applyPitchYawLimits } from '../../../../../../../../../core/blitz/applyPitchYawLimits'; -import { - InitialTurretRotation, - PitchLimits, - YawLimits, -} from '../../../../../../../../../core/blitzkrieg/modelDefinitions'; -import { resolveJsxTree } from '../../../../../../../../../core/blitzkrieg/resolveJsxTree'; -import { normalizeAnglePI } from '../../../../../../../../../core/math/normalizeAngle180'; - -interface TurretContainerProps { - turretOrigin: Vector3; - gunOrigin: Vector3; - modelObjects: Object3D[]; - armorObjects: Object3D[]; - children: ReactNode; - yaw: number; - pitch: number; - model: number; - yawLimits?: YawLimits; - pitchLimits: PitchLimits; - onYawStart: () => void; - onYawEnd: (pitch: number, yaw: number) => void; - gunContainer: RefObject; - initialTurretRotation?: InitialTurretRotation; -} - -export const TurretContainer = forwardRef( - ( - { - gunContainer, - turretOrigin, - yaw, - children, - modelObjects, - yawLimits, - model, - onYawEnd, - pitch, - gunOrigin, - armorObjects, - pitchLimits, - onYawStart, - initialTurretRotation, - }: TurretContainerProps, - ref, - ) => { - const canvas = useThree((state) => state.gl.domElement); - const turretContainer = useRef(null); - const position = new Vector3() - .sub(turretOrigin) - .applyAxisAngle(new Vector3(0, 0, 1), yaw); - const rotation = new Euler(0, 0, yaw); - - if (initialTurretRotation) { - const pitch = -initialTurretRotation.pitch * (Math.PI / 180); - const yaw = -initialTurretRotation.yaw * (Math.PI / 180); - const roll = -initialTurretRotation.roll * (Math.PI / 180); - - position - .applyAxisAngle(new Vector3(1, 0, 0), pitch) - .applyAxisAngle(new Vector3(0, 1, 0), roll) - .applyAxisAngle(new Vector3(0, 0, 1), yaw); - rotation.x += pitch; - rotation.y += roll; - rotation.z += yaw; - } - - position.add(turretOrigin); - - useImperativeHandle(ref, () => turretContainer.current!); - - return ( - - {modelObjects.map((object) => { - const isTurret = object.name.startsWith('turret_'); - const isCurrentTurret = - object.name === `turret_${model.toString().padStart(2, '0')}`; - const isVisible = isCurrentTurret; - let draftPitch = 0; - let draftYaw = 0; - - function handlePointerDown(event: ThreeEvent) { - event.stopPropagation(); - - if (isTurret) { - draftYaw = yaw; - draftPitch = pitch; - - onYawStart(); - window.addEventListener('pointermove', handlePointerMove); - window.addEventListener('pointerup', handlePointerUp); - } - } - function handlePointerMove(event: PointerEvent) { - [draftPitch, draftYaw] = applyPitchYawLimits( - draftPitch, - draftYaw + event.movementX * (Math.PI / canvas.width), - pitchLimits, - yawLimits, - ); - - if (gunContainer.current) { - gunContainer.current.position - .set(0, 0, 0) - .sub(turretOrigin) - .sub(gunOrigin) - .applyAxisAngle(new Vector3(1, 0, 0), draftPitch) - .add(turretOrigin) - .add(gunOrigin); - gunContainer.current.rotation.x = draftPitch; - } - - if (turretContainer.current) { - turretContainer.current.position - .set(0, 0, 0) - .sub(turretOrigin) - .applyAxisAngle(new Vector3(0, 0, 1), draftYaw); - turretContainer.current.rotation.z = draftYaw; - - if (initialTurretRotation) { - const pitch = -initialTurretRotation.pitch * (Math.PI / 180); - const yaw = -initialTurretRotation.yaw * (Math.PI / 180); - const roll = -initialTurretRotation.roll * (Math.PI / 180); - - turretContainer.current.position - .applyAxisAngle(new Vector3(1, 0, 0), pitch) - .applyAxisAngle(new Vector3(0, 1, 0), roll) - .applyAxisAngle(new Vector3(0, 0, 1), yaw); - } - - turretContainer.current.position.add(turretOrigin); - } - } - function handlePointerUp() { - onYawEnd(normalizeAnglePI(draftPitch), normalizeAnglePI(draftYaw)); - window.removeEventListener('pointermove', handlePointerMove); - window.removeEventListener('pointerup', handlePointerUp); - } - - if (!isVisible) return null; - return ( - - ); - })} - - {armorObjects.map((object) => { - const isCurrentTurret = object.name.startsWith( - `turret_${model.toString().padStart(2, '0')}`, - ); - const isVisible = isCurrentTurret; - - if (!isVisible) return null; - - return ( - - - - ); - })} - - {children} - - ); - }, -); diff --git a/src/app/tools/tankopedia/[id]/components/Model/components/TankModel/index.tsx b/src/app/tools/tankopedia/[id]/components/Model/components/TankModel/index.tsx deleted file mode 100644 index e5cb06b57..000000000 --- a/src/app/tools/tankopedia/[id]/components/Model/components/TankModel/index.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import { useLoader } from '@react-three/fiber'; -import { - forwardRef, - useEffect, - useImperativeHandle, - useRef, - useState, -} from 'react'; -import { Group, Vector3 } from 'three'; -import { GLTFLoader } from 'three-stdlib'; -import { X_AXIS } from '../../../../../../../../constants/axis'; -import { asset } from '../../../../../../../../core/blitzkrieg/asset'; -import { - ModelDefinitions, - modelDefinitions, -} from '../../../../../../../../core/blitzkrieg/modelDefinitions'; -import mutateTankopedia, { - useTankopedia, -} from '../../../../../../../../stores/tankopedia'; -import { GunContainer } from './components/GunContainer'; -import { HullContainer } from './components/HullContainer'; -import { TurretContainer } from './components/TurretContainer'; - -export const TankModel = forwardRef((_props, ref) => { - // "TypeError: dispatcher.use is not a function" - // const awaitedModelDefinitions = use(modelDefinitions); - const [awaitedModelDefinitions, setAwaitedModelDefinitions] = useState< - ModelDefinitions | undefined - >(undefined); - const protagonist = useTankopedia((state) => { - if (!state.areTanksAssigned) return; - return state.protagonist; - }); - - if (!protagonist) return null; - - const modelGltf = useLoader( - GLTFLoader, - asset(`3d/tanks/models/${protagonist.tank.id}.glb`), - ); - const armorGltf = useLoader( - GLTFLoader, - asset(`3d/tanks/armor/${protagonist.tank.id}.glb`), - ); - const model = useTankopedia((state) => state.model); - const hullContainer = useRef(null); - const turretContainer = useRef(null); - const gunContainer = useRef(null); - - useImperativeHandle(ref, () => hullContainer.current!); - useEffect(() => { - (async () => { - setAwaitedModelDefinitions(await modelDefinitions); - })(); - }, []); - - if (!awaitedModelDefinitions) return null; - - const tankModelDefinition = awaitedModelDefinitions[protagonist.tank.id]; - const turretModelDefinition = - tankModelDefinition.turrets[protagonist.turret.id]; - const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id]; - const turretOrigin = new Vector3( - tankModelDefinition.turretOrigin[0], - tankModelDefinition.turretOrigin[1], - -tankModelDefinition.turretOrigin[2], - ).applyAxisAngle(X_AXIS, Math.PI / 2); - const gunOrigin = new Vector3( - turretModelDefinition.gunOrigin[0], - turretModelDefinition.gunOrigin[1], - -turretModelDefinition.gunOrigin[2], - ).applyAxisAngle(X_AXIS, Math.PI / 2); - - return ( - - mutateTankopedia((draft) => { - draft.model.controlsEnabled = false; - }) - } - onYawEnd={(yaw) => { - mutateTankopedia((draft) => { - draft.model.controlsEnabled = true; - draft.model.hullYaw = yaw; - }); - }} - onTrackStart={() => { - mutateTankopedia((draft) => { - draft.model.controlsEnabled = false; - }); - }} - onTrackEnd={() => { - mutateTankopedia((draft) => { - draft.model.controlsEnabled = true; - }); - }} - > - - mutateTankopedia((state) => { - state.model.controlsEnabled = false; - }) - } - yawLimits={turretModelDefinition.yaw} - pitchLimits={gunModelDefinition.pitch} - pitch={model.gunPitch} - onYawEnd={(pitch, yaw) => { - mutateTankopedia((state) => { - state.model.controlsEnabled = true; - state.model.gunPitch = pitch; - state.model.turretYaw = yaw; - }); - }} - gunContainer={gunContainer} - turretOrigin={turretOrigin} - yaw={model.turretYaw} - > - { - mutateTankopedia((state) => { - state.model.controlsEnabled = false; - }); - }} - pitchLimits={gunModelDefinition.pitch} - turretContainer={turretContainer} - yaw={model.turretYaw} - onPitchEnd={(pitch, yaw) => { - mutateTankopedia((state) => { - state.model.controlsEnabled = true; - state.model.gunPitch = pitch; - state.model.turretYaw = yaw; - }); - }} - yawLimits={turretModelDefinition.yaw} - gunOrigin={gunOrigin} - pitch={model.gunPitch} - model={gunModelDefinition.model} - modelObjects={Object.values(modelGltf.nodes)} - turretOrigin={turretOrigin} - /> - - - ); -}); diff --git a/src/components/ArmorHighlightMaterial.tsx b/src/components/ArmorHighlightMaterial.tsx deleted file mode 100644 index c7772ee37..000000000 --- a/src/components/ArmorHighlightMaterial.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { GreaterEqualDepth } from 'three'; - -const INFLATE = '0.0'; - -export function ArmorHighlightMaterial() { - return ( - - ); -} diff --git a/src/components/ArmorMesh.tsx b/src/components/ArmorMesh.tsx new file mode 100644 index 000000000..a7389bf79 --- /dev/null +++ b/src/components/ArmorMesh.tsx @@ -0,0 +1,22 @@ +import { MeshProps } from '@react-three/fiber'; + +interface ArmorMeshProps extends MeshProps {} + +export function ArmorMesh(props: ArmorMeshProps) { + return ( + <> + + + + + + + + + ); +} diff --git a/src/components/HeadsUpDisplay.tsx b/src/components/HeadsUpDisplay.tsx new file mode 100644 index 000000000..b64759a1f --- /dev/null +++ b/src/components/HeadsUpDisplay.tsx @@ -0,0 +1,53 @@ +import { createPortal, useFrame, useThree } from '@react-three/fiber'; +import { isInteger } from 'lodash'; +import { ReactNode } from 'react'; +import { Scene } from 'three'; + +interface HeadsUpDisplayProps { + priority?: number; + children: ReactNode; +} +/** + * Renders `children` above the default scene, even if the priority is a + * negative number. + */ +export function HeadsUpDisplay({ + priority = 1, + children, +}: HeadsUpDisplayProps) { + const { gl: renderer, scene: defaultScene, camera } = useThree(); + const scene = new Scene(); + const portal = createPortal(children, scene, { + events: { priority: priority + 1 }, + }); + + useFrame(() => { + if (priority === 1) { + renderer.clear(); + renderer.render(defaultScene, camera); + } + + renderer.clearDepth(); + renderer.render(scene, camera); + }, priority); + + renderer.autoClear = false; + if (!isInteger(priority)) { + throw new TypeError('Priority property must be an integer'); + } + + return portal; +} + +/** + * Use this component if the default scene disappears on usage of the + * HeadsUpDisplay; caused because there are not HeadsUpDisplay components + * with `1` priority. + */ +export function RenderDefault() { + const { gl: renderer, scene, camera } = useThree(); + + useFrame(() => renderer.render(scene, camera)); + + return null; +}