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 {
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<HTMLDivElement>(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() {
}
>
<TankModel />
{mode === 'armor' && <TankArmor />}
<TankArmor />
</Suspense>
</Canvas>
</div>
<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 {
canvasWrapper.current?.requestFullscreen();
}
}}
>
{isFullScreen ? (
<ExitFullScreenIcon />
) : (
<EnterFullScreenIcon />
)}
</Button>
</Flex>
<Options canvas={canvasWrapper} isFullScreen={isFullScreen} />
<RotationInputs />
</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 { 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<Group>(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 (
<HeadsUpDisplay>
<Lighting />
<group
{...props}
ref={wrapper}
rotation={[-Math.PI / 2, 0, model.hullYaw]}
visible={controlsEnabled}
>
{armorNodes.map((node) => {
const isHull = node.name.startsWith('hull_');
@@ -139,31 +127,12 @@ export function TankArmor({ ...props }: TankArmorProps) {
<ArmorMesh
key={node.uuid}
geometry={(node as Mesh).geometry}
caliber={antagonist.shell.caliber}
canSplash={splashCapable}
isExplosive={explosiveCapable}
isSpaced={spaced ?? false}
normalization={normalization}
penetration={penetration}
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) => {
const isWheel = node.name.startsWith('chassis_wheel_');
const isTrack = node.name.startsWith('chassis_track_');
@@ -176,14 +145,8 @@ export function TankArmor({ ...props }: TankArmorProps) {
<ArmorMesh
key={node.uuid}
geometry={(node as Mesh).geometry}
caliber={antagonist.shell.caliber}
canSplash={splashCapable}
isExplosive={explosiveCapable}
isSpaced
normalization={normalization}
penetration={penetration}
thickness={thickness}
ricochet={ricochet}
isExternalModule
/>
);
@@ -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) {
<ArmorMesh
key={node.uuid}
geometry={(node as Mesh).geometry}
caliber={antagonist.shell.caliber}
canSplash={splashCapable}
isExplosive={explosiveCapable}
isSpaced
normalization={normalization}
penetration={penetration}
thickness={thickness}
ricochet={ricochet}
isExternalModule
/>
);
+22 -18
View File
@@ -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),
},
}}
/>
</mesh>
@@ -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];
}
+26 -23
View File
@@ -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<Tankopedia>()(() => ({
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<Tankopedia>()(
subscribeWithSelector<Tankopedia>(() => ({
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));