mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 18:57:03 +02:00
feat: code split
This commit is contained in:
1 parent
2cf0bda352
commit
0a4e696bc1
15 files changed
+308
-121
No files matched your search
@@ -3,15 +3,16 @@ import { useThree } from '@react-three/fiber';
|
||||
import { RefObject, useEffect, useRef } from 'react';
|
||||
import { Box3, Object3D } from 'three';
|
||||
import { OrbitControls as OrbitControlsClass } from 'three-stdlib';
|
||||
import { useTankopedia } from '../../../../../stores/tankopedia';
|
||||
|
||||
interface ControlsProps {
|
||||
enabled: boolean;
|
||||
fit: RefObject<Object3D>;
|
||||
}
|
||||
|
||||
export function Controls({ enabled, fit }: ControlsProps) {
|
||||
export function Controls({ fit }: ControlsProps) {
|
||||
const camera = useThree((state) => state.camera);
|
||||
const orbitControls = useRef<OrbitControlsClass>(null);
|
||||
const enabled = useTankopedia((state) => state.model.controlsEnabled);
|
||||
|
||||
useEffect(() => {
|
||||
const box = new Box3().setFromObject(fit.current!);
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import { useLoader } from '@react-three/fiber';
|
||||
import { forwardRef, use, useImperativeHandle, useRef } from 'react';
|
||||
import { Group, Vector3 } from 'three';
|
||||
import { GLTFLoader } from 'three-stdlib';
|
||||
import { X_AXIS } from '../../../../../constants/axis';
|
||||
import { modelDefinitions } from '../../../../../core/blitzkrieg/modelDefinitions';
|
||||
import mutateTankopedia, {
|
||||
useTankopedia,
|
||||
} from '../../../../../stores/tankopedia';
|
||||
import { GunContainer } from './GunContainer';
|
||||
import { HullContainer } from './HullContainer';
|
||||
import { TurretContainer } from './TurretContainer';
|
||||
|
||||
interface ModelProps {
|
||||
tankId: number;
|
||||
gunId: number;
|
||||
turretId: number;
|
||||
}
|
||||
|
||||
export const Model = forwardRef<Group, ModelProps>(
|
||||
({ gunId, tankId, turretId }, ref) => {
|
||||
const awaitedModelDefinitions = use(modelDefinitions);
|
||||
const model = useTankopedia((state) => state.model);
|
||||
const gltf = useLoader(GLTFLoader, `/test/${tankId}.glb`);
|
||||
const gunContainer = useRef<Group>(null);
|
||||
const hullContainer = useRef<Group>(null);
|
||||
const turretContainer = useRef<Group>(null);
|
||||
|
||||
useImperativeHandle(ref, () => hullContainer.current!);
|
||||
|
||||
const tankModelDefinition = awaitedModelDefinitions[tankId];
|
||||
const turretModelDefinition = tankModelDefinition.turrets[turretId];
|
||||
const gunModelDefinition = turretModelDefinition.guns[gunId];
|
||||
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 (
|
||||
<HullContainer
|
||||
objects={gltf.scene.children[0].children}
|
||||
yaw={model.hullYaw}
|
||||
ref={hullContainer}
|
||||
onYawStart={() =>
|
||||
mutateTankopedia((state) => {
|
||||
state.model.controlsEnabled = false;
|
||||
})
|
||||
}
|
||||
onYawEnd={(yaw) => {
|
||||
mutateTankopedia((state) => {
|
||||
state.model.controlsEnabled = true;
|
||||
state.model.hullYaw = yaw;
|
||||
});
|
||||
}}
|
||||
onTrackStart={() => {
|
||||
mutateTankopedia((state) => {
|
||||
state.model.controlsEnabled = false;
|
||||
});
|
||||
}}
|
||||
onTrackEnd={() => {
|
||||
mutateTankopedia((state) => {
|
||||
state.model.controlsEnabled = true;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<TurretContainer
|
||||
initialTurretRotation={tankModelDefinition.turretRotation}
|
||||
gunOrigin={gunOrigin}
|
||||
ref={turretContainer}
|
||||
objects={gltf.scene.children[0].children}
|
||||
model={turretModelDefinition.model}
|
||||
onYawStart={() =>
|
||||
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}
|
||||
>
|
||||
<GunContainer
|
||||
ref={gunContainer}
|
||||
initialTurretRotation={tankModelDefinition.turretRotation}
|
||||
onPitchStart={() => {
|
||||
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}
|
||||
objects={gltf.scene.children[0].children}
|
||||
turretOrigin={turretOrigin}
|
||||
/>
|
||||
</TurretContainer>
|
||||
</HullContainer>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,150 @@
|
||||
import { Flex, TextField } from '@radix-ui/themes';
|
||||
import { clamp } from 'lodash';
|
||||
import { use, useEffect, useRef } from 'react';
|
||||
import { modelDefinitions } from '../../../../../core/blitzkrieg/modelDefinitions';
|
||||
import { normalizeAnglePI } from '../../../../../core/math/normalizeAngle180';
|
||||
import mutateTankopedia, {
|
||||
useTankopedia,
|
||||
} from '../../../../../stores/tankopedia';
|
||||
|
||||
interface RotationInputsProps {
|
||||
tankId: number;
|
||||
gunId: number;
|
||||
turretId: number;
|
||||
}
|
||||
|
||||
export function RotationInputs({
|
||||
tankId,
|
||||
gunId,
|
||||
turretId,
|
||||
}: RotationInputsProps) {
|
||||
const model = useTankopedia((state) => state.model);
|
||||
const awaitedModelDefinitions = use(modelDefinitions);
|
||||
const turretYawInput = useRef<HTMLInputElement>(null);
|
||||
const hullYawInput = useRef<HTMLInputElement>(null);
|
||||
const gunPitchInput = useRef<HTMLInputElement>(null);
|
||||
|
||||
const tankModelDefinition = awaitedModelDefinitions[tankId];
|
||||
const turretModelDefinition = tankModelDefinition.turrets[turretId];
|
||||
const gunModelDefinition = turretModelDefinition.guns[gunId];
|
||||
|
||||
useEffect(() => {
|
||||
hullYawInput.current!.value = `${-Math.round(
|
||||
model.hullYaw * (180 / Math.PI),
|
||||
)}`;
|
||||
}, [model.hullYaw]);
|
||||
useEffect(() => {
|
||||
turretYawInput.current!.value = `${-Math.round(
|
||||
model.turretYaw * (180 / Math.PI),
|
||||
)}`;
|
||||
}, [model.turretYaw]);
|
||||
useEffect(() => {
|
||||
gunPitchInput.current!.value = `${-Math.round(
|
||||
model.gunPitch * (180 / Math.PI),
|
||||
)}`;
|
||||
}, [model.gunPitch]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
gap="2"
|
||||
align="center"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 16,
|
||||
bottom: 16,
|
||||
}}
|
||||
>
|
||||
<TextField.Root
|
||||
variant="surface"
|
||||
style={{
|
||||
width: 115,
|
||||
}}
|
||||
>
|
||||
<TextField.Slot>Hull</TextField.Slot>
|
||||
<TextField.Input
|
||||
defaultValue={Math.round(model.hullYaw * (180 / Math.PI))}
|
||||
onBlur={() => {
|
||||
const normalized = normalizeAnglePI(
|
||||
-Number(hullYawInput.current?.value) * (Math.PI / 180),
|
||||
);
|
||||
mutateTankopedia((state) => {
|
||||
state.model.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
|
||||
variant="surface"
|
||||
style={{
|
||||
width: 115,
|
||||
}}
|
||||
>
|
||||
<TextField.Slot>Turret</TextField.Slot>
|
||||
<TextField.Input
|
||||
defaultValue={Math.round(model.turretYaw * (180 / Math.PI))}
|
||||
onBlur={() => {
|
||||
const normalized = normalizeAnglePI(
|
||||
-Number(turretYawInput.current?.value) * (Math.PI / 180),
|
||||
);
|
||||
mutateTankopedia((state) => {
|
||||
state.model.turretYaw = normalized;
|
||||
});
|
||||
turretYawInput.current!.value = `${Math.round(normalized)}`;
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
turretYawInput.current?.blur();
|
||||
}
|
||||
}}
|
||||
onFocus={() => turretYawInput.current?.focus()}
|
||||
ref={turretYawInput}
|
||||
style={{ textAlign: 'right' }}
|
||||
/>
|
||||
<TextField.Slot>°</TextField.Slot>
|
||||
</TextField.Root>
|
||||
|
||||
<TextField.Root
|
||||
variant="surface"
|
||||
style={{
|
||||
width: 115,
|
||||
}}
|
||||
>
|
||||
<TextField.Slot>Gun</TextField.Slot>
|
||||
<TextField.Input
|
||||
defaultValue={-Math.round(model.gunPitch * (180 / Math.PI))}
|
||||
onBlur={() => {
|
||||
const clamped = clamp(
|
||||
Number(gunPitchInput.current?.value),
|
||||
gunModelDefinition.pitch.min,
|
||||
gunModelDefinition.pitch.max,
|
||||
);
|
||||
mutateTankopedia((state) => {
|
||||
state.model.gunPitch = -clamped * (Math.PI / 180);
|
||||
});
|
||||
gunPitchInput.current!.value = `${Math.round(clamped)}`;
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
gunPitchInput.current?.blur();
|
||||
}
|
||||
}}
|
||||
onFocus={() => gunPitchInput.current?.focus()}
|
||||
ref={gunPitchInput}
|
||||
style={{ textAlign: 'right' }}
|
||||
/>
|
||||
<TextField.Slot>°</TextField.Slot>
|
||||
</TextField.Root>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -21,9 +21,9 @@ import {
|
||||
} from '@radix-ui/themes';
|
||||
import { Canvas, useLoader } from '@react-three/fiber';
|
||||
import { go } from 'fuzzysort';
|
||||
import { clamp, debounce } from 'lodash';
|
||||
import { debounce } from 'lodash';
|
||||
import Link from 'next/link';
|
||||
import { use, useEffect, useRef, useState } from 'react';
|
||||
import { use, useRef, useState } from 'react';
|
||||
import { Group, Vector3 } from 'three';
|
||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
|
||||
import { Flag } from '../../../../components/Flag';
|
||||
@@ -36,11 +36,11 @@ import {
|
||||
tankDefinitions,
|
||||
tankNamesDiacritics,
|
||||
} from '../../../../core/blitzkrieg/tankDefinitions';
|
||||
import { normalizeAnglePI } from '../../../../core/math/normalizeAngle180';
|
||||
import * as styles from '../page.css';
|
||||
import { Controls } from './components/Control';
|
||||
import { GunContainer } from './components/GunContainer';
|
||||
import { HullContainer } from './components/HullContainer';
|
||||
import { RotationInputs } from './components/RotationInputs';
|
||||
import { SceneProps } from './components/SceneProps';
|
||||
import { TurretContainer } from './components/TurretContainer';
|
||||
|
||||
@@ -85,23 +85,8 @@ export default function Page({ params }: { params: { id: string } }) {
|
||||
const [hullYaw, setHullYaw] = useState(0);
|
||||
const [gunPitch, setGunPitch] = useState(0);
|
||||
const [controlsEnabled, setControlsEnabled] = useState(true);
|
||||
const turretYawInput = useRef<HTMLInputElement>(null);
|
||||
const hullYawInput = useRef<HTMLInputElement>(null);
|
||||
const gunPitchInput = useRef<HTMLInputElement>(null);
|
||||
const gunContainer = useRef<Group>(null);
|
||||
|
||||
useEffect(() => {
|
||||
hullYawInput.current!.value = `${-Math.round(hullYaw * (180 / Math.PI))}`;
|
||||
}, [hullYaw]);
|
||||
useEffect(() => {
|
||||
turretYawInput.current!.value = `${-Math.round(
|
||||
turretYaw * (180 / Math.PI),
|
||||
)}`;
|
||||
}, [turretYaw]);
|
||||
useEffect(() => {
|
||||
gunPitchInput.current!.value = `${-Math.round(gunPitch * (180 / Math.PI))}`;
|
||||
}, [gunPitch]);
|
||||
|
||||
function handlePointerDown() {
|
||||
window.addEventListener('pointermove', handlePointerMove);
|
||||
window.addEventListener('pointerup', handlePointerUp);
|
||||
@@ -153,7 +138,7 @@ export default function Page({ params }: { params: { id: string } }) {
|
||||
camera={{ fov: 20 }}
|
||||
onPointerDown={handlePointerDown}
|
||||
>
|
||||
<Controls enabled={controlsEnabled} fit={hullContainer} />
|
||||
<Controls fit={hullContainer} />
|
||||
<SceneProps />
|
||||
|
||||
<HullContainer
|
||||
@@ -220,104 +205,11 @@ export default function Page({ params }: { params: { id: string } }) {
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
<Flex
|
||||
gap="2"
|
||||
align="center"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 16,
|
||||
bottom: 16,
|
||||
}}
|
||||
>
|
||||
<TextField.Root
|
||||
variant="surface"
|
||||
style={{
|
||||
width: 115,
|
||||
}}
|
||||
>
|
||||
<TextField.Slot>Hull</TextField.Slot>
|
||||
<TextField.Input
|
||||
defaultValue={Math.round(hullYaw * (180 / Math.PI))}
|
||||
onBlur={() => {
|
||||
const normalized = normalizeAnglePI(
|
||||
-Number(hullYawInput.current?.value) * (Math.PI / 180),
|
||||
);
|
||||
setHullYaw(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
|
||||
variant="surface"
|
||||
style={{
|
||||
width: 115,
|
||||
}}
|
||||
>
|
||||
<TextField.Slot>Turret</TextField.Slot>
|
||||
<TextField.Input
|
||||
defaultValue={Math.round(turretYaw * (180 / Math.PI))}
|
||||
onBlur={() => {
|
||||
const normalized = normalizeAnglePI(
|
||||
-Number(turretYawInput.current?.value) *
|
||||
(Math.PI / 180),
|
||||
);
|
||||
setTurretYaw(normalized);
|
||||
turretYawInput.current!.value = `${Math.round(
|
||||
normalized,
|
||||
)}`;
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
turretYawInput.current?.blur();
|
||||
}
|
||||
}}
|
||||
onFocus={() => turretYawInput.current?.focus()}
|
||||
ref={turretYawInput}
|
||||
style={{ textAlign: 'right' }}
|
||||
/>
|
||||
<TextField.Slot>°</TextField.Slot>
|
||||
</TextField.Root>
|
||||
|
||||
<TextField.Root
|
||||
variant="surface"
|
||||
style={{
|
||||
width: 115,
|
||||
}}
|
||||
>
|
||||
<TextField.Slot>Gun</TextField.Slot>
|
||||
<TextField.Input
|
||||
defaultValue={-Math.round(gunPitch * (180 / Math.PI))}
|
||||
onBlur={() => {
|
||||
const clamped = clamp(
|
||||
Number(gunPitchInput.current?.value),
|
||||
gunModelDefinition.pitch.min,
|
||||
gunModelDefinition.pitch.max,
|
||||
);
|
||||
setGunPitch(-clamped * (Math.PI / 180));
|
||||
gunPitchInput.current!.value = `${Math.round(clamped)}`;
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
gunPitchInput.current?.blur();
|
||||
}
|
||||
}}
|
||||
onFocus={() => gunPitchInput.current?.focus()}
|
||||
ref={gunPitchInput}
|
||||
style={{ textAlign: 'right' }}
|
||||
/>
|
||||
<TextField.Slot>°</TextField.Slot>
|
||||
</TextField.Root>
|
||||
</Flex>
|
||||
<RotationInputs
|
||||
gunId={gun.id}
|
||||
tankId={tank.id}
|
||||
turretId={turret.id}
|
||||
/>
|
||||
</Flex>
|
||||
</Card>
|
||||
|
||||
|
||||
Reference in new issue
Block a user