diff --git a/frontend/apps/artcraft/app/src/pages/PageScene/engine/editor/actions/ColorAction.ts b/frontend/apps/artcraft/app/src/pages/PageScene/engine/editor/actions/ColorAction.ts new file mode 100644 index 0000000000..3cde028590 --- /dev/null +++ b/frontend/apps/artcraft/app/src/pages/PageScene/engine/editor/actions/ColorAction.ts @@ -0,0 +1,24 @@ +import type Editor from "../../editor"; +import type { UndoableAction } from "../HistoryManager"; + +// Records a color change. apply() and revert() both call +// activeScene.setColor — the engine handles material magic. The action +// captures the uuid + before/after hex pair at construction time. +export class ColorAction implements UndoableAction { + readonly label = "Color"; + + constructor( + private readonly editor: Editor, + private readonly uuid: string, + private readonly before: string, + private readonly after: string, + ) {} + + apply(): void { + this.editor.activeScene.setColor(this.uuid, this.after); + } + + revert(): void { + this.editor.activeScene.setColor(this.uuid, this.before); + } +} diff --git a/frontend/apps/artcraft/app/src/pages/PageScene/engine/editor/actions/CreateAction.ts b/frontend/apps/artcraft/app/src/pages/PageScene/engine/editor/actions/CreateAction.ts new file mode 100644 index 0000000000..945d3763f8 --- /dev/null +++ b/frontend/apps/artcraft/app/src/pages/PageScene/engine/editor/actions/CreateAction.ts @@ -0,0 +1,36 @@ +import * as THREE from "three"; +import type Editor from "../../editor"; +import type { UndoableAction } from "../HistoryManager"; +import { + ObjectSnap, + recreateFromSnapshot, + snapshotObject, +} from "./snapshots"; + +// Records the creation of an object. apply() re-creates from the +// captured snapshot (used on redo). revert() removes the object using +// the lower-level utils.deleteObject path so it does NOT recurse +// through editor.deleteObject (which would try to record a fresh +// delete entry). +export class CreateAction implements UndoableAction { + readonly label: string; + private readonly snap: ObjectSnap; + + constructor( + private readonly editor: Editor, + obj: THREE.Object3D, + ) { + this.snap = snapshotObject(obj); + this.label = `Create ${this.snap.name}`; + } + + async apply(): Promise { + await recreateFromSnapshot(this.editor, this.snap); + this.editor.selection.refreshOutliner(); + } + + async revert(): Promise { + this.editor.utils.deleteObject(this.snap.uuid); + this.editor.selection.refreshOutliner(); + } +} diff --git a/frontend/apps/artcraft/app/src/pages/PageScene/engine/editor/actions/DeleteAction.ts b/frontend/apps/artcraft/app/src/pages/PageScene/engine/editor/actions/DeleteAction.ts new file mode 100644 index 0000000000..2c051bee5b --- /dev/null +++ b/frontend/apps/artcraft/app/src/pages/PageScene/engine/editor/actions/DeleteAction.ts @@ -0,0 +1,35 @@ +import * as THREE from "three"; +import type Editor from "../../editor"; +import type { UndoableAction } from "../HistoryManager"; +import { + ObjectSnap, + recreateFromSnapshot, + snapshotObject, +} from "./snapshots"; + +// Records the deletion of an object. Mirror of CreateAction with +// apply/revert swapped. apply() removes via utils.deleteObject (the +// non-recording path) so a redo of a delete during replay can't +// re-trigger recordDelete. +export class DeleteAction implements UndoableAction { + readonly label: string; + private readonly snap: ObjectSnap; + + constructor( + private readonly editor: Editor, + obj: THREE.Object3D, + ) { + this.snap = snapshotObject(obj); + this.label = `Delete ${this.snap.name}`; + } + + async apply(): Promise { + this.editor.utils.deleteObject(this.snap.uuid); + this.editor.selection.refreshOutliner(); + } + + async revert(): Promise { + await recreateFromSnapshot(this.editor, this.snap); + this.editor.selection.refreshOutliner(); + } +} diff --git a/frontend/apps/artcraft/app/src/pages/PageScene/engine/editor/actions/LockAction.ts b/frontend/apps/artcraft/app/src/pages/PageScene/engine/editor/actions/LockAction.ts new file mode 100644 index 0000000000..306d114ff0 --- /dev/null +++ b/frontend/apps/artcraft/app/src/pages/PageScene/engine/editor/actions/LockAction.ts @@ -0,0 +1,27 @@ +import type { SelectionBridge } from "../SelectionBridge"; +import type { UndoableAction } from "../HistoryManager"; + +// Records a lock-toggle. Routes both apply() and revert() through +// SelectionBridge.setLockState so the gizmo attach/detach side effects +// fire — the same path the user-toggle uses, just at a target state +// instead of a flip. +export class LockAction implements UndoableAction { + readonly label: string; + + constructor( + private readonly selection: SelectionBridge, + private readonly uuid: string, + private readonly before: boolean, + private readonly after: boolean, + ) { + this.label = after ? "Lock" : "Unlock"; + } + + apply(): void { + this.selection.setLockState(this.uuid, this.after); + } + + revert(): void { + this.selection.setLockState(this.uuid, this.before); + } +} diff --git a/frontend/apps/artcraft/app/src/pages/PageScene/engine/editor/actions/TransformAction.ts b/frontend/apps/artcraft/app/src/pages/PageScene/engine/editor/actions/TransformAction.ts new file mode 100644 index 0000000000..4d885194fe --- /dev/null +++ b/frontend/apps/artcraft/app/src/pages/PageScene/engine/editor/actions/TransformAction.ts @@ -0,0 +1,58 @@ +import type Editor from "../../editor"; +import type { UndoableAction } from "../HistoryManager"; +import { + TransformSnap, + snapshotTransform, + transformsEqual, + writeTransform, +} from "./snapshots"; + +// Records a transform change. Caller holds the instance between the +// drag/edit boundary callbacks (gizmo dragstart/dragend, panel input +// focus/blur) and calls commit() at the end. If the transform didn't +// actually change, commit() returns false and the caller drops the +// action without recording. +// +// Begin/end transaction state lives on the action itself — no need for +// a per-manager pendingTransform field. +export class TransformAction implements UndoableAction { + readonly label = "Transform"; + private readonly before: TransformSnap; + private after?: TransformSnap; + + constructor( + private readonly editor: Editor, + private readonly uuid: string, + ) { + const obj = editor.activeScene.scene.getObjectByProperty("uuid", uuid); + if (!obj) { + this.before = { + position: { x: 0, y: 0, z: 0 }, + rotation: { x: 0, y: 0, z: 0 }, + scale: { x: 1, y: 1, z: 1 }, + }; + return; + } + this.before = snapshotTransform(obj); + } + + // Capture the after-state from the live object. Returns true if it + // differs from the before-state (worth recording). + commit(): boolean { + const obj = this.editor.activeScene.scene.getObjectByProperty( + "uuid", + this.uuid, + ); + if (!obj) return false; + this.after = snapshotTransform(obj); + return !transformsEqual(this.before, this.after); + } + + apply(): void { + if (this.after) writeTransform(this.editor, this.uuid, this.after); + } + + revert(): void { + writeTransform(this.editor, this.uuid, this.before); + } +} diff --git a/frontend/apps/artcraft/app/src/pages/PageScene/engine/editor/actions/VisibilityAction.ts b/frontend/apps/artcraft/app/src/pages/PageScene/engine/editor/actions/VisibilityAction.ts new file mode 100644 index 0000000000..ef420324f4 --- /dev/null +++ b/frontend/apps/artcraft/app/src/pages/PageScene/engine/editor/actions/VisibilityAction.ts @@ -0,0 +1,36 @@ +import type Editor from "../../editor"; +import type { UndoableAction } from "../HistoryManager"; + +// Records a visibility toggle. Writes both obj.visible and +// userData.visible directly. The outliner refresh fires in +// HistoryManager's replay wrapper so the row icon stays in sync. +export class VisibilityAction implements UndoableAction { + readonly label: string; + + constructor( + private readonly editor: Editor, + private readonly uuid: string, + private readonly before: boolean, + private readonly after: boolean, + ) { + this.label = after ? "Show" : "Hide"; + } + + apply(): void { + this.write(this.after); + } + + revert(): void { + this.write(this.before); + } + + private write(visible: boolean): void { + const obj = this.editor.activeScene.scene.getObjectByProperty( + "uuid", + this.uuid, + ); + if (!obj) return; + obj.visible = visible; + obj.userData["visible"] = visible; + } +}