Consolidate duplicated editor code, trim comments, refresh README
Split GraphCanvas (1076 -> 831 lines) into EdgeLayer and graphMenus; the port and marker menus were the same list and are now one function. Value specs of the inspector and the list tab share valueSpecs.ts, and the remaining copies (compound commands, type glyphs, typed asset lookup, name rule, play/editor world setup, marquee) are merged. Four copies had already drifted apart; merging them fixes: - "Disconnect N links" in the port menu is one undo step, not N - "+ Add collider" is hidden on a placed container instance, as in the hierarchy menu, instead of always failing - a bool property coerced from the text "false" becomes false - a port command on an unknown node type reports instead of crashing Comment-only edits leave the bundle byte-identical. README corrected in 35 places (step limit, jump-run clock, suite duration, 409 body, ...). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9212e61c4c
commit
8f92a06c84
96 files changed
+999
-1261
No files matched your search
+11
-26
@@ -6,6 +6,7 @@ import { resolvedHolder } from '../core/model/instance';
|
||||
import {
|
||||
createEmptyProject,
|
||||
findAsset,
|
||||
findAssetOf,
|
||||
findObjectHolder,
|
||||
findScene,
|
||||
type Project,
|
||||
@@ -89,13 +90,8 @@ export type EditorEvent =
|
||||
type Listener = () => void;
|
||||
|
||||
/**
|
||||
* Single source of truth for the editor: the project, undo stack, selection, mode and
|
||||
* save status. Panels subscribe to coarse events and refresh themselves; there is no
|
||||
* framework and no virtual DOM.
|
||||
*
|
||||
* **Play mode edits a copy.** `enterPlay()` swaps `project` for a deep clone (same ids) with
|
||||
* its own undo stack; every panel and command keeps working on it, the running world mirrors
|
||||
* it, and `exitPlay()` throws the copy away and restores the saved model untouched.
|
||||
* Single source of truth for the editor. Play mode swaps `project` for a deep clone (same ids)
|
||||
* with its own undo stack; panels and commands keep working on it, and `exitPlay()` discards it.
|
||||
*/
|
||||
export class EditorState {
|
||||
project: Project = createEmptyProject('', 'Untitled');
|
||||
@@ -105,9 +101,8 @@ export class EditorState {
|
||||
listSlot: ListSlot | null = null;
|
||||
mode: EditorMode = 'edit';
|
||||
/**
|
||||
* Play is on but the world is not being ticked. A sub-state of `mode === 'play'` rather than a
|
||||
* third mode: everything that distinguishes playing from editing (the runtime copy, the
|
||||
* keyboard, edits not being saved) is just as true while paused.
|
||||
* A sub-state of play rather than a third mode: the runtime copy, the keyboard and unsaved edits all
|
||||
* still apply.
|
||||
*/
|
||||
paused = false;
|
||||
saveStatus: SaveStatus = 'saved';
|
||||
@@ -118,7 +113,6 @@ export class EditorState {
|
||||
|
||||
private readonly listeners = new Map<EditorEvent, Set<Listener>>();
|
||||
|
||||
/** Subscribes to an event; returns the unsubscribe function. */
|
||||
on(event: EditorEvent, listener: Listener): () => void {
|
||||
let set = this.listeners.get(event);
|
||||
if (!set) this.listeners.set(event, (set = new Set()));
|
||||
@@ -147,10 +141,7 @@ export class EditorState {
|
||||
this.emit('mode');
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches to play mode: from now on `project` is a runtime copy of the model. Returns
|
||||
* that copy (the world is instantiated from it). Selection is kept – ids are identical.
|
||||
*/
|
||||
/** Returns the runtime copy; the selection survives because ids are identical. */
|
||||
enterPlay(): Project {
|
||||
if (this.mode === 'play') return this.project;
|
||||
const copy = deserializeProject(serializeProject(this.project), this.project.id);
|
||||
@@ -171,7 +162,6 @@ export class EditorState {
|
||||
this.emit('mode');
|
||||
}
|
||||
|
||||
/** Leaves play mode: the runtime copy is discarded, the saved model comes back unchanged. */
|
||||
exitPlay(): void {
|
||||
if (this.mode !== 'play') return;
|
||||
if (this.edit) {
|
||||
@@ -285,7 +275,6 @@ export class EditorState {
|
||||
this.emit('selection');
|
||||
}
|
||||
|
||||
/** Ctrl/Shift-click: adds the object to the selection or removes it again. */
|
||||
toggleObject(holderId: string, objectId: string): void {
|
||||
const s = this.selection;
|
||||
const current = s?.kind === 'object' && s.holderId === holderId ? s.objectIds : [];
|
||||
@@ -295,7 +284,6 @@ export class EditorState {
|
||||
this.select(objectSelection(holderId, next, current.includes(objectId) ? undefined : objectId));
|
||||
}
|
||||
|
||||
/** Ctrl/Shift-click: adds the node to the selection or removes it again. */
|
||||
toggleNode(assetId: string, nodeId: string): void {
|
||||
const s = this.selection;
|
||||
const current = s?.kind === 'node' && s.assetId === assetId ? s.nodeIds : [];
|
||||
@@ -305,12 +293,10 @@ export class EditorState {
|
||||
this.select(nodeSelection(assetId, next, held ? undefined : nodeId) ?? { kind: 'asset', assetId });
|
||||
}
|
||||
|
||||
/** Ids of the selected scene objects (empty unless objects are selected). */
|
||||
get selectedObjectIds(): string[] {
|
||||
return this.selection?.kind === 'object' ? this.selection.objectIds : [];
|
||||
}
|
||||
|
||||
/** Ids of the selected graph nodes (empty unless nodes are selected). */
|
||||
get selectedNodeIds(): string[] {
|
||||
return this.selection?.kind === 'node' ? this.selection.nodeIds : [];
|
||||
}
|
||||
@@ -327,9 +313,9 @@ export class EditorState {
|
||||
if (alive.length !== s.objectIds.length) this.select(objectSelection(s.holderId, alive, s.objectId));
|
||||
}
|
||||
if (s.kind === 'node') {
|
||||
const asset = findAsset(this.project, s.assetId);
|
||||
const asset = findAssetOf(this.project, s.assetId, 'behavior');
|
||||
// Fall back to the behavior asset so the graph stays open when its node vanished (undo).
|
||||
if (!asset || asset.type !== 'behavior') this.select(null);
|
||||
if (!asset) this.select(null);
|
||||
else {
|
||||
const alive = s.nodeIds.filter((id) => asset.graph.nodes.some((n) => n.id === id));
|
||||
if (alive.length !== s.nodeIds.length)
|
||||
@@ -337,9 +323,9 @@ export class EditorState {
|
||||
}
|
||||
}
|
||||
if (s.kind === 'frame') {
|
||||
const asset = findAsset(this.project, s.assetId);
|
||||
const asset = findAssetOf(this.project, s.assetId, 'texture');
|
||||
// Same fallback as a graph node: the texture stays open when its frame vanished.
|
||||
if (!asset || asset.type !== 'texture') this.select(null);
|
||||
if (!asset) this.select(null);
|
||||
else if (!asset.frames.some((f) => f.id === s.frameId))
|
||||
this.select({ kind: 'asset', assetId: s.assetId });
|
||||
}
|
||||
@@ -350,8 +336,7 @@ export class EditorState {
|
||||
const s = this.selection;
|
||||
if (!s) return null;
|
||||
if (s.kind !== 'asset' && s.kind !== 'frame') return null;
|
||||
const asset = findAsset(this.project, s.assetId);
|
||||
return asset && asset.type === 'texture' ? asset.id : null;
|
||||
return findAssetOf(this.project, s.assetId, 'texture')?.id ?? null;
|
||||
}
|
||||
|
||||
/** The behavior asset the current selection refers to (asset, placed object or node), if any. */
|
||||
|
||||
Reference in new issue
Block a user