feat: optimized state handling

This commit is contained in:
TrèsAbhi committed 2024-01-13 13:44:27 -06:00
1 parent 033d361cea
commit 230b2df5fd
6 files changed
+145 -186

No files matched your search

@@ -1,16 +1,4 @@
import { import { Card, Flex, Tabs, Theme } from '@radix-ui/themes';
EnterFullScreenIcon,
ExitFullScreenIcon,
GearIcon,
} from '@radix-ui/react-icons';
import {
Button,
Card,
DropdownMenu,
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 { Suspense, use, useEffect, useRef, useState } from 'react'; import { Suspense, use, useEffect, useRef, useState } from 'react';
@@ -26,6 +14,7 @@ import { Controls } from '../Control';
import { Lighting } from '../Lighting'; import { Lighting } from '../Lighting';
import { RotationInputs } from '../RotationInputs'; import { RotationInputs } from '../RotationInputs';
import { SceneProps } from '../SceneProps'; import { SceneProps } from '../SceneProps';
import { Options } from './components/Options';
import { TankArmor } from './components/TankArmor'; import { TankArmor } from './components/TankArmor';
import { TankModel } from './components/TankModel'; import { TankModel } from './components/TankModel';
@@ -35,8 +24,6 @@ export function TankDisplay() {
const canvasWrapper = useRef<HTMLDivElement>(null); const canvasWrapper = useRef<HTMLDivElement>(null);
const [isFullScreen, setIsFullScreen] = useState(false); const [isFullScreen, setIsFullScreen] = useState(false);
const mode = useTankopedia((state) => state.mode); const mode = useTankopedia((state) => state.mode);
const showGrid = useTankopedia((state) => state.model.showGrid);
const showSpacedArmor = useTankopedia((state) => state.model.showSpacedArmor);
const protagonist = useTankopedia((state) => { const protagonist = useTankopedia((state) => {
if (!state.areTanksAssigned) return; if (!state.areTanksAssigned) return;
return state.protagonist; return state.protagonist;
@@ -143,68 +130,12 @@ export function TankDisplay() {
} }
> >
<TankModel /> <TankModel />
{mode === 'armor' && <TankArmor />} <TankArmor />
</Suspense> </Suspense>
</Canvas> </Canvas>
</div> </div>
<Flex <Options canvas={canvasWrapper} isFullScreen={isFullScreen} />
gap="4"
style={{
position: 'absolute',
top: 18,
right: 18,
}}
>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Button color={showGrid ? 'purple' : 'gray'} variant="ghost">
<GearIcon />
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.CheckboxItem
checked={showGrid}
onCheckedChange={(checked) => {
mutateTankopedia((draft) => {
draft.model.showGrid = checked;
});
}}
>
Show grid
</DropdownMenu.CheckboxItem>
<DropdownMenu.CheckboxItem
checked={showSpacedArmor}
onCheckedChange={(checked) => {
mutateTankopedia((draft) => {
draft.model.showSpacedArmor = checked;
});
}}
>
Spaced armor
</DropdownMenu.CheckboxItem>
</DropdownMenu.Content>
</DropdownMenu.Root>
<Button
variant="ghost"
onClick={() => {
if (isFullScreen) {
document.exitFullscreen();
} else {
canvasWrapper.current?.requestFullscreen();
}
}}
>
{isFullScreen ? (
<ExitFullScreenIcon />
) : (
<EnterFullScreenIcon />
)}
</Button>
</Flex>
<RotationInputs /> <RotationInputs />
</Flex> </Flex>
@@ -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<HTMLElement>;
}
export function Options({ isFullScreen, canvas }: OptionsProps) {
const showGrid = useTankopedia((state) => state.model.showGrid);
const showSpacedArmor = useTankopedia((state) => state.model.showSpacedArmor);
return (
<Flex
gap="4"
style={{
position: 'absolute',
top: 18,
right: 18,
}}
>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Button color={showGrid ? 'purple' : 'gray'} variant="ghost">
<GearIcon />
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.CheckboxItem
checked={showGrid}
onCheckedChange={(checked) => {
mutateTankopedia((draft) => {
draft.model.showGrid = checked;
});
}}
>
Show grid
</DropdownMenu.CheckboxItem>
<DropdownMenu.CheckboxItem
checked={showSpacedArmor}
onCheckedChange={(checked) => {
mutateTankopedia((draft) => {
draft.model.showSpacedArmor = checked;
});
}}
>
Spaced armor
</DropdownMenu.CheckboxItem>
</DropdownMenu.Content>
</DropdownMenu.Root>
<Button
variant="ghost"
onClick={() => {
if (isFullScreen) {
document.exitFullscreen();
} else {
canvas.current?.requestFullscreen();
}
}}
>
{isFullScreen ? <ExitFullScreenIcon /> : <EnterFullScreenIcon />}
</Button>
</Flex>
);
}
@@ -1,13 +1,10 @@
import { GroupProps, useLoader } from '@react-three/fiber'; import { GroupProps, useLoader } from '@react-three/fiber';
import { useEffect, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { Euler, Mesh, Vector3 } from 'three'; import { Euler, Group, Mesh, Vector3 } from 'three';
import { GLTFLoader } from 'three-stdlib'; import { GLTFLoader } from 'three-stdlib';
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 } 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 { asset } from '../../../../../../../core/blitzkrieg/asset';
import { import {
ModelDefinitions, ModelDefinitions,
@@ -21,7 +18,7 @@ import { Lighting } from '../../Lighting';
interface TankArmorProps extends GroupProps {} interface TankArmorProps extends GroupProps {}
export function TankArmor({ ...props }: TankArmorProps) { export function TankArmor({ ...props }: TankArmorProps) {
const controlsEnabled = useTankopedia((state) => state.model.controlsEnabled); const wrapper = useRef<Group>(null);
const [awaitedModelDefinitions, setAwaitedModelDefinitions] = useState< const [awaitedModelDefinitions, setAwaitedModelDefinitions] = useState<
ModelDefinitions | undefined ModelDefinitions | undefined
>(undefined); >(undefined);
@@ -29,10 +26,6 @@ export function TankArmor({ ...props }: TankArmorProps) {
if (!state.areTanksAssigned) return; if (!state.areTanksAssigned) return;
return state.protagonist; return state.protagonist;
}); });
const antagonist = useTankopedia((state) => {
if (!state.areTanksAssigned) return;
return state.antagonist;
});
const showSpacedArmor = useTankopedia((state) => state.model.showSpacedArmor); const showSpacedArmor = useTankopedia((state) => state.model.showSpacedArmor);
const model = useTankopedia((state) => state.model); 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 ok to have hooks after this early termination because this will never be true
// it's more for typescript to stop throwing a fit // it's more for typescript to stop throwing a fit
if (!protagonist || !antagonist) return null; if (!protagonist) return null;
const armorGltf = useLoader( const armorGltf = useLoader(
GLTFLoader, GLTFLoader,
@@ -77,11 +81,6 @@ export function TankArmor({ ...props }: TankArmorProps) {
.sub(turretOrigin) .sub(turretOrigin)
.applyAxisAngle(new Vector3(0, 0, 1), model.turretYaw); .applyAxisAngle(new Vector3(0, 0, 1), model.turretYaw);
const turretRotation = new Euler(0, 0, 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) { if (tankModelDefinition.turretRotation) {
const pitch = -tankModelDefinition.turretRotation.pitch * (Math.PI / 180); const pitch = -tankModelDefinition.turretRotation.pitch * (Math.PI / 180);
@@ -99,25 +98,14 @@ export function TankArmor({ ...props }: TankArmorProps) {
turretPosition.add(turretOrigin); 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 ( return (
<HeadsUpDisplay> <HeadsUpDisplay>
<Lighting /> <Lighting />
<group <group
{...props} {...props}
ref={wrapper}
rotation={[-Math.PI / 2, 0, model.hullYaw]} rotation={[-Math.PI / 2, 0, model.hullYaw]}
visible={controlsEnabled}
> >
{armorNodes.map((node) => { {armorNodes.map((node) => {
const isHull = node.name.startsWith('hull_'); const isHull = node.name.startsWith('hull_');
@@ -139,31 +127,12 @@ export function TankArmor({ ...props }: TankArmorProps) {
<ArmorMesh <ArmorMesh
key={node.uuid} key={node.uuid}
geometry={(node as Mesh).geometry} geometry={(node as Mesh).geometry}
caliber={antagonist.shell.caliber}
canSplash={splashCapable}
isExplosive={explosiveCapable}
isSpaced={spaced ?? false} isSpaced={spaced ?? false}
normalization={normalization}
penetration={penetration}
thickness={thickness} thickness={thickness}
ricochet={ricochet}
/> />
); );
})} })}
{/* <ArmorMesh
geometry={tracksGeometry!}
caliber={antagonist.shell.caliber}
canSplash={splashCapable}
isExplosive={explosiveCapable}
isSpaced
normalization={normalization}
penetration={penetration}
thickness={50}
ricochet={ricochet}
isExternalModule
/> */}
{modelNodes.map((node) => { {modelNodes.map((node) => {
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_');
@@ -176,14 +145,8 @@ export function TankArmor({ ...props }: TankArmorProps) {
<ArmorMesh <ArmorMesh
key={node.uuid} key={node.uuid}
geometry={(node as Mesh).geometry} geometry={(node as Mesh).geometry}
caliber={antagonist.shell.caliber}
canSplash={splashCapable}
isExplosive={explosiveCapable}
isSpaced isSpaced
normalization={normalization}
penetration={penetration}
thickness={thickness} thickness={thickness}
ricochet={ricochet}
isExternalModule isExternalModule
/> />
); );
@@ -215,14 +178,8 @@ export function TankArmor({ ...props }: TankArmorProps) {
key={node.uuid} key={node.uuid}
geometry={(node as Mesh).geometry} geometry={(node as Mesh).geometry}
position={turretOrigin} position={turretOrigin}
caliber={antagonist.shell.caliber}
canSplash={splashCapable}
isExplosive={explosiveCapable}
isSpaced={spaced ?? false} isSpaced={spaced ?? false}
normalization={normalization}
penetration={penetration}
thickness={thickness} thickness={thickness}
ricochet={ricochet}
/> />
); );
})} })}
@@ -258,14 +215,8 @@ export function TankArmor({ ...props }: TankArmorProps) {
key={node.uuid} key={node.uuid}
geometry={(node as Mesh).geometry} geometry={(node as Mesh).geometry}
position={turretOrigin.clone().add(gunOrigin)} position={turretOrigin.clone().add(gunOrigin)}
caliber={antagonist.shell.caliber}
canSplash={splashCapable}
isExplosive={explosiveCapable}
isSpaced={spaced ?? false} isSpaced={spaced ?? false}
normalization={normalization}
penetration={penetration}
thickness={thickness} thickness={thickness}
ricochet={ricochet}
/> />
); );
})} })}
@@ -283,14 +234,8 @@ export function TankArmor({ ...props }: TankArmorProps) {
<ArmorMesh <ArmorMesh
key={node.uuid} key={node.uuid}
geometry={(node as Mesh).geometry} geometry={(node as Mesh).geometry}
caliber={antagonist.shell.caliber}
canSplash={splashCapable}
isExplosive={explosiveCapable}
isSpaced isSpaced
normalization={normalization}
penetration={penetration}
thickness={thickness} thickness={thickness}
ricochet={ricochet}
isExternalModule isExternalModule
/> />
); );
+22 -18
View File
@@ -1,33 +1,33 @@
import { MeshProps } from '@react-three/fiber'; import { MeshProps } from '@react-three/fiber';
import { MeshStandardMaterial } from 'three'; import { MeshStandardMaterial } from 'three';
import CustomShaderMaterial from 'three-custom-shader-material'; 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 fragmentShader from './shaders/fragment.glsl';
import vertexShader from './shaders/vertex.glsl'; import vertexShader from './shaders/vertex.glsl';
interface ArmorMeshProps extends MeshProps { interface ArmorMeshProps extends MeshProps {
isExplosive: boolean;
canSplash: boolean;
isSpaced: boolean; isSpaced: boolean;
isExternalModule?: boolean; isExternalModule?: boolean;
thickness: number; thickness: number;
penetration: number;
caliber: number;
ricochet: number;
normalization: number;
} }
export function ArmorMesh({ export function ArmorMesh({
isExplosive,
canSplash,
isSpaced, isSpaced,
isExternalModule = false, isExternalModule = false,
thickness, thickness,
penetration,
caliber,
ricochet,
normalization,
...props ...props
}: ArmorMeshProps) { }: ArmorMeshProps) {
const shell = useTankopedia((state) => {
if (!state.areTanksAssigned) return;
return state.antagonist.shell;
});
if (!shell) return null;
return ( return (
<> <>
{!isSpaced && ( {!isSpaced && (
@@ -45,15 +45,19 @@ export function ArmorMesh({
fragmentShader={fragmentShader} fragmentShader={fragmentShader}
vertexShader={vertexShader} vertexShader={vertexShader}
uniforms={{ uniforms={{
isExplosive: { value: isExplosive }, isExplosive: { value: isExplosive(shell.type) },
canSplash: { value: canSplash }, canSplash: { value: canSplash(shell.type) },
isSpaced: { value: isSpaced }, isSpaced: { value: isSpaced },
isExternalModule: { value: isExternalModule }, isExternalModule: { value: isExternalModule },
thickness: { value: thickness }, thickness: { value: thickness },
penetration: { value: penetration }, penetration: {
caliber: { value: caliber }, value: resolveNearPenetration(shell.penetration),
ricochet: { value: ricochet * (Math.PI / 180) }, },
normalization: { value: normalization * (Math.PI / 180) }, caliber: { value: shell.caliber },
ricochet: { value: degToRad(shell.ricochet ?? 90) },
normalization: {
value: degToRad(shell.normalization ?? 0),
},
}} }}
/> />
</mesh> </mesh>
@@ -1,5 +1,5 @@
import { Vector2Tuple } from 'three'; import { Vector2Tuple } from 'three';
export function numericPenetration(penetration: number | Vector2Tuple) { export function resolveNearPenetration(penetration: number | Vector2Tuple) {
return typeof penetration === 'number' ? penetration : penetration[0]; return typeof penetration === 'number' ? penetration : penetration[0];
} }
+26 -23
View File
@@ -1,5 +1,6 @@
import { produce } from 'immer'; import { produce } from 'immer';
import { create } from 'zustand'; import { create } from 'zustand';
import { subscribeWithSelector } from 'zustand/middleware';
import { TankType, TreeTypeString } from '../components/Tanks'; import { TankType, TreeTypeString } from '../components/Tanks';
import { import {
GunDefinition, GunDefinition,
@@ -54,29 +55,31 @@ type Tankopedia = {
| { areTanksAssigned: false } | { areTanksAssigned: false }
); );
export const useTankopedia = create<Tankopedia>()(() => ({ export const useTankopedia = create<Tankopedia>()(
sort: { subscribeWithSelector<Tankopedia>(() => ({
by: 'tier', sort: {
direction: 'descending', by: 'tier',
}, direction: 'descending',
filters: { },
tiers: [], filters: {
types: [], tiers: [],
treeTypes: [], types: [],
nations: [], treeTypes: [],
test: 'include', nations: [],
}, test: 'include',
model: { },
gunPitch: 0, model: {
hullYaw: 0, gunPitch: 0,
turretYaw: 0, hullYaw: 0,
controlsEnabled: true, turretYaw: 0,
showGrid: true, controlsEnabled: true,
showSpacedArmor: true, showGrid: true,
}, showSpacedArmor: true,
areTanksAssigned: false, },
mode: 'model', areTanksAssigned: false,
})); mode: 'model',
})),
);
export default function mutateTankopedia(recipe: (draft: Tankopedia) => void) { export default function mutateTankopedia(recipe: (draft: Tankopedia) => void) {
useTankopedia.setState(produce(recipe)); useTankopedia.setState(produce(recipe));