update: organize editor controls panel
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Braces,
|
Braces,
|
||||||
|
ChevronDown,
|
||||||
Download,
|
Download,
|
||||||
Expand,
|
Expand,
|
||||||
Keyboard,
|
Keyboard,
|
||||||
@@ -11,6 +12,8 @@ import {
|
|||||||
RotateCw,
|
RotateCw,
|
||||||
Save,
|
Save,
|
||||||
Undo2,
|
Undo2,
|
||||||
|
Unlock,
|
||||||
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { EditorCinematicManifestPanel } from "@/components/editor/EditorCinematicManifestPanel";
|
import { EditorCinematicManifestPanel } from "@/components/editor/EditorCinematicManifestPanel";
|
||||||
import { EditorDialogueManifestPanel } from "@/components/editor/EditorDialogueManifestPanel";
|
import { EditorDialogueManifestPanel } from "@/components/editor/EditorDialogueManifestPanel";
|
||||||
@@ -25,6 +28,9 @@ interface EditorControlsProps {
|
|||||||
mapNodes: MapNode[];
|
mapNodes: MapNode[];
|
||||||
nodesCount: number;
|
nodesCount: number;
|
||||||
selectedNodeName: string | null;
|
selectedNodeName: string | null;
|
||||||
|
isSelectionLocked: boolean;
|
||||||
|
onSelectionLockToggle: () => void;
|
||||||
|
onClearSelection: () => void;
|
||||||
undoCount: number;
|
undoCount: number;
|
||||||
redoCount: number;
|
redoCount: number;
|
||||||
onUndo: () => void;
|
onUndo: () => void;
|
||||||
@@ -50,6 +56,33 @@ const EDITOR_SHORTCUTS = [
|
|||||||
["WASD", "Move when locked"],
|
["WASD", "Move when locked"],
|
||||||
] as const;
|
] 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 (
|
||||||
|
<details className="editor-panel-group" open={defaultOpen}>
|
||||||
|
<summary className="editor-panel-group-summary">
|
||||||
|
<span>{title}</span>
|
||||||
|
<span className="editor-panel-group-meta">
|
||||||
|
{summary ? <span>{summary}</span> : null}
|
||||||
|
<ChevronDown size={15} aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
</summary>
|
||||||
|
<div className="editor-panel-group-content">{children}</div>
|
||||||
|
</details>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function EditorControls({
|
export function EditorControls({
|
||||||
transformMode,
|
transformMode,
|
||||||
onTransformModeChange,
|
onTransformModeChange,
|
||||||
@@ -57,6 +90,9 @@ export function EditorControls({
|
|||||||
mapNodes,
|
mapNodes,
|
||||||
nodesCount,
|
nodesCount,
|
||||||
selectedNodeName,
|
selectedNodeName,
|
||||||
|
isSelectionLocked,
|
||||||
|
onSelectionLockToggle,
|
||||||
|
onClearSelection,
|
||||||
undoCount,
|
undoCount,
|
||||||
redoCount,
|
redoCount,
|
||||||
onUndo,
|
onUndo,
|
||||||
@@ -79,173 +115,215 @@ export function EditorControls({
|
|||||||
<p>Select an object, choose a transform mode, then drag the gizmo.</p>
|
<p>Select an object, choose a transform mode, then drag the gizmo.</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section
|
<EditorPanelGroup title="Editor" summary="Map tools" defaultOpen>
|
||||||
className="editor-control-section"
|
<EditorPanelGroup title="Shortcuts" summary="Keys">
|
||||||
aria-labelledby="transform-heading"
|
<section
|
||||||
>
|
className="editor-control-section"
|
||||||
<div className="editor-section-heading">
|
aria-labelledby="shortcuts-heading"
|
||||||
<h3 id="transform-heading">Transform</h3>
|
|
||||||
<span>T / R / S</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="editor-transform-buttons">
|
|
||||||
{TRANSFORM_OPTIONS.map(({ mode, label, shortcut, Icon }) => (
|
|
||||||
<button
|
|
||||||
key={mode}
|
|
||||||
className={`editor-transform-button ${transformMode === mode ? "active" : ""}`}
|
|
||||||
onClick={() => onTransformModeChange(mode)}
|
|
||||||
aria-pressed={transformMode === mode}
|
|
||||||
>
|
|
||||||
<Icon size={16} aria-hidden="true" />
|
|
||||||
<span>{label}</span>
|
|
||||||
<kbd>{shortcut}</kbd>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="editor-history-buttons">
|
|
||||||
<button
|
|
||||||
className="editor-history-button"
|
|
||||||
onClick={onUndo}
|
|
||||||
disabled={undoCount === 0}
|
|
||||||
>
|
>
|
||||||
<Undo2 size={15} aria-hidden="true" />
|
<div className="editor-section-heading">
|
||||||
Undo
|
<h3 id="shortcuts-heading">Shortcuts</h3>
|
||||||
<span>{undoCount}</span>
|
<Keyboard size={15} aria-hidden="true" />
|
||||||
</button>
|
</div>
|
||||||
<button
|
|
||||||
className="editor-history-button"
|
|
||||||
onClick={onRedo}
|
|
||||||
disabled={redoCount === 0}
|
|
||||||
>
|
|
||||||
<Redo2 size={15} aria-hidden="true" />
|
|
||||||
Redo
|
|
||||||
<span>{redoCount}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section
|
<dl className="editor-shortcuts-list">
|
||||||
className="editor-control-section"
|
{EDITOR_SHORTCUTS.map(([keys, description]) => (
|
||||||
aria-labelledby="file-heading"
|
<div key={keys}>
|
||||||
>
|
<dt>{keys}</dt>
|
||||||
<div className="editor-section-heading">
|
<dd>{description}</dd>
|
||||||
<h3 id="file-heading">File</h3>
|
</div>
|
||||||
</div>
|
))}
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
</EditorPanelGroup>
|
||||||
|
|
||||||
<button
|
<section
|
||||||
className="editor-action-button editor-action-button-primary"
|
className="editor-control-section"
|
||||||
onClick={onExportJson}
|
aria-labelledby="transform-heading"
|
||||||
>
|
>
|
||||||
<Download size={16} aria-hidden="true" />
|
<div className="editor-section-heading">
|
||||||
Export JSON
|
<h3 id="transform-heading">Transform</h3>
|
||||||
</button>
|
<span>T / R / S</span>
|
||||||
|
|
||||||
{onSaveToServer && (
|
|
||||||
<button className="editor-action-button" onClick={onSaveToServer}>
|
|
||||||
<Save size={16} aria-hidden="true" />
|
|
||||||
Save to server
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section
|
|
||||||
className="editor-control-section"
|
|
||||||
aria-labelledby="view-heading"
|
|
||||||
>
|
|
||||||
<div className="editor-section-heading">
|
|
||||||
<h3 id="view-heading">View</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{onPlayerMode && (
|
|
||||||
<button
|
|
||||||
className={`editor-player-button ${isPlayerMode ? "active" : ""}`}
|
|
||||||
onClick={onPlayerMode}
|
|
||||||
aria-pressed={isPlayerMode}
|
|
||||||
>
|
|
||||||
<Lock size={16} aria-hidden="true" />
|
|
||||||
{viewModeLabel}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section
|
|
||||||
className="editor-control-section"
|
|
||||||
aria-labelledby="selection-heading"
|
|
||||||
>
|
|
||||||
<div className="editor-section-heading">
|
|
||||||
<h3 id="selection-heading">Selection</h3>
|
|
||||||
<span>{nodesCount} nodes</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{selectedNodeIndex !== null ? (
|
|
||||||
<div className="editor-selected-info">
|
|
||||||
<Box size={17} aria-hidden="true" />
|
|
||||||
<div>
|
|
||||||
<strong>
|
|
||||||
{selectedNodeName || `Node ${selectedNodeIndex + 1}`}
|
|
||||||
</strong>
|
|
||||||
<span>
|
|
||||||
Index {selectedNodeIndex + 1} of {nodesCount}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<div className="editor-no-selection">
|
<div className="editor-transform-buttons">
|
||||||
<MousePointer2 size={17} aria-hidden="true" />
|
{TRANSFORM_OPTIONS.map(({ mode, label, shortcut, Icon }) => (
|
||||||
No object selected
|
<button
|
||||||
|
key={mode}
|
||||||
|
className={`editor-transform-button ${transformMode === mode ? "active" : ""}`}
|
||||||
|
onClick={() => onTransformModeChange(mode)}
|
||||||
|
aria-pressed={transformMode === mode}
|
||||||
|
>
|
||||||
|
<Icon size={16} aria-hidden="true" />
|
||||||
|
<span>{label}</span>
|
||||||
|
<kbd>{shortcut}</kbd>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section
|
<div className="editor-history-buttons">
|
||||||
className="editor-control-section"
|
<button
|
||||||
aria-labelledby="shortcuts-heading"
|
className="editor-history-button"
|
||||||
>
|
onClick={onUndo}
|
||||||
<div className="editor-section-heading">
|
disabled={undoCount === 0}
|
||||||
<h3 id="shortcuts-heading">Shortcuts</h3>
|
|
||||||
<Keyboard size={15} aria-hidden="true" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<dl className="editor-shortcuts-list">
|
|
||||||
{EDITOR_SHORTCUTS.map(([keys, description]) => (
|
|
||||||
<div key={keys}>
|
|
||||||
<dt>{keys}</dt>
|
|
||||||
<dd>{description}</dd>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</dl>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="editor-json-section" aria-labelledby="json-heading">
|
|
||||||
<div className="editor-section-heading">
|
|
||||||
<h3 id="json-heading">JSON</h3>
|
|
||||||
<span>{jsonPreview.label}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<pre className="editor-json-view" aria-label={jsonPreview.label}>
|
|
||||||
{jsonPreview.lines.map((line) => (
|
|
||||||
<code
|
|
||||||
key={line.number}
|
|
||||||
className={line.isSelected ? "is-selected" : undefined}
|
|
||||||
>
|
>
|
||||||
<span>{line.number}</span>
|
<Undo2 size={15} aria-hidden="true" />
|
||||||
{line.content || " "}
|
Undo
|
||||||
</code>
|
<span>{undoCount}</span>
|
||||||
))}
|
</button>
|
||||||
</pre>
|
<button
|
||||||
|
className="editor-history-button"
|
||||||
|
onClick={onRedo}
|
||||||
|
disabled={redoCount === 0}
|
||||||
|
>
|
||||||
|
<Redo2 size={15} aria-hidden="true" />
|
||||||
|
Redo
|
||||||
|
<span>{redoCount}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div className="editor-json-hint">
|
<section
|
||||||
<Braces size={14} aria-hidden="true" />
|
className="editor-control-section"
|
||||||
{selectedNodeIndex === null
|
aria-labelledby="selection-heading"
|
||||||
? "Raw map JSON"
|
>
|
||||||
: `Selected node ${selectedNodeIndex + 1} raw lines`}
|
<div className="editor-section-heading">
|
||||||
</div>
|
<h3 id="selection-heading">Selection</h3>
|
||||||
</section>
|
<span>{nodesCount} nodes</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<EditorCinematicManifestPanel onPreviewCinematic={onPreviewCinematic} />
|
{selectedNodeIndex !== null ? (
|
||||||
<EditorDialogueManifestPanel />
|
<div className="editor-selected-info">
|
||||||
<EditorSrtPanel />
|
<Box size={17} aria-hidden="true" />
|
||||||
|
<div>
|
||||||
|
<strong>
|
||||||
|
{selectedNodeName || `Node ${selectedNodeIndex + 1}`}
|
||||||
|
</strong>
|
||||||
|
<span>
|
||||||
|
Index {selectedNodeIndex + 1} of {nodesCount}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="editor-selected-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onSelectionLockToggle}
|
||||||
|
aria-pressed={isSelectionLocked}
|
||||||
|
aria-label={
|
||||||
|
isSelectionLocked ? "Unlock selection" : "Lock selection"
|
||||||
|
}
|
||||||
|
title={
|
||||||
|
isSelectionLocked ? "Unlock selection" : "Lock selection"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isSelectionLocked ? (
|
||||||
|
<Lock size={14} aria-hidden="true" />
|
||||||
|
) : (
|
||||||
|
<Unlock size={14} aria-hidden="true" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClearSelection}
|
||||||
|
aria-label="Clear selection"
|
||||||
|
title="Clear selection"
|
||||||
|
>
|
||||||
|
<X size={14} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="editor-no-selection">
|
||||||
|
<MousePointer2 size={17} aria-hidden="true" />
|
||||||
|
No object selected
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section
|
||||||
|
className="editor-control-section"
|
||||||
|
aria-labelledby="view-heading"
|
||||||
|
>
|
||||||
|
<div className="editor-section-heading">
|
||||||
|
<h3 id="view-heading">View</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{onPlayerMode && (
|
||||||
|
<button
|
||||||
|
className={`editor-player-button ${isPlayerMode ? "active" : ""}`}
|
||||||
|
onClick={onPlayerMode}
|
||||||
|
aria-pressed={isPlayerMode}
|
||||||
|
>
|
||||||
|
<Lock size={16} aria-hidden="true" />
|
||||||
|
{viewModeLabel}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section
|
||||||
|
className="editor-json-section"
|
||||||
|
aria-labelledby="json-heading"
|
||||||
|
>
|
||||||
|
<div className="editor-section-heading">
|
||||||
|
<h3 id="json-heading">JSON</h3>
|
||||||
|
<span>{jsonPreview.label}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<pre className="editor-json-view" aria-label={jsonPreview.label}>
|
||||||
|
{jsonPreview.lines.map((line) => (
|
||||||
|
<code
|
||||||
|
key={line.number}
|
||||||
|
className={line.isSelected ? "is-selected" : undefined}
|
||||||
|
>
|
||||||
|
<span>{line.number}</span>
|
||||||
|
{line.content || " "}
|
||||||
|
</code>
|
||||||
|
))}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<div className="editor-json-hint">
|
||||||
|
<Braces size={14} aria-hidden="true" />
|
||||||
|
{selectedNodeIndex === null
|
||||||
|
? "Raw map JSON"
|
||||||
|
: `Selected node ${selectedNodeIndex + 1} raw lines`}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section
|
||||||
|
className="editor-control-section"
|
||||||
|
aria-labelledby="file-heading"
|
||||||
|
>
|
||||||
|
<div className="editor-section-heading">
|
||||||
|
<h3 id="file-heading">File</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="editor-action-button editor-action-button-primary"
|
||||||
|
onClick={onExportJson}
|
||||||
|
>
|
||||||
|
<Download size={16} aria-hidden="true" />
|
||||||
|
Export JSON
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{onSaveToServer && (
|
||||||
|
<button className="editor-action-button" onClick={onSaveToServer}>
|
||||||
|
<Save size={16} aria-hidden="true" />
|
||||||
|
Save to server
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</EditorPanelGroup>
|
||||||
|
|
||||||
|
<EditorPanelGroup title="Cinematics" summary="Timeline">
|
||||||
|
<EditorCinematicManifestPanel
|
||||||
|
onPreviewCinematic={onPreviewCinematic}
|
||||||
|
/>
|
||||||
|
</EditorPanelGroup>
|
||||||
|
<EditorPanelGroup title="Dialogues" summary="Manifest">
|
||||||
|
<EditorDialogueManifestPanel />
|
||||||
|
</EditorPanelGroup>
|
||||||
|
<EditorPanelGroup title="SRT" summary="Subtitles">
|
||||||
|
<EditorSrtPanel />
|
||||||
|
</EditorPanelGroup>
|
||||||
</aside>
|
</aside>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ interface EditorMapProps {
|
|||||||
sceneData: SceneData;
|
sceneData: SceneData;
|
||||||
selectedNodeIndex: number | null;
|
selectedNodeIndex: number | null;
|
||||||
onSelectNode: (index: number | null) => void;
|
onSelectNode: (index: number | null) => void;
|
||||||
|
isSelectionLocked: boolean;
|
||||||
hoveredNodeIndex: number | null;
|
hoveredNodeIndex: number | null;
|
||||||
onHoverNode: (index: number | null) => void;
|
onHoverNode: (index: number | null) => void;
|
||||||
transformMode: TransformMode;
|
transformMode: TransformMode;
|
||||||
@@ -28,6 +29,7 @@ interface EditorNodeCommonProps {
|
|||||||
isHovered: boolean;
|
isHovered: boolean;
|
||||||
objectsMapRef: EditorNodeObjectRef;
|
objectsMapRef: EditorNodeObjectRef;
|
||||||
onSelectNode: (index: number | null) => void;
|
onSelectNode: (index: number | null) => void;
|
||||||
|
isSelectionLocked: boolean;
|
||||||
onHoverNode: (index: number | null) => void;
|
onHoverNode: (index: number | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,11 +110,13 @@ function getNodeHighlightColor(
|
|||||||
function createEditorNodePointerHandlers(
|
function createEditorNodePointerHandlers(
|
||||||
index: number,
|
index: number,
|
||||||
onSelectNode: (index: number | null) => void,
|
onSelectNode: (index: number | null) => void,
|
||||||
|
isSelectionLocked: boolean,
|
||||||
onHoverNode: (index: number | null) => void,
|
onHoverNode: (index: number | null) => void,
|
||||||
): EditorNodePointerHandlers {
|
): EditorNodePointerHandlers {
|
||||||
return {
|
return {
|
||||||
onClick: (event) => {
|
onClick: (event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
if (isSelectionLocked) return;
|
||||||
onSelectNode(index);
|
onSelectNode(index);
|
||||||
},
|
},
|
||||||
onPointerEnter: (event) => {
|
onPointerEnter: (event) => {
|
||||||
@@ -130,6 +134,7 @@ export function EditorMap({
|
|||||||
sceneData,
|
sceneData,
|
||||||
selectedNodeIndex,
|
selectedNodeIndex,
|
||||||
onSelectNode,
|
onSelectNode,
|
||||||
|
isSelectionLocked,
|
||||||
hoveredNodeIndex,
|
hoveredNodeIndex,
|
||||||
onHoverNode,
|
onHoverNode,
|
||||||
transformMode,
|
transformMode,
|
||||||
@@ -192,8 +197,9 @@ export function EditorMap({
|
|||||||
<axesHelper args={[10]} />
|
<axesHelper args={[10]} />
|
||||||
|
|
||||||
<group
|
<group
|
||||||
onClick={(e: ThreeEvent<MouseEvent>) => {
|
onClick={(event: ThreeEvent<MouseEvent>) => {
|
||||||
e.stopPropagation();
|
event.stopPropagation();
|
||||||
|
if (isSelectionLocked) return;
|
||||||
onSelectNode(null);
|
onSelectNode(null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -211,6 +217,7 @@ export function EditorMap({
|
|||||||
isHovered={hoveredNodeIndex === index}
|
isHovered={hoveredNodeIndex === index}
|
||||||
objectsMapRef={objectsMapRef}
|
objectsMapRef={objectsMapRef}
|
||||||
onSelectNode={onSelectNode}
|
onSelectNode={onSelectNode}
|
||||||
|
isSelectionLocked={isSelectionLocked}
|
||||||
onHoverNode={onHoverNode}
|
onHoverNode={onHoverNode}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -224,6 +231,7 @@ export function EditorMap({
|
|||||||
isHovered={hoveredNodeIndex === index}
|
isHovered={hoveredNodeIndex === index}
|
||||||
objectsMapRef={objectsMapRef}
|
objectsMapRef={objectsMapRef}
|
||||||
onSelectNode={onSelectNode}
|
onSelectNode={onSelectNode}
|
||||||
|
isSelectionLocked={isSelectionLocked}
|
||||||
onHoverNode={onHoverNode}
|
onHoverNode={onHoverNode}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -251,6 +259,7 @@ function EditorModelNode({
|
|||||||
isHovered,
|
isHovered,
|
||||||
objectsMapRef,
|
objectsMapRef,
|
||||||
onSelectNode,
|
onSelectNode,
|
||||||
|
isSelectionLocked,
|
||||||
onHoverNode,
|
onHoverNode,
|
||||||
}: EditorNodeCommonProps & {
|
}: EditorNodeCommonProps & {
|
||||||
modelUrl: string;
|
modelUrl: string;
|
||||||
@@ -269,6 +278,7 @@ function EditorModelNode({
|
|||||||
const pointerHandlers = createEditorNodePointerHandlers(
|
const pointerHandlers = createEditorNodePointerHandlers(
|
||||||
index,
|
index,
|
||||||
onSelectNode,
|
onSelectNode,
|
||||||
|
isSelectionLocked,
|
||||||
onHoverNode,
|
onHoverNode,
|
||||||
);
|
);
|
||||||
useRegisteredEditorNode(groupRef, index, node, objectsMapRef);
|
useRegisteredEditorNode(groupRef, index, node, objectsMapRef);
|
||||||
@@ -343,12 +353,14 @@ function EditorFallbackNode({
|
|||||||
isHovered,
|
isHovered,
|
||||||
objectsMapRef,
|
objectsMapRef,
|
||||||
onSelectNode,
|
onSelectNode,
|
||||||
|
isSelectionLocked,
|
||||||
onHoverNode,
|
onHoverNode,
|
||||||
}: EditorNodeCommonProps) {
|
}: EditorNodeCommonProps) {
|
||||||
const meshRef = useRef<THREE.Mesh>(null);
|
const meshRef = useRef<THREE.Mesh>(null);
|
||||||
const pointerHandlers = createEditorNodePointerHandlers(
|
const pointerHandlers = createEditorNodePointerHandlers(
|
||||||
index,
|
index,
|
||||||
onSelectNode,
|
onSelectNode,
|
||||||
|
isSelectionLocked,
|
||||||
onHoverNode,
|
onHoverNode,
|
||||||
);
|
);
|
||||||
useRegisteredEditorNode(meshRef, index, node, objectsMapRef);
|
useRegisteredEditorNode(meshRef, index, node, objectsMapRef);
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ interface EditorSceneProps {
|
|||||||
sceneData: SceneData;
|
sceneData: SceneData;
|
||||||
selectedNodeIndex: number | null;
|
selectedNodeIndex: number | null;
|
||||||
onSelectNode: (index: number | null) => void;
|
onSelectNode: (index: number | null) => void;
|
||||||
|
isSelectionLocked: boolean;
|
||||||
hoveredNodeIndex: number | null;
|
hoveredNodeIndex: number | null;
|
||||||
onHoverNode: (index: number | null) => void;
|
onHoverNode: (index: number | null) => void;
|
||||||
transformMode: TransformMode;
|
transformMode: TransformMode;
|
||||||
@@ -35,6 +36,7 @@ export function EditorScene({
|
|||||||
sceneData,
|
sceneData,
|
||||||
selectedNodeIndex,
|
selectedNodeIndex,
|
||||||
onSelectNode,
|
onSelectNode,
|
||||||
|
isSelectionLocked,
|
||||||
hoveredNodeIndex,
|
hoveredNodeIndex,
|
||||||
onHoverNode,
|
onHoverNode,
|
||||||
transformMode,
|
transformMode,
|
||||||
@@ -68,7 +70,7 @@ export function EditorScene({
|
|||||||
if (selectedNodeIndex !== null) {
|
if (selectedNodeIndex !== null) {
|
||||||
switch (e.key.toLowerCase()) {
|
switch (e.key.toLowerCase()) {
|
||||||
case "escape":
|
case "escape":
|
||||||
onSelectNode(null);
|
if (!isSelectionLocked) onSelectNode(null);
|
||||||
break;
|
break;
|
||||||
case "t":
|
case "t":
|
||||||
onTransformModeChange("translate");
|
onTransformModeChange("translate");
|
||||||
@@ -85,7 +87,14 @@ export function EditorScene({
|
|||||||
|
|
||||||
window.addEventListener("keydown", handleKeyDown);
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||||
}, [selectedNodeIndex, onSelectNode, onTransformModeChange, onUndo, onRedo]);
|
}, [
|
||||||
|
isSelectionLocked,
|
||||||
|
selectedNodeIndex,
|
||||||
|
onSelectNode,
|
||||||
|
onTransformModeChange,
|
||||||
|
onUndo,
|
||||||
|
onRedo,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -113,6 +122,7 @@ export function EditorScene({
|
|||||||
sceneData={sceneData}
|
sceneData={sceneData}
|
||||||
selectedNodeIndex={selectedNodeIndex}
|
selectedNodeIndex={selectedNodeIndex}
|
||||||
onSelectNode={onSelectNode}
|
onSelectNode={onSelectNode}
|
||||||
|
isSelectionLocked={isSelectionLocked}
|
||||||
hoveredNodeIndex={hoveredNodeIndex}
|
hoveredNodeIndex={hoveredNodeIndex}
|
||||||
onHoverNode={onHoverNode}
|
onHoverNode={onHoverNode}
|
||||||
transformMode={transformMode}
|
transformMode={transformMode}
|
||||||
|
|||||||
+89
-1
@@ -1081,6 +1081,61 @@ canvas {
|
|||||||
line-height: 1.45;
|
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 {
|
.editor-control-section {
|
||||||
padding: 14px 12px;
|
padding: 14px 12px;
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.09);
|
border-top: 1px solid rgba(255, 255, 255, 0.09);
|
||||||
@@ -1252,7 +1307,8 @@ canvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.editor-selected-info {
|
.editor-selected-info {
|
||||||
display: flex;
|
display: grid;
|
||||||
|
grid-template-columns: 17px 1fr auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 11px;
|
gap: 11px;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
@@ -1262,6 +1318,38 @@ canvas {
|
|||||||
color: #050505;
|
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 strong,
|
||||||
.editor-selected-info span {
|
.editor-selected-info span {
|
||||||
display: block;
|
display: block;
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export function EditorPage(): React.JSX.Element {
|
|||||||
const [transformMode, setTransformMode] =
|
const [transformMode, setTransformMode] =
|
||||||
useState<TransformMode>("translate");
|
useState<TransformMode>("translate");
|
||||||
const [isPlayerMode, setIsPlayerMode] = useState(false);
|
const [isPlayerMode, setIsPlayerMode] = useState(false);
|
||||||
|
const [isSelectionLocked, setIsSelectionLocked] = useState(false);
|
||||||
const [cinematicPreviewRequest, setCinematicPreviewRequest] =
|
const [cinematicPreviewRequest, setCinematicPreviewRequest] =
|
||||||
useState<EditorCinematicPreviewRequest | null>(null);
|
useState<EditorCinematicPreviewRequest | null>(null);
|
||||||
|
|
||||||
@@ -47,6 +48,14 @@ export function EditorPage(): React.JSX.Element {
|
|||||||
setSelectedNodeIndex(index);
|
setSelectedNodeIndex(index);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleClearSelection = useCallback(() => {
|
||||||
|
setSelectedNodeIndex(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSelectionLockToggle = useCallback(() => {
|
||||||
|
setIsSelectionLocked((locked) => !locked);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleHoverNode = useCallback((index: number | null) => {
|
const handleHoverNode = useCallback((index: number | null) => {
|
||||||
setHoveredNodeIndex(index);
|
setHoveredNodeIndex(index);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -180,6 +189,7 @@ export function EditorPage(): React.JSX.Element {
|
|||||||
sceneData={sceneData!}
|
sceneData={sceneData!}
|
||||||
selectedNodeIndex={selectedNodeIndex}
|
selectedNodeIndex={selectedNodeIndex}
|
||||||
onSelectNode={handleSelectNode}
|
onSelectNode={handleSelectNode}
|
||||||
|
isSelectionLocked={isSelectionLocked}
|
||||||
hoveredNodeIndex={hoveredNodeIndex}
|
hoveredNodeIndex={hoveredNodeIndex}
|
||||||
onHoverNode={handleHoverNode}
|
onHoverNode={handleHoverNode}
|
||||||
transformMode={transformMode}
|
transformMode={transformMode}
|
||||||
@@ -207,6 +217,9 @@ export function EditorPage(): React.JSX.Element {
|
|||||||
? sceneData.mapNodes[selectedNodeIndex].name || null
|
? sceneData.mapNodes[selectedNodeIndex].name || null
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
isSelectionLocked={isSelectionLocked}
|
||||||
|
onSelectionLockToggle={handleSelectionLockToggle}
|
||||||
|
onClearSelection={handleClearSelection}
|
||||||
undoCount={undoCount}
|
undoCount={undoCount}
|
||||||
redoCount={redoCount}
|
redoCount={redoCount}
|
||||||
onUndo={handleUndo}
|
onUndo={handleUndo}
|
||||||
|
|||||||
Reference in New Issue
Block a user