mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 02:37:05 +02:00
feat: better plate editing integration
This commit is contained in:
1 parent
15cf4ec0f1
commit
2c0f9184db
6 files changed
+181
-88
No files matched your search
@@ -0,0 +1,96 @@
|
||||
import { Button, Dialog, Flex, Text, TextField } from '@radix-ui/themes';
|
||||
import { useRef } from 'react';
|
||||
import { ModelArmor } from '../../../../../core/blitzkit/modelDefinitions';
|
||||
import { resolveArmorIndex } from '../../../../../core/blitzkit/resolveArmorIndex';
|
||||
import * as Duel from '../../../../../stores/duel';
|
||||
import * as TankopediaEphemeral from '../../../../../stores/tankopediaEphemeral';
|
||||
|
||||
export function Prompts() {
|
||||
const input = useRef<HTMLInputElement>(null);
|
||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||
const editingPlate = TankopediaEphemeral.use((state) => state.editingPlate);
|
||||
const duelStore = Duel.useStore();
|
||||
|
||||
return (
|
||||
<Dialog.Root
|
||||
open={editingPlate !== undefined}
|
||||
onOpenChange={() => {
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
draft.editingPlate = undefined;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Dialog.Content style={{ width: 'fit-content' }}>
|
||||
<Flex align="center" gap="2" direction="column">
|
||||
<Text>Edit thickness</Text>
|
||||
|
||||
<TextField.Root
|
||||
mt="2"
|
||||
ref={input}
|
||||
type="number"
|
||||
onFocus={() => {
|
||||
input.current?.select();
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (!editingPlate) return;
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
input.current?.blur();
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
draft.editingPlate = undefined;
|
||||
draft.highlightArmor = undefined;
|
||||
const { protagonist } = duelStore.getState();
|
||||
let armor: ModelArmor;
|
||||
|
||||
if (editingPlate.name.startsWith('hull_')) {
|
||||
armor = draft.model.armor;
|
||||
} else if (editingPlate.name.startsWith('turret_')) {
|
||||
armor = draft.model.turrets[protagonist.turret.id].armor;
|
||||
} else if (editingPlate.name.startsWith('gun_')) {
|
||||
armor =
|
||||
draft.model.turrets[protagonist.turret.id].guns[
|
||||
protagonist.gun.id
|
||||
].armor;
|
||||
} else return;
|
||||
|
||||
const index = resolveArmorIndex(editingPlate.name);
|
||||
|
||||
if (index === undefined) return;
|
||||
|
||||
const input = event.currentTarget.valueAsNumber;
|
||||
|
||||
if (!isNaN(input)) {
|
||||
armor.thickness[index] = input;
|
||||
}
|
||||
});
|
||||
} else if (event.key === 'Escape') {
|
||||
input.current?.blur();
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
draft.highlightArmor = undefined;
|
||||
});
|
||||
}
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}}
|
||||
defaultValue={editingPlate?.default.toFixed(0)}
|
||||
>
|
||||
<TextField.Slot side="right">mm</TextField.Slot>
|
||||
</TextField.Root>
|
||||
|
||||
<Flex gap="2" width="100%">
|
||||
<Dialog.Close>
|
||||
<Button style={{ flex: 1 }} color="red" variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
</Dialog.Close>
|
||||
<Dialog.Close>
|
||||
<Button style={{ flex: 1 }}>Apply</Button>
|
||||
</Dialog.Close>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EyeClosedIcon, EyeOpenIcon } from '@radix-ui/react-icons';
|
||||
import { Box, Flex, Text } from '@radix-ui/themes';
|
||||
import { EyeClosedIcon, EyeOpenIcon, ReloadIcon } from '@radix-ui/react-icons';
|
||||
import { Box, Button, Checkbox, Flex, Text } from '@radix-ui/themes';
|
||||
import { ThicknessRange } from '../../../../../components/StaticArmor/components/StaticArmorScene';
|
||||
import * as TankopediaEphemeral from '../../../../../stores/tankopediaEphemeral';
|
||||
import * as TankopediaPersistent from '../../../../../stores/tankopediaPersistent';
|
||||
@@ -20,6 +20,8 @@ export function Thicknesses({ thicknessRange }: ThicknessesProps) {
|
||||
);
|
||||
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
|
||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||
const editStatic = TankopediaEphemeral.use((state) => state.editStatic);
|
||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@@ -136,6 +138,37 @@ export function Thicknesses({ thicknessRange }: ThicknessesProps) {
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Flex align="center" gap="2" mt="2">
|
||||
<Button
|
||||
color="red"
|
||||
size="1"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
draft.editStatic = false;
|
||||
draft.model = tankopediaEphemeralStore.getInitialState().model;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ReloadIcon />
|
||||
Reset
|
||||
</Button>
|
||||
|
||||
<Flex
|
||||
gap="2"
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
draft.editStatic = !draft.editStatic;
|
||||
draft.highlightArmor = undefined;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Text size="1">Edit</Text>
|
||||
<Checkbox size="2" checked={editStatic} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { HistorySection } from './components/HistorySection';
|
||||
import { CharacteristicsSection } from './components/Model/CharacteristicsSection';
|
||||
import { HeroSection } from './components/Model/HeroSection';
|
||||
import { TechTreeSection } from './components/Model/TechTreeSection';
|
||||
import { Prompts } from './components/Prompts';
|
||||
import { ShotDisplaySection } from './components/ShotDisplaySection';
|
||||
import { VideoSection } from './components/VideoSection';
|
||||
|
||||
@@ -74,6 +75,8 @@ export default function Page({ params }: { params: { id: string } }) {
|
||||
|
||||
return (
|
||||
<PageWrapper p="0" noMaxWidth color="purple" size={1600}>
|
||||
<Prompts />
|
||||
|
||||
<HeroSection id={id} />
|
||||
<ShotDisplaySection />
|
||||
{!exempt && <AdMidSectionResponsive />}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { Card, Code, Flex, Text, TextField } from '@radix-ui/themes';
|
||||
import { Card, Code, Flex, Text } from '@radix-ui/themes';
|
||||
import { Html } from '@react-three/drei';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useRef } from 'react';
|
||||
import { radToDeg } from 'three/src/math/MathUtils';
|
||||
import { ModelArmor } from '../../../core/blitzkit/modelDefinitions';
|
||||
import { resolveArmorIndex } from '../../../core/blitzkit/resolveArmorIndex';
|
||||
import * as App from '../../../stores/app';
|
||||
import * as Duel from '../../../stores/duel';
|
||||
import * as TankopediaEphemeral from '../../../stores/tankopediaEphemeral';
|
||||
@@ -19,10 +17,6 @@ export function ArmorPlateDisplay() {
|
||||
const devThicknessInput = useRef<HTMLInputElement>(null);
|
||||
const duelStore = Duel.useStore();
|
||||
|
||||
useEffect(() => {
|
||||
devThicknessInput.current?.focus();
|
||||
});
|
||||
|
||||
if (highlightArmor === undefined) return null;
|
||||
|
||||
return (
|
||||
@@ -52,60 +46,12 @@ export function ArmorPlateDisplay() {
|
||||
</Text>
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{developerMode && (
|
||||
<>
|
||||
<Text mt="2" color="gray" size="2">
|
||||
<b>DEV:</b> <Code>{highlightArmor.name}</Code>
|
||||
</Text>
|
||||
<TextField.Root
|
||||
type="number"
|
||||
style={{ width: 96 }}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
devThicknessInput.current?.blur();
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
draft.highlightArmor = undefined;
|
||||
const { protagonist } = duelStore.getState();
|
||||
let armor: ModelArmor;
|
||||
|
||||
if (highlightArmor.name.startsWith('hull_')) {
|
||||
armor = draft.model.armor;
|
||||
} else if (highlightArmor.name.startsWith('turret_')) {
|
||||
armor =
|
||||
draft.model.turrets[protagonist.turret.id].armor;
|
||||
} else if (highlightArmor.name.startsWith('gun_')) {
|
||||
armor =
|
||||
draft.model.turrets[protagonist.turret.id].guns[
|
||||
protagonist.gun.id
|
||||
].armor;
|
||||
} else return;
|
||||
|
||||
const index = resolveArmorIndex(highlightArmor.name);
|
||||
|
||||
if (index === undefined) return;
|
||||
|
||||
const input = event.currentTarget.valueAsNumber;
|
||||
|
||||
if (!isNaN(input)) {
|
||||
armor.thickness[index] = input;
|
||||
}
|
||||
});
|
||||
} else if (event.key === 'Escape') {
|
||||
devThicknessInput.current?.blur();
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
draft.highlightArmor = undefined;
|
||||
});
|
||||
}
|
||||
}}
|
||||
key={highlightArmor.name}
|
||||
defaultValue={highlightArmor.thickness.toFixed(0)}
|
||||
mt="1"
|
||||
autoFocus
|
||||
ref={devThicknessInput}
|
||||
size="1"
|
||||
>
|
||||
<TextField.Slot side="right">mm</TextField.Slot>
|
||||
</TextField.Root>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
@@ -104,7 +104,6 @@ export function SpacedArmorSceneComponent({
|
||||
|
||||
let color: Color;
|
||||
let opacity: number;
|
||||
let renderOrder = 1;
|
||||
let depthWrite = true;
|
||||
|
||||
switch (props.type) {
|
||||
@@ -121,7 +120,6 @@ export function SpacedArmorSceneComponent({
|
||||
case ArmorType.External:
|
||||
color = externalModuleColor;
|
||||
opacity = 1 / 8;
|
||||
renderOrder = 0;
|
||||
depthWrite = false;
|
||||
break;
|
||||
}
|
||||
@@ -142,7 +140,7 @@ export function SpacedArmorSceneComponent({
|
||||
const outlineMaterial = useMemo(
|
||||
() =>
|
||||
new LineBasicMaterial({
|
||||
color: color.clone().multiplyScalar(1 - 2 ** -3),
|
||||
color: color.clone().multiplyScalar(2 ** 2),
|
||||
}),
|
||||
[thickness],
|
||||
);
|
||||
@@ -210,37 +208,51 @@ export function SpacedArmorSceneComponent({
|
||||
onClick(event) {
|
||||
event.stopPropagation();
|
||||
|
||||
const bounds = new Box3().setFromObject(event.object);
|
||||
const point = bounds.min
|
||||
.clone()
|
||||
.add(bounds.max)
|
||||
.divideScalar(2)
|
||||
.setY(bounds.max.y);
|
||||
const cameraNormal = camera.position.clone().sub(point).normalize();
|
||||
const surfaceNormal = event
|
||||
.normal!.clone()
|
||||
.applyQuaternion(
|
||||
event.object.getWorldQuaternion(new Quaternion()),
|
||||
);
|
||||
const angle = surfaceNormal.angleTo(cameraNormal);
|
||||
const thicknessAngled = thickness / Math.sin(Math.PI / 2 - angle);
|
||||
const { editStatic } = tankopediaEphemeralStore.getState();
|
||||
|
||||
mutateTankopediaEphemeralStore((draft) => {
|
||||
draft.highlightArmor = {
|
||||
type: props.type,
|
||||
name: props.name,
|
||||
point,
|
||||
thickness,
|
||||
thicknessAngled,
|
||||
angle,
|
||||
color: `#${color.getHexString()}`,
|
||||
};
|
||||
});
|
||||
if (editStatic) {
|
||||
mutateTankopediaEphemeralStore((draft) => {
|
||||
draft.editingPlate = {
|
||||
name: props.name,
|
||||
default: thickness,
|
||||
};
|
||||
});
|
||||
} else {
|
||||
const bounds = new Box3().setFromObject(event.object);
|
||||
const point = bounds.min
|
||||
.clone()
|
||||
.add(bounds.max)
|
||||
.divideScalar(2)
|
||||
.setY(bounds.max.y);
|
||||
const cameraNormal = camera.position
|
||||
.clone()
|
||||
.sub(point)
|
||||
.normalize();
|
||||
const surfaceNormal = event
|
||||
.normal!.clone()
|
||||
.applyQuaternion(
|
||||
event.object.getWorldQuaternion(new Quaternion()),
|
||||
);
|
||||
const angle = surfaceNormal.angleTo(cameraNormal);
|
||||
const thicknessAngled = thickness / Math.sin(Math.PI / 2 - angle);
|
||||
|
||||
mutateTankopediaEphemeralStore((draft) => {
|
||||
draft.highlightArmor = {
|
||||
type: props.type,
|
||||
name: props.name,
|
||||
point,
|
||||
thickness,
|
||||
thicknessAngled,
|
||||
angle,
|
||||
color: `#${color.getHexString()}`,
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
})}
|
||||
|
||||
<lineSegments material={outlineMaterial}>
|
||||
<edgesGeometry args={[(node as Mesh).geometry]} />
|
||||
<edgesGeometry args={[(node as Mesh).geometry, 45]} />
|
||||
</lineSegments>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -13,6 +13,8 @@ interface TankopediaEphemeral {
|
||||
skills: Record<string, number>;
|
||||
controlsEnabled: boolean;
|
||||
model: ModelDefinition;
|
||||
editStatic: boolean;
|
||||
editingPlate?: { name: string; default: number };
|
||||
highlightArmor?: {
|
||||
name: string;
|
||||
point: Vector3;
|
||||
@@ -91,6 +93,7 @@ export const { Provider, use, useMutation, useStore } = createNextSafeStore(
|
||||
(model: ModelDefinition) =>
|
||||
create<TankopediaEphemeral>()(
|
||||
subscribeWithSelector<TankopediaEphemeral>(() => ({
|
||||
editStatic: false,
|
||||
skills: {},
|
||||
model,
|
||||
controlsEnabled: true,
|
||||
|
||||
Reference in new issue
Block a user