From 230b2df5fd20240ebfb22a7728ec29ab9e342c7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Sat, 13 Jan 2024 13:44:27 -0600 Subject: [PATCH] feat: optimized state handling --- .../[id]/components/Model/TankDisplay.tsx | 77 +--------------- .../components/Model/components/Options.tsx | 76 ++++++++++++++++ .../components/Model/components/TankArmor.tsx | 87 ++++--------------- src/components/ArmorMesh/index.tsx | 40 +++++---- ...netration.ts => resolveNearPenetration.ts} | 2 +- src/stores/tankopedia.ts | 49 ++++++----- 6 files changed, 145 insertions(+), 186 deletions(-) create mode 100644 src/app/tools/tankopedia/[id]/components/Model/components/Options.tsx rename src/core/blitz/{numericPenetration.ts => resolveNearPenetration.ts} (59%) diff --git a/src/app/tools/tankopedia/[id]/components/Model/TankDisplay.tsx b/src/app/tools/tankopedia/[id]/components/Model/TankDisplay.tsx index fff394397..5d0083941 100644 --- a/src/app/tools/tankopedia/[id]/components/Model/TankDisplay.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/TankDisplay.tsx @@ -1,16 +1,4 @@ -import { - EnterFullScreenIcon, - ExitFullScreenIcon, - GearIcon, -} from '@radix-ui/react-icons'; -import { - Button, - Card, - DropdownMenu, - Flex, - Tabs, - Theme, -} from '@radix-ui/themes'; +import { Card, Flex, Tabs, Theme } from '@radix-ui/themes'; import { Html } from '@react-three/drei'; import { Canvas } from '@react-three/fiber'; import { Suspense, use, useEffect, useRef, useState } from 'react'; @@ -26,6 +14,7 @@ import { Controls } from '../Control'; import { Lighting } from '../Lighting'; import { RotationInputs } from '../RotationInputs'; import { SceneProps } from '../SceneProps'; +import { Options } from './components/Options'; import { TankArmor } from './components/TankArmor'; import { TankModel } from './components/TankModel'; @@ -35,8 +24,6 @@ export function TankDisplay() { const canvasWrapper = useRef(null); const [isFullScreen, setIsFullScreen] = useState(false); const mode = useTankopedia((state) => state.mode); - const showGrid = useTankopedia((state) => state.model.showGrid); - const showSpacedArmor = useTankopedia((state) => state.model.showSpacedArmor); const protagonist = useTankopedia((state) => { if (!state.areTanksAssigned) return; return state.protagonist; @@ -143,68 +130,12 @@ export function TankDisplay() { } > - {mode === 'armor' && } + - - - - - - - - { - mutateTankopedia((draft) => { - draft.model.showGrid = checked; - }); - }} - > - Show grid - - - { - mutateTankopedia((draft) => { - draft.model.showSpacedArmor = checked; - }); - }} - > - Spaced armor - - - - - - + diff --git a/src/app/tools/tankopedia/[id]/components/Model/components/Options.tsx b/src/app/tools/tankopedia/[id]/components/Model/components/Options.tsx new file mode 100644 index 000000000..d8f760aa8 --- /dev/null +++ b/src/app/tools/tankopedia/[id]/components/Model/components/Options.tsx @@ -0,0 +1,76 @@ +import { + EnterFullScreenIcon, + ExitFullScreenIcon, + GearIcon, +} from '@radix-ui/react-icons'; +import { Button, DropdownMenu, Flex } from '@radix-ui/themes'; +import { RefObject } from 'react'; +import mutateTankopedia, { + useTankopedia, +} from '../../../../../../../stores/tankopedia'; + +interface OptionsProps { + isFullScreen: boolean; + canvas: RefObject; +} + +export function Options({ isFullScreen, canvas }: OptionsProps) { + const showGrid = useTankopedia((state) => state.model.showGrid); + const showSpacedArmor = useTankopedia((state) => state.model.showSpacedArmor); + + return ( + + + + + + + + { + mutateTankopedia((draft) => { + draft.model.showGrid = checked; + }); + }} + > + Show grid + + + { + mutateTankopedia((draft) => { + draft.model.showSpacedArmor = checked; + }); + }} + > + Spaced armor + + + + + + + ); +} 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 b7992d64a..6c90b15d5 100644 --- a/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor.tsx @@ -1,13 +1,10 @@ import { GroupProps, useLoader } from '@react-three/fiber'; -import { useEffect, useState } from 'react'; -import { Euler, Mesh, Vector3 } from 'three'; +import { useEffect, useRef, useState } from 'react'; +import { Euler, Group, Mesh, Vector3 } from 'three'; import { GLTFLoader } from 'three-stdlib'; import { ArmorMesh } from '../../../../../../../components/ArmorMesh'; import { HeadsUpDisplay } from '../../../../../../../components/HeadsUpDisplay'; import { X_AXIS } from '../../../../../../../constants/axis'; -import { canSplash } from '../../../../../../../core/blitz/canSplash'; -import { isExplosive } from '../../../../../../../core/blitz/isExplosive'; -import { numericPenetration } from '../../../../../../../core/blitz/numericPenetration'; import { asset } from '../../../../../../../core/blitzkrieg/asset'; import { ModelDefinitions, @@ -21,7 +18,7 @@ import { Lighting } from '../../Lighting'; interface TankArmorProps extends GroupProps {} export function TankArmor({ ...props }: TankArmorProps) { - const controlsEnabled = useTankopedia((state) => state.model.controlsEnabled); + const wrapper = useRef(null); const [awaitedModelDefinitions, setAwaitedModelDefinitions] = useState< ModelDefinitions | undefined >(undefined); @@ -29,10 +26,6 @@ export function TankArmor({ ...props }: TankArmorProps) { if (!state.areTanksAssigned) return; return state.protagonist; }); - const antagonist = useTankopedia((state) => { - if (!state.areTanksAssigned) return; - return state.antagonist; - }); const showSpacedArmor = useTankopedia((state) => state.model.showSpacedArmor); const model = useTankopedia((state) => state.model); @@ -42,9 +35,20 @@ export function TankArmor({ ...props }: TankArmorProps) { })(); }, []); + useEffect(() => { + const unsubscribe = useTankopedia.subscribe( + (state) => state.model.controlsEnabled, + (controlsEnabled) => { + if (wrapper.current) wrapper.current.visible = controlsEnabled; + }, + ); + + return unsubscribe; + }); + // it's ok to have hooks after this early termination because this will never be true // it's more for typescript to stop throwing a fit - if (!protagonist || !antagonist) return null; + if (!protagonist) return null; const armorGltf = useLoader( GLTFLoader, @@ -77,11 +81,6 @@ export function TankArmor({ ...props }: TankArmorProps) { .sub(turretOrigin) .applyAxisAngle(new Vector3(0, 0, 1), model.turretYaw); const turretRotation = new Euler(0, 0, model.turretYaw); - const splashCapable = canSplash(antagonist.shell.type); - const explosiveCapable = isExplosive(antagonist.shell.type); - const penetration = numericPenetration(antagonist.shell.penetration); - const ricochet = antagonist.shell.ricochet ?? Math.PI / 2; - const normalization = antagonist.shell.normalization ?? 0; if (tankModelDefinition.turretRotation) { const pitch = -tankModelDefinition.turretRotation.pitch * (Math.PI / 180); @@ -99,25 +98,14 @@ export function TankArmor({ ...props }: TankArmorProps) { turretPosition.add(turretOrigin); - // const tracksGeometry = mergeBufferGeometries( - // modelNodes - // .map((node) => { - // const isWheel = node.name.startsWith('chassis_wheel_'); - // const isTrack = node.name.startsWith('chassis_track_'); - // const isVisible = isWheel || isTrack; - // if (isVisible) return (node as Mesh).geometry; - // }) - // .filter(Boolean), - // ); - return ( {armorNodes.map((node) => { const isHull = node.name.startsWith('hull_'); @@ -139,31 +127,12 @@ export function TankArmor({ ...props }: TankArmorProps) { ); })} - {/* */} - {modelNodes.map((node) => { const isWheel = node.name.startsWith('chassis_wheel_'); const isTrack = node.name.startsWith('chassis_track_'); @@ -176,14 +145,8 @@ export function TankArmor({ ...props }: TankArmorProps) { ); @@ -215,14 +178,8 @@ export function TankArmor({ ...props }: TankArmorProps) { key={node.uuid} geometry={(node as Mesh).geometry} position={turretOrigin} - caliber={antagonist.shell.caliber} - canSplash={splashCapable} - isExplosive={explosiveCapable} isSpaced={spaced ?? false} - normalization={normalization} - penetration={penetration} thickness={thickness} - ricochet={ricochet} /> ); })} @@ -258,14 +215,8 @@ export function TankArmor({ ...props }: TankArmorProps) { key={node.uuid} geometry={(node as Mesh).geometry} position={turretOrigin.clone().add(gunOrigin)} - caliber={antagonist.shell.caliber} - canSplash={splashCapable} - isExplosive={explosiveCapable} isSpaced={spaced ?? false} - normalization={normalization} - penetration={penetration} thickness={thickness} - ricochet={ricochet} /> ); })} @@ -283,14 +234,8 @@ export function TankArmor({ ...props }: TankArmorProps) { ); diff --git a/src/components/ArmorMesh/index.tsx b/src/components/ArmorMesh/index.tsx index 6665bf895..d2cbcf875 100644 --- a/src/components/ArmorMesh/index.tsx +++ b/src/components/ArmorMesh/index.tsx @@ -1,33 +1,33 @@ import { MeshProps } from '@react-three/fiber'; import { MeshStandardMaterial } from 'three'; import CustomShaderMaterial from 'three-custom-shader-material'; +import { degToRad } from 'three/src/math/MathUtils'; +import { canSplash } from '../../core/blitz/canSplash'; +import { isExplosive } from '../../core/blitz/isExplosive'; +import { resolveNearPenetration } from '../../core/blitz/resolveNearPenetration'; +import { useTankopedia } from '../../stores/tankopedia'; import fragmentShader from './shaders/fragment.glsl'; import vertexShader from './shaders/vertex.glsl'; interface ArmorMeshProps extends MeshProps { - isExplosive: boolean; - canSplash: boolean; isSpaced: boolean; isExternalModule?: boolean; thickness: number; - penetration: number; - caliber: number; - ricochet: number; - normalization: number; } export function ArmorMesh({ - isExplosive, - canSplash, isSpaced, isExternalModule = false, thickness, - penetration, - caliber, - ricochet, - normalization, ...props }: ArmorMeshProps) { + const shell = useTankopedia((state) => { + if (!state.areTanksAssigned) return; + return state.antagonist.shell; + }); + + if (!shell) return null; + return ( <> {!isSpaced && ( @@ -45,15 +45,19 @@ export function ArmorMesh({ fragmentShader={fragmentShader} vertexShader={vertexShader} uniforms={{ - isExplosive: { value: isExplosive }, - canSplash: { value: canSplash }, + isExplosive: { value: isExplosive(shell.type) }, + canSplash: { value: canSplash(shell.type) }, isSpaced: { value: isSpaced }, isExternalModule: { value: isExternalModule }, thickness: { value: thickness }, - penetration: { value: penetration }, - caliber: { value: caliber }, - ricochet: { value: ricochet * (Math.PI / 180) }, - normalization: { value: normalization * (Math.PI / 180) }, + penetration: { + value: resolveNearPenetration(shell.penetration), + }, + caliber: { value: shell.caliber }, + ricochet: { value: degToRad(shell.ricochet ?? 90) }, + normalization: { + value: degToRad(shell.normalization ?? 0), + }, }} /> diff --git a/src/core/blitz/numericPenetration.ts b/src/core/blitz/resolveNearPenetration.ts similarity index 59% rename from src/core/blitz/numericPenetration.ts rename to src/core/blitz/resolveNearPenetration.ts index e182dbdb0..c6680dffe 100644 --- a/src/core/blitz/numericPenetration.ts +++ b/src/core/blitz/resolveNearPenetration.ts @@ -1,5 +1,5 @@ import { Vector2Tuple } from 'three'; -export function numericPenetration(penetration: number | Vector2Tuple) { +export function resolveNearPenetration(penetration: number | Vector2Tuple) { return typeof penetration === 'number' ? penetration : penetration[0]; } diff --git a/src/stores/tankopedia.ts b/src/stores/tankopedia.ts index 63441ffce..6c30ba19d 100644 --- a/src/stores/tankopedia.ts +++ b/src/stores/tankopedia.ts @@ -1,5 +1,6 @@ import { produce } from 'immer'; import { create } from 'zustand'; +import { subscribeWithSelector } from 'zustand/middleware'; import { TankType, TreeTypeString } from '../components/Tanks'; import { GunDefinition, @@ -54,29 +55,31 @@ type Tankopedia = { | { areTanksAssigned: false } ); -export const useTankopedia = create()(() => ({ - sort: { - by: 'tier', - direction: 'descending', - }, - filters: { - tiers: [], - types: [], - treeTypes: [], - nations: [], - test: 'include', - }, - model: { - gunPitch: 0, - hullYaw: 0, - turretYaw: 0, - controlsEnabled: true, - showGrid: true, - showSpacedArmor: true, - }, - areTanksAssigned: false, - mode: 'model', -})); +export const useTankopedia = create()( + subscribeWithSelector(() => ({ + sort: { + by: 'tier', + direction: 'descending', + }, + filters: { + tiers: [], + types: [], + treeTypes: [], + nations: [], + test: 'include', + }, + model: { + gunPitch: 0, + hullYaw: 0, + turretYaw: 0, + controlsEnabled: true, + showGrid: true, + showSpacedArmor: true, + }, + areTanksAssigned: false, + mode: 'model', + })), +); export default function mutateTankopedia(recipe: (draft: Tankopedia) => void) { useTankopedia.setState(produce(recipe));