diff --git a/public/test/1.glb b/public/test/1.glb new file mode 100644 index 000000000..90ce7b846 Binary files /dev/null and b/public/test/1.glb differ diff --git a/public/test/15697.glb b/public/test/15697.glb new file mode 100644 index 000000000..741c751e3 Binary files /dev/null and b/public/test/15697.glb differ diff --git a/public/test/4417.glb b/public/test/4417.glb new file mode 100644 index 000000000..cae09b64d Binary files /dev/null and b/public/test/4417.glb differ diff --git a/public/test/4481.glb b/public/test/4481.glb new file mode 100644 index 000000000..7dd1fb630 Binary files /dev/null and b/public/test/4481.glb differ diff --git a/public/test/6225.glb b/public/test/6225.glb new file mode 100644 index 000000000..dd6b9917d Binary files /dev/null and b/public/test/6225.glb differ diff --git a/public/test/6753.glb b/public/test/6753.glb new file mode 100644 index 000000000..f2d2dc378 Binary files /dev/null and b/public/test/6753.glb differ diff --git a/public/test/7297.glb b/public/test/7297.glb new file mode 100644 index 000000000..900677128 Binary files /dev/null and b/public/test/7297.glb differ diff --git a/public/test/7425.glb b/public/test/7425.glb index ce27c7394..d8ec346fb 100644 Binary files a/public/test/7425.glb and b/public/test/7425.glb differ diff --git a/scripts/buildAssets.ts b/scripts/buildAssets.ts index e7e27426b..232a44fcc 100644 --- a/scripts/buildAssets.ts +++ b/scripts/buildAssets.ts @@ -591,7 +591,7 @@ if (allTargets || targets?.includes('tankModels')) { const nationVehicleId = tank.id; const id = (nationVehicleId << 8) + (NATION_IDS[nation] << 4) + 1; - // if (id !== 15697) continue; // chieftain TODO: investigate vertices stream over read + // if (id !== 15697) continue; // chieftain // if (id !== 24609) continue; // concept 1b // if (id !== 16401) continue; // waffle // if (id !== 7425) continue; // isu 152 @@ -600,9 +600,10 @@ if (allTargets || targets?.includes('tankModels')) { // if (id !== 7297) continue; // 60tp // if (id !== 1) continue; // t-34 // if (id !== 6753) continue; // type 71 - if (id !== 5137) continue; // tiger ii + // if (id !== 5137) continue; // tiger ii // if (id !== 11633) continue; // forest witch // if (id !== 6225) continue; // fv215b + if (id !== 4481) continue; // kran console.log(`Building model ${id} @ ${nation}/${tankIndex}`); diff --git a/src/app/tools/tankopedia/[id]/components/Control.tsx b/src/app/tools/tankopedia/[id]/components/Control.tsx index e80d7044c..f2a395625 100644 --- a/src/app/tools/tankopedia/[id]/components/Control.tsx +++ b/src/app/tools/tankopedia/[id]/components/Control.tsx @@ -3,15 +3,16 @@ import { useThree } from '@react-three/fiber'; import { RefObject, useEffect, useRef } from 'react'; import { Box3, Object3D } from 'three'; import { OrbitControls as OrbitControlsClass } from 'three-stdlib'; +import { useTankopedia } from '../../../../../stores/tankopedia'; interface ControlsProps { - enabled: boolean; fit: RefObject; } -export function Controls({ enabled, fit }: ControlsProps) { +export function Controls({ fit }: ControlsProps) { const camera = useThree((state) => state.camera); const orbitControls = useRef(null); + const enabled = useTankopedia((state) => state.model.controlsEnabled); useEffect(() => { const box = new Box3().setFromObject(fit.current!); diff --git a/src/app/tools/tankopedia/[id]/components/Model.tsx b/src/app/tools/tankopedia/[id]/components/Model.tsx new file mode 100644 index 000000000..264b7b9c3 --- /dev/null +++ b/src/app/tools/tankopedia/[id]/components/Model.tsx @@ -0,0 +1,126 @@ +import { useLoader } from '@react-three/fiber'; +import { forwardRef, use, useImperativeHandle, useRef } from 'react'; +import { Group, Vector3 } from 'three'; +import { GLTFLoader } from 'three-stdlib'; +import { X_AXIS } from '../../../../../constants/axis'; +import { modelDefinitions } from '../../../../../core/blitzkrieg/modelDefinitions'; +import mutateTankopedia, { + useTankopedia, +} from '../../../../../stores/tankopedia'; +import { GunContainer } from './GunContainer'; +import { HullContainer } from './HullContainer'; +import { TurretContainer } from './TurretContainer'; + +interface ModelProps { + tankId: number; + gunId: number; + turretId: number; +} + +export const Model = forwardRef( + ({ gunId, tankId, turretId }, ref) => { + const awaitedModelDefinitions = use(modelDefinitions); + const model = useTankopedia((state) => state.model); + const gltf = useLoader(GLTFLoader, `/test/${tankId}.glb`); + const gunContainer = useRef(null); + const hullContainer = useRef(null); + const turretContainer = useRef(null); + + useImperativeHandle(ref, () => hullContainer.current!); + + const tankModelDefinition = awaitedModelDefinitions[tankId]; + const turretModelDefinition = tankModelDefinition.turrets[turretId]; + const gunModelDefinition = turretModelDefinition.guns[gunId]; + 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((state) => { + state.model.controlsEnabled = false; + }) + } + onYawEnd={(yaw) => { + mutateTankopedia((state) => { + state.model.controlsEnabled = true; + state.model.hullYaw = yaw; + }); + }} + onTrackStart={() => { + mutateTankopedia((state) => { + state.model.controlsEnabled = false; + }); + }} + onTrackEnd={() => { + mutateTankopedia((state) => { + state.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} + objects={gltf.scene.children[0].children} + turretOrigin={turretOrigin} + /> + + + ); + }, +); diff --git a/src/app/tools/tankopedia/[id]/components/RotationInputs.tsx b/src/app/tools/tankopedia/[id]/components/RotationInputs.tsx new file mode 100644 index 000000000..0ba164840 --- /dev/null +++ b/src/app/tools/tankopedia/[id]/components/RotationInputs.tsx @@ -0,0 +1,150 @@ +import { Flex, TextField } from '@radix-ui/themes'; +import { clamp } from 'lodash'; +import { use, useEffect, useRef } from 'react'; +import { modelDefinitions } from '../../../../../core/blitzkrieg/modelDefinitions'; +import { normalizeAnglePI } from '../../../../../core/math/normalizeAngle180'; +import mutateTankopedia, { + useTankopedia, +} from '../../../../../stores/tankopedia'; + +interface RotationInputsProps { + tankId: number; + gunId: number; + turretId: number; +} + +export function RotationInputs({ + tankId, + gunId, + turretId, +}: RotationInputsProps) { + const model = useTankopedia((state) => state.model); + const awaitedModelDefinitions = use(modelDefinitions); + const turretYawInput = useRef(null); + const hullYawInput = useRef(null); + const gunPitchInput = useRef(null); + + const tankModelDefinition = awaitedModelDefinitions[tankId]; + const turretModelDefinition = tankModelDefinition.turrets[turretId]; + const gunModelDefinition = turretModelDefinition.guns[gunId]; + + useEffect(() => { + hullYawInput.current!.value = `${-Math.round( + model.hullYaw * (180 / Math.PI), + )}`; + }, [model.hullYaw]); + useEffect(() => { + turretYawInput.current!.value = `${-Math.round( + model.turretYaw * (180 / Math.PI), + )}`; + }, [model.turretYaw]); + useEffect(() => { + gunPitchInput.current!.value = `${-Math.round( + model.gunPitch * (180 / Math.PI), + )}`; + }, [model.gunPitch]); + + return ( + + + Hull + { + const normalized = normalizeAnglePI( + -Number(hullYawInput.current?.value) * (Math.PI / 180), + ); + mutateTankopedia((state) => { + state.model.hullYaw = normalized; + }); + hullYawInput.current!.value = `${Math.round(normalized)}`; + }} + onKeyDown={(event) => { + if (event.key === 'Enter') { + hullYawInput.current?.blur(); + } + }} + onFocus={() => hullYawInput.current?.focus()} + ref={hullYawInput} + style={{ textAlign: 'right' }} + /> + ° + + + + Turret + { + const normalized = normalizeAnglePI( + -Number(turretYawInput.current?.value) * (Math.PI / 180), + ); + mutateTankopedia((state) => { + state.model.turretYaw = normalized; + }); + turretYawInput.current!.value = `${Math.round(normalized)}`; + }} + onKeyDown={(event) => { + if (event.key === 'Enter') { + turretYawInput.current?.blur(); + } + }} + onFocus={() => turretYawInput.current?.focus()} + ref={turretYawInput} + style={{ textAlign: 'right' }} + /> + ° + + + + Gun + { + const clamped = clamp( + Number(gunPitchInput.current?.value), + gunModelDefinition.pitch.min, + gunModelDefinition.pitch.max, + ); + mutateTankopedia((state) => { + state.model.gunPitch = -clamped * (Math.PI / 180); + }); + gunPitchInput.current!.value = `${Math.round(clamped)}`; + }} + onKeyDown={(event) => { + if (event.key === 'Enter') { + gunPitchInput.current?.blur(); + } + }} + onFocus={() => gunPitchInput.current?.focus()} + ref={gunPitchInput} + style={{ textAlign: 'right' }} + /> + ° + + + ); +} diff --git a/src/app/tools/tankopedia/[id]/page.tsx b/src/app/tools/tankopedia/[id]/page.tsx index 930af82f4..957a9bd86 100644 --- a/src/app/tools/tankopedia/[id]/page.tsx +++ b/src/app/tools/tankopedia/[id]/page.tsx @@ -21,9 +21,9 @@ import { } from '@radix-ui/themes'; import { Canvas, useLoader } from '@react-three/fiber'; import { go } from 'fuzzysort'; -import { clamp, debounce } from 'lodash'; +import { debounce } from 'lodash'; import Link from 'next/link'; -import { use, useEffect, useRef, useState } from 'react'; +import { use, useRef, useState } from 'react'; import { Group, Vector3 } from 'three'; import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; import { Flag } from '../../../../components/Flag'; @@ -36,11 +36,11 @@ import { tankDefinitions, tankNamesDiacritics, } from '../../../../core/blitzkrieg/tankDefinitions'; -import { normalizeAnglePI } from '../../../../core/math/normalizeAngle180'; import * as styles from '../page.css'; import { Controls } from './components/Control'; import { GunContainer } from './components/GunContainer'; import { HullContainer } from './components/HullContainer'; +import { RotationInputs } from './components/RotationInputs'; import { SceneProps } from './components/SceneProps'; import { TurretContainer } from './components/TurretContainer'; @@ -85,23 +85,8 @@ export default function Page({ params }: { params: { id: string } }) { const [hullYaw, setHullYaw] = useState(0); const [gunPitch, setGunPitch] = useState(0); const [controlsEnabled, setControlsEnabled] = useState(true); - const turretYawInput = useRef(null); - const hullYawInput = useRef(null); - const gunPitchInput = useRef(null); const gunContainer = useRef(null); - useEffect(() => { - hullYawInput.current!.value = `${-Math.round(hullYaw * (180 / Math.PI))}`; - }, [hullYaw]); - useEffect(() => { - turretYawInput.current!.value = `${-Math.round( - turretYaw * (180 / Math.PI), - )}`; - }, [turretYaw]); - useEffect(() => { - gunPitchInput.current!.value = `${-Math.round(gunPitch * (180 / Math.PI))}`; - }, [gunPitch]); - function handlePointerDown() { window.addEventListener('pointermove', handlePointerMove); window.addEventListener('pointerup', handlePointerUp); @@ -153,7 +138,7 @@ export default function Page({ params }: { params: { id: string } }) { camera={{ fov: 20 }} onPointerDown={handlePointerDown} > - + )} - - - Hull - { - const normalized = normalizeAnglePI( - -Number(hullYawInput.current?.value) * (Math.PI / 180), - ); - setHullYaw(normalized); - hullYawInput.current!.value = `${Math.round(normalized)}`; - }} - onKeyDown={(event) => { - if (event.key === 'Enter') { - hullYawInput.current?.blur(); - } - }} - onFocus={() => hullYawInput.current?.focus()} - ref={hullYawInput} - style={{ textAlign: 'right' }} - /> - ° - - - - Turret - { - const normalized = normalizeAnglePI( - -Number(turretYawInput.current?.value) * - (Math.PI / 180), - ); - setTurretYaw(normalized); - turretYawInput.current!.value = `${Math.round( - normalized, - )}`; - }} - onKeyDown={(event) => { - if (event.key === 'Enter') { - turretYawInput.current?.blur(); - } - }} - onFocus={() => turretYawInput.current?.focus()} - ref={turretYawInput} - style={{ textAlign: 'right' }} - /> - ° - - - - Gun - { - const clamped = clamp( - Number(gunPitchInput.current?.value), - gunModelDefinition.pitch.min, - gunModelDefinition.pitch.max, - ); - setGunPitch(-clamped * (Math.PI / 180)); - gunPitchInput.current!.value = `${Math.round(clamped)}`; - }} - onKeyDown={(event) => { - if (event.key === 'Enter') { - gunPitchInput.current?.blur(); - } - }} - onFocus={() => gunPitchInput.current?.focus()} - ref={gunPitchInput} - style={{ textAlign: 'right' }} - /> - ° - - + diff --git a/src/constants/axis.ts b/src/constants/axis.ts new file mode 100644 index 000000000..ca2463dd5 --- /dev/null +++ b/src/constants/axis.ts @@ -0,0 +1,5 @@ +import { Vector3 } from 'three'; + +export const X_AXIS = new Vector3(1, 0, 0); +export const Y_AXIS = new Vector3(0, 1, 0); +export const Z_AXIS = new Vector3(0, 0, 1); diff --git a/src/stores/tankopedia.ts b/src/stores/tankopedia.ts index 3ec510c30..98b98a0ab 100644 --- a/src/stores/tankopedia.ts +++ b/src/stores/tankopedia.ts @@ -19,6 +19,12 @@ type Tankopedia = { nations: string[]; test: TankopediaTestTankDisplay; }; + model: { + hullYaw: number; + turretYaw: number; + gunPitch: number; + controlsEnabled: boolean; + }; }; export const useTankopedia = create()(() => ({ @@ -33,6 +39,12 @@ export const useTankopedia = create()(() => ({ nations: [], test: 'include', }, + model: { + gunPitch: 0, + hullYaw: 0, + turretYaw: 0, + controlsEnabled: true, + }, })); export default function mutateTankopedia(recipe: (draft: Tankopedia) => void) {