import { Box, Braces, ChevronDown, Download, Expand, Keyboard, Lock, MousePointer2, Move3D, Plus, Redo2, RotateCw, Save, Trash2, ScanSearch, Undo2, Unlock, X, } from "lucide-react"; import { useState } from "react"; import { EditorCinematicManifestPanel } from "@/components/editor/EditorCinematicManifestPanel"; 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 { Vector3Tuple } from "@/types/three/three"; interface EditorControlsProps { transformMode: TransformMode; onTransformModeChange: (mode: TransformMode) => void; selectedNodeIndex: number | null; selectedNodeIndexes: number[]; mapNodes: MapNode[]; nodesCount: number; selectedNodeName: string | null; selectedNodeScale: Vector3Tuple | null; lockTerrainSelection: boolean; onLockTerrainSelectionChange: (locked: boolean) => void; isSelectionLocked: boolean; onSelectionLockToggle: () => void; onClearSelection: () => void; snapToTerrain: boolean; onSnapToTerrainToggle: () => void; onSnapAllToTerrain: () => 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; onRedo: () => void; cameraActionLabel: string; onCameraAction: () => void; onExportJson: () => void; onSaveToServer?: (() => void | Promise) | undefined; onPlayerMode?: (() => void) | undefined; onPreviewCinematic?: ((cinematic: CinematicDefinition) => void) | undefined; isPlayerMode?: boolean; } const TRANSFORM_OPTIONS = [ { mode: "translate", label: "Translate", shortcut: "T", Icon: Move3D }, { mode: "rotate", label: "Rotate", shortcut: "R", Icon: RotateCw }, { mode: "scale", label: "Scale", shortcut: "S", Icon: Expand }, ] as const; const EDITOR_SHORTCUTS = [ ["Click", "Select object"], ["Shift + Right click", "Toggle multi-selection"], ["T / R / S", "Transform mode"], ["Ctrl Z / Y", "Undo / redo"], ["Esc / X button", "Clear selection"], ["WASD", "Move when locked"], ] as const; interface EditorPanelGroupProps { title: string; summary?: string; defaultOpen?: boolean; children: React.ReactNode; } function EditorPanelGroup({ title, summary, defaultOpen = false, children, }: EditorPanelGroupProps): React.JSX.Element { return (
{title} {summary ? {summary} : null}
{children}
); } interface EditorScaleFieldProps { axis: 0 | 1 | 2; label: string; value: number; onCommit: (axis: 0 | 1 | 2, value: number) => void; } function EditorScaleField({ axis, label, onCommit, value, }: EditorScaleFieldProps): React.JSX.Element { const [draftValue, setDraftValue] = useState(() => String(Number(value.toFixed(4))), ); const commitDraftValue = (): void => { const nextValue = Number(draftValue); if (!draftValue.trim() || Number.isNaN(nextValue)) { setDraftValue(String(Number(value.toFixed(4)))); return; } onCommit(axis, nextValue); }; return ( ); } export function EditorControls({ transformMode, onTransformModeChange, selectedNodeIndex, selectedNodeIndexes, mapNodes, nodesCount, selectedNodeName, selectedNodeScale, lockTerrainSelection, onLockTerrainSelectionChange, isSelectionLocked, onSelectionLockToggle, onClearSelection, snapToTerrain, onSnapToTerrainToggle, onSnapAllToTerrain, newNodeName, onNewNodeNameChange, onAddNode, onDeleteSelectedNode, onSelectedScaleChange, undoCount, redoCount, onUndo, onRedo, cameraActionLabel, onCameraAction, onExportJson, onSaveToServer, onPlayerMode, onPreviewCinematic, isPlayerMode, }: EditorControlsProps): React.JSX.Element { const viewModeLabel = isPlayerMode ? "View locked" : "Lock view"; const jsonPreview = getJsonPreview(mapNodes, selectedNodeIndex); const selectedNode = selectedNodeIndex !== null ? mapNodes[selectedNodeIndex] : null; const selectionCount = selectedNodeIndexes.length; const transformValues = getTransformValues(selectedNode ?? null); return ( <> ); } function formatNumber(value: number): string { return Number.isInteger(value) ? String(value) : value.toFixed(2); } function formatVector(values: readonly [number, number, number]): string { return `X ${formatNumber(values[0])} · Y ${formatNumber(values[1])} · Z ${formatNumber(values[2])}`; } function formatRotation(values: readonly [number, number, number]): string { const degrees = values.map((value) => (value * 180) / Math.PI) as [ number, number, number, ]; return `X ${formatNumber(degrees[0])}° · Y ${formatNumber(degrees[1])}° · Z ${formatNumber(degrees[2])}°`; } function getTransformValues( node: MapNode | null, ): Record { if (!node) { return { translate: "No selection", rotate: "No selection", scale: "No selection", }; } return { translate: formatVector(node.position), rotate: formatRotation(node.rotation), scale: formatVector(node.scale), }; } interface JsonPreviewLine { number: number; content: string; isSelected: boolean; } interface JsonPreview { label: string; lines: JsonPreviewLine[]; } function getJsonPreview( mapNodes: MapNode[], selectedNodeIndex: number | null, ): JsonPreview { const { lines, ranges } = formatMapNodesWithRanges(mapNodes); if (selectedNodeIndex === null || !ranges[selectedNodeIndex]) { return { label: `${lines.length} raw lines`, lines: lines.map((content, index) => ({ number: index + 1, content, isSelected: false, })), }; } const range = ranges[selectedNodeIndex]; const selectedLines = lines.slice(range.start - 1, range.end); return { label: `Lines ${range.start}-${range.end}`, lines: selectedLines.map((content, index) => ({ number: range.start + index, content, isSelected: true, })), }; } function formatMapNodesWithRanges(mapNodes: MapNode[]): { lines: string[]; ranges: Array<{ start: number; end: number }>; } { const lines = ["["]; const ranges: Array<{ start: number; end: number }> = []; mapNodes.forEach((node, index) => { 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}`); if (index < mapNodes.length - 1) { objectLines[objectLines.length - 1] += ","; } const start = lines.length + 1; lines.push(...objectLines); ranges.push({ start, end: lines.length }); }); lines.push("]"); return { lines, ranges }; }