feat: async updates

This commit is contained in:
TrèsAbhi committed 2024-01-13 18:24:04 -06:00
1 parent 5e0e80e965
commit 237f676af8
9 files changed
+248 -304

No files matched your search

+1
View File
@@ -51,6 +51,7 @@
"lods", "lods",
"metallicness", "metallicness",
"MIPMAPCOUNT", "MIPMAPCOUNT",
"modeltransform",
"MURICA", "MURICA",
"NEGATIVEX", "NEGATIVEX",
"NEGATIVEY", "NEGATIVEY",
@@ -1,7 +1,6 @@
import { Card, Flex, Tabs, Theme } from '@radix-ui/themes'; import { Card, Flex, Tabs, Theme } from '@radix-ui/themes';
import { Html } from '@react-three/drei'; import { Html } from '@react-three/drei';
import { Canvas } from '@react-three/fiber'; import { Canvas } from '@react-three/fiber';
import { useRouter } from 'next/navigation';
import { Suspense, use, useEffect, useRef, useState } from 'react'; import { Suspense, use, useEffect, useRef, useState } from 'react';
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits'; import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinitions'; import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinitions';
@@ -20,7 +19,6 @@ import { TankArmor } from './components/TankArmor';
import { TankModel } from './components/TankModel'; import { TankModel } from './components/TankModel';
export function TankDisplay() { export function TankDisplay() {
const router = useRouter();
const canvas = useRef<HTMLCanvasElement>(null); const canvas = useRef<HTMLCanvasElement>(null);
const awaitedModelDefinitions = use(modelDefinitions); const awaitedModelDefinitions = use(modelDefinitions);
const canvasWrapper = useRef<HTMLDivElement>(null); const canvasWrapper = useRef<HTMLDivElement>(null);
@@ -66,24 +64,16 @@ export function TankDisplay() {
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id]; const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
mutateTankopedia((draft) => { mutateTankopedia((draft) => {
[draft.model.physical.gunPitch, draft.model.physical.turretYaw] = [draft.model.physical.pitch, draft.model.physical.yaw] =
applyPitchYawLimits( applyPitchYawLimits(
draft.model.physical.gunPitch, draft.model.physical.pitch,
draft.model.physical.turretYaw, draft.model.physical.yaw,
gunModelDefinition.pitch, gunModelDefinition.pitch,
turretModelDefinition.yaw, turretModelDefinition.yaw,
); );
}); });
}, [protagonist?.gun, protagonist?.turret]); }, [protagonist?.gun, protagonist?.turret]);
useEffect(() => {
if (location.hash.startsWith('#')) {
mutateTankopedia((draft) => {
draft.mode = location.hash.match(/#(.+)/)![1] as TankopediaMode;
});
}
}, []);
if (!protagonist) return null; if (!protagonist) return null;
return ( return (
@@ -107,8 +97,6 @@ export function TankDisplay() {
<Tabs.Root <Tabs.Root
value={mode} value={mode}
onValueChange={(mode) => { onValueChange={(mode) => {
router.push(`#${mode}`);
mutateTankopedia((draft) => { mutateTankopedia((draft) => {
draft.mode = mode as TankopediaMode; draft.mode = mode as TankopediaMode;
}); });
@@ -2,14 +2,19 @@ import { GroupProps, useLoader } from '@react-three/fiber';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { Euler, Group, Mesh, Vector3 } from 'three'; import { Euler, Group, Mesh, Vector3 } from 'three';
import { GLTFLoader } from 'three-stdlib'; import { GLTFLoader } from 'three-stdlib';
import { degToRad } from 'three/src/math/MathUtils';
import { ArmorMesh } from '../../../../../../../components/ArmorMesh'; import { ArmorMesh } from '../../../../../../../components/ArmorMesh';
import { HeadsUpDisplay } from '../../../../../../../components/HeadsUpDisplay'; import { HeadsUpDisplay } from '../../../../../../../components/HeadsUpDisplay';
import { X_AXIS } from '../../../../../../../constants/axis'; import { X_AXIS, Y_AXIS, Z_AXIS } from '../../../../../../../constants/axis';
import { asset } from '../../../../../../../core/blitzkrieg/asset'; import { asset } from '../../../../../../../core/blitzkrieg/asset';
import { import {
ModelDefinitions, ModelDefinitions,
modelDefinitions, modelDefinitions,
} from '../../../../../../../core/blitzkrieg/modelDefinitions'; } from '../../../../../../../core/blitzkrieg/modelDefinitions';
import {
ModelTransformEventData,
modelTransformEvent,
} from '../../../../../../../core/blitzkrieg/modelTransform';
import { nameToArmorId } from '../../../../../../../core/blitzkrieg/nameToArmorId'; import { nameToArmorId } from '../../../../../../../core/blitzkrieg/nameToArmorId';
import { resolveArmor } from '../../../../../../../core/blitzkrieg/resolveThickness'; import { resolveArmor } from '../../../../../../../core/blitzkrieg/resolveThickness';
import { useTankopedia } from '../../../../../../../stores/tankopedia'; import { useTankopedia } from '../../../../../../../stores/tankopedia';
@@ -17,7 +22,6 @@ import { useTankopedia } from '../../../../../../../stores/tankopedia';
interface TankArmorProps extends GroupProps {} interface TankArmorProps extends GroupProps {}
export function TankArmor({ ...props }: TankArmorProps) { export function TankArmor({ ...props }: TankArmorProps) {
const mode = useTankopedia((state) => state.mode);
const wrapper = useRef<Group>(null); const wrapper = useRef<Group>(null);
const [awaitedModelDefinitions, setAwaitedModelDefinitions] = useState< const [awaitedModelDefinitions, setAwaitedModelDefinitions] = useState<
ModelDefinitions | undefined ModelDefinitions | undefined
@@ -29,7 +33,9 @@ export function TankArmor({ ...props }: TankArmorProps) {
const showSpacedArmor = useTankopedia( const showSpacedArmor = useTankopedia(
(state) => state.model.visual.showSpacedArmor, (state) => state.model.visual.showSpacedArmor,
); );
const physical = useTankopedia((state) => state.model.physical); const turretContainer = useRef<Group>(null);
const gunContainer = useRef<Group>(null);
const initialTankopediaState = useTankopedia.getState();
useEffect(() => { useEffect(() => {
(async () => { (async () => {
@@ -37,8 +43,83 @@ export function TankArmor({ ...props }: TankArmorProps) {
})(); })();
}, []); }, []);
// it's ok to have hooks after this early termination because this will never be true useEffect(() => {
// it's more for typescript to stop throwing a fit if (!awaitedModelDefinitions) return;
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();
const turretRotation = new Euler();
const gunPosition = new Vector3();
const gunRotation = new Euler();
function handleModelTransform({ yaw, pitch }: ModelTransformEventData) {
gunPosition
.set(0, 0, 0)
.sub(turretOrigin)
.sub(gunOrigin)
.applyAxisAngle(X_AXIS, pitch)
.add(gunOrigin)
.add(turretOrigin);
gunRotation.set(pitch, 0, 0);
gunContainer.current?.position.copy(gunPosition);
gunContainer.current?.rotation.copy(gunRotation);
if (yaw === undefined) return;
turretPosition
.set(0, 0, 0)
.sub(turretOrigin)
.applyAxisAngle(new Vector3(0, 0, 1), yaw);
turretRotation.set(0, 0, yaw);
if (tankModelDefinition.turretRotation) {
const initialPitch = -degToRad(
tankModelDefinition.turretRotation.pitch,
);
const initialYaw = -degToRad(tankModelDefinition.turretRotation.yaw);
const initialRoll = -degToRad(tankModelDefinition.turretRotation.roll);
turretPosition
.applyAxisAngle(X_AXIS, initialPitch)
.applyAxisAngle(Y_AXIS, initialRoll)
.applyAxisAngle(Z_AXIS, initialYaw);
turretRotation.x += initialPitch;
turretRotation.y += initialRoll;
turretRotation.z += initialYaw;
}
turretPosition.add(turretOrigin);
turretContainer.current?.position.copy(turretPosition);
turretContainer.current?.rotation.copy(turretRotation);
}
modelTransformEvent.on(handleModelTransform);
return () => {
modelTransformEvent.off(handleModelTransform);
};
}, [awaitedModelDefinitions]);
useEffect(() => {
const unsubscribe = useTankopedia.subscribe(
(state) => state.mode,
(mode) => {
if (wrapper.current) wrapper.current.visible = mode === 'armor';
},
);
return unsubscribe;
});
if (!protagonist) return null; if (!protagonist) return null;
const armorGltf = useLoader( const armorGltf = useLoader(
@@ -68,34 +149,14 @@ export function TankArmor({ ...props }: TankArmorProps) {
turretModelDefinition.gunOrigin[1], turretModelDefinition.gunOrigin[1],
-turretModelDefinition.gunOrigin[2], -turretModelDefinition.gunOrigin[2],
).applyAxisAngle(X_AXIS, Math.PI / 2); ).applyAxisAngle(X_AXIS, Math.PI / 2);
const turretPosition = new Vector3()
.sub(turretOrigin)
.applyAxisAngle(new Vector3(0, 0, 1), physical.turretYaw);
const turretRotation = new Euler(0, 0, physical.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 ( return (
<HeadsUpDisplay> <HeadsUpDisplay>
<group <group
{...props} {...props}
visible={mode === 'armor'}
ref={wrapper} ref={wrapper}
rotation={[-Math.PI / 2, 0, physical.hullYaw]} rotation={[-Math.PI / 2, 0, 0]}
visible={initialTankopediaState.mode === 'armor'}
> >
{armorNodes.map((node) => { {armorNodes.map((node) => {
const isHull = node.name.startsWith('hull_'); const isHull = node.name.startsWith('hull_');
@@ -142,7 +203,7 @@ export function TankArmor({ ...props }: TankArmorProps) {
); );
})} })}
<group position={turretPosition} rotation={turretRotation}> <group ref={turretContainer}>
{armorNodes.map((node) => { {armorNodes.map((node) => {
const isCurrentTurret = node.name.startsWith( const isCurrentTurret = node.name.startsWith(
`turret_${turretModelDefinition.model `turret_${turretModelDefinition.model
@@ -173,15 +234,7 @@ export function TankArmor({ ...props }: TankArmorProps) {
/> />
); );
})} })}
<group <group ref={gunContainer}>
position={new Vector3()
.sub(turretOrigin)
.sub(gunOrigin)
.applyAxisAngle(new Vector3(1, 0, 0), physical.gunPitch)
.add(turretOrigin)
.add(gunOrigin)}
rotation={[physical.gunPitch, 0, 0]}
>
{armorNodes.map((node) => { {armorNodes.map((node) => {
const isCurrentGun = node.name.startsWith( const isCurrentGun = node.name.startsWith(
`gun_${gunModelDefinition.model.toString().padStart(2, '0')}`, `gun_${gunModelDefinition.model.toString().padStart(2, '0')}`,
@@ -9,13 +9,18 @@ import {
Vector3, Vector3,
} from 'three'; } from 'three';
import { GLTFLoader } from 'three-stdlib'; import { GLTFLoader } from 'three-stdlib';
import { X_AXIS } from '../../../../../../../constants/axis'; import { degToRad } from 'three/src/math/MathUtils';
import { X_AXIS, Y_AXIS, Z_AXIS } from '../../../../../../../constants/axis';
import { applyPitchYawLimits } from '../../../../../../../core/blitz/applyPitchYawLimits'; import { applyPitchYawLimits } from '../../../../../../../core/blitz/applyPitchYawLimits';
import { asset } from '../../../../../../../core/blitzkrieg/asset'; import { asset } from '../../../../../../../core/blitzkrieg/asset';
import { import {
ModelDefinitions, ModelDefinitions,
modelDefinitions, modelDefinitions,
} from '../../../../../../../core/blitzkrieg/modelDefinitions'; } from '../../../../../../../core/blitzkrieg/modelDefinitions';
import {
ModelTransformEventData,
modelTransformEvent,
} from '../../../../../../../core/blitzkrieg/modelTransform';
import { resolveJsxTree } from '../../../../../../../core/blitzkrieg/resolveJsxTree'; import { resolveJsxTree } from '../../../../../../../core/blitzkrieg/resolveJsxTree';
import { normalizeAnglePI } from '../../../../../../../core/math/normalizeAngle180'; import { normalizeAnglePI } from '../../../../../../../core/math/normalizeAngle180';
import mutateTankopedia, { import mutateTankopedia, {
@@ -28,11 +33,14 @@ export function TankModel() {
const [awaitedModelDefinitions, setAwaitedModelDefinitions] = useState< const [awaitedModelDefinitions, setAwaitedModelDefinitions] = useState<
ModelDefinitions | undefined ModelDefinitions | undefined
>(undefined); >(undefined);
const canvas = useThree((state) => state.gl.domElement);
const protagonist = useTankopedia((state) => { const protagonist = useTankopedia((state) => {
if (!state.areTanksAssigned) return; if (!state.areTanksAssigned) return;
return state.protagonist; return state.protagonist;
}); });
if (!protagonist) return null;
const canvas = useThree((state) => state.gl.domElement);
const physical = useTankopedia((state) => state.model.physical); const physical = useTankopedia((state) => state.model.physical);
const hullContainer = useRef<Group>(null); const hullContainer = useRef<Group>(null);
const turretContainer = useRef<Group>(null); const turretContainer = useRef<Group>(null);
@@ -44,7 +52,78 @@ export function TankModel() {
})(); })();
}, []); }, []);
if (!protagonist) return null; useEffect(() => {
if (!awaitedModelDefinitions) return;
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();
const turretRotation = new Euler();
const gunPosition = new Vector3();
const gunRotation = new Euler();
function handleModelTransform({ yaw, pitch }: ModelTransformEventData) {
gunPosition
.set(0, 0, 0)
.sub(turretOrigin)
.sub(gunOrigin)
.applyAxisAngle(X_AXIS, pitch)
.add(gunOrigin)
.add(turretOrigin);
gunRotation.set(pitch, 0, 0);
gunContainer.current?.position.copy(gunPosition);
gunContainer.current?.rotation.copy(gunRotation);
if (yaw === undefined) return;
turretPosition
.set(0, 0, 0)
.sub(turretOrigin)
.applyAxisAngle(new Vector3(0, 0, 1), yaw);
turretRotation.set(0, 0, yaw);
if (tankModelDefinition.turretRotation) {
const initialPitch = -degToRad(
tankModelDefinition.turretRotation.pitch,
);
const initialYaw = -degToRad(tankModelDefinition.turretRotation.yaw);
const initialRoll = -degToRad(tankModelDefinition.turretRotation.roll);
turretPosition
.applyAxisAngle(X_AXIS, initialPitch)
.applyAxisAngle(Y_AXIS, initialRoll)
.applyAxisAngle(Z_AXIS, initialYaw);
turretRotation.x += initialPitch;
turretRotation.y += initialRoll;
turretRotation.z += initialYaw;
}
turretPosition.add(turretOrigin);
turretContainer.current?.position.copy(turretPosition);
turretContainer.current?.rotation.copy(turretRotation);
}
modelTransformEvent.on(handleModelTransform);
return () => {
modelTransformEvent.off(handleModelTransform);
};
}, [awaitedModelDefinitions]);
if (!awaitedModelDefinitions) return;
const tankModelDefinition = awaitedModelDefinitions[protagonist.tank.id];
const turretModelDefinition =
tankModelDefinition.turrets[protagonist.turret.id];
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
const gltf = useLoader( const gltf = useLoader(
GLTFLoader, GLTFLoader,
@@ -52,51 +131,13 @@ export function TankModel() {
); );
const nodes = Object.values(gltf.nodes); const nodes = Object.values(gltf.nodes);
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), physical.turretYaw);
const turretRotation = new Euler(0, 0, physical.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 ( return (
<group ref={hullContainer} rotation={[-Math.PI / 2, 0, physical.hullYaw]}> <group ref={hullContainer} rotation={[-Math.PI / 2, 0, 0]}>
{nodes.map((node) => { {nodes.map((node) => {
const isHull = node.name === 'hull'; const isHull = node.name === 'hull';
const isWheel = node.name.startsWith('chassis_wheel_'); const isWheel = node.name.startsWith('chassis_wheel_');
const isTrack = node.name.startsWith('chassis_track_'); const isTrack = node.name.startsWith('chassis_track_');
const isVisible = isHull || isWheel || isTrack; const isVisible = isHull || isWheel || isTrack;
let draftHullYaw = 0;
function handlePointerDown(event: ThreeEvent<PointerEvent>) { function handlePointerDown(event: ThreeEvent<PointerEvent>) {
event.stopPropagation(); event.stopPropagation();
@@ -104,31 +145,12 @@ export function TankModel() {
draft.model.visual.controlsEnabled = false; draft.model.visual.controlsEnabled = false;
}); });
if (isHull) { if (isTrack) {
draftHullYaw = physical.hullYaw; window.addEventListener('pointermove', handlePointerMove);
window.addEventListener('pointerup', handlePointerUp);
window.addEventListener('pointermove', handlePointerMoveHull);
window.addEventListener('pointerup', handlePointerUpHull);
} else if (isTrack) {
window.addEventListener('pointermove', handlePointerMoveTrack);
window.addEventListener('pointerup', handlePointerUpTrack);
} }
} }
function handlePointerMoveHull(event: PointerEvent) { function handlePointerMove(event: PointerEvent) {
draftHullYaw += event.movementX * ((2 * Math.PI) / canvas.width);
if (hullContainer.current) {
hullContainer.current.rotation.z = draftHullYaw;
}
}
function handlePointerUpHull() {
mutateTankopedia((draft) => {
draft.model.visual.controlsEnabled = true;
draft.model.physical.hullYaw = normalizeAnglePI(draftHullYaw);
});
window.removeEventListener('pointermove', handlePointerMoveHull);
window.removeEventListener('pointerup', handlePointerUpHull);
}
function handlePointerMoveTrack(event: PointerEvent) {
const mesh = node as Mesh; const mesh = node as Mesh;
const material = mesh.material as MeshStandardMaterial; const material = mesh.material as MeshStandardMaterial;
const offset = new Vector2( const offset = new Vector2(
@@ -141,12 +163,12 @@ export function TankModel() {
material.roughnessMap?.offset.add(offset); material.roughnessMap?.offset.add(offset);
material.normalMap?.offset.add(offset); material.normalMap?.offset.add(offset);
} }
function handlePointerUpTrack() { function handlePointerUp() {
mutateTankopedia((draft) => { mutateTankopedia((draft) => {
draft.model.visual.controlsEnabled = true; draft.model.visual.controlsEnabled = true;
}); });
window.removeEventListener('pointermove', handlePointerMoveTrack); window.removeEventListener('pointermove', handlePointerMove);
window.removeEventListener('pointerup', handlePointerUpTrack); window.removeEventListener('pointerup', handlePointerUp);
} }
if (!isVisible) return null; if (!isVisible) return null;
@@ -167,26 +189,22 @@ export function TankModel() {
); );
})} })}
<group <group ref={turretContainer}>
position={turretPosition}
rotation={turretRotation}
ref={turretContainer}
>
{nodes.map((node) => { {nodes.map((node) => {
const isTurret = node.name.startsWith('turret_'); const isTurret = node.name.startsWith('turret_');
const isCurrentTurret = const isCurrentTurret =
node.name === node.name ===
`turret_${turretModelDefinition.model.toString().padStart(2, '0')}`; `turret_${turretModelDefinition.model.toString().padStart(2, '0')}`;
const isVisible = isCurrentTurret; const isVisible = isCurrentTurret;
let draftPitch = 0; let pitch = 0;
let draftYaw = 0; let yaw = 0;
function handlePointerDown(event: ThreeEvent<PointerEvent>) { function handlePointerDown(event: ThreeEvent<PointerEvent>) {
event.stopPropagation(); event.stopPropagation();
if (isTurret) { if (isTurret) {
draftYaw = physical.turretYaw; yaw = physical.yaw;
draftPitch = physical.gunPitch; pitch = physical.pitch;
mutateTankopedia((draft) => { mutateTankopedia((draft) => {
draft.model.visual.controlsEnabled = false; draft.model.visual.controlsEnabled = false;
@@ -196,53 +214,19 @@ export function TankModel() {
} }
} }
function handlePointerMove(event: PointerEvent) { function handlePointerMove(event: PointerEvent) {
[draftPitch, draftYaw] = applyPitchYawLimits( [pitch, yaw] = applyPitchYawLimits(
draftPitch, pitch,
draftYaw + event.movementX * (Math.PI / canvas.width), yaw + event.movementX * (Math.PI / canvas.width),
gunModelDefinition.pitch, gunModelDefinition.pitch,
turretModelDefinition.yaw, turretModelDefinition.yaw,
); );
modelTransformEvent.emit({ pitch, 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() { function handlePointerUp() {
mutateTankopedia((state) => { mutateTankopedia((state) => {
state.model.visual.controlsEnabled = true; state.model.visual.controlsEnabled = true;
state.model.physical.gunPitch = normalizeAnglePI(draftPitch); state.model.physical.pitch = normalizeAnglePI(pitch);
state.model.physical.turretYaw = normalizeAnglePI(draftYaw); state.model.physical.yaw = normalizeAnglePI(yaw);
}); });
window.removeEventListener('pointermove', handlePointerMove); window.removeEventListener('pointermove', handlePointerMove);
window.removeEventListener('pointerup', handlePointerUp); window.removeEventListener('pointerup', handlePointerUp);
@@ -267,16 +251,7 @@ export function TankModel() {
); );
})} })}
<group <group ref={gunContainer}>
position={new Vector3()
.sub(turretOrigin)
.sub(gunOrigin)
.applyAxisAngle(new Vector3(1, 0, 0), physical.gunPitch)
.add(turretOrigin)
.add(gunOrigin)}
rotation={[physical.gunPitch, 0, 0]}
ref={gunContainer}
>
{nodes.map((node) => { {nodes.map((node) => {
const isCurrentMantlet = const isCurrentMantlet =
node.name === node.name ===
@@ -287,8 +262,8 @@ export function TankModel() {
node.name === node.name ===
`gun_${gunModelDefinition.model.toString().padStart(2, '0')}`; `gun_${gunModelDefinition.model.toString().padStart(2, '0')}`;
const isVisible = isCurrentGun || isCurrentMantlet; const isVisible = isCurrentGun || isCurrentMantlet;
let draftPitch = 0; let pitch = 0;
let draftYaw = 0; let yaw = 0;
function handlePointerDown(event: ThreeEvent<PointerEvent>) { function handlePointerDown(event: ThreeEvent<PointerEvent>) {
event.stopPropagation(); event.stopPropagation();
@@ -296,59 +271,25 @@ export function TankModel() {
mutateTankopedia((draft) => { mutateTankopedia((draft) => {
draft.model.visual.controlsEnabled = false; draft.model.visual.controlsEnabled = false;
}); });
draftPitch = physical.gunPitch; pitch = physical.pitch;
draftYaw = physical.turretYaw; yaw = physical.yaw;
window.addEventListener('pointermove', handlePointerMove); window.addEventListener('pointermove', handlePointerMove);
window.addEventListener('pointerup', handlePointerUp); window.addEventListener('pointerup', handlePointerUp);
} }
function handlePointerMove(event: PointerEvent) { function handlePointerMove(event: PointerEvent) {
[draftPitch, draftYaw] = applyPitchYawLimits( [pitch, yaw] = applyPitchYawLimits(
draftPitch - event.movementY * (Math.PI / canvas.height), pitch - event.movementY * (Math.PI / canvas.height),
draftYaw + event.movementX * (Math.PI / canvas.width), yaw + event.movementX * (Math.PI / canvas.width),
gunModelDefinition.pitch, gunModelDefinition.pitch,
turretModelDefinition.yaw, turretModelDefinition.yaw,
); );
modelTransformEvent.emit({ pitch, 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() { function handlePointerUp() {
mutateTankopedia((state) => { mutateTankopedia((state) => {
state.model.visual.controlsEnabled = true; state.model.visual.controlsEnabled = true;
state.model.physical.gunPitch = normalizeAnglePI(draftPitch); state.model.physical.pitch = normalizeAnglePI(pitch);
state.model.physical.turretYaw = normalizeAnglePI(draftYaw); state.model.physical.yaw = normalizeAnglePI(yaw);
}); });
window.removeEventListener('pointermove', handlePointerMove); window.removeEventListener('pointermove', handlePointerMove);
window.removeEventListener('pointerup', handlePointerUp); window.removeEventListener('pointerup', handlePointerUp);
@@ -2,7 +2,6 @@ import { Flex, TextField } from '@radix-ui/themes';
import { use, useEffect, useRef } from 'react'; import { use, useEffect, useRef } from 'react';
import { applyPitchYawLimits } from '../../../../../core/blitz/applyPitchYawLimits'; import { applyPitchYawLimits } from '../../../../../core/blitz/applyPitchYawLimits';
import { modelDefinitions } from '../../../../../core/blitzkrieg/modelDefinitions'; import { modelDefinitions } from '../../../../../core/blitzkrieg/modelDefinitions';
import { normalizeAnglePI } from '../../../../../core/math/normalizeAngle180';
import mutateTankopedia, { import mutateTankopedia, {
useTankopedia, useTankopedia,
} from '../../../../../stores/tankopedia'; } from '../../../../../stores/tankopedia';
@@ -11,28 +10,22 @@ export function RotationInputs() {
const physical = useTankopedia((state) => state.model.physical); const physical = useTankopedia((state) => state.model.physical);
const awaitedModelDefinitions = use(modelDefinitions); const awaitedModelDefinitions = use(modelDefinitions);
const turretYawInput = useRef<HTMLInputElement>(null); const turretYawInput = useRef<HTMLInputElement>(null);
const hullYawInput = useRef<HTMLInputElement>(null);
const gunPitchInput = useRef<HTMLInputElement>(null); const gunPitchInput = useRef<HTMLInputElement>(null);
const protagonist = useTankopedia((state) => { const protagonist = useTankopedia((state) => {
if (!state.areTanksAssigned) return; if (!state.areTanksAssigned) return;
return state.protagonist; return state.protagonist;
}); });
useEffect(() => {
hullYawInput.current!.value = `${-Math.round(
physical.hullYaw * (180 / Math.PI),
)}`;
}, [physical.hullYaw]);
useEffect(() => { useEffect(() => {
turretYawInput.current!.value = `${-Math.round( turretYawInput.current!.value = `${-Math.round(
physical.turretYaw * (180 / Math.PI), physical.yaw * (180 / Math.PI),
)}`; )}`;
}, [physical.turretYaw]); }, [physical.yaw]);
useEffect(() => { useEffect(() => {
gunPitchInput.current!.value = `${-Math.round( gunPitchInput.current!.value = `${-Math.round(
physical.gunPitch * (180 / Math.PI), physical.pitch * (180 / Math.PI),
)}`; )}`;
}, [physical.gunPitch]); }, [physical.pitch]);
if (!protagonist) return null; if (!protagonist) return null;
@@ -55,38 +48,9 @@ export function RotationInputs() {
<Flex <Flex
gap="2" gap="2"
style={{ style={{
width: 352, width: 256,
}} }}
> >
<TextField.Root
style={{
flex: 1,
}}
>
<TextField.Slot>Hull</TextField.Slot>
<TextField.Input
defaultValue={Math.round(physical.hullYaw * (180 / Math.PI))}
onBlur={() => {
const normalized = normalizeAnglePI(
-Number(hullYawInput.current?.value) * (Math.PI / 180),
);
mutateTankopedia((state) => {
state.model.physical.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' }}
/>
<TextField.Slot>°</TextField.Slot>
</TextField.Root>
<TextField.Root <TextField.Root
style={{ style={{
flex: 1, flex: 1,
@@ -94,17 +58,17 @@ export function RotationInputs() {
> >
<TextField.Slot>Turret</TextField.Slot> <TextField.Slot>Turret</TextField.Slot>
<TextField.Input <TextField.Input
defaultValue={Math.round(physical.turretYaw * (180 / Math.PI))} defaultValue={Math.round(physical.yaw * (180 / Math.PI))}
onBlur={() => { onBlur={() => {
const [pitch, yaw] = applyPitchYawLimits( const [pitch, yaw] = applyPitchYawLimits(
physical.gunPitch, physical.pitch,
-Number(turretYawInput.current?.value) * (Math.PI / 180), -Number(turretYawInput.current?.value) * (Math.PI / 180),
gunModelDefinition.pitch, gunModelDefinition.pitch,
turretModelDefinition.yaw, turretModelDefinition.yaw,
); );
mutateTankopedia((state) => { mutateTankopedia((state) => {
state.model.physical.gunPitch = pitch; state.model.physical.pitch = pitch;
state.model.physical.turretYaw = yaw; state.model.physical.yaw = yaw;
}); });
turretYawInput.current!.value = `${Math.round( turretYawInput.current!.value = `${Math.round(
yaw * (180 / Math.PI), yaw * (180 / Math.PI),
@@ -129,17 +93,17 @@ export function RotationInputs() {
> >
<TextField.Slot>Gun</TextField.Slot> <TextField.Slot>Gun</TextField.Slot>
<TextField.Input <TextField.Input
defaultValue={-Math.round(physical.gunPitch * (180 / Math.PI))} defaultValue={-Math.round(physical.pitch * (180 / Math.PI))}
onBlur={() => { onBlur={() => {
const [pitch, yaw] = applyPitchYawLimits( const [pitch, yaw] = applyPitchYawLimits(
-Number(gunPitchInput.current?.value) * (Math.PI / 180), -Number(gunPitchInput.current?.value) * (Math.PI / 180),
physical.turretYaw, physical.yaw,
gunModelDefinition.pitch, gunModelDefinition.pitch,
turretModelDefinition.yaw, turretModelDefinition.yaw,
); );
mutateTankopedia((state) => { mutateTankopedia((state) => {
state.model.physical.gunPitch = pitch; state.model.physical.pitch = pitch;
state.model.physical.turretYaw = yaw; state.model.physical.yaw = yaw;
}); });
gunPitchInput.current!.value = `${Math.round( gunPitchInput.current!.value = `${Math.round(
pitch * (180 / Math.PI), pitch * (180 / Math.PI),
+3 -26
View File
@@ -18,9 +18,8 @@ export default function Page({ params }: { params: { id: string } }) {
useEffect(() => { useEffect(() => {
mutateTankopedia((draft) => { mutateTankopedia((draft) => {
draft.model.physical.hullYaw = 0; draft.model.physical.yaw = 0;
draft.model.physical.turretYaw = 0; draft.model.physical.pitch = 0;
draft.model.physical.gunPitch = 0;
draft.mode = 'model'; draft.mode = 'model';
draft.areTanksAssigned = true; draft.areTanksAssigned = true;
@@ -129,6 +128,7 @@ export default function Page({ params }: { params: { id: string } }) {
{protagonist.turret.guns.map((thisGun, index) => { {protagonist.turret.guns.map((thisGun, index) => {
return ( return (
<ModuleButton <ModuleButton
key={thisGun.id}
type="module" type="module"
module="gun" module="gun"
tier={thisGun.tier} tier={thisGun.tier}
@@ -146,29 +146,6 @@ export default function Page({ params }: { params: { id: string } }) {
}} }}
/> />
); );
// return (
// <Tooltip content={thisGun.name} key={thisGun.id}>
// <Button
// onClick={() => {
// mutateTankopedia((draft) => {
// if (!draft.areTanksAssigned) return;
// draft.protagonist.gun = thisGun;
// });
// }}
// variant={
// protagonist.gun.id === thisGun.id ? 'solid' : 'soft'
// }
// >
// <img
// src={asset('icons/modules/gun.webp')}
// width={32}
// height={32}
// />
// {TIER_ROMAN_NUMERALS[thisGun.tier]}
// </Button>
// </Tooltip>
// );
})} })}
</Flex> </Flex>
</Flex> </Flex>
+14
View File
@@ -0,0 +1,14 @@
export class EventManager<Type> {
private callbacks: Set<(data: Type) => void> = new Set();
on(callback: (data: Type) => void) {
this.callbacks.add(callback);
}
off(callback: (data: Type) => void) {
this.callbacks.delete(callback);
}
emit(data: Type) {
this.callbacks.forEach((callback) => callback(data));
}
}
+8
View File
@@ -0,0 +1,8 @@
import { EventManager } from './eventManager';
export interface ModelTransformEventData {
yaw?: number;
pitch: number;
}
export const modelTransformEvent = new EventManager<ModelTransformEventData>();
+4 -6
View File
@@ -29,9 +29,8 @@ type Tankopedia = {
}; };
model: { model: {
physical: { physical: {
hullYaw: number; yaw: number;
turretYaw: number; pitch: number;
gunPitch: number;
}; };
visual: { visual: {
controlsEnabled: boolean; controlsEnabled: boolean;
@@ -74,9 +73,8 @@ export const useTankopedia = create<Tankopedia>()(
}, },
model: { model: {
physical: { physical: {
gunPitch: 0, yaw: 0,
hullYaw: 0, pitch: 0,
turretYaw: 0,
}, },
visual: { visual: {
controlsEnabled: true, controlsEnabled: true,