Editing while playing: play runs on a runtime copy, new Game tab (Scene | Game | Graph)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
cb8885359f
commit
c3a533c4e5
14 files changed
+408
-118
No files matched your search
+58
-6
@@ -2,6 +2,7 @@ import { type Command, CommandError } from '../core/commands/Command';
|
||||
import { CommandStack } from '../core/commands/CommandStack';
|
||||
import type { SceneAsset } from '../core/model/Asset';
|
||||
import { createEmptyProject, findScene, type Project } from '../core/model/Project';
|
||||
import { deserializeProject, serializeProject } from '../core/store/serialize';
|
||||
|
||||
export type Selection =
|
||||
| { kind: 'asset'; assetId: string }
|
||||
@@ -19,7 +20,9 @@ export type EditorEvent =
|
||||
| 'selection'
|
||||
| 'mode'
|
||||
| 'save'
|
||||
| 'notice';
|
||||
| 'notice'
|
||||
/** The running world advanced a frame (play mode): values may have changed without a command. */
|
||||
| 'runtime';
|
||||
|
||||
type Listener = () => void;
|
||||
|
||||
@@ -27,6 +30,10 @@ 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.
|
||||
*/
|
||||
export class EditorState {
|
||||
project: Project = createEmptyProject('', 'Untitled');
|
||||
@@ -37,6 +44,8 @@ export class EditorState {
|
||||
saveStatus: SaveStatus = 'saved';
|
||||
/** Last user-facing message (errors, hints). */
|
||||
notice = '';
|
||||
/** The real (saved) model and its undo stack while `project` is the play-mode copy. */
|
||||
private edit: { project: Project; commands: CommandStack } | null = null;
|
||||
|
||||
private readonly listeners = new Map<EditorEvent, Set<Listener>>();
|
||||
|
||||
@@ -54,6 +63,8 @@ export class EditorState {
|
||||
// --- project lifecycle ---------------------------------------------------------------
|
||||
|
||||
loadProject(project: Project, revision: number): void {
|
||||
this.edit = null;
|
||||
this.mode = 'edit';
|
||||
this.project = project;
|
||||
this.revision = revision;
|
||||
this.commands = new CommandStack(project);
|
||||
@@ -63,6 +74,39 @@ export class EditorState {
|
||||
this.emit('project');
|
||||
this.emit('selection');
|
||||
this.emit('save');
|
||||
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.
|
||||
*/
|
||||
enterPlay(): Project {
|
||||
if (this.mode === 'play') return this.project;
|
||||
const copy = deserializeProject(serializeProject(this.project), this.project.id);
|
||||
this.edit = { project: this.project, commands: this.commands };
|
||||
this.project = copy;
|
||||
this.commands = new CommandStack(copy);
|
||||
this.mode = 'play';
|
||||
this.emit('mode');
|
||||
this.emit('project');
|
||||
return copy;
|
||||
}
|
||||
|
||||
/** Leaves play mode: the runtime copy is discarded, the saved model comes back unchanged. */
|
||||
exitPlay(): void {
|
||||
if (this.mode !== 'play') return;
|
||||
if (this.edit) {
|
||||
this.project = this.edit.project;
|
||||
this.commands = this.edit.commands;
|
||||
this.edit = null;
|
||||
}
|
||||
this.mode = 'edit';
|
||||
this.emit('mode');
|
||||
this.emit('project');
|
||||
this.validateSelection();
|
||||
// A save postponed while playing runs now.
|
||||
this.emit('save');
|
||||
}
|
||||
|
||||
get activeScene(): SceneAsset | undefined {
|
||||
@@ -71,9 +115,11 @@ export class EditorState {
|
||||
|
||||
// --- editing -------------------------------------------------------------------------
|
||||
|
||||
/** Runs a command through the undo stack. Returns false (and sets a notice) when invalid. */
|
||||
/**
|
||||
* Runs a command through the undo stack. Returns false (and sets a notice) when invalid.
|
||||
* In play mode the command changes the runtime copy only (see `enterPlay`).
|
||||
*/
|
||||
execute(command: Command): boolean {
|
||||
if (this.mode === 'play') return false;
|
||||
try {
|
||||
this.commands.execute(command);
|
||||
} catch (error) {
|
||||
@@ -88,13 +134,13 @@ export class EditorState {
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
if (this.mode === 'play' || !this.commands.undo()) return;
|
||||
if (!this.commands.undo()) return;
|
||||
this.validateSelection();
|
||||
this.markDirty();
|
||||
}
|
||||
|
||||
redo(): void {
|
||||
if (this.mode === 'play' || !this.commands.redo()) return;
|
||||
if (!this.commands.redo()) return;
|
||||
this.validateSelection();
|
||||
this.markDirty();
|
||||
}
|
||||
@@ -107,9 +153,15 @@ export class EditorState {
|
||||
this.emit('project');
|
||||
}
|
||||
|
||||
/** The world ticked: panels showing live values refresh (no model event, nothing to save). */
|
||||
runtimeTick(): void {
|
||||
this.emit('runtime');
|
||||
}
|
||||
|
||||
private markDirty(): void {
|
||||
this.validateSelection();
|
||||
this.saveStatus = 'dirty';
|
||||
// Play-mode edits live in the runtime copy and are never saved.
|
||||
if (this.mode === 'edit') this.saveStatus = 'dirty';
|
||||
this.emit('project');
|
||||
this.emit('save');
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user