PageScene: move switchCameraView into CameraController via DI

CameraController now owns switchCameraView. The body lifted from
SceneUtils.switchCameraView gets cross-subsystem capabilities
(scene access, transform-controls removal, selection write,
editor-state and object-panel store actions) through a new
CameraControllerDeps object that Editor wires up in initialize().

This keeps subsystems from importing each other directly: the
controller sees a narrow callback surface, not SelectionBridge or
SceneUtils. Same precedent as the existing ViewportController setters.

Deletes:
- SceneUtils.switchCameraView (helper.ts)
- Editor.switchCameraView facade

The space-bar handler in MouseControls now points directly at
cameraController.switchCameraView via the callback Editor passes in.
This commit is contained in:
Tanish Manku committed 2026-04-30 23:47:59 -07:00
1 parent 554a76d35f
commit 89ac302392
3 files changed
+98 -70

No files matched your search

@@ -137,7 +137,19 @@ class Editor {
// load paths invoke updateSurfaceIdAttributeToMesh as a callback.
this.postProcessing = new PostProcessingPipeline();
this.gizmo = new GizmoController();
this.cameraController = new CameraController();
this.cameraController = new CameraController({
getThreeScene: () => this.activeScene.scene,
getHotItems: () => this.activeScene.hot_items ?? null,
removeTransformControls: () => this.utils.removeTransformControls(),
setSelected: (obj) => {
this.selection.selected = obj ?? undefined;
this.selection.publishSelect();
this.selection.updateSelectedUI();
},
setEditorState: (state) =>
usePageSceneStore.getState().setEditorState(state),
hideObjectPanel: () => usePageSceneStore.getState().hideObjectPanel(),
});
this.selection = new SelectionBridge({
getSceneManager: () => this.sceneManager,
cameraName: this.cameraController.camera_name,
@@ -353,7 +365,7 @@ class Editor {
this.cameraController.freeCamState,
this.cameraController.lockControls,
this.cameraController.camera_last_pos,
this.switchCameraView.bind(this),
this.cameraController.switchCameraView.bind(this.cameraController),
this.deleteObject.bind(this),
this.viewport.canvReference,
this.mouse,
@@ -671,10 +683,6 @@ class Editor {
});
}
switchCameraView() {
this.utils.switchCameraView();
}
deleteObject(uuid: string) {
this.mouse_controls?.clearFKVisuals();
this.mouse_controls?.removeTransformControls(true);
@@ -5,7 +5,7 @@ import {
lookAtFromCamera,
type FreeCamControlState,
} from "../cameraMath";
import { CameraAspectRatio } from "~/pages/PageScene/enums";
import { CameraAspectRatio, EditorStates } from "~/pages/PageScene/enums";
import { usePageSceneStore } from "~/pages/PageScene/PageSceneStore";
export type RenderDimensions = {
@@ -14,6 +14,18 @@ export type RenderDimensions = {
aspectRatio: number;
};
// Capabilities CameraController needs from outside its own state.
// Editor wires these in initialize() so CameraController never imports
// sibling subsystems directly.
export type CameraControllerDeps = {
getThreeScene: () => THREE.Scene;
getHotItems: () => THREE.Object3D[] | null;
removeTransformControls: () => void;
setSelected: (obj: THREE.Object3D | null) => void;
setEditorState: (state: EditorStates) => void;
hideObjectPanel: () => void;
};
// Owns the editor's camera-related state: the live PerspectiveCameras,
// the cam_obj proxy in the scene, FreeCam control state, lockControls,
// and the per-frame camera tick that ran inline in renderSingleFrame.
@@ -42,12 +54,82 @@ export class CameraController {
render_width: number;
render_height: number;
constructor() {
private deps: CameraControllerDeps;
constructor(deps: CameraControllerDeps) {
this.deps = deps;
const dims = this.getRenderDimensions();
this.render_width = dims.width;
this.render_height = dims.height;
}
// Toggles between free-camera (edit) mode and the active render
// camera's perspective. Lifted from SceneUtils.switchCameraView so
// the camera state stays in one place.
switchCameraView() {
this.camera_person_mode = !this.camera_person_mode;
if (this.freeCamState) {
this.freeCamState.velocity.set(0, 0, 0);
}
if (!this.cam_obj) return;
if (this.camera_person_mode && this.camera) {
this.last_cam_pos.copy(this.camera.position);
this.last_cam_rot.copy(this.camera.rotation);
this.camera.position.copy(this.cam_obj.position);
this.camera.rotation.copy(this.cam_obj.rotation);
if (this.lockControls) {
this.deps.getThreeScene().add(this.lockControls.getObject());
}
// useFreeCam reads editorState from the store and enables
// itself when CAMERA_VIEW; nothing to do here.
this.cam_obj.scale.set(0, 0, 0);
this.deps.removeTransformControls();
this.deps.setSelected(this.cam_obj);
this.deps.setEditorState(EditorStates.CAMERA_VIEW);
const hot = this.deps.getHotItems();
if (hot) {
hot.forEach((element) => {
element.visible = false;
});
}
// Workaround: in camera mode, a right-click should pan rather
// than open the browser context menu. Defer the listener attach
// until after the letterbox swap settles.
setTimeout(
() =>
document
.getElementById("letterbox")
?.addEventListener("contextmenu", function (event) {
event.preventDefault();
}),
250,
);
} else if (this.camera) {
this.camera.position.copy(this.last_cam_pos);
this.camera.rotation.copy(this.last_cam_rot);
if (this.lockControls) {
this.deps.getThreeScene().remove(this.lockControls.getObject());
}
this.cam_obj.scale.set(1, 1, 1);
const hot = this.deps.getHotItems();
if (hot) {
hot.forEach((element) => {
element.visible = true;
});
}
this.deps.hideObjectPanel();
this.deps.setEditorState(EditorStates.EDIT);
}
}
setFreeCamState(state: FreeCamControlState | null) {
this.freeCamState = state;
}
@@ -2,7 +2,6 @@ import Scene from "./scene";
import Editor from "./editor";
import * as THREE from "three";
import { XYZ } from "../datastructures/common";
import { EditorStates } from "../enums";
import { usePageSceneStore } from "../PageSceneStore";
export class SceneUtils {
@@ -112,67 +111,6 @@ export class SceneUtils {
}
}
switchCameraView() {
const cam = this.editor.cameraController;
cam.camera_person_mode = !cam.camera_person_mode;
if (cam.freeCamState) {
cam.freeCamState.velocity.set(0, 0, 0);
}
if (cam.cam_obj) {
if (cam.camera_person_mode && cam.camera) {
cam.last_cam_pos.copy(cam.camera.position);
cam.last_cam_rot.copy(cam.camera.rotation);
cam.camera.position.copy(cam.cam_obj.position);
cam.camera.rotation.copy(cam.cam_obj.rotation);
if (cam.lockControls) {
this.editor.activeScene.scene.add(cam.lockControls.getObject());
}
// useFreeCam reads editorState from the store and enables
// itself when CAMERA_VIEW; nothing to do here.
cam.cam_obj.scale.set(0, 0, 0);
this.removeTransformControls();
this.editor.selection.selected = cam.cam_obj;
this.editor.publishSelect();
this.editor.updateSelectedUI();
usePageSceneStore.getState().setEditorState(EditorStates.CAMERA_VIEW);
if (this.editor.activeScene.hot_items) {
this.editor.activeScene.hot_items.forEach((element) => {
element.visible = false;
});
}
// Make a better solution later but for now this is so that in camera mode when you right click it does not bring up the context menu and allows you to pan.
setTimeout(
() =>
document
.getElementById("letterbox")
?.addEventListener("contextmenu", function (event) {
event.preventDefault();
}),
250,
);
} else if (cam.camera) {
cam.camera.position.copy(cam.last_cam_pos);
cam.camera.rotation.copy(cam.last_cam_rot);
if (cam.lockControls) {
this.editor.activeScene.scene.remove(cam.lockControls.getObject());
}
cam.cam_obj.scale.set(1, 1, 1);
if (this.editor.activeScene.hot_items) {
this.editor.activeScene.hot_items.forEach((element) => {
element.visible = true;
});
}
usePageSceneStore.getState().hideObjectPanel();
usePageSceneStore.getState().setEditorState(EditorStates.EDIT);
}
}
}
// Returns the "check sum" of the editors selected object.
getSelectedSum(): number {
if (this.editor.sceneManager?.selected_objects === undefined) {