PageScene: SaveManager takes Deps, no Editor reference

Resolves the longstanding `// TODO Refactor remove editor.` Drops
the Editor import; constructor takes a SaveManagerDeps surface
inline-built by Editor (Phase 2 idiom — same shape as
CameraControllerDeps, GizmoControllerDeps, etc.).

Deps surface (10 fields, single-purpose):
- getVersion / setVersion — engine version (load may upgrade)
- getActiveScene — proxy serialization target
- getRenderer — for save thumbnail
- removeTransformControls — pre-save cleanup
- getCamera / refreshCamObj / changeRenderCameraAspectRatio — camera
- setPositivePrompt — restore prompt on load
- saveSceneState / loadSceneState — backend API

current_scene_media_token moves into SaveManager as private state
(only the load path used it; nothing outside SaveManager read it).
The field on Editor is dropped along with the two `= null` writes
in the constructor and initialize.

CameraController gains refreshCamObj(scene) — re-anchors cam_obj to
the named entity in the scene + sets layers. Replaces the inline
post-load setup that SaveManager used to do directly. Editor.initialize
also routes through the same method now (was inline at line ~410).
This commit is contained in:
Tanish Manku committed 2026-05-01 03:40:50 -07:00
1 parent 8780935b62
commit 3c8fa9010b
3 files changed
+130 -102

No files matched your search

