diff --git a/frontend/apps/artcraft/app/src/pages/PageScene/actions/addCharacter.ts b/frontend/apps/artcraft/app/src/pages/PageScene/actions/addCharacter.ts index dd146fc44e..a91f04c177 100644 --- a/frontend/apps/artcraft/app/src/pages/PageScene/actions/addCharacter.ts +++ b/frontend/apps/artcraft/app/src/pages/PageScene/actions/addCharacter.ts @@ -23,6 +23,6 @@ export async function addCharacter( name: obj.name || (item.name ?? "character"), mediaId: item.media_id, }); - editor.updateOutliner?.(); + editor.selection.refreshOutliner(); return obj.uuid; } diff --git a/frontend/apps/artcraft/app/src/pages/PageScene/actions/addObject.ts b/frontend/apps/artcraft/app/src/pages/PageScene/actions/addObject.ts index 787ea5c07d..af0e476d82 100644 --- a/frontend/apps/artcraft/app/src/pages/PageScene/actions/addObject.ts +++ b/frontend/apps/artcraft/app/src/pages/PageScene/actions/addObject.ts @@ -21,6 +21,6 @@ export async function addObject( name: obj.name || (item.name ?? "object"), mediaId: item.media_id, }); - editor.updateOutliner?.(); + editor.selection.refreshOutliner(); return obj.uuid; } diff --git a/frontend/apps/artcraft/app/src/pages/PageScene/actions/addShape.ts b/frontend/apps/artcraft/app/src/pages/PageScene/actions/addShape.ts index f0a3f8d577..65f6bff292 100644 --- a/frontend/apps/artcraft/app/src/pages/PageScene/actions/addShape.ts +++ b/frontend/apps/artcraft/app/src/pages/PageScene/actions/addShape.ts @@ -21,6 +21,6 @@ export async function addShape( name: obj.name || (item.name ?? "shape"), mediaId: item.media_id, }); - editor.updateOutliner?.(); + editor.selection.refreshOutliner(); return obj.uuid; } diff --git a/frontend/apps/artcraft/app/src/pages/PageScene/actions/selectObject.ts b/frontend/apps/artcraft/app/src/pages/PageScene/actions/selectObject.ts index 830aa814e8..3541c8fe20 100644 --- a/frontend/apps/artcraft/app/src/pages/PageScene/actions/selectObject.ts +++ b/frontend/apps/artcraft/app/src/pages/PageScene/actions/selectObject.ts @@ -11,5 +11,5 @@ export function deselectObject(editor: Editor): void { if (editor.sceneManager) { editor.sceneManager.selected_objects = undefined; } - editor.publishSelect(); + editor.selection.publishSelect(); } diff --git a/frontend/apps/artcraft/app/src/pages/PageScene/comps/ControlPanelSceneObject/ControlPanelSceneObject.tsx b/frontend/apps/artcraft/app/src/pages/PageScene/comps/ControlPanelSceneObject/ControlPanelSceneObject.tsx index 2dab5e711f..8dbf2b2823 100644 --- a/frontend/apps/artcraft/app/src/pages/PageScene/comps/ControlPanelSceneObject/ControlPanelSceneObject.tsx +++ b/frontend/apps/artcraft/app/src/pages/PageScene/comps/ControlPanelSceneObject/ControlPanelSceneObject.tsx @@ -84,7 +84,7 @@ export const ControlPanelSceneObject = () => { } setInputsUpdated(false); - editorEngine.setSelectedObject( + editorEngine.sceneManager?.updateSelectedTransform( localToEngine(localPosition), localToEngine(localRotation), localToEngine(localScale), @@ -103,7 +103,7 @@ export const ControlPanelSceneObject = () => { setLocalRotation(engineToLocal(vectors.rotation)); setLocalScale(engineToLocal(vectors.scale)); - setLocked(editorEngine.isObjectLocked(editorEngine?.selected?.uuid || "")); + setLocked(editorEngine.selection.isObjectLocked(editorEngine?.selected?.uuid || "")); setColor(editorEngine?.selected?.userData.color); }, [currentSceneObject, editorEngine]); @@ -220,7 +220,7 @@ export const ControlPanelSceneObject = () => { className="h-0 w-0 cursor-pointer opacity-0" id={colorInputId} onChange={(e: ChangeEvent) => { - editorEngine?.setColor( + editorEngine?.activeScene.setColor( editorEngine?.selected?.uuid || "", e.target.value, ); diff --git a/frontend/apps/artcraft/app/src/pages/PageScene/comps/Outliner/Outliner.tsx b/frontend/apps/artcraft/app/src/pages/PageScene/comps/Outliner/Outliner.tsx index d7c13b1f61..066406f3f5 100644 --- a/frontend/apps/artcraft/app/src/pages/PageScene/comps/Outliner/Outliner.tsx +++ b/frontend/apps/artcraft/app/src/pages/PageScene/comps/Outliner/Outliner.tsx @@ -48,7 +48,7 @@ const OutlinerRow = ({ item }: { item: OutlinerItem }) => { const handleToggleLock = (e: React.MouseEvent) => { e.stopPropagation(); usePageSceneStore.getState().toggleOutlinerLock(item.id); - editorEngine?.lockUnlockObject(item.id); + editorEngine?.selection.lockUnlockObject(item.id); }; const handleToggleVisibility = (e: React.MouseEvent) => { diff --git a/frontend/apps/artcraft/app/src/pages/PageScene/contexts/EngineContext/EngineProvider.tsx b/frontend/apps/artcraft/app/src/pages/PageScene/contexts/EngineContext/EngineProvider.tsx index 13ab1ea4f0..61b9e18465 100644 --- a/frontend/apps/artcraft/app/src/pages/PageScene/contexts/EngineContext/EngineProvider.tsx +++ b/frontend/apps/artcraft/app/src/pages/PageScene/contexts/EngineContext/EngineProvider.tsx @@ -59,9 +59,7 @@ export const EngineProvider = ({ sceneToken, children }: Props) => { const sceneGenerationMetadata = getSceneGenerationMetaData( activeEditorRef.current, ); - const cacheJson = activeEditorRef.current.cacheScene({ - sceneTitle: "", - sceneToken: "", + const cacheJson = activeEditorRef.current.save_manager.getSceneJson({ sceneGenerationMetadata: sceneGenerationMetadata, }); const cacheString = JSON.stringify(cacheJson); 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 4ca92131d8..5c42e0a6d2 100644 --- a/frontend/apps/artcraft/app/src/pages/PageScene/engine/editor.ts +++ b/frontend/apps/artcraft/app/src/pages/PageScene/engine/editor.ts @@ -4,7 +4,6 @@ import Scene from "./scene.js"; import { APIManager } from "./api_manager.js"; import { PointerLockControls } from "three/addons/controls/PointerLockControls.js"; import { CameraAspectRatio } from "~/pages/PageScene/enums"; -import { XYZ } from "../datastructures/common"; import { SceneUtils } from "./helper"; import { MouseControls } from "./keybinds_controls"; import { SaveManager } from "./save_manager"; @@ -158,6 +157,15 @@ class Editor { getSceneManager: () => this.sceneManager, cameraName: this.cameraController.camera_name, version: this.version, + toggleObjectLocked: (uuid) => this.utils.toggleObjectLocked(uuid), + isObjectLocked: (uuid) => this.utils.isObjectLocked(uuid), + removeTransformControls: () => + this.utils.removeTransformControls(false), + attachGizmoToCurrentSelection: () => { + this.gizmo.addToScene(this.activeScene.scene); + const selected = this.sceneManager?.selected_objects?.[0]; + if (selected) this.gizmo.attach(selected); + }, }); this.activeScene = new Scene( @@ -634,29 +642,6 @@ class Editor { this.selection.refreshOutliner(); } - isObjectLipsync(object_uuid: string) { - return this.utils.isObjectLipsync(object_uuid); - } - - isObjectLocked(object_uuid: string): boolean { - return this.utils.isObjectLocked(object_uuid); - } - - lockUnlockObject(object_uuid: string): boolean { - const res = this.utils.lockUnlockObject(object_uuid); - this.selection.updateSelectedUI(); - return res; - } - - setColor(object_uuid: string, hex_color: string) { - this.activeScene.setColor(object_uuid, hex_color); - } - - // TO UPDATE selected objects in the scene might want to add to the scene ... - async setSelectedObject(position: XYZ, rotation: XYZ, scale: XYZ) { - this.utils.setSelectedObject(position, rotation, scale); - } - public async saveScene({ sceneTitle, sceneToken, @@ -673,20 +658,6 @@ class Editor { }); } - public cacheScene({ - sceneTitle, - sceneToken, - sceneGenerationMetadata, - }: { - sceneTitle: string; - sceneToken: string; - sceneGenerationMetadata: SceneGenereationMetaData; - }) { - return this.save_manager.getSceneJson({ - sceneGenerationMetadata: sceneGenerationMetadata, - }); - } - deleteObject(uuid: string) { this.mouse_controls?.clearFKVisuals(); this.mouse_controls?.removeTransformControls(true); @@ -826,20 +797,6 @@ class Editor { setIs3DEditorInitialized(false); console.log("3D Editor Engine unmounted"); } - - // Facades — external callers in actions/* and helper.ts target these - // names. Internals reach the bridge directly via this.selection.*. - updateOutliner() { - this.selection.updateOutliner(); - } - - updateSelectedUI() { - this.selection.updateSelectedUI(); - } - - publishSelect() { - this.selection.publishSelect(); - } } export default Editor; diff --git a/frontend/apps/artcraft/app/src/pages/PageScene/engine/editor/SelectionBridge.ts b/frontend/apps/artcraft/app/src/pages/PageScene/engine/editor/SelectionBridge.ts index 6d68fcac4e..6251b044f7 100644 --- a/frontend/apps/artcraft/app/src/pages/PageScene/engine/editor/SelectionBridge.ts +++ b/frontend/apps/artcraft/app/src/pages/PageScene/engine/editor/SelectionBridge.ts @@ -13,7 +13,7 @@ import { AssetType, ClipGroup } from "~/enums"; import { usePageSceneStore } from "../../PageSceneStore"; import type { SceneManager } from "../scene_manager_api"; -export type SelectionEngineRefs = { +export type SelectionBridgeDeps = { // Lazy: SceneManager is created in Editor.initialize, after the bridge. getSceneManager: () => SceneManager | undefined; // The reserved camera-entity name ("::CAM::") used to distinguish @@ -21,6 +21,14 @@ export type SelectionEngineRefs = { cameraName: string; // Editor's data version, written into the object panel. version: number; + + // Lock/unlock plumbing — the userData mutation lives in SceneUtils, + // and the gizmo attach/detach lives on GizmoController. The bridge + // calls them through these callbacks instead of importing either. + toggleObjectLocked: (uuid: string) => boolean; // returns new locked state + isObjectLocked: (uuid: string) => boolean; + removeTransformControls: () => void; + attachGizmoToCurrentSelection: () => void; }; export class SelectionBridge { @@ -32,7 +40,26 @@ export class SelectionBridge { // compared in renderSingleFrame against utils.getSelectedSum(). last_selected_sum: number = 0; - constructor(private readonly engine: SelectionEngineRefs) {} + constructor(private readonly engine: SelectionBridgeDeps) {} + + // Toggle the locked flag on an object's userData, then either yank + // the gizmo (lock) or re-attach it to the current selection (unlock). + // Returns the new locked state. + lockUnlockObject(object_uuid: string): boolean { + const locked = this.engine.toggleObjectLocked(object_uuid); + if (locked) { + this.engine.removeTransformControls(); + } else { + this.engine.attachGizmoToCurrentSelection(); + } + this.updateSelectedUI(); + return locked; + } + + // True if the object is currently locked. Pure passthrough. + isObjectLocked(object_uuid: string): boolean { + return this.engine.isObjectLocked(object_uuid); + } setSelected(object: THREE.Object3D[] | undefined) { const sceneManager = this.engine.getSceneManager(); 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 6a5db0a3ff..b3311a5617 100644 --- a/frontend/apps/artcraft/app/src/pages/PageScene/engine/helper.ts +++ b/frontend/apps/artcraft/app/src/pages/PageScene/engine/helper.ts @@ -1,7 +1,6 @@ import Scene from "./scene"; import Editor from "./editor"; import * as THREE from "three"; -import { XYZ } from "../datastructures/common"; import { usePageSceneStore } from "../PageSceneStore"; export class SceneUtils { @@ -19,26 +18,6 @@ export class SceneUtils { ); } - // For react to see if an object has lipsync capability or not. - isObjectLipsync(object_uuid: string) { - const object = this.scene.get_object_by_uuid(object_uuid); - let hasLipsync = false; - if (object) { - object.traverse((c: THREE.Object3D) => { - if (c instanceof THREE.Mesh) { - if (c.morphTargetInfluences && c.morphTargetDictionary) { - const blendShapeIndexE = c.morphTargetDictionary["E"]; - // console.log(c.morphTargetDictionary, blendShapeIndexE) - if (blendShapeIndexE !== null) { - hasLipsync = true; - } - } - } - }); - } - return hasLipsync; - } - // Returns if the object is locked or unlocked. isObjectLocked(object_uuid: string): boolean { const object = this.scene.get_object_by_uuid(object_uuid); @@ -48,31 +27,20 @@ export class SceneUtils { } return object.userData["locked"]; } - //console.log("No object found."); return false; } - // Locks or unlocks and object and returns its new state, - lockUnlockObject(object_uuid: string): boolean { + // Pure userData mutation: flips the locked flag and returns the new + // value. Higher-level wiring (gizmo attach/detach, selection refresh) + // lives on SelectionBridge.lockUnlockObject. + toggleObjectLocked(object_uuid: string): boolean { const object = this.scene.get_object_by_uuid(object_uuid); - if (object) { - if (object.userData["locked"] == undefined) { - object.userData["locked"] = false; - } - object.userData["locked"] = !object.userData["locked"]; - - if (object.userData["locked"]) { - this.removeTransformControls(false); - } 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"]; + if (!object) return false; + if (object.userData["locked"] == undefined) { + object.userData["locked"] = false; } - //console.log("No object found."); - return false; + object.userData["locked"] = !object.userData["locked"]; + return object.userData["locked"]; } // Removes transform controls and publishes selected. @@ -86,31 +54,13 @@ export class SceneUtils { } if (remove_outline) { outlinePass.selectedObjects = []; - this.editor.publishSelect(); + this.editor.selection.publishSelect(); } this.editor.gizmo.detach(); this.editor.gizmo.removeFromScene(this.editor.activeScene.scene); if (remove_outline) outlinePass.selectedObjects = []; } - // TO UPDATE selected objects in the scene might want to add to the scene ... - async setSelectedObject(position: XYZ, rotation: XYZ, scale: XYZ) { - if (this.editor.sceneManager?.selected_objects) { - const object = this.editor.sceneManager?.selected_objects[0]; - if (object != undefined || object != null) { - object.position.x = position.x; - object.position.y = position.y; - object.position.z = position.z; - object.rotation.x = THREE.MathUtils.degToRad(rotation.x); - object.rotation.y = THREE.MathUtils.degToRad(rotation.y); - object.rotation.z = THREE.MathUtils.degToRad(rotation.z); - object.scale.x = scale.x; - object.scale.y = scale.y; - object.scale.z = scale.z; - } - } - } - // Returns the "check sum" of the editors selected object. getSelectedSum(): number { if (this.editor.sceneManager?.selected_objects === undefined) { @@ -206,7 +156,7 @@ function removeObject3D(object3D) { usePageSceneStore.getState().removeSceneObject(uuid); this.editor.selection.selected = undefined; - this.editor.publishSelect(); + this.editor.selection.publishSelect(); usePageSceneStore.getState().hideObjectPanel(); } } diff --git a/frontend/apps/artcraft/app/src/pages/PageScene/engine/scene_manager_api.ts b/frontend/apps/artcraft/app/src/pages/PageScene/engine/scene_manager_api.ts index b1eb019b7a..4814875ba2 100644 --- a/frontend/apps/artcraft/app/src/pages/PageScene/engine/scene_manager_api.ts +++ b/frontend/apps/artcraft/app/src/pages/PageScene/engine/scene_manager_api.ts @@ -9,6 +9,7 @@ import Scene from "./scene"; import { MouseControls } from "./keybinds_controls"; import { ClipGroup } from "~/enums"; import { AssetType } from "~/enums"; +import { XYZ } from "../datastructures/common"; import { usePageSceneStore } from "~/pages/PageScene/PageSceneStore"; import { CommandInputTypes, @@ -257,6 +258,20 @@ export class SceneManager implements SceneManagerAPI { } } + // Writes position/rotation/scale onto the currently selected object. + // Rotation is taken in degrees (matches the Object Panel input). + public updateSelectedTransform(position: XYZ, rotation: XYZ, scale: XYZ) { + const object = this.selected_objects?.[0]; + if (!object) return; + object.position.set(position.x, position.y, position.z); + object.rotation.set( + THREE.MathUtils.degToRad(rotation.x), + THREE.MathUtils.degToRad(rotation.y), + THREE.MathUtils.degToRad(rotation.z), + ); + object.scale.set(scale.x, scale.y, scale.z); + } + // Converts a 3d object to signal item format. private convert_object( object: THREE.Object3D,