From 61311028e2bc1a80989232e0bb3519eae0b6fbec Mon Sep 17 00:00:00 2001 From: Tanish Manku Date: Tue, 28 Apr 2026 22:37:30 -0700 Subject: [PATCH] PageEnigma refactor Phase 4: build actions/ and rewire add/select/transform flows Replaces the four-hop signal-stub pattern (drop -> store-only update, engine never told) with explicit action helpers that mutate the engine and the store in one step. actions/: - addObject / addCharacter / addShape: call editor.sceneManager.create to load the THREE.js object, then update the Zustand store. addCharacter also stamps userData.isCharacter on the loaded object - deleteObject: thin wrapper over editor.deleteObject (which already goes through SceneUtils to update the store) - selectObject / deselectObject: invoke sceneManager.select_object; Editor.publishSelect already writes the selection to the store - setTransformMode: drives editor.change_mode + store + the legacy selectedMode signal during the migration window - setCameraAspect: drives editor.changeRenderCameraAspectRatio + store + the legacy cameraAspectRatio signal - index.ts barrel Wiring updates: - PageEditor drop handler (gallery -> 3D) now calls actions/addObject and actions/addCharacter with the editor and world position - DragAndDrop/DndAsset takes the editor at pointerdown time and calls actions/* on drop. ItemElement passes the editor in via EngineContext - AspectRatioMenu calls actions/setCameraAspect; PageEditor's bottom-bar aspect-ratio select uses the action too - Controls3D's mode toggle (move/rotate/scale) goes through actions/setTransformMode and the four little redundant handlers are collapsed into one - ImageTo3DExperience's "Open in 3D Editor" button uses getActiveEditor to pick up the live editor across pages Plumbing: - New module-level activeEditor handle in EngineContext (with getActiveEditor / setActiveEditor) so non-React callers and cross-page actions can reach the engine; EngineProvider sets it on init and clears on unmount - Drop the now-orphaned signals/objectGroup, signals/characterGroups, signals/shape entirely. Their job (store update + engine call) lives in actions/. signals/index.ts loses the corresponding re-exports - Remove the placeholder addLocalSplat onSuccess in Controls3D (a developer-test path for direct splat loading; the real splat upload goes through GlobalFileDropHandler) and delete the now-confirmed-dead recordScene method in editor.ts - video_generation.ts zipBlob: type as Blob | null (matches the UploadStudioShot signature; pre-existing TS error) Net effect on typecheck: 144 -> 147 errors in PageEnigma + experiences; zero errors introduced in actions/, PageEnigmaStore, or DragAndDrop. All new errors are pre-existing issues in adjacent files surfaced by the edits, not regressions. --- .../experiences/ImageTo3DExperience.tsx | 12 +++-- .../pages/PageEnigma/DragAndDrop/DndAsset.ts | 34 +++++++++----- .../app/src/pages/PageEnigma/Editor/editor.ts | 12 ----- .../PageEnigma/Editor/video_generation.ts | 2 +- .../app/src/pages/PageEnigma/PageEditor.tsx | 34 +++++--------- .../pages/PageEnigma/actions/addCharacter.ts | 28 ++++++++++++ .../src/pages/PageEnigma/actions/addObject.ts | 26 +++++++++++ .../src/pages/PageEnigma/actions/addShape.ts | 26 +++++++++++ .../pages/PageEnigma/actions/deleteObject.ts | 9 ++++ .../app/src/pages/PageEnigma/actions/index.ts | 7 +++ .../pages/PageEnigma/actions/selectObject.ts | 15 +++++++ .../PageEnigma/actions/setCameraAspect.ts | 14 ++++++ .../PageEnigma/actions/setTransformMode.ts | 17 +++++++ .../comps/AspectRatioMenu/AspectRatioMenu.tsx | 9 +++- .../comps/Controls3D/Controls3D.tsx | 44 +++---------------- .../SidePanelTabs/sharedComps/ItemElement.tsx | 5 ++- .../contexts/EngineContext/EngineContext.tsx | 10 +++++ .../contexts/EngineContext/EngineProvider.tsx | 4 +- .../signals/characterGroups/addCharacter.ts | 43 ------------------ .../signals/characterGroups/characterGroup.ts | 7 --- .../characterGroups/deleteCharacter.ts | 11 ----- .../signals/characterGroups/index.ts | 3 -- .../app/src/pages/PageEnigma/signals/index.ts | 3 -- .../signals/objectGroup/addObject.ts | 34 -------------- .../signals/objectGroup/deleteObject.ts | 11 ----- .../PageEnigma/signals/objectGroup/index.ts | 3 -- .../signals/objectGroup/objectGroup.ts | 7 --- .../app/src/pages/PageEnigma/signals/shape.ts | 13 ------ 28 files changed, 215 insertions(+), 228 deletions(-) create mode 100644 frontend/apps/artcraft/app/src/pages/PageEnigma/actions/addCharacter.ts create mode 100644 frontend/apps/artcraft/app/src/pages/PageEnigma/actions/addObject.ts create mode 100644 frontend/apps/artcraft/app/src/pages/PageEnigma/actions/addShape.ts create mode 100644 frontend/apps/artcraft/app/src/pages/PageEnigma/actions/deleteObject.ts create mode 100644 frontend/apps/artcraft/app/src/pages/PageEnigma/actions/index.ts create mode 100644 frontend/apps/artcraft/app/src/pages/PageEnigma/actions/selectObject.ts create mode 100644 frontend/apps/artcraft/app/src/pages/PageEnigma/actions/setCameraAspect.ts create mode 100644 frontend/apps/artcraft/app/src/pages/PageEnigma/actions/setTransformMode.ts delete mode 100644 frontend/apps/artcraft/app/src/pages/PageEnigma/signals/characterGroups/addCharacter.ts delete mode 100644 frontend/apps/artcraft/app/src/pages/PageEnigma/signals/characterGroups/characterGroup.ts delete mode 100644 frontend/apps/artcraft/app/src/pages/PageEnigma/signals/characterGroups/deleteCharacter.ts delete mode 100644 frontend/apps/artcraft/app/src/pages/PageEnigma/signals/characterGroups/index.ts delete mode 100644 frontend/apps/artcraft/app/src/pages/PageEnigma/signals/objectGroup/addObject.ts delete mode 100644 frontend/apps/artcraft/app/src/pages/PageEnigma/signals/objectGroup/deleteObject.ts delete mode 100644 frontend/apps/artcraft/app/src/pages/PageEnigma/signals/objectGroup/index.ts delete mode 100644 frontend/apps/artcraft/app/src/pages/PageEnigma/signals/objectGroup/objectGroup.ts delete mode 100644 frontend/apps/artcraft/app/src/pages/PageEnigma/signals/shape.ts diff --git a/frontend/apps/artcraft/app/src/components/experiences/ImageTo3DExperience.tsx b/frontend/apps/artcraft/app/src/components/experiences/ImageTo3DExperience.tsx index 8fd1c12335..2a0cc0c9c2 100644 --- a/frontend/apps/artcraft/app/src/components/experiences/ImageTo3DExperience.tsx +++ b/frontend/apps/artcraft/app/src/components/experiences/ImageTo3DExperience.tsx @@ -25,7 +25,8 @@ import { import { toast } from "react-hot-toast"; import { v4 as uuidv4 } from "uuid"; import { useTabStore } from "../../pages/Stores/TabState"; -import { addObject } from "../../pages/PageEnigma/signals/objectGroup/addObject"; +import { addObject } from "../../pages/PageEnigma/actions"; +import { getActiveEditor } from "../../pages/PageEnigma/contexts/EngineContext"; import { set3DPageMounted } from "../../pages/PageEnigma/Editor/editor"; import { AssetType } from "~/enums"; import type { MediaItem } from "../../pages/PageEnigma/models"; @@ -989,8 +990,13 @@ export const ImageTo3DExperience = ({ } as MediaItem & { position: { x: number; y: number; z: number }; }; - addObject(mediaItem); - toast.success("Added to 3D scene"); + const editor = getActiveEditor(); + if (editor) { + void addObject(editor, mediaItem); + toast.success("Added to 3D scene"); + } else { + toast.error("3D editor isn't ready yet"); + } }, 500); }} > diff --git a/frontend/apps/artcraft/app/src/pages/PageEnigma/DragAndDrop/DndAsset.ts b/frontend/apps/artcraft/app/src/pages/PageEnigma/DragAndDrop/DndAsset.ts index c9e3c2bc88..e5f65a3e0e 100644 --- a/frontend/apps/artcraft/app/src/pages/PageEnigma/DragAndDrop/DndAsset.ts +++ b/frontend/apps/artcraft/app/src/pages/PageEnigma/DragAndDrop/DndAsset.ts @@ -1,8 +1,6 @@ import React from "react"; import { MediaItem } from "~/pages/PageEnigma/models"; import { - addCharacter, - addObject, canDrop, currPosition, dragItem, @@ -10,8 +8,13 @@ import { reopenAfterDragSignal, } from "~/pages/PageEnigma/signals"; import { pageHeight, pageWidth } from "~/signals"; -import { addShape } from "~/pages/PageEnigma/signals/shape"; import { AssetType } from "~/enums"; +import type Editor from "~/pages/PageEnigma/Editor/editor"; +import { + addCharacter, + addObject, + addShape, +} from "~/pages/PageEnigma/actions"; class DndAsset { public dropId: string = ""; @@ -22,14 +25,20 @@ class DndAsset { public notDropText = ""; public isDragging: boolean = false; public dragThreshold: number = 5; + private editor: Editor | null = null; constructor() { this.onPointerMove = this.onPointerMove.bind(this); this.onPointerUp = this.onPointerUp.bind(this); } - onPointerDown(event: React.PointerEvent, item: MediaItem) { + onPointerDown( + event: React.PointerEvent, + item: MediaItem, + editor: Editor | null, + ) { if (event.button === 0) { + this.editor = editor; dragItem.value = item; currPosition.value = { currX: event.pageX, @@ -54,6 +63,7 @@ class DndAsset { this.notDropText = ""; assetModalVisibleDuringDrag.value = reopenAfterDragSignal.value; } + this.editor = null; } overCanvas(positionX: number, positionY: number) { @@ -74,26 +84,26 @@ class DndAsset { assetModalVisibleDuringDrag.value = true; dragItem.value = null; currPosition.value = { currX: 0, currY: 0 }; + this.editor = null; return; } - if (dragItem.value) { + const editor = this.editor; + if (dragItem.value && editor) { const positionX = event.pageX; const positionY = event.pageY; if (this.overCanvas(positionX, positionY)) { const mediaItem = dragItem.value; if (mediaItem.type === AssetType.CHARACTER) { - addCharacter(mediaItem); - } - if ( + void addCharacter(editor, mediaItem); + } else if ( mediaItem.type === AssetType.OBJECT || mediaItem.type === AssetType.SPLAT || mediaItem.type === AssetType.SKYBOX ) { - addObject(mediaItem); - } - if (mediaItem.type === AssetType.SHAPE) { - addShape(mediaItem); + void addObject(editor, mediaItem); + } else if (mediaItem.type === AssetType.SHAPE) { + void addShape(editor, mediaItem); } } } diff --git a/frontend/apps/artcraft/app/src/pages/PageEnigma/Editor/editor.ts b/frontend/apps/artcraft/app/src/pages/PageEnigma/Editor/editor.ts index cdcd065501..90a9fbdec3 100644 --- a/frontend/apps/artcraft/app/src/pages/PageEnigma/Editor/editor.ts +++ b/frontend/apps/artcraft/app/src/pages/PageEnigma/Editor/editor.ts @@ -1127,12 +1127,6 @@ class Editor { ); } - async recordScene() { - // TODO: timeline-driven frame recording was removed in the refactor; - // rewire video capture to a non-timeline trigger (likely - // VideoGeneration owning its own frame loop). - } - // Render the scene to the camera, this is called in the update. async renderScene() { if ( @@ -1141,19 +1135,13 @@ class Editor { this.rawRenderer && this.render_composer ) { - //const then = performance.now(); this.composer.render(); - //const now = performance.now(); - //console.log("Performance: ", now - then); - //this.rawRenderer.render(this.activeScene.scene, this.render_camera!); - // this.render_composer.render(); } else if (this.renderer && this.render_camera && !this.rendering) { this.renderer.setSize(this.render_width, this.render_height); this.renderer.render(this.activeScene.scene, this.render_camera); } else if (this.rendering && this.renderer) { this.renderer.setSize(this.render_width, this.render_height); } - // await this.recordScene(); } async useCachedMediaTokens(): Promise { diff --git a/frontend/apps/artcraft/app/src/pages/PageEnigma/Editor/video_generation.ts b/frontend/apps/artcraft/app/src/pages/PageEnigma/Editor/video_generation.ts index 0f951c2a67..cb32d94354 100644 --- a/frontend/apps/artcraft/app/src/pages/PageEnigma/Editor/video_generation.ts +++ b/frontend/apps/artcraft/app/src/pages/PageEnigma/Editor/video_generation.ts @@ -64,7 +64,7 @@ export class VideoGeneration { // TODO: timeline-driven engineFrameBuffers was removed; rewire video // capture to a non-timeline trigger (likely an explicit frame loop // owned by VideoGeneration). - const zipBlob: Blob | undefined = undefined; + const zipBlob: Blob | null = null; if (!zipBlob) { await this.handleError(`"Payload blob is null."`, 3000); } diff --git a/frontend/apps/artcraft/app/src/pages/PageEnigma/PageEditor.tsx b/frontend/apps/artcraft/app/src/pages/PageEnigma/PageEditor.tsx index db72dfac9e..0da1bdf762 100644 --- a/frontend/apps/artcraft/app/src/pages/PageEnigma/PageEditor.tsx +++ b/frontend/apps/artcraft/app/src/pages/PageEnigma/PageEditor.tsx @@ -31,8 +31,8 @@ import { gridVisibility, setGridVisibility, DomLevels, - setCameraAspectRatio, } from "~/pages/PageEnigma/signals"; +import { setCameraAspect } from "~/pages/PageEnigma/actions"; import { EditorCanvas } from "./comps/EngineCanvases"; import { SceneContainer } from "./comps/SceneContainer"; import { Outliner } from "./comps/Outliner"; @@ -60,8 +60,7 @@ import { topNavMediaUrl, } from "~/components/signaled/TopBar/TopBar"; import { uploadPlaneFromMediaToken } from "~/components/reusable/UploadModalMedia/uploadPlane"; -import { addObject } from "./signals/objectGroup/addObject"; -import { addCharacter } from "./signals/characterGroups/addCharacter"; +import { addObject, addCharacter } from "./actions"; import { AssetType, AUTH_STATUS } from "~/enums"; import { v4 as uuidv4 } from "uuid"; import { MediaItem } from "~/pages/PageEnigma/models"; @@ -283,7 +282,8 @@ export const PageEditor = () => { }; const onAspectRatioSelect = (newRatio: CameraAspectRatio) => { - setCameraAspectRatio(newRatio); + if (!editorEngine) return; + setCameraAspect(editorEngine, newRatio); }; // MOVE THIS don't throw this in here // Image drop from gallery/library modal logic @@ -317,44 +317,32 @@ export const PageEditor = () => { } (async () => { + if (!editorEngine) { + console.warn("Cannot drop asset: editor engine not ready"); + return; + } try { if (item.mediaClass === "dimensional") { - // Handle 3D models const isCharacter = item.assetType === "character"; const mediaItem: MediaItem = { version: 1, type: isCharacter ? AssetType.CHARACTER : AssetType.OBJECT, media_id: item.id || uuidv4(), name: item.label || (isCharacter ? "Character" : "3D Object"), - ...(worldPosition && { - position: { - x: worldPosition.x, - y: worldPosition.y, - z: worldPosition.z, - }, - }), }; if (isCharacter) { - addCharacter(mediaItem); + await addCharacter(editorEngine, mediaItem, worldPosition); } else { - addObject(mediaItem); + await addObject(editorEngine, mediaItem, worldPosition); } } else { - // Handle images (create image planes) const mediaItem: MediaItem = { version: 1, type: AssetType.OBJECT, media_id: item.id || uuidv4(), name: item.label || "Image Plane", - ...(worldPosition && { - position: { - x: worldPosition.x, - y: worldPosition.y, - z: worldPosition.z, - }, - }), }; - addObject(mediaItem); + await addObject(editorEngine, mediaItem, worldPosition); await uploadPlaneFromMediaToken({ title: item.label || "Image Plane", diff --git a/frontend/apps/artcraft/app/src/pages/PageEnigma/actions/addCharacter.ts b/frontend/apps/artcraft/app/src/pages/PageEnigma/actions/addCharacter.ts new file mode 100644 index 0000000000..e83647c7cb --- /dev/null +++ b/frontend/apps/artcraft/app/src/pages/PageEnigma/actions/addCharacter.ts @@ -0,0 +1,28 @@ +import * as THREE from "three"; +import type Editor from "../Editor/editor"; +import { MediaItem } from "../models"; +import { usePageEnigmaStore } from "../PageEnigmaStore"; + +export async function addCharacter( + editor: Editor, + item: MediaItem, + position?: THREE.Vector3, +): Promise { + const obj = await editor.sceneManager?.create( + item.media_id, + item.name ?? "character", + position ?? new THREE.Vector3(), + ); + if (!obj) return undefined; + + obj.userData.isCharacter = true; + + usePageEnigmaStore.getState().addCharacter({ + id: obj.uuid, + kind: "character", + name: obj.name || (item.name ?? "character"), + mediaId: item.media_id, + }); + editor.updateOutliner?.(); + return obj.uuid; +} diff --git a/frontend/apps/artcraft/app/src/pages/PageEnigma/actions/addObject.ts b/frontend/apps/artcraft/app/src/pages/PageEnigma/actions/addObject.ts new file mode 100644 index 0000000000..184ff3c40d --- /dev/null +++ b/frontend/apps/artcraft/app/src/pages/PageEnigma/actions/addObject.ts @@ -0,0 +1,26 @@ +import * as THREE from "three"; +import type Editor from "../Editor/editor"; +import { MediaItem } from "../models"; +import { usePageEnigmaStore } from "../PageEnigmaStore"; + +export async function addObject( + editor: Editor, + item: MediaItem, + position?: THREE.Vector3, +): Promise { + const obj = await editor.sceneManager?.create( + item.media_id, + item.name ?? "object", + position ?? new THREE.Vector3(), + ); + if (!obj) return undefined; + + usePageEnigmaStore.getState().addObject({ + id: obj.uuid, + kind: "object", + name: obj.name || (item.name ?? "object"), + mediaId: item.media_id, + }); + editor.updateOutliner?.(); + return obj.uuid; +} diff --git a/frontend/apps/artcraft/app/src/pages/PageEnigma/actions/addShape.ts b/frontend/apps/artcraft/app/src/pages/PageEnigma/actions/addShape.ts new file mode 100644 index 0000000000..26f38e4699 --- /dev/null +++ b/frontend/apps/artcraft/app/src/pages/PageEnigma/actions/addShape.ts @@ -0,0 +1,26 @@ +import * as THREE from "three"; +import type Editor from "../Editor/editor"; +import { MediaItem } from "../models"; +import { usePageEnigmaStore } from "../PageEnigmaStore"; + +export async function addShape( + editor: Editor, + item: MediaItem, + position?: THREE.Vector3, +): Promise { + const obj = await editor.sceneManager?.create( + item.media_id, + item.name ?? "shape", + position ?? new THREE.Vector3(), + ); + if (!obj) return undefined; + + usePageEnigmaStore.getState().addShape({ + id: obj.uuid, + kind: "shape", + name: obj.name || (item.name ?? "shape"), + mediaId: item.media_id, + }); + editor.updateOutliner?.(); + return obj.uuid; +} diff --git a/frontend/apps/artcraft/app/src/pages/PageEnigma/actions/deleteObject.ts b/frontend/apps/artcraft/app/src/pages/PageEnigma/actions/deleteObject.ts new file mode 100644 index 0000000000..41b620f361 --- /dev/null +++ b/frontend/apps/artcraft/app/src/pages/PageEnigma/actions/deleteObject.ts @@ -0,0 +1,9 @@ +import type Editor from "../Editor/editor"; + +// Editor.deleteObject already removes the THREE.js object via SceneUtils +// and updates the store via the helper that calls +// usePageEnigmaStore.getState().removeSceneObject. This action exists for +// symmetry with addObject and to give callers a single import surface. +export function deleteObject(editor: Editor, id: string): void { + editor.deleteObject(id); +} diff --git a/frontend/apps/artcraft/app/src/pages/PageEnigma/actions/index.ts b/frontend/apps/artcraft/app/src/pages/PageEnigma/actions/index.ts new file mode 100644 index 0000000000..cf2a381356 --- /dev/null +++ b/frontend/apps/artcraft/app/src/pages/PageEnigma/actions/index.ts @@ -0,0 +1,7 @@ +export * from "./addObject"; +export * from "./addCharacter"; +export * from "./addShape"; +export * from "./deleteObject"; +export * from "./selectObject"; +export * from "./setTransformMode"; +export * from "./setCameraAspect"; diff --git a/frontend/apps/artcraft/app/src/pages/PageEnigma/actions/selectObject.ts b/frontend/apps/artcraft/app/src/pages/PageEnigma/actions/selectObject.ts new file mode 100644 index 0000000000..dce771eb0a --- /dev/null +++ b/frontend/apps/artcraft/app/src/pages/PageEnigma/actions/selectObject.ts @@ -0,0 +1,15 @@ +import type Editor from "../Editor/editor"; + +// Selecting in the engine triggers Editor.publishSelect which writes the +// selection into the store, so this action only needs to invoke the engine +// path. +export function selectObject(editor: Editor, id: string): void { + editor.sceneManager?.select_object(id); +} + +export function deselectObject(editor: Editor): void { + if (editor.sceneManager) { + editor.sceneManager.selected_objects = undefined; + } + editor.publishSelect(); +} diff --git a/frontend/apps/artcraft/app/src/pages/PageEnigma/actions/setCameraAspect.ts b/frontend/apps/artcraft/app/src/pages/PageEnigma/actions/setCameraAspect.ts new file mode 100644 index 0000000000..5a7b566384 --- /dev/null +++ b/frontend/apps/artcraft/app/src/pages/PageEnigma/actions/setCameraAspect.ts @@ -0,0 +1,14 @@ +import type Editor from "../Editor/editor"; +import { CameraAspectRatio } from "../enums"; +import { setCameraAspectRatio as setCameraAspectSignal } from "../signals/engine"; +import { usePageEnigmaStore } from "../PageEnigmaStore"; + +export function setCameraAspect( + editor: Editor, + ratio: CameraAspectRatio, +): void { + editor.changeRenderCameraAspectRatio(ratio); + usePageEnigmaStore.getState().setCameraAspectRatio(ratio); + // Keep the legacy signal in sync until Phase 5 retires it. + setCameraAspectSignal(ratio); +} diff --git a/frontend/apps/artcraft/app/src/pages/PageEnigma/actions/setTransformMode.ts b/frontend/apps/artcraft/app/src/pages/PageEnigma/actions/setTransformMode.ts new file mode 100644 index 0000000000..79c7a557af --- /dev/null +++ b/frontend/apps/artcraft/app/src/pages/PageEnigma/actions/setTransformMode.ts @@ -0,0 +1,17 @@ +import type Editor from "../Editor/editor"; +import { usePageEnigmaStore, TransformMode } from "../PageEnigmaStore"; +import { selectedMode } from "../signals/selectedMode"; + +const ENGINE_MODE: Record = { + move: "translate", + rotate: "rotate", + scale: "scale", +}; + +export function setTransformMode(editor: Editor, mode: TransformMode): void { + editor.change_mode(ENGINE_MODE[mode]); + usePageEnigmaStore.getState().setTransformMode(mode); + usePageEnigmaStore.getState().setSelectedMode(mode); + // Keep the legacy signal in sync until Phase 5 retires it. + selectedMode.value = mode; +} diff --git a/frontend/apps/artcraft/app/src/pages/PageEnigma/comps/AspectRatioMenu/AspectRatioMenu.tsx b/frontend/apps/artcraft/app/src/pages/PageEnigma/comps/AspectRatioMenu/AspectRatioMenu.tsx index 93fb85dc99..f8360705b5 100644 --- a/frontend/apps/artcraft/app/src/pages/PageEnigma/comps/AspectRatioMenu/AspectRatioMenu.tsx +++ b/frontend/apps/artcraft/app/src/pages/PageEnigma/comps/AspectRatioMenu/AspectRatioMenu.tsx @@ -4,12 +4,16 @@ import { faRectangleVertical, faSquare, } from "@fortawesome/pro-solid-svg-icons"; +import { useContext } from "react"; import { CameraAspectRatio } from "~/pages/PageEnigma/enums"; -import { cameraAspectRatio, setCameraAspectRatio } from "~/pages/PageEnigma/signals"; +import { cameraAspectRatio } from "~/pages/PageEnigma/signals"; +import { setCameraAspect } from "~/pages/PageEnigma/actions"; +import { EngineContext } from "~/pages/PageEnigma/contexts/EngineContext"; import { ButtonDropdown } from "@storyteller/ui-button-dropdown"; export const AspectRatioMenu = () => { useSignals(); + const editor = useContext(EngineContext); const buttonIcon = cameraAspectRatio.value === CameraAspectRatio.HORIZONTAL_16_9 ? faRectangle @@ -32,7 +36,8 @@ export const AspectRatioMenu = () => { : "1:1 Squared"; const handleChangeAspectRatio = (newRatio: CameraAspectRatio) => { - setCameraAspectRatio(newRatio); + if (!editor) return; + setCameraAspect(editor, newRatio); }; return (
diff --git a/frontend/apps/artcraft/app/src/pages/PageEnigma/comps/Controls3D/Controls3D.tsx b/frontend/apps/artcraft/app/src/pages/PageEnigma/comps/Controls3D/Controls3D.tsx index 8f7e5b43df..61a0d971f6 100644 --- a/frontend/apps/artcraft/app/src/pages/PageEnigma/comps/Controls3D/Controls3D.tsx +++ b/frontend/apps/artcraft/app/src/pages/PageEnigma/comps/Controls3D/Controls3D.tsx @@ -23,6 +23,8 @@ import { useTabStore } from "~/pages/Stores/TabState"; // import { AssetType } from "~/enums"; import { AssetModal } from "../AssetMenu/AssetModal"; import { selectedMode, transformSpace } from "../../signals/selectedMode"; +import { setTransformMode } from "../../actions"; +import { TransformMode } from "../../PageEnigmaStore"; import { useSignals } from "@preact/signals-react/runtime"; import { outlinerState } from "../../signals/outliner/outliner"; // eslint-disable-next-line import/no-unresolved @@ -85,41 +87,12 @@ export const Controls3D = () => { ]); const handleModeChange = (value: string) => { - selectedMode.value = value; - switch (value) { - case "move": - handleMoveArrows(); - break; - case "rotate": - handleRotateArrows(); - break; - case "scale": - handleZoomArrows(); - break; - default: - console.log("Unknown option"); + if (!editorEngine) return; + if (value === "move" || value === "rotate" || value === "scale") { + setTransformMode(editorEngine, value as TransformMode); } }; - const handleMoveArrows = () => { - if (!editorEngine) { - return; - } - editorEngine.change_mode("translate"); - }; - const handleRotateArrows = () => { - if (!editorEngine) { - return; - } - editorEngine.change_mode("rotate"); - }; - const handleZoomArrows = () => { - if (!editorEngine) { - return; - } - editorEngine.change_mode("scale"); - }; - // ----- TODO LATER - BFlat for auto add 3d model to scene ----- // Function to add the generated 3D model to the scene @@ -379,12 +352,7 @@ export const Controls3D = () => { setUploadSplatIsShowing(false)} - onSuccess={(_buffer, _shouldFlip) => { - setUploadSplatIsShowing(false); - // TODO: addLocalSplat lived on the deleted timeline; rewire to a - // direct editor.addLocalSplat once the imperative engine grows - // that method. - }} + onSuccess={() => setUploadSplatIsShowing(false)} title="Upload an spz file" titleIcon={faCube} /> diff --git a/frontend/apps/artcraft/app/src/pages/PageEnigma/comps/SidePanelTabs/sharedComps/ItemElement.tsx b/frontend/apps/artcraft/app/src/pages/PageEnigma/comps/SidePanelTabs/sharedComps/ItemElement.tsx index ce530f6ae8..09bbb9aaea 100644 --- a/frontend/apps/artcraft/app/src/pages/PageEnigma/comps/SidePanelTabs/sharedComps/ItemElement.tsx +++ b/frontend/apps/artcraft/app/src/pages/PageEnigma/comps/SidePanelTabs/sharedComps/ItemElement.tsx @@ -1,7 +1,9 @@ // TODO: Move into shared component. +import { useContext } from "react"; import { MediaItem } from "~/pages/PageEnigma/models"; import { useSignals } from "@preact/signals-react/runtime"; import DndAsset from "~/pages/PageEnigma/DragAndDrop/DndAsset"; +import { EngineContext } from "~/pages/PageEnigma/contexts/EngineContext"; import { Badge } from "@storyteller/ui-badge"; import { AssetType } from "~/enums"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -37,6 +39,7 @@ const patchExpressionObjectType = (mediaType: string) => { export const ItemElement = ({ item }: Props) => { useSignals(); + const editor = useContext(EngineContext); return (
@@ -55,7 +58,7 @@ export const ItemElement = ({ item }: Props) => {
DndAsset.onPointerDown(event, item)} + onPointerDown={(event) => DndAsset.onPointerDown(event, item, editor)} style={{ cursor: "grab", pointerEvents: "auto" }} > (null); + +// Module-level handle on the active Editor instance. EngineProvider keeps +// this in sync with its lifecycle. React components should still use +// useContext(EngineContext) — this exists for non-React callers (and for +// cross-page actions that fire before the 3D tab has mounted). +let activeEditor: Editor | null = null; +export const getActiveEditor = (): Editor | null => activeEditor; +export const setActiveEditor = (editor: Editor | null): void => { + activeEditor = editor; +}; diff --git a/frontend/apps/artcraft/app/src/pages/PageEnigma/contexts/EngineContext/EngineProvider.tsx b/frontend/apps/artcraft/app/src/pages/PageEnigma/contexts/EngineContext/EngineProvider.tsx index a96dddd81b..16af26d611 100644 --- a/frontend/apps/artcraft/app/src/pages/PageEnigma/contexts/EngineContext/EngineProvider.tsx +++ b/frontend/apps/artcraft/app/src/pages/PageEnigma/contexts/EngineContext/EngineProvider.tsx @@ -1,5 +1,5 @@ import { ReactNode, useEffect, useRef, useState } from "react"; -import { EngineContext } from "./EngineContext"; +import { EngineContext, setActiveEditor } from "./EngineContext"; import Editor, { is3DSceneLoaded } from "~/pages/PageEnigma/Editor/editor"; import { useSignals } from "@preact/signals-react/runtime"; @@ -53,6 +53,7 @@ export const EngineProvider = ({ sceneToken, children }: Props) => { cacheJsonString: cacheString, }); setEditor(newEditor); + setActiveEditor(newEditor); } else if (tab !== "3D") { if (!activeEditorRef.current) { return; @@ -78,6 +79,7 @@ export const EngineProvider = ({ sceneToken, children }: Props) => { // Unmount/Destructor flow activeEditorRef.current?.unmountEngine(); activeEditorRef.current = null; + setActiveEditor(null); sceneContainerSignal.value = null; editorCanvasSignal.value = null; camViewCanvasSignal.value = null; diff --git a/frontend/apps/artcraft/app/src/pages/PageEnigma/signals/characterGroups/addCharacter.ts b/frontend/apps/artcraft/app/src/pages/PageEnigma/signals/characterGroups/addCharacter.ts deleted file mode 100644 index edd9fcf5ec..0000000000 --- a/frontend/apps/artcraft/app/src/pages/PageEnigma/signals/characterGroups/addCharacter.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { CharacterTrack, MediaItem } from "~/pages/PageEnigma/models"; -import { characterGroup } from "~/pages/PageEnigma/signals"; -import { usePageEnigmaStore } from "~/pages/PageEnigma/PageEnigmaStore"; - -export function addCharacter(character: MediaItem) { - // TODO: After editor.ts is migrated, replace with actions/addCharacter which - // calls editor.addCharacter(character) and updates the store in one step. - usePageEnigmaStore.getState().addCharacter({ - id: character.object_uuid ?? character.media_id, - kind: "character", - name: character.name ?? "character", - mediaId: character.media_id, - }); - addNewCharacter(character); -} - -export function addNewCharacter(data: MediaItem) { - const newCharacterGroups = { - ...characterGroup.value, - characters: [...characterGroup.value.characters], - }; - - const newCharacter = { - object_uuid: data.object_uuid, - name: data.name, - media_id: data.media_id, - mediaType: data.media_type, - animationType: data.maybe_animation_type, - muted: false, - minimized: false, - animationClips: [], - positionKeyframes: [], - expressionClips: [], - lipSyncClips: [], - } as CharacterTrack; - - newCharacterGroups.characters.push(newCharacter); - newCharacterGroups.characters.sort((charA, charB) => - charA.object_uuid < charB.object_uuid ? -1 : 1, - ); - - characterGroup.value = newCharacterGroups; -} diff --git a/frontend/apps/artcraft/app/src/pages/PageEnigma/signals/characterGroups/characterGroup.ts b/frontend/apps/artcraft/app/src/pages/PageEnigma/signals/characterGroups/characterGroup.ts deleted file mode 100644 index ff145c1573..0000000000 --- a/frontend/apps/artcraft/app/src/pages/PageEnigma/signals/characterGroups/characterGroup.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { CharacterGroup } from "~/pages/PageEnigma/models"; -import { signal } from "@preact/signals-core"; - -export const characterGroup = signal({ - id: "CG1", - characters: [], -}); diff --git a/frontend/apps/artcraft/app/src/pages/PageEnigma/signals/characterGroups/deleteCharacter.ts b/frontend/apps/artcraft/app/src/pages/PageEnigma/signals/characterGroups/deleteCharacter.ts deleted file mode 100644 index e6ec64ccf8..0000000000 --- a/frontend/apps/artcraft/app/src/pages/PageEnigma/signals/characterGroups/deleteCharacter.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { MediaItem } from "~/pages/PageEnigma/models"; -import { characterGroup } from "~/pages/PageEnigma/signals"; - -export function deleteCharacter(item: MediaItem) { - characterGroup.value = { - ...characterGroup.value, - characters: characterGroup.value.characters.filter( - (character) => character.object_uuid !== item.object_uuid, - ), - }; -} diff --git a/frontend/apps/artcraft/app/src/pages/PageEnigma/signals/characterGroups/index.ts b/frontend/apps/artcraft/app/src/pages/PageEnigma/signals/characterGroups/index.ts deleted file mode 100644 index 1f8cd1defa..0000000000 --- a/frontend/apps/artcraft/app/src/pages/PageEnigma/signals/characterGroups/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./addCharacter"; -export * from "./characterGroup"; -export * from "./deleteCharacter"; diff --git a/frontend/apps/artcraft/app/src/pages/PageEnigma/signals/index.ts b/frontend/apps/artcraft/app/src/pages/PageEnigma/signals/index.ts index 8ea898092f..775a24ad6f 100644 --- a/frontend/apps/artcraft/app/src/pages/PageEnigma/signals/index.ts +++ b/frontend/apps/artcraft/app/src/pages/PageEnigma/signals/index.ts @@ -1,15 +1,12 @@ -export * from "./characterGroups"; export * from "./demoAssets"; export * from "./editorOverlays"; export * from "./engine"; export * from "./dragAndDrop"; export * from "./hotkeys"; export * from "./myMovies"; -export * from "./objectGroup"; export * from "./objectPanel"; export * from "./outliner"; export * from "./selectedItem"; -export * from "./shape"; export * from "./sidePanel"; export * from "./sizing"; export * from "./stylizeTab"; diff --git a/frontend/apps/artcraft/app/src/pages/PageEnigma/signals/objectGroup/addObject.ts b/frontend/apps/artcraft/app/src/pages/PageEnigma/signals/objectGroup/addObject.ts deleted file mode 100644 index 50a9b59def..0000000000 --- a/frontend/apps/artcraft/app/src/pages/PageEnigma/signals/objectGroup/addObject.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { MediaItem, ObjectTrack, Keyframe } from "~/pages/PageEnigma/models"; -import { objectGroup } from "~/pages/PageEnigma/signals"; -import { usePageEnigmaStore } from "~/pages/PageEnigma/PageEnigmaStore"; - -export function addObject( - object: MediaItem & { position?: { x: number; y: number; z: number } } -) { - // TODO: After editor.ts is migrated, replace with actions/addObject which - // calls editor.addObject(object) and updates the store in one step. - usePageEnigmaStore.getState().addObject({ - id: object.object_uuid ?? object.media_id, - kind: "object", - name: object.name ?? "object", - mediaId: object.media_id, - }); - addObjectToTimeline(object); -} - -export function addObjectToTimeline(mediaItem: MediaItem) { - const oldObjectGroup = objectGroup.value; - - const newObject = { - object_uuid: mediaItem.object_uuid, - name: mediaItem.name ?? "unknown", - keyframes: [] as Keyframe[], - } as ObjectTrack; - - objectGroup.value = { - ...oldObjectGroup, - objects: [...oldObjectGroup.objects, newObject].sort((objA, objB) => - objA.object_uuid < objB.object_uuid ? -1 : 1, - ), - }; -} diff --git a/frontend/apps/artcraft/app/src/pages/PageEnigma/signals/objectGroup/deleteObject.ts b/frontend/apps/artcraft/app/src/pages/PageEnigma/signals/objectGroup/deleteObject.ts deleted file mode 100644 index 7cdaf0f14c..0000000000 --- a/frontend/apps/artcraft/app/src/pages/PageEnigma/signals/objectGroup/deleteObject.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { MediaItem } from "~/pages/PageEnigma/models"; -import { objectGroup } from "~/pages/PageEnigma/signals"; - -export function deleteObject(item: MediaItem) { - objectGroup.value = { - ...objectGroup.value, - objects: objectGroup.value.objects.filter( - (obj) => obj.object_uuid !== item.object_uuid, - ), - }; -} diff --git a/frontend/apps/artcraft/app/src/pages/PageEnigma/signals/objectGroup/index.ts b/frontend/apps/artcraft/app/src/pages/PageEnigma/signals/objectGroup/index.ts deleted file mode 100644 index 1a0f315c2b..0000000000 --- a/frontend/apps/artcraft/app/src/pages/PageEnigma/signals/objectGroup/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./addObject"; -export * from "./deleteObject"; -export * from "./objectGroup"; diff --git a/frontend/apps/artcraft/app/src/pages/PageEnigma/signals/objectGroup/objectGroup.ts b/frontend/apps/artcraft/app/src/pages/PageEnigma/signals/objectGroup/objectGroup.ts deleted file mode 100644 index 274fef65cd..0000000000 --- a/frontend/apps/artcraft/app/src/pages/PageEnigma/signals/objectGroup/objectGroup.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { signal } from "@preact/signals-core"; -import { ObjectGroup } from "~/pages/PageEnigma/models"; - -export const objectGroup = signal({ - id: "OB1", - objects: [], -}); diff --git a/frontend/apps/artcraft/app/src/pages/PageEnigma/signals/shape.ts b/frontend/apps/artcraft/app/src/pages/PageEnigma/signals/shape.ts deleted file mode 100644 index 22b6e83b09..0000000000 --- a/frontend/apps/artcraft/app/src/pages/PageEnigma/signals/shape.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { MediaItem } from "~/pages/PageEnigma/models"; -import { usePageEnigmaStore } from "~/pages/PageEnigma/PageEnigmaStore"; - -export function addShape(item: MediaItem) { - // TODO: After editor.ts is migrated, replace with actions/addShape which - // calls editor.addShape(item) and updates the store in one step. - usePageEnigmaStore.getState().addShape({ - id: item.object_uuid ?? item.media_id, - kind: "shape", - name: item.name ?? "shape", - mediaId: item.media_id, - }); -}