@@ -67,8 +67,6 @@ class Editor {
frames: number;
lastFrameTime: number;
current_scene_media_token: string | null;
can_initialize: boolean;
positive_prompt: string;
@@ -198,7 +196,25 @@ class Editor {
this.shouldRender = false;
this.utils = new SceneUtils(this, this.activeScene);
this.save_manager = new SaveManager(this);
this.save_manager = new SaveManager({
getVersion: () => this.version,
setVersion: (v) => {
this.version = v;
},
getActiveScene: () => this.activeScene,
getRenderer: () => this.renderer,
removeTransformControls: () => this.utils.removeTransformControls(),
getCamera: () => this.cameraController.camera,
refreshCamObj: () =>
this.cameraController.refreshCamObj(this.activeScene.scene),
changeRenderCameraAspectRatio: (ratio) =>
this.cameraController.changeRenderCameraAspectRatio(ratio),
setPositivePrompt: (prompt) => {
this.positive_prompt = prompt;
},
saveSceneState: (args) => this.api_manager.saveSceneState(args),
loadSceneState: (token) => this.api_manager.loadSceneState(token),
});
this.viewport = new ViewportController({
getCamera: () => this.cameraController.camera,
getRenderCamera: () => this.cameraController.render_camera,
@@ -212,9 +228,6 @@ class Editor {
// apply/revert + dependencies. HistoryManager just stores them.
this.history = new HistoryManager({ capacity: 64 });
// Scene State
this.current_scene_media_token = null;
this.positive_prompt =
"((masterpiece, best quality, 8K, detailed)), colorful, epic, fantasy, (fox, red fox:1.2), no humans, 1other, ((koi pond)), outdoors, pond, rocks, stones, koi fish, ((watercolor))), lilypad, fish swimming around.";
@@ -391,12 +404,7 @@ class Editor {
this.viewport.setupResizeObserver();
// saving state of the scene
this.current_scene_media_token = null;
this.cameraController.cam_obj = this.activeScene.get_object_by_name(
this.cameraController.camera_name,
);
this.cameraController.refreshCamObj(this.activeScene.scene);
this.mouse_controls = new MouseControls(
this.cameraController.camera,
@@ -63,6 +63,17 @@ export class CameraController {
this.render_height = dims.height;
}
// Re-anchor the camera-person object to the named entity in the scene
// and put it on layer 1 (along with its children). Used after
// load/restore flows when a fresh scene replaces the previous one.
refreshCamObj(scene: THREE.Scene): void {
this.cam_obj = scene.getObjectByName(this.camera_name);
this.cam_obj?.layers.set(1);
this.cam_obj?.children.forEach((child) => {
child.layers.set(1);
});
}
// 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.
@@ -1,38 +1,80 @@
import * as THREE from "three";
import { Camera } from "@storyteller/common";
import { SceneGenereationMetaData } from "../models/sceneGenerationMetadata";
import { StoryTellerProxyScene } from "../proxy/storyteller_proxy_scene";
import { usePageSceneStore } from "../PageSceneStore";
import { CameraAspectRatio } from "../enums";
import type Scene from "./scene";
const showEditorLoader = (message?: string) =>
usePageSceneStore.getState().showEditorLoader(message);
const hideEditorLoader = () =>
usePageSceneStore.getState().hideEditorLoader();
import Editor from "./editor";
import { Camera } from "@storyteller/common";
export type EditorInitializeConfig = {
sceneToken: string;
};
// need to move this into the
export type SaveSceneStateArgs = {
saveJson: string;
sceneTitle: string;
sceneToken?: string;
sceneThumbnail: Blob | undefined;
};
// Narrow contract between SaveManager and the rest of the engine. The
// manager doesn't import Editor — every cross-subsystem reach is a
// callback or getter on this deps object (Phase 2 idiom).
export type SaveManagerDeps = {
// Engine version. Read on save, written on load (older formats may
// upgrade themselves through this).
getVersion: () => number;
setVersion: (v: number) => void;
// Active scene reference for proxy serialization.
getActiveScene: () => Scene;
// Renderer canvas — only used to capture the save-thumbnail.
getRenderer: () => THREE.WebGLRenderer | undefined;
// Yank the transform gizmo before snapshot so it doesn't end up in
// the saved scene.
removeTransformControls: () => void;
// Camera state. getCamera reads pose for save + load-restore;
// refreshCamObj re-anchors the camera-person object after a load
// replaces the scene; changeRenderCameraAspectRatio applies a saved
// aspect ratio.
getCamera: () => THREE.PerspectiveCamera | null;
refreshCamObj: () => void;
changeRenderCameraAspectRatio: (ratio: CameraAspectRatio) => void;
// Editor field setter used during load.
setPositivePrompt: (prompt: string) => void;
// Backend API.
saveSceneState: (args: SaveSceneStateArgs) => Promise<string>;
loadSceneState: (token: string) => Promise<unknown>;
};
export class SaveManager {
editor: Editor;
constructor(editor: Editor) {
this.editor = editor;
}
// Token of the most-recently loaded scene. Internal load tracking
// — nothing outside SaveManager reads it.
private currentSceneMediaToken: string | null = null;
constructor(private readonly deps: SaveManagerDeps) {}
public getSceneJson({
sceneGenerationMetadata,
}: {
sceneGenerationMetadata: SceneGenereationMetaData;
}) {
const proxyScene = new StoryTellerProxyScene(
this.editor.version,
this.editor.activeScene,
);
const scene_json = proxyScene.saveToScene(this.editor.version);
console.log(scene_json);
const version = this.deps.getVersion();
const scene = this.deps.getActiveScene();
const camera = this.deps.getCamera();
const proxyScene = new StoryTellerProxyScene(version, scene);
const scene_json = proxyScene.saveToScene(version);
const sceneState = usePageSceneStore.getState();
const camerasData = sceneState.cameras.map((cam: Camera) => ({
@@ -44,24 +86,21 @@ export class SaveManager {
lookAt: cam.lookAt,
}));
const save_data = {
version: this.editor.version,
return {
version,
scene: scene_json,
...sceneGenerationMetadata,
timeline: "",
skybox: this.editor.activeScene.skybox,
skybox: scene.skybox,
camera_data: {
position: this.editor.cameraController.camera?.position,
rotation: this.editor.cameraController.camera?.rotation,
position: camera?.position,
rotation: camera?.rotation,
},
cameras: camerasData,
selectedCameraId: sceneState.selectedCameraId,
};
return save_data;
}
// TODO Move this function into scene manager.
public async saveScene({
sceneTitle,
sceneToken,
@@ -71,44 +110,55 @@ export class SaveManager {
sceneToken?: string;
sceneGenerationMetadata: SceneGenereationMetaData;
}): Promise<string> {
// remove controls when saving scene.
this.editor.utils.removeTransformControls();
this.deps.removeTransformControls();
showEditorLoader();
const sceneJson = this.getSceneJson({
sceneGenerationMetadata,
});
const sceneJson = this.getSceneJson({ sceneGenerationMetadata });
// TODO turn scene information into and object ...
let sceneThumbnail = undefined;
if (this.editor.renderer) {
const imgData = this.editor.renderer.domElement.toDataURL();
const response = await fetch(imgData); // Fetch the data URL
sceneThumbnail = await response.blob(); // Convert to Blob
let sceneThumbnail: Blob | undefined = undefined;
const renderer = this.deps.getRenderer();
if (renderer) {
const imgData = renderer.domElement.toDataURL();
const response = await fetch(imgData);
sceneThumbnail = await response.blob();
}
const result = await this.editor.api_manager.saveSceneState({
const result = await this.deps.saveSceneState({
saveJson: JSON.stringify(sceneJson),
sceneTitle,
sceneToken,
sceneThumbnail,
});
// this means error going to have to fix the plumbing on this because not using
// the api manager
hideEditorLoader();
console.debug("Save Scene Result: ", result);
return result; // if this is an empty string it is an error. need to migrate to api manager.
return result;
}
public async loadCache(cacheJson: string) {
showEditorLoader();
const scene_json = JSON.parse(cacheJson);
await this.loadFromJson(scene_json);
hideEditorLoader();
}
public async loadScene(scene_media_token: string) {
showEditorLoader();
this.currentSceneMediaToken = scene_media_token;
const scene_json = await this.deps
.loadSceneState(this.currentSceneMediaToken)
.catch((err) => {
hideEditorLoader();
throw err;
});
await this.loadFromJson(scene_json);
hideEditorLoader();
}
private async loadFromJson(scene_json: any) {
const proxyScene = new StoryTellerProxyScene(
this.editor.version,
this.editor.activeScene,
);
const version = this.deps.getVersion();
const scene = this.deps.getActiveScene();
const proxyScene = new StoryTellerProxyScene(version, scene);
await proxyScene.loadFromSceneJson(
scene_json["scene"],
@@ -117,11 +167,10 @@ export class SaveManager {
);
const camera_data = scene_json["camera_data"];
const liveCamera = this.editor.cameraController.camera;
const liveCamera = this.deps.getCamera();
if (camera_data && liveCamera) {
const camera_position: THREE.Vector3 = camera_data["position"];
const camera_rotation: THREE.Euler = camera_data["rotation"];
liveCamera.position.copy(camera_position);
liveCamera.rotation.copy(camera_rotation);
}
@@ -144,57 +193,17 @@ export class SaveManager {
.setSelectedCameraId(scene_json.selectedCameraId);
}
// Restore prompt text into the editor (PromptBox3D will pick it up
// through the editor's positive_prompt field).
if (scene_json.positivePrompt) {
this.editor.positive_prompt = scene_json.positivePrompt;
this.deps.setPositivePrompt(scene_json.positivePrompt);
}
if (scene_json.cameraAspectRatio) {
this.editor.cameraController.changeRenderCameraAspectRatio(
scene_json.cameraAspectRatio,
);
this.deps.changeRenderCameraAspectRatio(scene_json.cameraAspectRatio);
usePageSceneStore
.getState()
.setCameraAspectRatio(scene_json.cameraAspectRatio);
}
this.editor.version = scene_json["version"];
const camCtrl = this.editor.cameraController;
camCtrl.cam_obj = this.editor.activeScene.get_object_by_name(
camCtrl.camera_name,
);
camCtrl.cam_obj?.layers.set(1);
camCtrl.cam_obj?.children.forEach((child) => {
child.layers.set(1);
});
}
public async loadCache(cacheJson: string) {
showEditorLoader();
const scene_json = JSON.parse(cacheJson);
await this.loadFromJson(scene_json);
hideEditorLoader();
}
// TODO Refactor remove editor.
public async loadScene(scene_media_token: string) {
showEditorLoader();
this.editor.current_scene_media_token = scene_media_token;
const scene_json = await this.editor.api_manager
.loadSceneState(this.editor.current_scene_media_token)
.catch((err) => {
hideEditorLoader();
throw err;
});
await this.loadFromJson(scene_json);
hideEditorLoader();
this.deps.setVersion(scene_json["version"]);
this.deps.refreshCamObj();
}
}