From 89ac3023923dd2fa5bd7d51e0dcbcfc520da970b Mon Sep 17 00:00:00 2001 From: Tanish Manku Date: Thu, 30 Apr 2026 23:47:59 -0700 Subject: [PATCH] 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. --- .../app/src/pages/PageScene/engine/editor.ts | 20 +++-- .../engine/editor/CameraController.ts | 86 ++++++++++++++++++- .../app/src/pages/PageScene/engine/helper.ts | 62 ------------- 3 files changed, 98 insertions(+), 70 deletions(-) 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 dfb3b55fd6..91d49a7a65 100644 --- a/frontend/apps/artcraft/app/src/pages/PageScene/engine/editor.ts +++ b/frontend/apps/artcraft/app/src/pages/PageScene/engine/editor.ts @@ -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); diff --git a/frontend/apps/artcraft/app/src/pages/PageScene/engine/editor/CameraController.ts b/frontend/apps/artcraft/app/src/pages/PageScene/engine/editor/CameraController.ts index 18b849f9bd..e50c9585b2 100644 --- a/frontend/apps/artcraft/app/src/pages/PageScene/engine/editor/CameraController.ts +++ b/frontend/apps/artcraft/app/src/pages/PageScene/engine/editor/CameraController.ts @@ -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; } 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 fd7ed34c6b..6a5db0a3ff 100644 --- a/frontend/apps/artcraft/app/src/pages/PageScene/engine/helper.ts +++ b/frontend/apps/artcraft/app/src/pages/PageScene/engine/helper.ts @@ -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) {