PageScene: extract GizmoController from editor.ts

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.
This commit is contained in:
Tanish Manku committed 2026-04-30 11:05:00 -07:00
1 parent 4c521ab55c
commit abf2e1d6c2
3 files changed
+129 -54

No files matched your search

@@ -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() {
@@ -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;
}
}
@@ -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 = [];
}