PageScene: route lock undo through gizmo side effects

SelectionBridge.lockUnlockObject toggled userData.locked AND
attached/detached the gizmo, but HistoryContext.setLocked only did
the userData write. After Ctrl+Z the lock flag reverted but the
gizmo state stayed wrong.

Extract the side-effect chain (setObjectLocked + gizmo attach/detach
+ updateSelectedUI) into a private SelectionBridge.applyLockState
helper. Both lockUnlockObject (toggle path) and the new
setLockState (history-replay path) call it. Add a SceneUtils
direct-set helper (setObjectLocked) so applyLockState can target a
specific state instead of toggling.

Wire HistoryContext.setLocked through selection.setLockState so undo
of a lock toggle re-runs the gizmo attach/detach. The
recordLockChange dep stays for now; it moves to the call site in a
later commit when the action layer migrates to LockAction.
This commit is contained in:
Tanish Manku committed 2026-05-01 02:54:19 -07:00
1 parent 4a837d49a7
commit d9680ae0a8
3 files changed
+36 -11

No files matched your search

@@ -163,6 +163,8 @@ class Editor {
cameraName: this.cameraController.camera_name,
version: this.version,
toggleObjectLocked: (uuid) => this.utils.toggleObjectLocked(uuid),
setObjectLocked: (uuid, locked) =>
this.utils.setObjectLocked(uuid, locked),
isObjectLocked: (uuid) => this.utils.isObjectLocked(uuid),
removeTransformControls: () =>
this.utils.removeTransformControls(false),
@@ -256,9 +258,9 @@ class Editor {
this.activeScene.setColor(uuid, color);
},
setLocked: (uuid, locked) => {
const obj = this.activeScene.scene.getObjectByProperty("uuid", uuid);
if (!obj) return;
obj.userData.locked = locked;
// Routes through SelectionBridge so the gizmo attach/detach
// side effects fire (matches the original lock toggle path).
this.selection.setLockState(uuid, locked);
},
setVisible: (uuid, visible) => {
const obj = this.activeScene.scene.getObjectByProperty("uuid", uuid);
@@ -26,6 +26,7 @@ export type SelectionBridgeDeps = {
// and the gizmo attach/detach lives on GizmoController. The bridge
// calls them through these callbacks instead of importing either.
toggleObjectLocked: (uuid: string) => boolean; // returns new locked state
setObjectLocked: (uuid: string, locked: boolean) => void; // direct set (history replay)
isObjectLocked: (uuid: string) => boolean;
removeTransformControls: () => void;
attachGizmoToCurrentSelection: () => void;
@@ -45,20 +46,35 @@ export class SelectionBridge {
constructor(private readonly engine: SelectionBridgeDeps) {}
// Toggle the locked flag on an object's userData, then either yank
// 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);
// Centralized side-effect chain shared by the user-toggle path and
// history replay. Sets userData.locked, attaches/detaches the gizmo,
// refreshes the inspector. No recording — the user-facing entry
// points decide whether to push to the undo stack.
private applyLockState(object_uuid: string, locked: boolean) {
this.engine.setObjectLocked(object_uuid, locked);
if (locked) {
this.engine.removeTransformControls();
} else {
this.engine.attachGizmoToCurrentSelection();
}
this.updateSelectedUI();
this.engine.recordLockChange?.(object_uuid, before, locked);
return locked;
}
// Toggle the locked flag on an object's userData, then either yank
// 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 after = !before;
this.applyLockState(object_uuid, after);
this.engine.recordLockChange?.(object_uuid, before, after);
return after;
}
// For history replay: set the locked state directly without recording.
// Runs the same gizmo attach/detach side effects as lockUnlockObject.
setLockState(object_uuid: string, locked: boolean) {
this.applyLockState(object_uuid, locked);
}
// True if the object is currently locked. Pure passthrough.
@@ -43,6 +43,13 @@ export class SceneUtils {
return object.userData["locked"];
}
// Direct setter used by history replay — sets userData.locked without
// toggling. Side effects (gizmo attach/detach) live on SelectionBridge.
setObjectLocked(object_uuid: string, locked: boolean) {
const object = this.scene.get_object_by_uuid(object_uuid);
if (object) object.userData["locked"] = locked;
}
// Removes transform controls and publishes selected.
removeTransformControls(remove_outline: boolean = true) {
if (this.editor.gizmo.control == undefined) {