From 9e0c4ddb9aa699e87d07177549582cddb1197ba9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com>
Date: Wed, 10 Jan 2024 13:00:37 -0600
Subject: [PATCH] feat: isolated tank model
---
.../[id]/components/Model/TankDisplay.tsx | 2 +
.../components/Model/components/TankArmor.tsx | 129 +++---
.../components/Model/components/TankModel.tsx | 414 ++++++++++++++++++
.../TankModel/components/GunContainer.tsx | 171 --------
.../TankModel/components/HullContainer.tsx | 135 ------
.../TankModel/components/TurretContainer.tsx | 191 --------
.../Model/components/TankModel/index.tsx | 157 -------
src/components/ArmorHighlightMaterial.tsx | 42 --
src/components/ArmorMesh.tsx | 22 +
src/components/HeadsUpDisplay.tsx | 53 +++
10 files changed, 547 insertions(+), 769 deletions(-)
create mode 100644 src/app/tools/tankopedia/[id]/components/Model/components/TankModel.tsx
delete mode 100644 src/app/tools/tankopedia/[id]/components/Model/components/TankModel/components/GunContainer.tsx
delete mode 100644 src/app/tools/tankopedia/[id]/components/Model/components/TankModel/components/HullContainer.tsx
delete mode 100644 src/app/tools/tankopedia/[id]/components/Model/components/TankModel/components/TurretContainer.tsx
delete mode 100644 src/app/tools/tankopedia/[id]/components/Model/components/TankModel/index.tsx
delete mode 100644 src/components/ArmorHighlightMaterial.tsx
create mode 100644 src/components/ArmorMesh.tsx
create mode 100644 src/components/HeadsUpDisplay.tsx
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;
+}