mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-04 01:18:34 +00:00
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.
This commit is contained in:
1 parent
6d22d0d3c3
commit
61311028e2
28 files changed
+215
-228
No files matched your search
@@ -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);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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<HTMLDivElement>, item: MediaItem) {
|
||||
onPointerDown(
|
||||
event: React.PointerEvent<HTMLDivElement>,
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<boolean> {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<string | undefined> {
|
||||
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;
|
||||
}
|
||||
@@ -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<string | undefined> {
|
||||
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;
|
||||
}
|
||||
@@ -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<string | undefined> {
|
||||
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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import type Editor from "../Editor/editor";
|
||||
import { usePageEnigmaStore, TransformMode } from "../PageEnigmaStore";
|
||||
import { selectedMode } from "../signals/selectedMode";
|
||||
|
||||
const ENGINE_MODE: Record<TransformMode, "translate" | "rotate" | "scale"> = {
|
||||
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;
|
||||
}
|
||||
+7
-2
@@ -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 (
|
||||
<div className="absolute right-0 top-0 m-2 flex flex-col items-end">
|
||||
|
||||
@@ -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 = () => {
|
||||
<UploadModalSplat
|
||||
isOpen={uploadSplatIsShowing}
|
||||
onClose={() => 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}
|
||||
/>
|
||||
|
||||
+4
-1
@@ -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 (
|
||||
<div className="group relative w-full select-none overflow-hidden transition-all duration-200">
|
||||
@@ -55,7 +58,7 @@ export const ItemElement = ({ item }: Props) => {
|
||||
|
||||
<div
|
||||
className="pointer-events-none relative aspect-[16/12] w-full select-none overflow-hidden rounded-xl border-[3px] border-white/5 bg-brand-secondary-600 object-cover object-center transition-all group-hover:border-brand-primary"
|
||||
onPointerDown={(event) => DndAsset.onPointerDown(event, item)}
|
||||
onPointerDown={(event) => DndAsset.onPointerDown(event, item, editor)}
|
||||
style={{ cursor: "grab", pointerEvents: "auto" }}
|
||||
>
|
||||
<img
|
||||
|
||||
+10
@@ -14,3 +14,13 @@ export interface EditorExpandedI extends Editor {
|
||||
}
|
||||
|
||||
export const EngineContext = createContext<Editor | null>(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;
|
||||
};
|
||||
+3
-1
@@ -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;
|
||||
|
||||
-43
@@ -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;
|
||||
}
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
import { CharacterGroup } from "~/pages/PageEnigma/models";
|
||||
import { signal } from "@preact/signals-core";
|
||||
|
||||
export const characterGroup = signal<CharacterGroup>({
|
||||
id: "CG1",
|
||||
characters: [],
|
||||
});
|
||||
-11
@@ -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,
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from "./addCharacter";
|
||||
export * from "./characterGroup";
|
||||
export * from "./deleteCharacter";
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from "./addObject";
|
||||
export * from "./deleteObject";
|
||||
export * from "./objectGroup";
|
||||
@@ -1,7 +0,0 @@
|
||||
import { signal } from "@preact/signals-core";
|
||||
import { ObjectGroup } from "~/pages/PageEnigma/models";
|
||||
|
||||
export const objectGroup = signal<ObjectGroup>({
|
||||
id: "OB1",
|
||||
objects: [],
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user