feat(editor): edit hierarchical map nodes

This commit is contained in:
Tom Boullay
2026-05-27 08:30:54 +02:00
parent ab100c683f
commit c2b16434fb
16 changed files with 740 additions and 64 deletions
+94 -5
View File
@@ -8,9 +8,11 @@ import {
Lock,
MousePointer2,
Move3D,
Plus,
Redo2,
RotateCw,
Save,
Trash2,
Undo2,
Unlock,
X,
@@ -19,18 +21,27 @@ import { EditorCinematicManifestPanel } from "@/components/editor/EditorCinemati
import { EditorDialogueManifestPanel } from "@/components/editor/EditorDialogueManifestPanel";
import { EditorSrtPanel } from "@/components/editor/EditorSrtPanel";
import type { CinematicDefinition } from "@/types/cinematics/cinematics";
import type { MapNode, TransformMode } from "@/types/editor/editor";
import type { EditableMapNode, TransformMode } from "@/types/editor/editor";
import type { Vector3Tuple } from "@/types/three/three";
interface EditorControlsProps {
transformMode: TransformMode;
onTransformModeChange: (mode: TransformMode) => void;
selectedNodeIndex: number | null;
mapNodes: MapNode[];
mapNodes: EditableMapNode[];
nodesCount: number;
selectedNodeName: string | null;
selectedNodeScale: Vector3Tuple | null;
isSelectionLocked: boolean;
onSelectionLockToggle: () => void;
onClearSelection: () => void;
snapToTerrain: boolean;
onSnapToTerrainToggle: () => void;
newNodeName: string;
onNewNodeNameChange: (value: string) => void;
onAddNode: () => void;
onDeleteSelectedNode: () => void;
onSelectedScaleChange: (axis: 0 | 1 | 2, value: number) => void;
undoCount: number;
redoCount: number;
onUndo: () => void;
@@ -90,9 +101,17 @@ export function EditorControls({
mapNodes,
nodesCount,
selectedNodeName,
selectedNodeScale,
isSelectionLocked,
onSelectionLockToggle,
onClearSelection,
snapToTerrain,
onSnapToTerrainToggle,
newNodeName,
onNewNodeNameChange,
onAddNode,
onDeleteSelectedNode,
onSelectedScaleChange,
undoCount,
redoCount,
onUndo,
@@ -181,6 +200,15 @@ export function EditorControls({
<span>{redoCount}</span>
</button>
</div>
<label className="editor-checkbox-row">
<input
type="checkbox"
checked={snapToTerrain}
onChange={onSnapToTerrainToggle}
/>
<span>Snap terrain on move</span>
</label>
</section>
<section
@@ -204,6 +232,14 @@ export function EditorControls({
</span>
</div>
<div className="editor-selected-actions">
<button
type="button"
onClick={onDeleteSelectedNode}
aria-label="Delete selected node"
title="Delete selected node"
>
<Trash2 size={14} aria-hidden="true" />
</button>
<button
type="button"
onClick={onSelectionLockToggle}
@@ -230,6 +266,26 @@ export function EditorControls({
<X size={14} aria-hidden="true" />
</button>
</div>
{selectedNodeScale ? (
<div className="editor-scale-fields">
{selectedNodeScale.map((value, axis) => (
<label key={axis}>
<span>{["X", "Y", "Z"][axis]}</span>
<input
type="number"
step="0.01"
value={Number(value.toFixed(4))}
onChange={(event) =>
onSelectedScaleChange(
axis as 0 | 1 | 2,
Number(event.target.value),
)
}
/>
</label>
))}
</div>
) : null}
</div>
) : (
<div className="editor-no-selection">
@@ -239,6 +295,32 @@ export function EditorControls({
)}
</section>
<section
className="editor-control-section"
aria-labelledby="add-node-heading"
>
<div className="editor-section-heading">
<h3 id="add-node-heading">Add Node</h3>
</div>
<div className="editor-add-node-row">
<input
type="text"
value={newNodeName}
onChange={(event) => onNewNodeNameChange(event.target.value)}
placeholder="model-folder-name"
/>
<button
type="button"
className="editor-action-button"
onClick={onAddNode}
>
<Plus size={16} aria-hidden="true" />
Add cube
</button>
</div>
</section>
<section
className="editor-control-section"
aria-labelledby="view-heading"
@@ -341,7 +423,7 @@ interface JsonPreview {
}
function getJsonPreview(
mapNodes: MapNode[],
mapNodes: EditableMapNode[],
selectedNodeIndex: number | null,
): JsonPreview {
const { lines, ranges } = formatMapNodesWithRanges(mapNodes);
@@ -370,7 +452,7 @@ function getJsonPreview(
};
}
function formatMapNodesWithRanges(mapNodes: MapNode[]): {
function formatMapNodesWithRanges(mapNodes: EditableMapNode[]): {
lines: string[];
ranges: Array<{ start: number; end: number }>;
} {
@@ -378,7 +460,14 @@ function formatMapNodesWithRanges(mapNodes: MapNode[]): {
const ranges: Array<{ start: number; end: number }> = [];
mapNodes.forEach((node, index) => {
const objectLines = JSON.stringify(node, null, 2)
const serializableNode = {
name: node.name,
position: node.position,
rotation: node.rotation,
scale: node.scale,
type: node.type,
};
const objectLines = JSON.stringify(serializableNode, null, 2)
.split("\n")
.map((line) => ` ${line}`);