mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-29 21:49:50 +01:00
feat: floating editor
This commit is contained in:
1 parent
0dc708ab2a
commit
71e0e8e09e
5 files changed
+95
-137
No files matched your search
@@ -1,96 +0,0 @@
|
|||||||
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) || input < 0) return;
|
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,6 @@ import { HistorySection } from './components/HistorySection';
|
|||||||
import { CharacteristicsSection } from './components/Model/CharacteristicsSection';
|
import { CharacteristicsSection } from './components/Model/CharacteristicsSection';
|
||||||
import { HeroSection } from './components/Model/HeroSection';
|
import { HeroSection } from './components/Model/HeroSection';
|
||||||
import { TechTreeSection } from './components/Model/TechTreeSection';
|
import { TechTreeSection } from './components/Model/TechTreeSection';
|
||||||
import { Prompts } from './components/Prompts';
|
|
||||||
import { ShotDisplaySection } from './components/ShotDisplaySection';
|
import { ShotDisplaySection } from './components/ShotDisplaySection';
|
||||||
import { VideoSection } from './components/VideoSection';
|
import { VideoSection } from './components/VideoSection';
|
||||||
|
|
||||||
@@ -75,8 +74,6 @@ export default function Page({ params }: { params: { id: string } }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper p="0" noMaxWidth color="purple" size={1600}>
|
<PageWrapper p="0" noMaxWidth color="purple" size={1600}>
|
||||||
<Prompts />
|
|
||||||
|
|
||||||
<HeroSection id={id} />
|
<HeroSection id={id} />
|
||||||
<ShotDisplaySection />
|
<ShotDisplaySection />
|
||||||
{!exempt && <AdMidSectionResponsive />}
|
{!exempt && <AdMidSectionResponsive />}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Card, Code, Flex, Text } from '@radix-ui/themes';
|
import { Card, Code, Flex, Text, TextField } from '@radix-ui/themes';
|
||||||
import { Html } from '@react-three/drei';
|
import { Html } from '@react-three/drei';
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { radToDeg } from 'three/src/math/MathUtils';
|
import { radToDeg } from 'three/src/math/MathUtils';
|
||||||
|
import { resolveArmorIndex } from '../../../core/blitzkit/resolveArmorIndex';
|
||||||
import * as App from '../../../stores/app';
|
import * as App from '../../../stores/app';
|
||||||
import * as Duel from '../../../stores/duel';
|
import * as Duel from '../../../stores/duel';
|
||||||
import * as TankopediaEphemeral from '../../../stores/tankopediaEphemeral';
|
import * as TankopediaEphemeral from '../../../stores/tankopediaEphemeral';
|
||||||
@@ -16,12 +17,16 @@ export function ArmorPlateDisplay() {
|
|||||||
const developerMode = App.use((state) => state.developerMode);
|
const developerMode = App.use((state) => state.developerMode);
|
||||||
const devThicknessInput = useRef<HTMLInputElement>(null);
|
const devThicknessInput = useRef<HTMLInputElement>(null);
|
||||||
const duelStore = Duel.useStore();
|
const duelStore = Duel.useStore();
|
||||||
|
const input = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
if (highlightArmor === undefined) return null;
|
if (highlightArmor === undefined) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group position={highlightArmor.point}>
|
<group position={highlightArmor.point}>
|
||||||
<Html center>
|
<Html
|
||||||
|
center
|
||||||
|
style={{ pointerEvents: highlightArmor.editingPlate ? 'auto' : 'none' }}
|
||||||
|
>
|
||||||
<Card
|
<Card
|
||||||
mb="4"
|
mb="4"
|
||||||
style={{
|
style={{
|
||||||
@@ -38,6 +43,7 @@ export function ArmorPlateDisplay() {
|
|||||||
mm
|
mm
|
||||||
</Text>
|
</Text>
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{highlightArmor.type !== ArmorType.External && (
|
{highlightArmor.type !== ArmorType.External && (
|
||||||
<Text size="2" color="gray">
|
<Text size="2" color="gray">
|
||||||
{highlightArmor.thicknessAngled.toFixed(0)}
|
{highlightArmor.thicknessAngled.toFixed(0)}
|
||||||
@@ -54,6 +60,67 @@ export function ArmorPlateDisplay() {
|
|||||||
</Text>
|
</Text>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{highlightArmor.editingPlate && (
|
||||||
|
<TextField.Root
|
||||||
|
mt="2"
|
||||||
|
ref={input}
|
||||||
|
type="number"
|
||||||
|
autoFocus
|
||||||
|
onFocus={(event) => event.target.select()}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (!highlightArmor.editingPlate) return;
|
||||||
|
|
||||||
|
const index = resolveArmorIndex(highlightArmor.name);
|
||||||
|
|
||||||
|
if (index === undefined) return;
|
||||||
|
|
||||||
|
const thickness = event.currentTarget.valueAsNumber;
|
||||||
|
|
||||||
|
if (isNaN(thickness) || thickness < 0) return;
|
||||||
|
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
input.current?.blur();
|
||||||
|
|
||||||
|
mutateTankopediaEphemeral((draft) => {
|
||||||
|
draft.highlightArmor = undefined;
|
||||||
|
const { protagonist } = duelStore.getState();
|
||||||
|
const tank = draft.model;
|
||||||
|
const turret = tank.turrets[protagonist.turret.id];
|
||||||
|
const gun = turret.guns[protagonist.gun.id];
|
||||||
|
const track = tank.tracks[protagonist.track.id];
|
||||||
|
|
||||||
|
console.log(highlightArmor.name);
|
||||||
|
if (highlightArmor.name.startsWith('hull_')) {
|
||||||
|
tank.armor.thickness[index] = thickness;
|
||||||
|
} else if (highlightArmor.name.startsWith('turret_')) {
|
||||||
|
turret.armor.thickness[index] = thickness;
|
||||||
|
} else if (highlightArmor.name.startsWith('gun_')) {
|
||||||
|
if (highlightArmor.name.includes('_armor_')) {
|
||||||
|
gun.armor.thickness[index] = thickness;
|
||||||
|
} else {
|
||||||
|
gun.thickness = thickness;
|
||||||
|
}
|
||||||
|
} else if (highlightArmor.name.startsWith('chassis_')) {
|
||||||
|
track.thickness = thickness;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (event.key === 'Escape') {
|
||||||
|
input.current?.blur();
|
||||||
|
mutateTankopediaEphemeral((draft) => {
|
||||||
|
draft.highlightArmor = undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
}}
|
||||||
|
defaultValue={highlightArmor.thickness.toFixed(0)}
|
||||||
|
>
|
||||||
|
<TextField.Slot side="right">mm</TextField.Slot>
|
||||||
|
</TextField.Root>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Card>
|
</Card>
|
||||||
</Html>
|
</Html>
|
||||||
|
|||||||
@@ -173,41 +173,31 @@ export function StaticArmorSceneComponent({
|
|||||||
|
|
||||||
const { editStatic } = tankopediaEphemeralStore.getState();
|
const { editStatic } = tankopediaEphemeralStore.getState();
|
||||||
|
|
||||||
if (editStatic) {
|
const bounds = new Box3().setFromObject(event.object);
|
||||||
mutateTankopediaEphemeralStore((draft) => {
|
const point = bounds.min
|
||||||
draft.editingPlate = {
|
.clone()
|
||||||
name: name,
|
.add(bounds.max)
|
||||||
default: thickness,
|
.divideScalar(2)
|
||||||
};
|
.setY(bounds.max.y);
|
||||||
});
|
const cameraNormal = camera.position.clone().sub(point).normalize();
|
||||||
} else {
|
const surfaceNormal = event
|
||||||
const bounds = new Box3().setFromObject(event.object);
|
.normal!.clone()
|
||||||
const point = bounds.min
|
.applyQuaternion(event.object.getWorldQuaternion(new Quaternion()));
|
||||||
.clone()
|
const angle = surfaceNormal.angleTo(cameraNormal);
|
||||||
.add(bounds.max)
|
const thicknessAngled = thickness / Math.sin(Math.PI / 2 - angle);
|
||||||
.divideScalar(2)
|
console.log(name);
|
||||||
.setY(bounds.max.y);
|
mutateTankopediaEphemeralStore((draft) => {
|
||||||
const cameraNormal = camera.position.clone().sub(point).normalize();
|
draft.highlightArmor = {
|
||||||
const surfaceNormal = event
|
type: props.type,
|
||||||
.normal!.clone()
|
name,
|
||||||
.applyQuaternion(
|
point,
|
||||||
event.object.getWorldQuaternion(new Quaternion()),
|
thickness,
|
||||||
);
|
thicknessAngled,
|
||||||
const angle = surfaceNormal.angleTo(cameraNormal);
|
angle,
|
||||||
const thicknessAngled = thickness / Math.sin(Math.PI / 2 - angle);
|
color: `#${color.getHexString()}`,
|
||||||
|
editingPlate: editStatic,
|
||||||
mutateTankopediaEphemeralStore((draft) => {
|
};
|
||||||
draft.highlightArmor = {
|
});
|
||||||
type: props.type,
|
|
||||||
name,
|
|
||||||
point,
|
|
||||||
thickness,
|
|
||||||
thicknessAngled,
|
|
||||||
angle,
|
|
||||||
color: `#${color.getHexString()}`,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ interface TankopediaEphemeral {
|
|||||||
controlsEnabled: boolean;
|
controlsEnabled: boolean;
|
||||||
model: ModelDefinition;
|
model: ModelDefinition;
|
||||||
editStatic: boolean;
|
editStatic: boolean;
|
||||||
editingPlate?: { name: string; default: number };
|
|
||||||
highlightArmor?: {
|
highlightArmor?: {
|
||||||
|
editingPlate: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
point: Vector3;
|
point: Vector3;
|
||||||
thickness: number;
|
thickness: number;
|
||||||
|
|||||||
Reference in new issue
Block a user