PageScene: route create/delete/lock/visible/paste through HistoryManager

Wires creation, deletion, lock, visibility, and paste through the new
HistoryManager. Mutator-recorded — every external call site keeps
the same signature it had before; recording happens inside the mutator
or its dep callback, so callers don't grow new responsibilities.

Recordings added:
- actions/addObject, addCharacter, addShape: editor.history.recordCreate(obj)
  replaces the legacy add_creation_undostack call.
- editor.deleteObject: snapshots the target before delegating to
  utils.deleteObject so undo can fully reconstruct it.
- SelectionBridge.lockUnlockObject: takes a recordLockChange dep
  callback, records before/after locked state inline. No coupling
  back to HistoryManager from the bridge.
- Outliner.handleToggleVisibility: records visible before/after at the
  call site (hideObject lives on SceneManager which has no history dep).
- SceneManager.paste: now returns the new Object3D so keymap's paste
  handler can record it via editor.history.recordCreate. The legacy
  add_creation_undostack call inside paste is removed.

keymap.ts: undo/redo target editor.history; deleteSelected routes
through editor.deleteObject (so history records); paste captures the
returned object and records.

Transform recording (gizmo drag, panel inputs) and color stay on the
old check_scene_for_updates diff path until follow-up commits. Old
SceneManager undo machinery (undoStack, undo/redo methods,
add_creation_undostack, the diff helpers) is still in place but only
the diff path drives transforms now.
This commit is contained in:
Tanish Manku committed 2026-05-01 01:46:21 -07:00
1 parent 94430a8817
commit 6632dd34cf
8 files changed
+25 -9

No files matched your search

@@ -17,7 +17,7 @@ export async function addCharacter(
obj.userData.isCharacter = true;
await editor.sceneManager?.add_creation_undostack(obj);
editor.history.recordCreate(obj);
usePageSceneStore.getState().addCharacter({
id: obj.uuid,
@@ -15,7 +15,7 @@ export async function addObject(
);
if (!obj) return undefined;
await editor.sceneManager?.add_creation_undostack(obj);
editor.history.recordCreate(obj);
usePageSceneStore.getState().addObject({
id: obj.uuid,
@@ -16,7 +16,7 @@ export async function addShape(
if (!obj) return undefined;
obj.name = item.name ?? "shape";
await editor.sceneManager?.add_creation_undostack(obj);
editor.history.recordCreate(obj);
usePageSceneStore.getState().addShape({
id: obj.uuid,
@@ -53,8 +53,10 @@ const OutlinerRow = ({ item }: { item: OutlinerItem }) => {
const handleToggleVisibility = (e: React.MouseEvent) => {
e.stopPropagation();
const before = item.visible;
usePageSceneStore.getState().toggleOutlinerVisibility(item.id);
editorEngine?.sceneManager?.hideObject(item.id);
editorEngine?.history.recordSetVisible(item.id, before, !before);
};
return (
@@ -171,6 +171,8 @@ class Editor {
const selected = this.sceneManager?.selected_objects?.[0];
if (selected) this.gizmo.attach(selected);
},
recordLockChange: (uuid, before, after) =>
this.history.recordSetLocked(uuid, before, after),
});
this.activeScene = new Scene(
@@ -732,6 +734,9 @@ class Editor {
}
deleteObject(uuid: string) {
// Snapshot for the undo stack before the object is gone.
const obj = this.activeScene.scene.getObjectByProperty("uuid", uuid);
if (obj) this.history.recordDelete(obj);
this.mouse_controls?.clearFKVisuals();
this.mouse_controls?.removeTransformControls(true);
this.utils.deleteObject(uuid);
@@ -29,6 +29,9 @@ export type SelectionBridgeDeps = {
isObjectLocked: (uuid: string) => boolean;
removeTransformControls: () => void;
attachGizmoToCurrentSelection: () => void;
// Push a lock-toggle entry onto the undo stack. Optional so the bridge
// can be constructed before HistoryManager (and to keep tests simple).
recordLockChange?: (uuid: string, before: boolean, after: boolean) => void;
};
export class SelectionBridge {
@@ -46,6 +49,7 @@ export class SelectionBridge {
// the gizmo (lock) or re-attach it to the current selection (unlock).
// Returns the new locked state.
lockUnlockObject(object_uuid: string): boolean {
const before = this.engine.isObjectLocked(object_uuid);
const locked = this.engine.toggleObjectLocked(object_uuid);
if (locked) {
this.engine.removeTransformControls();
@@ -53,6 +57,7 @@ export class SelectionBridge {
this.engine.attachGizmoToCurrentSelection();
}
this.updateSelectedUI();
this.engine.recordLockChange?.(object_uuid, before, locked);
return locked;
}
@@ -34,8 +34,10 @@ const setGizmoMode = (
const deleteSelected = (editor: Editor) => {
const mc = editor.mouse_controls;
if (!mc?.selected) return;
// Route through editor.deleteObject so HistoryManager records the
// deletion. mc.deleteObject (the legacy direct path) bypasses history.
mc.selected.forEach((sel) => {
mc.deleteObject(sel.uuid);
editor.deleteObject(sel.uuid);
});
mc.selected = [];
mc.removeTransformControls();
@@ -70,11 +72,11 @@ const openAssetModal = () => {
};
const undo = async (editor: Editor) => {
await editor.sceneManager?.undo();
await editor.history.undo();
};
const redo = async (editor: Editor) => {
await editor.sceneManager?.redo();
await editor.history.redo();
};
const copy = async (editor: Editor) => {
@@ -82,7 +84,8 @@ const copy = async (editor: Editor) => {
};
const paste = async (editor: Editor) => {
await editor.sceneManager?.paste();
const obj = await editor.sceneManager?.paste();
if (obj) editor.history.recordCreate(obj);
};
export const buildKeymap = (): KeyBinding[] => [
@@ -306,7 +306,7 @@ export class SceneManager implements SceneManagerAPI {
}
}
public async paste() {
public async paste(): Promise<THREE.Object3D | undefined> {
if (this.copiedObject && this.copiedObject.name != "::CAM::") {
const userdata = this.copiedObject.userData;
const position = this.copiedObject.position.clone();
@@ -353,8 +353,9 @@ export class SceneManager implements SceneManagerAPI {
}
await this.copy();
await this.add_creation_undostack(obj);
return obj;
}
return undefined;
}
public onMouseMove(event: MouseEvent) {