diff --git a/public/test/16401.glb b/public/test/16401.glb new file mode 100644 index 000000000..f6aacaf5a Binary files /dev/null and b/public/test/16401.glb differ diff --git a/scripts/buildAssets.ts b/scripts/buildAssets.ts index 7fe2a22df..087b9f9a5 100644 --- a/scripts/buildAssets.ts +++ b/scripts/buildAssets.ts @@ -408,26 +408,26 @@ if (allTargets || targets?.includes('definitions')) { }), ); - console.log('Committing tank definitions...'); - await commitMultipleFiles( - 'tresabhi', - 'blitzkrieg-assets', - 'main', - 'definitions', - [ - { - content: JSON.stringify(tankDefinitions), - encoding: 'utf-8', - path: 'definitions/tanks.json', - }, - { - content: JSON.stringify(modelDefinitions), - encoding: 'utf-8', - path: 'definitions/models.json', - }, - ], - true, - ); + console.log('Committing definitions...'); + // await commitMultipleFiles( + // 'tresabhi', + // 'blitzkrieg-assets', + // 'main', + // 'definitions', + // [ + // { + // content: JSON.stringify(tankDefinitions), + // encoding: 'utf-8', + // path: 'definitions/tanks.json', + // }, + // { + // content: JSON.stringify(modelDefinitions), + // encoding: 'utf-8', + // path: 'definitions/models.json', + // }, + // ], + // true, + // ); } if ( @@ -587,8 +587,9 @@ if (allTargets || targets?.includes('tankModels')) { // if (id !== 15697) continue; // chieftain TODO: investigate vertices stream over read // if (id !== 24609) continue; // concept 1b + if (id !== 16401) continue; // waffle // if (id !== 7425) continue; // isu 152 - if (id !== 10369) continue; // mino + // if (id !== 10369) continue; // mino // if (id !== 4417) continue; // amx m4 mle // if (id !== 7297) continue; // 60tp // if (id !== 1) continue; // t-34 diff --git a/src/app/tools/tankopedia/[id]/components/GunContainer.tsx b/src/app/tools/tankopedia/[id]/components/GunContainer.tsx index a49bf13d8..da8f8d64c 100644 --- a/src/app/tools/tankopedia/[id]/components/GunContainer.tsx +++ b/src/app/tools/tankopedia/[id]/components/GunContainer.tsx @@ -1,6 +1,6 @@ import { ThreeEvent, useThree } from '@react-three/fiber'; import { clamp } from 'lodash'; -import { RefObject, useRef } from 'react'; +import { RefObject, forwardRef, useImperativeHandle, useRef } from 'react'; import { Group, Mesh, Object3D, Vector3 } from 'three'; import { PitchLimits, @@ -12,8 +12,8 @@ import { normalizeAnglePI } from '../../../../../core/math/normalizeAngle180'; interface GunContainerProps { turretOrigin: Vector3; gunOrigin: Vector3; - gunPitch: number; - turretYaw: number; + pitch: number; + yaw: number; objects: Object3D[]; model: number; yawLimits?: YawLimits; @@ -23,113 +23,178 @@ interface GunContainerProps { turretContainer: RefObject; } -export function GunContainer({ - gunOrigin, - turretOrigin, - gunPitch, - turretYaw, - objects, - yawLimits, - onPitchStart, - turretContainer, - pitchLimits, - model, - onPitchEnd, -}: GunContainerProps) { - const canvas = useThree((state) => state.gl.domElement); - const gunContainer = useRef(null); +export const GunContainer = forwardRef( + ( + { + gunOrigin, + turretOrigin, + pitch, + yaw, + objects, + yawLimits, + onPitchStart, + turretContainer, + pitchLimits, + model, + onPitchEnd, + }: GunContainerProps, + ref, + ) => { + const canvas = useThree((state) => state.gl.domElement); + const gunContainer = useRef(null); - return ( - - {objects.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 draftMantletPitch = 0; - let draftTurretYaw = 0; + useImperativeHandle(ref, () => gunContainer.current!); - function handlePointerDown(event: ThreeEvent) { - event.stopPropagation(); + return ( + + {objects.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; - onPitchStart(); - draftMantletPitch = gunPitch; - draftTurretYaw = turretYaw; - window.addEventListener('pointermove', handlePointerMove); - window.addEventListener('pointerup', handlePointerUp); - } - function handlePointerMove(event: PointerEvent) { - if (yawLimits) { - draftTurretYaw = clamp( - draftTurretYaw + event.movementX * (Math.PI / canvas.width), - -yawLimits.max * (Math.PI / 180), - -yawLimits.min * (Math.PI / 180), + function handlePointerDown(event: ThreeEvent) { + event.stopPropagation(); + + onPitchStart(); + draftPitch = pitch; + draftYaw = yaw; + window.addEventListener('pointermove', handlePointerMove); + window.addEventListener('pointerup', handlePointerUp); + } + function handlePointerMove(event: PointerEvent) { + if (yawLimits) { + draftYaw = clamp( + draftYaw + event.movementX * (Math.PI / canvas.width), + -yawLimits.max * (Math.PI / 180), + -yawLimits.min * (Math.PI / 180), + ); + } else { + draftYaw += event.movementX * ((2 * Math.PI) / canvas.width); + } + + let lowerPitch = -pitchLimits.max * (Math.PI / 180); + let upperPitch = -pitchLimits.min * (Math.PI / 180); + const transition = (pitchLimits.transition ?? 10) * (Math.PI / 180); + + if (pitchLimits.back) { + const range = pitchLimits.back.range * (Math.PI / 180); + const yawRotatedAbs = Math.abs( + normalizeAnglePI(draftYaw - Math.PI), + ); + + if (yawRotatedAbs <= range + transition) { + if (yawRotatedAbs <= range) { + // inside range + lowerPitch = -pitchLimits.back.max * (Math.PI / 180); + upperPitch = -pitchLimits.back.min * (Math.PI / 180); + } else { + // inside transition + const transitionProgress = + (yawRotatedAbs - range) / transition; + const maxInterpolatedPitch = + (1 - transitionProgress) * pitchLimits.back.max + + transitionProgress * pitchLimits.max; + const minInterpolatedPitch = + (1 - transitionProgress) * pitchLimits.back.min + + transitionProgress * pitchLimits.min; + + lowerPitch = -maxInterpolatedPitch * (Math.PI / 180); + upperPitch = -minInterpolatedPitch * (Math.PI / 180); + } + } + } + + if (pitchLimits.front) { + const range = pitchLimits.front.range * (Math.PI / 180); + const yawAbs = Math.abs(normalizeAnglePI(draftYaw)); + + if (yawAbs <= range + transition) { + if (yawAbs <= range) { + // inside range + lowerPitch = -pitchLimits.front.max * (Math.PI / 180); + upperPitch = -pitchLimits.front.min * (Math.PI / 180); + } else { + // inside transition + const transitionProgress = (yawAbs - range) / transition; + const maxInterpolatedPitch = + (1 - transitionProgress) * pitchLimits.front.max + + transitionProgress * pitchLimits.max; + const minInterpolatedPitch = + (1 - transitionProgress) * pitchLimits.front.min + + transitionProgress * pitchLimits.min; + + lowerPitch = -maxInterpolatedPitch * (Math.PI / 180); + upperPitch = -minInterpolatedPitch * (Math.PI / 180); + } + } + } + + draftPitch = clamp( + draftPitch - event.movementY * (Math.PI / canvas.height), + lowerPitch, + upperPitch, ); - } else { - draftTurretYaw += event.movementX * ((2 * Math.PI) / canvas.width); + + 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) + .add(turretOrigin); + turretContainer.current.rotation.z = draftYaw; + } } - draftMantletPitch = clamp( - draftMantletPitch - event.movementY * (Math.PI / canvas.height), - -pitchLimits.max * (Math.PI / 180), - -pitchLimits.min * (Math.PI / 180), + function handlePointerUp() { + onPitchEnd( + normalizeAnglePI(draftPitch), + normalizeAnglePI(draftYaw), + ); + window.removeEventListener('pointermove', handlePointerMove); + window.removeEventListener('pointerup', handlePointerUp); + } + + if (!isVisible) return null; + return ( + ); - - if (gunContainer.current) { - gunContainer.current.position - .set(0, 0, 0) - .sub(turretOrigin) - .sub(gunOrigin) - .applyAxisAngle(new Vector3(1, 0, 0), draftMantletPitch) - .add(turretOrigin) - .add(gunOrigin); - gunContainer.current.rotation.x = draftMantletPitch; - } - - if (turretContainer.current) { - turretContainer.current.position - .set(0, 0, 0) - .sub(turretOrigin) - .applyAxisAngle(new Vector3(0, 0, 1), draftTurretYaw) - .add(turretOrigin); - turretContainer.current.rotation.z = draftTurretYaw; - } - } - function handlePointerUp() { - onPitchEnd( - normalizeAnglePI(draftMantletPitch), - normalizeAnglePI(draftTurretYaw), - ); - window.removeEventListener('pointermove', handlePointerMove); - window.removeEventListener('pointerup', handlePointerUp); - } - - if (!isVisible) return null; - return ( - - ); - })} - - ); -} + })} + + ); + }, +); diff --git a/src/app/tools/tankopedia/[id]/components/TurretContainer.tsx b/src/app/tools/tankopedia/[id]/components/TurretContainer.tsx index 57b099fb1..6090b0418 100644 --- a/src/app/tools/tankopedia/[id]/components/TurretContainer.tsx +++ b/src/app/tools/tankopedia/[id]/components/TurretContainer.tsx @@ -1,28 +1,49 @@ import { ThreeEvent, useThree } from '@react-three/fiber'; -import { ReactNode, forwardRef, useImperativeHandle, useRef } from 'react'; +import { clamp } from 'lodash'; +import { + ReactNode, + RefObject, + forwardRef, + useImperativeHandle, + useRef, +} from 'react'; import { Group, Mesh, Object3D, Vector3 } from 'three'; +import { + PitchLimits, + YawLimits, +} from '../../../../../core/blitzkrieg/modelDefinitions'; import { resolveJsxTree } from '../../../../../core/blitzkrieg/resolveJsxTree'; import { normalizeAnglePI } from '../../../../../core/math/normalizeAngle180'; interface TurretContainerProps { - origin: Vector3; + turretOrigin: Vector3; + gunOrigin: Vector3; objects: Object3D[]; children: ReactNode; yaw: number; + pitch: number; model: number; + yawLimits?: YawLimits; + pitchLimits: PitchLimits; onYawStart: () => void; - onYawEnd: (yaw: number) => void; + onYawEnd: (pitch: number, yaw: number) => void; + gunContainer: RefObject; } export const TurretContainer = forwardRef( ( { - origin, + gunContainer, + turretOrigin, yaw, children, objects, + yawLimits, model, onYawEnd, + pitch, + gunOrigin, + pitchLimits, onYawStart, }: TurretContainerProps, ref, @@ -35,9 +56,9 @@ export const TurretContainer = forwardRef( return ( @@ -46,6 +67,7 @@ export const TurretContainer = forwardRef( const isCurrentTurret = object.name === `turret_${model.toString().padStart(2, '0')}`; const isVisible = isCurrentTurret; + let draftPitch = 0; let draftYaw = 0; function handlePointerDown(event: ThreeEvent) { @@ -53,6 +75,7 @@ export const TurretContainer = forwardRef( if (isTurret) { draftYaw = yaw; + draftPitch = pitch; onYawStart(); window.addEventListener('pointermove', handlePointerMove); @@ -60,19 +83,97 @@ export const TurretContainer = forwardRef( } } function handlePointerMove(event: PointerEvent) { - draftYaw += event.movementX * ((2 * Math.PI) / canvas.width); + if (yawLimits) { + draftYaw = clamp( + draftYaw + event.movementX * (Math.PI / canvas.width), + -yawLimits.max * (Math.PI / 180), + -yawLimits.min * (Math.PI / 180), + ); + } else { + draftYaw += event.movementX * ((2 * Math.PI) / canvas.width); + } + + let lowerPitch = -pitchLimits.max * (Math.PI / 180); + let upperPitch = -pitchLimits.min * (Math.PI / 180); + const transition = (pitchLimits.transition ?? 10) * (Math.PI / 180); + + if (pitchLimits.back) { + const range = pitchLimits.back.range * (Math.PI / 180); + const yawRotatedAbs = Math.abs( + normalizeAnglePI(draftYaw - Math.PI), + ); + + if (yawRotatedAbs <= range + transition) { + if (yawRotatedAbs <= range) { + // inside range + lowerPitch = -pitchLimits.back.max * (Math.PI / 180); + upperPitch = -pitchLimits.back.min * (Math.PI / 180); + } else { + // inside transition + const transitionProgress = + (yawRotatedAbs - range) / transition; + const maxInterpolatedPitch = + (1 - transitionProgress) * pitchLimits.back.max + + transitionProgress * pitchLimits.max; + const minInterpolatedPitch = + (1 - transitionProgress) * pitchLimits.back.min + + transitionProgress * pitchLimits.min; + + lowerPitch = -maxInterpolatedPitch * (Math.PI / 180); + upperPitch = -minInterpolatedPitch * (Math.PI / 180); + } + } + } + + if (pitchLimits.front) { + const range = pitchLimits.front.range * (Math.PI / 180); + const yawAbs = Math.abs(normalizeAnglePI(draftYaw)); + + if (yawAbs <= range + transition) { + if (yawAbs <= range) { + // inside range + lowerPitch = -pitchLimits.front.max * (Math.PI / 180); + upperPitch = -pitchLimits.front.min * (Math.PI / 180); + } else { + // inside transition + const transitionProgress = (yawAbs - range) / transition; + const maxInterpolatedPitch = + (1 - transitionProgress) * pitchLimits.front.max + + transitionProgress * pitchLimits.max; + const minInterpolatedPitch = + (1 - transitionProgress) * pitchLimits.front.min + + transitionProgress * pitchLimits.min; + + lowerPitch = -maxInterpolatedPitch * (Math.PI / 180); + upperPitch = -minInterpolatedPitch * (Math.PI / 180); + } + } + } + + draftPitch = clamp(draftPitch, lowerPitch, upperPitch); + + 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(origin) + .sub(turretOrigin) .applyAxisAngle(new Vector3(0, 0, 1), draftYaw) - .add(origin); + .add(turretOrigin); turretContainer.current.rotation.z = draftYaw; } } function handlePointerUp() { - onYawEnd(normalizeAnglePI(draftYaw)); + onYawEnd(normalizeAnglePI(draftPitch), normalizeAnglePI(draftYaw)); window.removeEventListener('pointermove', handlePointerMove); window.removeEventListener('pointerup', handlePointerUp); } diff --git a/src/app/tools/tankopedia/[id]/page.tsx b/src/app/tools/tankopedia/[id]/page.tsx index fb730bd0f..29d91db92 100644 --- a/src/app/tools/tankopedia/[id]/page.tsx +++ b/src/app/tools/tankopedia/[id]/page.tsx @@ -163,22 +163,29 @@ export default function Page({ params }: { params: { id: string } }) { }} > setControlsEnabled(false)} - onYawEnd={(yaw) => { + yawLimits={turretModelDefinition.yaw} + pitchLimits={gunModelDefinition.pitch} + pitch={gunPitch} + onYawEnd={(pitch, yaw) => { setControlsEnabled(true); + setGunPitch(pitch); setTurretYaw(yaw); }} - origin={turretOrigin} + gunContainer={gunContainer} + turretOrigin={turretOrigin} yaw={turretYaw} > setControlsEnabled(false)} pitchLimits={gunModelDefinition.pitch} turretContainer={turretContainer} - turretYaw={turretYaw} + yaw={turretYaw} onPitchEnd={(pitch, yaw) => { setControlsEnabled(true); setGunPitch(pitch); @@ -186,7 +193,7 @@ export default function Page({ params }: { params: { id: string } }) { }} yawLimits={turretModelDefinition.yaw} gunOrigin={gunOrigin} - gunPitch={gunPitch} + pitch={gunPitch} model={gunModelDefinition.model} objects={gltf.scene.children[0].children} turretOrigin={turretOrigin}