From abf2e1d6c2da0d10cb861d4ee7622cee12b1e18e Mon Sep 17 00:00:00 2001 From: Tanish Manku Date: Thu, 30 Apr 2026 11:05:00 -0700 Subject: [PATCH] PageScene: extract GizmoController from editor.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third step of the editor.ts decomposition. Pulls TransformControls ownership and the gizmo lifecycle out of Editor into a focused module. GizmoController owns: control (TransformControls). Methods take their dependencies as method arguments — no `editor` reference, no `import type Editor`, no circular import: configure(camera, domElement, scene, callbacks) changeMode(type, space) setSpace(space) canToggleSpace() attach(object), detach() addToScene(scene), removeFromScene(scene) setVisible(v), isVisible() Editor exposes the gizmo as `editor.gizmo` and constructs it empty in the constructor; configure() is called from initialize() once camera and renderer exist. The two existing public methods stay on Editor as facades, so the external callers (actions/setTransformMode.ts, comps/Controls3D.tsx, engine/keymap.ts) keep working unchanged: Editor.change_mode reads the store's transformSpace and forwards. Editor.toggleTransformSpace toggles the store and pushes into gizmo. External readers updated: helper.ts (SceneUtils.lockUnlockObject + removeTransformControls) routes through editor.gizmo.attach/detach/addToScene/removeFromScene instead of poking editor.control directly. Editor.snapShotOfCurrentFrame uses editor.gizmo.setVisible / isVisible instead of touching control.visible. Editor passes editor.gizmo.control to the MouseControls constructor where it previously passed editor.control. Dead code removed: - transform_interaction on Editor: passed by value to MouseControls (which has its own internal copy that overrides) and the `= true` write in change_mode mutated a field nothing read. Pure dead code. - The "if (this.control == undefined) return;" guard immediately after `new TransformControls(...)` — that branch was unreachable. - Unused TransformControls import in editor.ts. Net: editor.ts down to 1196 LOC. Typecheck error count drops by 3. --- .../app/src/pages/PageScene/engine/editor.ts | 76 +++++++--------- .../engine/editor/GizmoController.ts | 91 +++++++++++++++++++ .../app/src/pages/PageScene/engine/helper.ts | 16 ++-- 3 files changed, 129 insertions(+), 54 deletions(-) create mode 100644 frontend/apps/artcraft/app/src/pages/PageScene/engine/editor/GizmoController.ts diff --git a/frontend/apps/artcraft/app/src/pages/PageScene/engine/editor.ts b/frontend/apps/artcraft/app/src/pages/PageScene/engine/editor.ts index 2df8751c23..254b423134 100644 --- a/frontend/apps/artcraft/app/src/pages/PageScene/engine/editor.ts +++ b/frontend/apps/artcraft/app/src/pages/PageScene/engine/editor.ts @@ -5,7 +5,6 @@ import { lookAtFromCamera, type FreeCamControlState, } from "./cameraMath"; -import { TransformControls } from "./TransformControls.js"; import Scene from "./scene.js"; import { APIManager } from "./api_manager.js"; import { PointerLockControls } from "three/addons/controls/PointerLockControls.js"; @@ -27,6 +26,7 @@ import { MediaUploadApi } from "~/Classes/ApiManager"; import { SceneManager } from "./scene_manager_api"; import { ViewportController } from "./editor/ViewportController"; import { PostProcessingPipeline } from "./editor/PostProcessingPipeline"; +import { GizmoController } from "./editor/GizmoController"; import Stats from "three/examples/jsm/libs/stats.module.js"; import { SparkRenderer } from "@sparkjsdev/spark"; @@ -63,13 +63,11 @@ class Editor { last_cam_pos: THREE.Vector3; last_cam_rot: THREE.Euler; - control: TransformControls | undefined; raycaster: THREE.Raycaster | undefined; mouse: THREE.Vector2 | undefined; selected: THREE.Object3D | undefined; last_selected: THREE.Object3D | undefined; last_selected_sum: number | undefined; - transform_interaction = false; rendering: boolean; api_manager: APIManager; freeCamState: FreeCamControlState | null = null; @@ -111,6 +109,8 @@ class Editor { viewport: ViewportController; // Owns the EffectComposer chains and post-process passes. postProcessing: PostProcessingPipeline; + // Owns the TransformControls gizmo. + gizmo: GizmoController; selectedCanvas: boolean; startRenderHeight: number; @@ -188,6 +188,7 @@ class Editor { // PostProcessingPipeline must exist before Scene because Scene's // load paths invoke updateSurfaceIdAttributeToMesh as a callback. this.postProcessing = new PostProcessingPipeline(); + this.gizmo = new GizmoController(); this.activeScene = new Scene( "" + this.version, @@ -432,33 +433,26 @@ class Editor { // FreeCam math + listeners now live in hooks/useFreeCam.ts; the // editor reads `freeCamState` (set by that hook) on every render. - this.control = new TransformControls(this.camera, this.renderer.domElement); - this.control.space = "world"; // Default to world space for translate mode - this.control.setScaleSnap(0.01); - this.control.setTranslationSnap(0.01); - this.control.setRotationSnap(0.01); - //console.log("Control Sensitivity:", this.control.sensitivity); + this.gizmo.configure( + this.camera, + this.renderer.domElement, + this.activeScene.scene, + { + onChange: () => this.renderScene(), + onDraggingChanged: (dragging) => { + this.updateSelectedUI(); + this.camera_last_pos.copy(new THREE.Vector3(-99999, -99999, -99999)); + this.focused = !dragging; + }, + }, + ); - // Base control and debug stuff remove debug in prod. - if (this.control == undefined) { - return; - } - this.control.addEventListener("change", this.renderScene.bind(this)); - this.control.addEventListener("dragging-changed", (event: any) => { - //TODO: any should be the following - this.updateSelectedUI(); - this.camera_last_pos.copy(new THREE.Vector3(-99999, -99999, -99999)); - this.focused = !event.value; - // this.update_properties() - }); - this.control.setSize(0.5); // Good default value for visuals. this.raycaster = new THREE.Raycaster(); // Configure raycaster to check both layers this.raycaster.layers.set(0); // Enable default layer this.raycaster.layers.enable(1); // Also check objects on the custom layer this.mouse = new THREE.Vector2(); - this.activeScene.scene.add(this.control); // Resets canvas size. this.viewport.onWindowResize(); @@ -485,12 +479,12 @@ class Editor { this.mouse, this.mouse, this.raycaster, - this.control, + this.gizmo.control, this.postProcessing.outlinePass, this.activeScene.scene, this.publishSelect.bind(this), this.updateSelectedUI.bind(this), - this.transform_interaction, + false, this.last_selected, this.getAssetType.bind(this), this.setSelected.bind(this), @@ -587,10 +581,8 @@ class Editor { store.setGridVisible(false); // Store and hide transform controls - const wasControlVisible = this.control?.visible ?? false; - if (this.control) { - this.control.visible = false; - } + const wasControlVisible = this.gizmo.isVisible(); + this.gizmo.setVisible(false); // Store and disable outline pass const outlinePass = this.postProcessing.outlinePass; @@ -696,9 +688,7 @@ class Editor { usePageSceneStore.getState().setGridVisible(wasGridVisible); // Restore transform controls visibility - if (this.control) { - this.control.visible = wasControlVisible; - } + this.gizmo.setVisible(wasControlVisible); // Restore outline pass if (outlinePass) { @@ -1051,26 +1041,22 @@ class Editor { console.log("3D Editor Engine unmounted"); } + // Facade for actions/setTransformMode + keymap dispatch. Reads the + // store's current transform space and forwards to the gizmo. change_mode(type: "translate" | "rotate" | "scale") { - if (this.control == undefined) { - return; - } - this.control.mode = type; - this.control.space = - type === "scale" - ? "local" - : usePageSceneStore.getState().transformSpace; - this.transform_interaction = true; + const space = + type === "scale" ? "local" : usePageSceneStore.getState().transformSpace; + this.gizmo.changeMode(type, space); } + // Facade for Controls3D button + keymap. Toggles the store's + // transform space and pushes the new value into the gizmo. toggleTransformSpace() { - if (this.control == undefined || this.control.mode === "scale") { - return; - } + if (!this.gizmo.canToggleSpace()) return; const store = usePageSceneStore.getState(); const next = store.transformSpace === "world" ? "local" : "world"; store.setTransformSpace(next); - this.control.space = next; + this.gizmo.setSpace(next); } togglePlayback() { diff --git a/frontend/apps/artcraft/app/src/pages/PageScene/engine/editor/GizmoController.ts b/frontend/apps/artcraft/app/src/pages/PageScene/engine/editor/GizmoController.ts new file mode 100644 index 0000000000..e7b444b4f0 --- /dev/null +++ b/frontend/apps/artcraft/app/src/pages/PageScene/engine/editor/GizmoController.ts @@ -0,0 +1,91 @@ +// Owns the TransformControls gizmo used for translating/rotating/scaling +// the selected object. Construction is two-phase to match the existing +// editor lifecycle: the controller is created up front (held as a field +// on Editor), then `configure()` wires it to the camera/renderer once +// initialize() has produced them. +// +// No `editor` reference, no `import type Editor`. Store access stays on +// Editor's facades (change_mode, toggleTransformSpace); this module only +// translates them into TransformControls calls. + +import type * as THREE from "three"; +import { TransformControls } from "../TransformControls.js"; + +export type TransformMode = "translate" | "rotate" | "scale"; +export type TransformSpace = "world" | "local"; + +export type GizmoCallbacks = { + // Fires on every internal TransformControls "change" event — Editor + // uses this to re-render the scene during a drag. + onChange: () => void; + // Fires when a drag starts or ends. `dragging` is true on drag start. + onDraggingChanged: (dragging: boolean) => void; +}; + +export class GizmoController { + control: TransformControls | undefined; + + configure( + camera: THREE.PerspectiveCamera | null, + domElement: HTMLElement, + scene: THREE.Scene, + callbacks: GizmoCallbacks, + ) { + if (camera == undefined) return; + this.control = new TransformControls(camera, domElement); + this.control.space = "world"; + this.control.setScaleSnap(0.01); + this.control.setTranslationSnap(0.01); + this.control.setRotationSnap(0.01); + this.control.setSize(0.5); + this.control.addEventListener("change", callbacks.onChange); + this.control.addEventListener("dragging-changed", (event: any) => { + callbacks.onDraggingChanged(event.value); + }); + scene.add(this.control); + } + + changeMode(type: TransformMode, space: TransformSpace) { + if (this.control == undefined) return; + this.control.mode = type; + this.control.space = space; + } + + setSpace(space: TransformSpace) { + if (this.control == undefined) return; + this.control.space = space; + } + + // True when the gizmo exists and is in a mode where toggling between + // world/local space is meaningful (scale is always local). + canToggleSpace(): boolean { + return this.control != undefined && this.control.mode !== "scale"; + } + + attach(object: THREE.Object3D) { + this.control?.attach(object); + } + + detach() { + this.control?.detach(); + } + + // Add/remove the gizmo from the scene. Used by SceneUtils when + // locking/unlocking objects: the gizmo is yanked out of the scene on + // lock and re-added on unlock. + addToScene(scene: THREE.Scene) { + if (this.control) scene.add(this.control); + } + + removeFromScene(scene: THREE.Scene) { + if (this.control) scene.remove(this.control); + } + + setVisible(visible: boolean) { + if (this.control) this.control.visible = visible; + } + + isVisible(): boolean { + return this.control?.visible ?? false; + } +} diff --git a/frontend/apps/artcraft/app/src/pages/PageScene/engine/helper.ts b/frontend/apps/artcraft/app/src/pages/PageScene/engine/helper.ts index aa46f099f3..22a9bb9520 100644 --- a/frontend/apps/artcraft/app/src/pages/PageScene/engine/helper.ts +++ b/frontend/apps/artcraft/app/src/pages/PageScene/engine/helper.ts @@ -64,12 +64,10 @@ export class SceneUtils { if (object.userData["locked"]) { this.removeTransformControls(false); - } else if (this.editor.control) { - this.scene.scene.add(this.editor.control); - if (this.editor.sceneManager?.selected_objects) - this.editor.control.attach( - this.editor.sceneManager?.selected_objects[0], - ); + } else { + this.editor.gizmo.addToScene(this.scene.scene); + const selected = this.editor.sceneManager?.selected_objects?.[0]; + if (selected) this.editor.gizmo.attach(selected); } return object.userData["locked"]; @@ -80,7 +78,7 @@ export class SceneUtils { // Removes transform controls and publishes selected. removeTransformControls(remove_outline: boolean = true) { - if (this.editor.control == undefined) { + if (this.editor.gizmo.control == undefined) { return; } const outlinePass = this.editor.postProcessing.outlinePass; @@ -92,8 +90,8 @@ export class SceneUtils { outlinePass.selectedObjects = []; this.editor.publishSelect(); } - this.editor.control.detach(); - this.editor.activeScene.scene.remove(this.editor.control); + this.editor.gizmo.detach(); + this.editor.gizmo.removeFromScene(this.editor.activeScene.scene); if (remove_outline) outlinePass.selectedObjects = []; }