From 15361db203f1b15e7665f5b81a04c4f14e4cf1c0 Mon Sep 17 00:00:00 2001 From: Tom Boullay Date: Tue, 12 May 2026 10:18:12 +0200 Subject: [PATCH] update: organize editor controls panel --- src/components/editor/EditorControls.tsx | 392 ++++++++++++-------- src/components/editor/scene/EditorMap.tsx | 16 +- src/components/editor/scene/EditorScene.tsx | 14 +- src/index.css | 90 ++++- src/pages/editor/page.tsx | 13 + 5 files changed, 363 insertions(+), 162 deletions(-) diff --git a/src/components/editor/EditorControls.tsx b/src/components/editor/EditorControls.tsx index b5cf0ee..e9b099d 100644 --- a/src/components/editor/EditorControls.tsx +++ b/src/components/editor/EditorControls.tsx @@ -1,6 +1,7 @@ import { Box, Braces, + ChevronDown, Download, Expand, Keyboard, @@ -11,6 +12,8 @@ import { RotateCw, Save, Undo2, + Unlock, + X, } from "lucide-react"; import { EditorCinematicManifestPanel } from "@/components/editor/EditorCinematicManifestPanel"; import { EditorDialogueManifestPanel } from "@/components/editor/EditorDialogueManifestPanel"; @@ -25,6 +28,9 @@ interface EditorControlsProps { mapNodes: MapNode[]; nodesCount: number; selectedNodeName: string | null; + isSelectionLocked: boolean; + onSelectionLockToggle: () => void; + onClearSelection: () => void; undoCount: number; redoCount: number; onUndo: () => void; @@ -50,6 +56,33 @@ const EDITOR_SHORTCUTS = [ ["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}
+
+ ); +} + export function EditorControls({ transformMode, onTransformModeChange, @@ -57,6 +90,9 @@ export function EditorControls({ mapNodes, nodesCount, selectedNodeName, + isSelectionLocked, + onSelectionLockToggle, + onClearSelection, undoCount, redoCount, onUndo, @@ -79,173 +115,215 @@ export function EditorControls({

Select an object, choose a transform mode, then drag the gizmo.

-
-
-

Transform

- T / R / S -
- -
- {TRANSFORM_OPTIONS.map(({ mode, label, shortcut, Icon }) => ( - - ))} -
- -
- - -
-
+
+

Shortcuts

+
-
-
-

File

-
+
+ {EDITOR_SHORTCUTS.map(([keys, description]) => ( +
+
{keys}
+
{description}
+
+ ))} +
+
+ - - - {onSaveToServer && ( - - )} - - -
-
-

View

-
- - {onPlayerMode && ( - - )} -
- -
-
-

Selection

- {nodesCount} nodes -
- - {selectedNodeIndex !== null ? ( -
-
-
-
-

Shortcuts

-
- -
- {EDITOR_SHORTCUTS.map(([keys, description]) => ( -
-
{keys}
-
{description}
-
- ))} -
-
- -
-
-

JSON

- {jsonPreview.label} -
- -
-            {jsonPreview.lines.map((line) => (
-              
+              
+
-
-
- +
+
+

Selection

+ {nodesCount} nodes +
- - - + {selectedNodeIndex !== null ? ( +
+
+ ) : ( +
+
+ )} +
+ +
+
+

View

+
+ + {onPlayerMode && ( + + )} +
+ +
+
+

JSON

+ {jsonPreview.label} +
+ +
+              {jsonPreview.lines.map((line) => (
+                
+                  {line.number}
+                  {line.content || " "}
+                
+              ))}
+            
+ +
+
+
+ +
+
+

File

+
+ + + + {onSaveToServer && ( + + )} +
+ + + + + + + + + + + ); diff --git a/src/components/editor/scene/EditorMap.tsx b/src/components/editor/scene/EditorMap.tsx index baea5a6..b6eeedc 100644 --- a/src/components/editor/scene/EditorMap.tsx +++ b/src/components/editor/scene/EditorMap.tsx @@ -11,6 +11,7 @@ interface EditorMapProps { sceneData: SceneData; selectedNodeIndex: number | null; onSelectNode: (index: number | null) => void; + isSelectionLocked: boolean; hoveredNodeIndex: number | null; onHoverNode: (index: number | null) => void; transformMode: TransformMode; @@ -28,6 +29,7 @@ interface EditorNodeCommonProps { isHovered: boolean; objectsMapRef: EditorNodeObjectRef; onSelectNode: (index: number | null) => void; + isSelectionLocked: boolean; onHoverNode: (index: number | null) => void; } @@ -108,11 +110,13 @@ function getNodeHighlightColor( function createEditorNodePointerHandlers( index: number, onSelectNode: (index: number | null) => void, + isSelectionLocked: boolean, onHoverNode: (index: number | null) => void, ): EditorNodePointerHandlers { return { onClick: (event) => { event.stopPropagation(); + if (isSelectionLocked) return; onSelectNode(index); }, onPointerEnter: (event) => { @@ -130,6 +134,7 @@ export function EditorMap({ sceneData, selectedNodeIndex, onSelectNode, + isSelectionLocked, hoveredNodeIndex, onHoverNode, transformMode, @@ -192,8 +197,9 @@ export function EditorMap({ ) => { - e.stopPropagation(); + onClick={(event: ThreeEvent) => { + event.stopPropagation(); + if (isSelectionLocked) return; onSelectNode(null); }} > @@ -211,6 +217,7 @@ export function EditorMap({ isHovered={hoveredNodeIndex === index} objectsMapRef={objectsMapRef} onSelectNode={onSelectNode} + isSelectionLocked={isSelectionLocked} onHoverNode={onHoverNode} /> ); @@ -224,6 +231,7 @@ export function EditorMap({ isHovered={hoveredNodeIndex === index} objectsMapRef={objectsMapRef} onSelectNode={onSelectNode} + isSelectionLocked={isSelectionLocked} onHoverNode={onHoverNode} /> ); @@ -251,6 +259,7 @@ function EditorModelNode({ isHovered, objectsMapRef, onSelectNode, + isSelectionLocked, onHoverNode, }: EditorNodeCommonProps & { modelUrl: string; @@ -269,6 +278,7 @@ function EditorModelNode({ const pointerHandlers = createEditorNodePointerHandlers( index, onSelectNode, + isSelectionLocked, onHoverNode, ); useRegisteredEditorNode(groupRef, index, node, objectsMapRef); @@ -343,12 +353,14 @@ function EditorFallbackNode({ isHovered, objectsMapRef, onSelectNode, + isSelectionLocked, onHoverNode, }: EditorNodeCommonProps) { const meshRef = useRef(null); const pointerHandlers = createEditorNodePointerHandlers( index, onSelectNode, + isSelectionLocked, onHoverNode, ); useRegisteredEditorNode(meshRef, index, node, objectsMapRef); diff --git a/src/components/editor/scene/EditorScene.tsx b/src/components/editor/scene/EditorScene.tsx index c071ef0..c9d585c 100644 --- a/src/components/editor/scene/EditorScene.tsx +++ b/src/components/editor/scene/EditorScene.tsx @@ -17,6 +17,7 @@ interface EditorSceneProps { sceneData: SceneData; selectedNodeIndex: number | null; onSelectNode: (index: number | null) => void; + isSelectionLocked: boolean; hoveredNodeIndex: number | null; onHoverNode: (index: number | null) => void; transformMode: TransformMode; @@ -35,6 +36,7 @@ export function EditorScene({ sceneData, selectedNodeIndex, onSelectNode, + isSelectionLocked, hoveredNodeIndex, onHoverNode, transformMode, @@ -68,7 +70,7 @@ export function EditorScene({ if (selectedNodeIndex !== null) { switch (e.key.toLowerCase()) { case "escape": - onSelectNode(null); + if (!isSelectionLocked) onSelectNode(null); break; case "t": onTransformModeChange("translate"); @@ -85,7 +87,14 @@ export function EditorScene({ window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); - }, [selectedNodeIndex, onSelectNode, onTransformModeChange, onUndo, onRedo]); + }, [ + isSelectionLocked, + selectedNodeIndex, + onSelectNode, + onTransformModeChange, + onUndo, + onRedo, + ]); return ( <> @@ -113,6 +122,7 @@ export function EditorScene({ sceneData={sceneData} selectedNodeIndex={selectedNodeIndex} onSelectNode={onSelectNode} + isSelectionLocked={isSelectionLocked} hoveredNodeIndex={hoveredNodeIndex} onHoverNode={onHoverNode} transformMode={transformMode} diff --git a/src/index.css b/src/index.css index 0578f80..3e5b7c4 100644 --- a/src/index.css +++ b/src/index.css @@ -1081,6 +1081,61 @@ canvas { line-height: 1.45; } +.editor-panel-group { + border-top: 1px solid rgba(255, 255, 255, 0.09); +} + +.editor-panel-group-summary { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 13px 12px; + color: #ffffff; + cursor: pointer; + font-size: 0.8rem; + font-weight: 800; + letter-spacing: 0.12em; + list-style: none; + text-transform: uppercase; + user-select: none; +} + +.editor-panel-group-summary::-webkit-details-marker { + display: none; +} + +.editor-panel-group-summary:hover { + color: #f2f2f2; +} + +.editor-panel-group-meta { + display: inline-flex; + align-items: center; + gap: 8px; + color: #777777; + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0; + text-transform: none; +} + +.editor-panel-group-meta svg { + transition: transform 160ms ease; +} + +.editor-panel-group[open] .editor-panel-group-meta svg { + transform: rotate(180deg); +} + +.editor-panel-group-content > .editor-control-section:first-child, +.editor-panel-group-content > .editor-json-section:first-child, +.editor-panel-group-content > .editor-cinematic-manifest-section:first-child, +.editor-panel-group-content > .editor-dialogue-manifest-section:first-child, +.editor-panel-group-content > .editor-srt-section:first-child { + border-top: 0; +} + .editor-control-section { padding: 14px 12px; border-top: 1px solid rgba(255, 255, 255, 0.09); @@ -1252,7 +1307,8 @@ canvas { } .editor-selected-info { - display: flex; + display: grid; + grid-template-columns: 17px 1fr auto; align-items: center; gap: 11px; background: #ffffff; @@ -1262,6 +1318,38 @@ canvas { color: #050505; } +.editor-selected-actions { + display: inline-flex; + gap: 6px; +} + +.editor-selected-actions button { + display: inline-flex; + align-items: center; + justify-content: center; + width: 27px; + height: 27px; + padding: 0; + color: #050505; + background: rgba(0, 0, 0, 0.06); + border: 0; + border-radius: 9px; + cursor: pointer; + transition: + background 160ms ease, + transform 160ms ease; +} + +.editor-selected-actions button:hover { + background: rgba(0, 0, 0, 0.12); + transform: translateY(-1px); +} + +.editor-selected-actions button[aria-pressed="true"] { + color: #ffffff; + background: #050505; +} + .editor-selected-info strong, .editor-selected-info span { display: block; diff --git a/src/pages/editor/page.tsx b/src/pages/editor/page.tsx index 3c326f3..ec60cbf 100644 --- a/src/pages/editor/page.tsx +++ b/src/pages/editor/page.tsx @@ -31,6 +31,7 @@ export function EditorPage(): React.JSX.Element { const [transformMode, setTransformMode] = useState("translate"); const [isPlayerMode, setIsPlayerMode] = useState(false); + const [isSelectionLocked, setIsSelectionLocked] = useState(false); const [cinematicPreviewRequest, setCinematicPreviewRequest] = useState(null); @@ -47,6 +48,14 @@ export function EditorPage(): React.JSX.Element { setSelectedNodeIndex(index); }, []); + const handleClearSelection = useCallback(() => { + setSelectedNodeIndex(null); + }, []); + + const handleSelectionLockToggle = useCallback(() => { + setIsSelectionLocked((locked) => !locked); + }, []); + const handleHoverNode = useCallback((index: number | null) => { setHoveredNodeIndex(index); }, []); @@ -180,6 +189,7 @@ export function EditorPage(): React.JSX.Element { sceneData={sceneData!} selectedNodeIndex={selectedNodeIndex} onSelectNode={handleSelectNode} + isSelectionLocked={isSelectionLocked} hoveredNodeIndex={hoveredNodeIndex} onHoverNode={handleHoverNode} transformMode={transformMode} @@ -207,6 +217,9 @@ export function EditorPage(): React.JSX.Element { ? sceneData.mapNodes[selectedNodeIndex].name || null : null } + isSelectionLocked={isSelectionLocked} + onSelectionLockToggle={handleSelectionLockToggle} + onClearSelection={handleClearSelection} undoCount={undoCount} redoCount={redoCount} onUndo={handleUndo}