diff --git a/frontend/apps/artcraft/app/src/enums/AssetFilterOption.ts b/frontend/apps/artcraft/app/src/enums/AssetFilterOption.ts index dfc97ea3be..86a6daeb22 100644 --- a/frontend/apps/artcraft/app/src/enums/AssetFilterOption.ts +++ b/frontend/apps/artcraft/app/src/enums/AssetFilterOption.ts @@ -1,6 +1 @@ -export enum AssetFilterOption { - ALL, - FEATURED, - MINE, - BOOKMARKED, -} +export { AssetFilterOption } from "@storyteller/ui-pagescene"; diff --git a/frontend/apps/artcraft/app/src/enums/AssetType.ts b/frontend/apps/artcraft/app/src/enums/AssetType.ts index 008a0d6c14..09662af2ab 100644 --- a/frontend/apps/artcraft/app/src/enums/AssetType.ts +++ b/frontend/apps/artcraft/app/src/enums/AssetType.ts @@ -1,11 +1,4 @@ -export enum AssetType { - ANIMATION = "animation", - AUDIO = "audio", - CAMERA = "camera", - CHARACTER = "character", - EXPRESSION = "expression", - OBJECT = "object", - SKYBOX = "skybox", - SHAPE = "shape", - SPLAT = "splat", -} +// Single source of truth lives in @storyteller/ui-pagescene; this +// re-export keeps the artcraft `~/enums` surface stable while the +// 3D scene lib owns the canonical enum identity. +export { AssetType } from "@storyteller/ui-pagescene"; diff --git a/frontend/apps/artcraft/app/src/enums/ClipGroup.ts b/frontend/apps/artcraft/app/src/enums/ClipGroup.ts index bae91bc0ba..7a27917a5b 100644 --- a/frontend/apps/artcraft/app/src/enums/ClipGroup.ts +++ b/frontend/apps/artcraft/app/src/enums/ClipGroup.ts @@ -1,7 +1 @@ -export enum ClipGroup { - CHARACTER = "character", - CAMERA = "camera", - GLOBAL_AUDIO = "global_audio", - OBJECT = "object", - PROMPT_TRAVEL = "prompt", -} +export { ClipGroup } from "@storyteller/ui-pagescene"; diff --git a/frontend/apps/artcraft/app/src/pages/PageScene/PageSceneStore.ts b/frontend/apps/artcraft/app/src/pages/PageScene/PageSceneStore.ts index 8536ced768..5a1ac46cdc 100644 --- a/frontend/apps/artcraft/app/src/pages/PageScene/PageSceneStore.ts +++ b/frontend/apps/artcraft/app/src/pages/PageScene/PageSceneStore.ts @@ -1,449 +1,27 @@ -import { create } from "zustand"; -import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; -import { Camera, FocalLengthDragging } from "@storyteller/common"; -import { CameraAspectRatio, EditorStates } from "./enums"; -import { AssetType, AssetFilterOption, ClipGroup } from "~/enums"; -import { MediaItem } from "./models"; -import { Simple3DVector } from "./datastructures/common"; +// Re-export shim. The Zustand store now lives in +// @storyteller/ui-pagescene; this file exists so importers across +// artcraft (and PageScene's own files still in apps/) can keep their +// existing import paths during the staged lib extraction. Once every +// caller imports from `@storyteller/ui-pagescene` directly, this file +// can be deleted. -export type { Camera, FocalLengthDragging }; - -export type SceneObjectKind = "object" | "character" | "shape"; - -export interface SceneObject { - id: string; - kind: SceneObjectKind; - name: string; - mediaId?: string; - mediaToken?: string; -} - -// Item in the outliner panel (with icon, visibility, lock state). -// Distinct from `SceneObject` — outliner tracks UI-side rows; the -// engine maintains its own object model. -export interface OutlinerItem { - id: string; - icon: IconDefinition; - name: string; - type: string; - visible: boolean; - locked: boolean; -} - -// The currently inspected object in the right-hand control panel. -// Distinct from both `SceneObject` and `OutlinerItem`; this carries -// engine-side identifiers used by the gizmo / property panel. -export interface ObjectPanelObject { - group: ClipGroup; - object_uuid: string; - object_name: string; - version: string; - objectVectors: Simple3DVector; -} - -export interface DragPosition { - currX: number; - currY: number; -} - -export interface PrecisionSelectorCoords { - x: number; - y: number; -} - -export interface SelectedSceneObject { - type: AssetType; - id: string; -} - -export type TransformMode = "move" | "rotate" | "scale"; -export type TransformSpace = "world" | "local"; -export type PoseMode = "select" | "pose"; - -export enum DomLevels { - NONE = 0, - INPUT = 1, - PANEL = 2, - DIALOGUE = 3, -} - -export interface HotkeyStatus { - disabled: boolean; - disabledBy: DomLevels; -} - -export interface EditorLoader { - isShowing: boolean; - message: string | undefined; -} - -const DEFAULT_CAMERAS: Camera[] = [ - { - id: "main", - label: "Main View", - focalLength: 17, - position: { x: -2.5, y: 2.5, z: 2.5 }, - rotation: { x: 0, y: 0, z: 0 }, - lookAt: { x: 0, y: 0, z: 0 }, - }, - { - id: "cam2", - label: "Camera 2", - focalLength: 10, - position: { x: 0, y: 0.6, z: 1.5 }, - rotation: { x: 0, y: 0, z: 0 }, - lookAt: { x: 0, y: 0, z: 0 }, - }, -]; - -interface PageSceneState { - // scene contents - objects: SceneObject[]; - characters: SceneObject[]; - shapes: SceneObject[]; - selectedObject: SelectedSceneObject | null; - - // cameras - cameras: Camera[]; - selectedCameraId: string; - cameraAspectRatio: CameraAspectRatio; - focalLengthDragging: FocalLengthDragging; - cameraFilter: AssetFilterOption; - - // editor mode - editorState: EditorStates; - transformMode: TransformMode; - transformSpace: TransformSpace; - selectedMode: string; - poseMode: PoseMode; - showPoseControls: boolean; - gridVisible: boolean; - ignoreKeyDelete: boolean; - hotkeyStatus: HotkeyStatus; - isPromptBoxFocused: boolean; - - // layout / panels - assetModalVisible: boolean; - assetModalVisibleDuringDrag: boolean; - reopenAfterDrag: boolean; - - // overlays - editorLoader: EditorLoader; - editorLetterBox: boolean; - showErrorDialog: boolean; - errorDialogTitle: string; - errorDialogMessage: string; - - // drag-and-drop - canDrop: boolean; - dragItem: MediaItem | null; - dragPosition: DragPosition; - - // object panel (right-hand inspector for selected object) - objectPanelShowing: boolean; - objectPanelCurrent: ObjectPanelObject | undefined; - - // outliner (left-hand scene tree) - outlinerItems: OutlinerItem[]; - outlinerSelectedItem: OutlinerItem | null; - outlinerShowing: boolean; - - // precision selector popover - precisionSelectorShowing: boolean; - precisionSelectorCoords: PrecisionSelectorCoords; - precisionSelectorValues: number[]; - precisionSelectedValue: number; - - // engine lifecycle flags (mirrored to other parts of the app) - is3DPageMounted: boolean; - is3DEditorInitialized: boolean; - is3DSceneLoaded: boolean; - - // canvas DOM refs (set by canvas components on mount; consumed by - // the engine + hooks) - sceneContainerEl: HTMLDivElement | null; - editorCanvasEl: HTMLCanvasElement | null; - camViewCanvasEl: HTMLCanvasElement | null; - - // ----- actions ----- - - // scene - addObject: (obj: SceneObject) => void; - addCharacter: (obj: SceneObject) => void; - addShape: (obj: SceneObject) => void; - removeSceneObject: (id: string) => void; - setSelectedObject: (sel: SelectedSceneObject | null) => void; - resetScene: () => void; - - // camera - addCamera: (camera: Camera) => void; - updateCamera: (id: string, updates: Partial) => void; - deleteCamera: (id: string) => void; - setSelectedCameraId: (id: string) => void; - setCameraAspectRatio: (ratio: CameraAspectRatio) => void; - setFocalLengthDragging: (state: FocalLengthDragging) => void; - setCameraFilter: (filter: AssetFilterOption) => void; - - // editor mode - setEditorState: (state: EditorStates) => void; - setTransformMode: (mode: TransformMode) => void; - setTransformSpace: (space: TransformSpace) => void; - setSelectedMode: (mode: string) => void; - setPoseMode: (mode: PoseMode) => void; - setShowPoseControls: (visible: boolean) => void; - setGridVisible: (visible: boolean) => void; - setIgnoreKeyDelete: (ignore: boolean) => void; - disableHotkeyInput: (level: DomLevels) => void; - enableHotkeyInput: (level: DomLevels) => void; - setIsPromptBoxFocused: (focused: boolean) => void; - - // layout - setAssetModalVisible: (visible: boolean) => void; - setAssetModalVisibleDuringDrag: (visible: boolean) => void; - setReopenAfterDrag: (reopen: boolean) => void; - - // overlays - showEditorLoader: (message?: string) => void; - hideEditorLoader: () => void; - toggleEditorLetterBox: (next?: boolean) => void; - setErrorDialog: (title: string, message: string) => void; - setShowErrorDialog: (show: boolean) => void; - - // drag-and-drop - setCanDrop: (canDrop: boolean) => void; - setDragItem: (item: MediaItem | null) => void; - setDragPosition: (pos: DragPosition) => void; - - // object panel - showObjectPanel: (obj?: ObjectPanelObject) => void; - hideObjectPanel: () => void; - updateObjectPanel: (obj: ObjectPanelObject) => void; - - // outliner - setOutlinerItems: (items: OutlinerItem[]) => void; - setOutlinerSelectedItem: (item: OutlinerItem | null) => void; - setOutlinerShowing: (showing: boolean) => void; - selectOutlinerItem: (id: string) => void; - toggleOutlinerVisibility: (id: string) => void; - toggleOutlinerLock: (id: string) => void; - - // precision selector - showPrecisionSelector: ( - coords: PrecisionSelectorCoords, - values: number[], - ) => void; - hidePrecisionSelector: () => void; - setPrecisionSelectedValue: (v: number) => void; - - // engine lifecycle - set3DPageMounted: (mounted: boolean) => void; - setIs3DEditorInitialized: (initialized: boolean) => void; - setIs3DSceneLoaded: (loaded: boolean) => void; - - // canvas refs - setSceneContainerEl: (el: HTMLDivElement | null) => void; - setEditorCanvasEl: (el: HTMLCanvasElement | null) => void; - setCamViewCanvasEl: (el: HTMLCanvasElement | null) => void; -} - -export const usePageSceneStore = create((set, get) => ({ - // initial state - objects: [], - characters: [], - shapes: [], - selectedObject: null, - - cameras: DEFAULT_CAMERAS, - selectedCameraId: "main", - cameraAspectRatio: CameraAspectRatio.HORIZONTAL_3_2, - focalLengthDragging: { isDragging: false, focalLength: 35 }, - cameraFilter: AssetFilterOption.ALL, - - editorState: EditorStates.EDIT, - transformMode: "move", - transformSpace: "world", - selectedMode: "move", - poseMode: "select", - showPoseControls: false, - gridVisible: true, - ignoreKeyDelete: false, - hotkeyStatus: { disabled: false, disabledBy: DomLevels.NONE }, - isPromptBoxFocused: false, - - assetModalVisible: false, - assetModalVisibleDuringDrag: true, - reopenAfterDrag: false, - - editorLoader: { isShowing: false, message: "Loading Editor Engine 🦊" }, - editorLetterBox: true, - showErrorDialog: false, - errorDialogTitle: "Error!", - errorDialogMessage: "Something went wrong.", - - canDrop: false, - dragItem: null, - dragPosition: { currX: 0, currY: 0 }, - - objectPanelShowing: false, - objectPanelCurrent: undefined, - - outlinerItems: [], - outlinerSelectedItem: null, - outlinerShowing: false, - - precisionSelectorShowing: false, - precisionSelectorCoords: { x: 0, y: 0 }, - precisionSelectorValues: [], - precisionSelectedValue: 0, - - is3DPageMounted: false, - is3DEditorInitialized: false, - is3DSceneLoaded: false, - - sceneContainerEl: null, - editorCanvasEl: null, - camViewCanvasEl: null, - - // scene actions - addObject: (obj) => - set((s) => ({ objects: [...s.objects, obj] })), - addCharacter: (obj) => - set((s) => ({ characters: [...s.characters, obj] })), - addShape: (obj) => - set((s) => ({ shapes: [...s.shapes, obj] })), - removeSceneObject: (id) => - set((s) => ({ - objects: s.objects.filter((o) => o.id !== id), - characters: s.characters.filter((o) => o.id !== id), - shapes: s.shapes.filter((o) => o.id !== id), - selectedObject: - s.selectedObject?.id === id ? null : s.selectedObject, - })), - setSelectedObject: (sel) => set({ selectedObject: sel }), - resetScene: () => - set({ objects: [], characters: [], shapes: [], selectedObject: null }), - - // camera actions - addCamera: (camera) => set((s) => ({ cameras: [...s.cameras, camera] })), - updateCamera: (id, updates) => - set((s) => ({ - cameras: s.cameras.map((c) => (c.id === id ? { ...c, ...updates } : c)), - })), - deleteCamera: (id) => { - if (id === "main") return; - set((s) => ({ - cameras: s.cameras.filter((c) => c.id !== id), - selectedCameraId: s.selectedCameraId === id ? "main" : s.selectedCameraId, - })); - }, - setSelectedCameraId: (id) => set({ selectedCameraId: id }), - setCameraAspectRatio: (ratio) => set({ cameraAspectRatio: ratio }), - setFocalLengthDragging: (state) => set({ focalLengthDragging: state }), - setCameraFilter: (filter) => set({ cameraFilter: filter }), - - // editor mode actions - setEditorState: (state) => set({ editorState: state }), - setTransformMode: (mode) => set({ transformMode: mode }), - setTransformSpace: (space) => set({ transformSpace: space }), - setSelectedMode: (mode) => set({ selectedMode: mode }), - setPoseMode: (mode) => set({ poseMode: mode }), - setShowPoseControls: (visible) => set({ showPoseControls: visible }), - setGridVisible: (visible) => set({ gridVisible: visible }), - setIgnoreKeyDelete: (ignore) => set({ ignoreKeyDelete: ignore }), - disableHotkeyInput: (level) => { - const status = get().hotkeyStatus; - if (status.disabled) { - if (level > status.disabledBy) { - set({ hotkeyStatus: { ...status, disabledBy: level } }); - } - } else { - set({ hotkeyStatus: { disabled: true, disabledBy: level } }); - } - }, - enableHotkeyInput: (level) => { - const status = get().hotkeyStatus; - if (status.disabled && level >= status.disabledBy) { - set({ hotkeyStatus: { disabled: false, disabledBy: DomLevels.NONE } }); - } - }, - setIsPromptBoxFocused: (focused) => set({ isPromptBoxFocused: focused }), - - // layout actions - setAssetModalVisible: (visible) => set({ assetModalVisible: visible }), - setAssetModalVisibleDuringDrag: (visible) => - set({ assetModalVisibleDuringDrag: visible }), - setReopenAfterDrag: (reopen) => set({ reopenAfterDrag: reopen }), - - // overlays actions - showEditorLoader: (message) => - set({ editorLoader: { isShowing: true, message } }), - hideEditorLoader: () => - set((s) => ({ - editorLoader: { isShowing: false, message: s.editorLoader.message }, - })), - toggleEditorLetterBox: (next) => - set((s) => ({ - editorLetterBox: next !== undefined ? next : !s.editorLetterBox, - })), - setErrorDialog: (title, message) => - set({ - errorDialogTitle: title, - errorDialogMessage: message, - showErrorDialog: true, - }), - setShowErrorDialog: (show) => set({ showErrorDialog: show }), - - // drag-and-drop actions - setCanDrop: (canDrop) => set({ canDrop }), - setDragItem: (item) => set({ dragItem: item }), - setDragPosition: (pos) => set({ dragPosition: pos }), - - // object panel actions - showObjectPanel: (obj) => - set((s) => ({ - objectPanelShowing: true, - objectPanelCurrent: obj ?? s.objectPanelCurrent, - })), - hideObjectPanel: () => set({ objectPanelShowing: false }), - updateObjectPanel: (obj) => set({ objectPanelCurrent: obj }), - - // outliner actions - setOutlinerItems: (items) => set({ outlinerItems: items }), - setOutlinerSelectedItem: (item) => set({ outlinerSelectedItem: item }), - setOutlinerShowing: (showing) => set({ outlinerShowing: showing }), - selectOutlinerItem: (id) => { - const item = get().outlinerItems.find((i) => i.id === id); - if (item) set({ outlinerSelectedItem: item }); - }, - toggleOutlinerVisibility: (id) => - set((s) => ({ - outlinerItems: s.outlinerItems.map((i) => - i.id === id ? { ...i, visible: !i.visible } : i, - ), - })), - toggleOutlinerLock: (id) => - set((s) => ({ - outlinerItems: s.outlinerItems.map((i) => - i.id === id ? { ...i, locked: !i.locked } : i, - ), - })), - - // precision selector actions - showPrecisionSelector: (coords, values) => - set({ - precisionSelectorShowing: true, - precisionSelectorCoords: coords, - precisionSelectorValues: values, - }), - hidePrecisionSelector: () => set({ precisionSelectorShowing: false }), - setPrecisionSelectedValue: (v) => set({ precisionSelectedValue: v }), - - set3DPageMounted: (mounted) => set({ is3DPageMounted: mounted }), - setIs3DEditorInitialized: (initialized) => - set({ is3DEditorInitialized: initialized }), - setIs3DSceneLoaded: (loaded) => set({ is3DSceneLoaded: loaded }), - - setSceneContainerEl: (el) => set({ sceneContainerEl: el }), - setEditorCanvasEl: (el) => set({ editorCanvasEl: el }), - setCamViewCanvasEl: (el) => set({ camViewCanvasEl: el }), -})); +export { + usePageSceneStore, + DomLevels, +} from "@storyteller/ui-pagescene"; +export type { + Camera, + FocalLengthDragging, + SceneObject, + SceneObjectKind, + OutlinerItem, + ObjectPanelObject, + DragPosition, + PrecisionSelectorCoords, + SelectedSceneObject, + TransformMode, + TransformSpace, + PoseMode, + HotkeyStatus, + EditorLoader, +} from "@storyteller/ui-pagescene"; diff --git a/frontend/apps/artcraft/tsconfig.json b/frontend/apps/artcraft/tsconfig.json index 2ccccbd843..48f4b555fe 100644 --- a/frontend/apps/artcraft/tsconfig.json +++ b/frontend/apps/artcraft/tsconfig.json @@ -58,6 +58,9 @@ { "path": "../../libs/components/pagedraw" }, + { + "path": "../../libs/components/pagescene" + }, { "path": "../../libs/components/settings-modal" }, diff --git a/frontend/libs/components/pagescene/src/index.ts b/frontend/libs/components/pagescene/src/index.ts index 95d0f6301e..73a1f60d0c 100644 --- a/frontend/libs/components/pagescene/src/index.ts +++ b/frontend/libs/components/pagescene/src/index.ts @@ -1,4 +1,43 @@ // Public API for @storyteller/ui-pagescene. -// (Stub during scaffold; populated incrementally as files migrate.) -export {}; +// Adapter (platform abstraction) +export type { + PageSceneAdapter, + PageSceneGenerateRequest, + PageSceneSavePayload, +} from "./lib/adapter"; + +// Store +export { usePageSceneStore } from "./lib/PageSceneStore"; +export type { + Camera, + FocalLengthDragging, + SceneObject, + SceneObjectKind, + OutlinerItem, + ObjectPanelObject, + DragPosition, + PrecisionSelectorCoords, + SelectedSceneObject, + TransformMode, + TransformSpace, + PoseMode, + HotkeyStatus, + EditorLoader, +} from "./lib/PageSceneStore"; +export { DomLevels } from "./lib/PageSceneStore"; + +// Enums (canonical for both lib + artcraft host) +export { + AssetType, + AssetFilterOption, + ClipGroup, + CameraAspectRatio, + EditorStates, +} from "./lib/enums"; + +// Datastructures +export type { XYZ, Simple3DVector } from "./lib/datastructures/common"; + +// Models (lib-internal types — only what's needed at the public boundary) +export type { MediaItem, AudioMediaItem } from "./lib/models/assets"; diff --git a/frontend/libs/components/pagescene/src/lib/PageSceneStore.ts b/frontend/libs/components/pagescene/src/lib/PageSceneStore.ts new file mode 100644 index 0000000000..bec343d67a --- /dev/null +++ b/frontend/libs/components/pagescene/src/lib/PageSceneStore.ts @@ -0,0 +1,454 @@ +import { create } from "zustand"; +import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; +import { Camera, FocalLengthDragging } from "@storyteller/common"; +import { + AssetType, + AssetFilterOption, + CameraAspectRatio, + ClipGroup, + EditorStates, +} from "./enums"; +import { MediaItem } from "./models"; +import { Simple3DVector } from "./datastructures/common"; + +export type { Camera, FocalLengthDragging }; + +export type SceneObjectKind = "object" | "character" | "shape"; + +export interface SceneObject { + id: string; + kind: SceneObjectKind; + name: string; + mediaId?: string; + mediaToken?: string; +} + +// Item in the outliner panel (with icon, visibility, lock state). +// Distinct from `SceneObject` — outliner tracks UI-side rows; the +// engine maintains its own object model. +export interface OutlinerItem { + id: string; + icon: IconDefinition; + name: string; + type: string; + visible: boolean; + locked: boolean; +} + +// The currently inspected object in the right-hand control panel. +// Distinct from both `SceneObject` and `OutlinerItem`; this carries +// engine-side identifiers used by the gizmo / property panel. +export interface ObjectPanelObject { + group: ClipGroup; + object_uuid: string; + object_name: string; + version: string; + objectVectors: Simple3DVector; +} + +export interface DragPosition { + currX: number; + currY: number; +} + +export interface PrecisionSelectorCoords { + x: number; + y: number; +} + +export interface SelectedSceneObject { + type: AssetType; + id: string; +} + +export type TransformMode = "move" | "rotate" | "scale"; +export type TransformSpace = "world" | "local"; +export type PoseMode = "select" | "pose"; + +export enum DomLevels { + NONE = 0, + INPUT = 1, + PANEL = 2, + DIALOGUE = 3, +} + +export interface HotkeyStatus { + disabled: boolean; + disabledBy: DomLevels; +} + +export interface EditorLoader { + isShowing: boolean; + message: string | undefined; +} + +const DEFAULT_CAMERAS: Camera[] = [ + { + id: "main", + label: "Main View", + focalLength: 17, + position: { x: -2.5, y: 2.5, z: 2.5 }, + rotation: { x: 0, y: 0, z: 0 }, + lookAt: { x: 0, y: 0, z: 0 }, + }, + { + id: "cam2", + label: "Camera 2", + focalLength: 10, + position: { x: 0, y: 0.6, z: 1.5 }, + rotation: { x: 0, y: 0, z: 0 }, + lookAt: { x: 0, y: 0, z: 0 }, + }, +]; + +interface PageSceneState { + // scene contents + objects: SceneObject[]; + characters: SceneObject[]; + shapes: SceneObject[]; + selectedObject: SelectedSceneObject | null; + + // cameras + cameras: Camera[]; + selectedCameraId: string; + cameraAspectRatio: CameraAspectRatio; + focalLengthDragging: FocalLengthDragging; + cameraFilter: AssetFilterOption; + + // editor mode + editorState: EditorStates; + transformMode: TransformMode; + transformSpace: TransformSpace; + selectedMode: string; + poseMode: PoseMode; + showPoseControls: boolean; + gridVisible: boolean; + ignoreKeyDelete: boolean; + hotkeyStatus: HotkeyStatus; + isPromptBoxFocused: boolean; + + // layout / panels + assetModalVisible: boolean; + assetModalVisibleDuringDrag: boolean; + reopenAfterDrag: boolean; + + // overlays + editorLoader: EditorLoader; + editorLetterBox: boolean; + showErrorDialog: boolean; + errorDialogTitle: string; + errorDialogMessage: string; + + // drag-and-drop + canDrop: boolean; + dragItem: MediaItem | null; + dragPosition: DragPosition; + + // object panel (right-hand inspector for selected object) + objectPanelShowing: boolean; + objectPanelCurrent: ObjectPanelObject | undefined; + + // outliner (left-hand scene tree) + outlinerItems: OutlinerItem[]; + outlinerSelectedItem: OutlinerItem | null; + outlinerShowing: boolean; + + // precision selector popover + precisionSelectorShowing: boolean; + precisionSelectorCoords: PrecisionSelectorCoords; + precisionSelectorValues: number[]; + precisionSelectedValue: number; + + // engine lifecycle flags (mirrored to other parts of the app) + is3DPageMounted: boolean; + is3DEditorInitialized: boolean; + is3DSceneLoaded: boolean; + + // canvas DOM refs (set by canvas components on mount; consumed by + // the engine + hooks) + sceneContainerEl: HTMLDivElement | null; + editorCanvasEl: HTMLCanvasElement | null; + camViewCanvasEl: HTMLCanvasElement | null; + + // ----- actions ----- + + // scene + addObject: (obj: SceneObject) => void; + addCharacter: (obj: SceneObject) => void; + addShape: (obj: SceneObject) => void; + removeSceneObject: (id: string) => void; + setSelectedObject: (sel: SelectedSceneObject | null) => void; + resetScene: () => void; + + // camera + addCamera: (camera: Camera) => void; + updateCamera: (id: string, updates: Partial) => void; + deleteCamera: (id: string) => void; + setSelectedCameraId: (id: string) => void; + setCameraAspectRatio: (ratio: CameraAspectRatio) => void; + setFocalLengthDragging: (state: FocalLengthDragging) => void; + setCameraFilter: (filter: AssetFilterOption) => void; + + // editor mode + setEditorState: (state: EditorStates) => void; + setTransformMode: (mode: TransformMode) => void; + setTransformSpace: (space: TransformSpace) => void; + setSelectedMode: (mode: string) => void; + setPoseMode: (mode: PoseMode) => void; + setShowPoseControls: (visible: boolean) => void; + setGridVisible: (visible: boolean) => void; + setIgnoreKeyDelete: (ignore: boolean) => void; + disableHotkeyInput: (level: DomLevels) => void; + enableHotkeyInput: (level: DomLevels) => void; + setIsPromptBoxFocused: (focused: boolean) => void; + + // layout + setAssetModalVisible: (visible: boolean) => void; + setAssetModalVisibleDuringDrag: (visible: boolean) => void; + setReopenAfterDrag: (reopen: boolean) => void; + + // overlays + showEditorLoader: (message?: string) => void; + hideEditorLoader: () => void; + toggleEditorLetterBox: (next?: boolean) => void; + setErrorDialog: (title: string, message: string) => void; + setShowErrorDialog: (show: boolean) => void; + + // drag-and-drop + setCanDrop: (canDrop: boolean) => void; + setDragItem: (item: MediaItem | null) => void; + setDragPosition: (pos: DragPosition) => void; + + // object panel + showObjectPanel: (obj?: ObjectPanelObject) => void; + hideObjectPanel: () => void; + updateObjectPanel: (obj: ObjectPanelObject) => void; + + // outliner + setOutlinerItems: (items: OutlinerItem[]) => void; + setOutlinerSelectedItem: (item: OutlinerItem | null) => void; + setOutlinerShowing: (showing: boolean) => void; + selectOutlinerItem: (id: string) => void; + toggleOutlinerVisibility: (id: string) => void; + toggleOutlinerLock: (id: string) => void; + + // precision selector + showPrecisionSelector: ( + coords: PrecisionSelectorCoords, + values: number[], + ) => void; + hidePrecisionSelector: () => void; + setPrecisionSelectedValue: (v: number) => void; + + // engine lifecycle + set3DPageMounted: (mounted: boolean) => void; + setIs3DEditorInitialized: (initialized: boolean) => void; + setIs3DSceneLoaded: (loaded: boolean) => void; + + // canvas refs + setSceneContainerEl: (el: HTMLDivElement | null) => void; + setEditorCanvasEl: (el: HTMLCanvasElement | null) => void; + setCamViewCanvasEl: (el: HTMLCanvasElement | null) => void; +} + +export const usePageSceneStore = create((set, get) => ({ + // initial state + objects: [], + characters: [], + shapes: [], + selectedObject: null, + + cameras: DEFAULT_CAMERAS, + selectedCameraId: "main", + cameraAspectRatio: CameraAspectRatio.HORIZONTAL_3_2, + focalLengthDragging: { isDragging: false, focalLength: 35 }, + cameraFilter: AssetFilterOption.ALL, + + editorState: EditorStates.EDIT, + transformMode: "move", + transformSpace: "world", + selectedMode: "move", + poseMode: "select", + showPoseControls: false, + gridVisible: true, + ignoreKeyDelete: false, + hotkeyStatus: { disabled: false, disabledBy: DomLevels.NONE }, + isPromptBoxFocused: false, + + assetModalVisible: false, + assetModalVisibleDuringDrag: true, + reopenAfterDrag: false, + + editorLoader: { isShowing: false, message: "Loading Editor Engine 🦊" }, + editorLetterBox: true, + showErrorDialog: false, + errorDialogTitle: "Error!", + errorDialogMessage: "Something went wrong.", + + canDrop: false, + dragItem: null, + dragPosition: { currX: 0, currY: 0 }, + + objectPanelShowing: false, + objectPanelCurrent: undefined, + + outlinerItems: [], + outlinerSelectedItem: null, + outlinerShowing: false, + + precisionSelectorShowing: false, + precisionSelectorCoords: { x: 0, y: 0 }, + precisionSelectorValues: [], + precisionSelectedValue: 0, + + is3DPageMounted: false, + is3DEditorInitialized: false, + is3DSceneLoaded: false, + + sceneContainerEl: null, + editorCanvasEl: null, + camViewCanvasEl: null, + + // scene actions + addObject: (obj) => + set((s) => ({ objects: [...s.objects, obj] })), + addCharacter: (obj) => + set((s) => ({ characters: [...s.characters, obj] })), + addShape: (obj) => + set((s) => ({ shapes: [...s.shapes, obj] })), + removeSceneObject: (id) => + set((s) => ({ + objects: s.objects.filter((o) => o.id !== id), + characters: s.characters.filter((o) => o.id !== id), + shapes: s.shapes.filter((o) => o.id !== id), + selectedObject: + s.selectedObject?.id === id ? null : s.selectedObject, + })), + setSelectedObject: (sel) => set({ selectedObject: sel }), + resetScene: () => + set({ objects: [], characters: [], shapes: [], selectedObject: null }), + + // camera actions + addCamera: (camera) => set((s) => ({ cameras: [...s.cameras, camera] })), + updateCamera: (id, updates) => + set((s) => ({ + cameras: s.cameras.map((c) => (c.id === id ? { ...c, ...updates } : c)), + })), + deleteCamera: (id) => { + if (id === "main") return; + set((s) => ({ + cameras: s.cameras.filter((c) => c.id !== id), + selectedCameraId: s.selectedCameraId === id ? "main" : s.selectedCameraId, + })); + }, + setSelectedCameraId: (id) => set({ selectedCameraId: id }), + setCameraAspectRatio: (ratio) => set({ cameraAspectRatio: ratio }), + setFocalLengthDragging: (state) => set({ focalLengthDragging: state }), + setCameraFilter: (filter) => set({ cameraFilter: filter }), + + // editor mode actions + setEditorState: (state) => set({ editorState: state }), + setTransformMode: (mode) => set({ transformMode: mode }), + setTransformSpace: (space) => set({ transformSpace: space }), + setSelectedMode: (mode) => set({ selectedMode: mode }), + setPoseMode: (mode) => set({ poseMode: mode }), + setShowPoseControls: (visible) => set({ showPoseControls: visible }), + setGridVisible: (visible) => set({ gridVisible: visible }), + setIgnoreKeyDelete: (ignore) => set({ ignoreKeyDelete: ignore }), + disableHotkeyInput: (level) => { + const status = get().hotkeyStatus; + if (status.disabled) { + if (level > status.disabledBy) { + set({ hotkeyStatus: { ...status, disabledBy: level } }); + } + } else { + set({ hotkeyStatus: { disabled: true, disabledBy: level } }); + } + }, + enableHotkeyInput: (level) => { + const status = get().hotkeyStatus; + if (status.disabled && level >= status.disabledBy) { + set({ hotkeyStatus: { disabled: false, disabledBy: DomLevels.NONE } }); + } + }, + setIsPromptBoxFocused: (focused) => set({ isPromptBoxFocused: focused }), + + // layout actions + setAssetModalVisible: (visible) => set({ assetModalVisible: visible }), + setAssetModalVisibleDuringDrag: (visible) => + set({ assetModalVisibleDuringDrag: visible }), + setReopenAfterDrag: (reopen) => set({ reopenAfterDrag: reopen }), + + // overlays actions + showEditorLoader: (message) => + set({ editorLoader: { isShowing: true, message } }), + hideEditorLoader: () => + set((s) => ({ + editorLoader: { isShowing: false, message: s.editorLoader.message }, + })), + toggleEditorLetterBox: (next) => + set((s) => ({ + editorLetterBox: next !== undefined ? next : !s.editorLetterBox, + })), + setErrorDialog: (title, message) => + set({ + errorDialogTitle: title, + errorDialogMessage: message, + showErrorDialog: true, + }), + setShowErrorDialog: (show) => set({ showErrorDialog: show }), + + // drag-and-drop actions + setCanDrop: (canDrop) => set({ canDrop }), + setDragItem: (item) => set({ dragItem: item }), + setDragPosition: (pos) => set({ dragPosition: pos }), + + // object panel actions + showObjectPanel: (obj) => + set((s) => ({ + objectPanelShowing: true, + objectPanelCurrent: obj ?? s.objectPanelCurrent, + })), + hideObjectPanel: () => set({ objectPanelShowing: false }), + updateObjectPanel: (obj) => set({ objectPanelCurrent: obj }), + + // outliner actions + setOutlinerItems: (items) => set({ outlinerItems: items }), + setOutlinerSelectedItem: (item) => set({ outlinerSelectedItem: item }), + setOutlinerShowing: (showing) => set({ outlinerShowing: showing }), + selectOutlinerItem: (id) => { + const item = get().outlinerItems.find((i) => i.id === id); + if (item) set({ outlinerSelectedItem: item }); + }, + toggleOutlinerVisibility: (id) => + set((s) => ({ + outlinerItems: s.outlinerItems.map((i) => + i.id === id ? { ...i, visible: !i.visible } : i, + ), + })), + toggleOutlinerLock: (id) => + set((s) => ({ + outlinerItems: s.outlinerItems.map((i) => + i.id === id ? { ...i, locked: !i.locked } : i, + ), + })), + + // precision selector actions + showPrecisionSelector: (coords, values) => + set({ + precisionSelectorShowing: true, + precisionSelectorCoords: coords, + precisionSelectorValues: values, + }), + hidePrecisionSelector: () => set({ precisionSelectorShowing: false }), + setPrecisionSelectedValue: (v) => set({ precisionSelectedValue: v }), + + set3DPageMounted: (mounted) => set({ is3DPageMounted: mounted }), + setIs3DEditorInitialized: (initialized) => + set({ is3DEditorInitialized: initialized }), + setIs3DSceneLoaded: (loaded) => set({ is3DSceneLoaded: loaded }), + + setSceneContainerEl: (el) => set({ sceneContainerEl: el }), + setEditorCanvasEl: (el) => set({ editorCanvasEl: el }), + setCamViewCanvasEl: (el) => set({ camViewCanvasEl: el }), +})); diff --git a/frontend/libs/components/pagescene/src/lib/datastructures/common.ts b/frontend/libs/components/pagescene/src/lib/datastructures/common.ts new file mode 100644 index 0000000000..85ba007300 --- /dev/null +++ b/frontend/libs/components/pagescene/src/lib/datastructures/common.ts @@ -0,0 +1,11 @@ +export type XYZ = { + x: number; + y: number; + z: number; +}; + +export type Simple3DVector = { + position: XYZ; + rotation: XYZ; + scale: XYZ; +}; diff --git a/frontend/libs/components/pagescene/src/lib/enums/AssetFilterOption.ts b/frontend/libs/components/pagescene/src/lib/enums/AssetFilterOption.ts new file mode 100644 index 0000000000..dfc97ea3be --- /dev/null +++ b/frontend/libs/components/pagescene/src/lib/enums/AssetFilterOption.ts @@ -0,0 +1,6 @@ +export enum AssetFilterOption { + ALL, + FEATURED, + MINE, + BOOKMARKED, +} diff --git a/frontend/libs/components/pagescene/src/lib/enums/AssetType.ts b/frontend/libs/components/pagescene/src/lib/enums/AssetType.ts new file mode 100644 index 0000000000..008a0d6c14 --- /dev/null +++ b/frontend/libs/components/pagescene/src/lib/enums/AssetType.ts @@ -0,0 +1,11 @@ +export enum AssetType { + ANIMATION = "animation", + AUDIO = "audio", + CAMERA = "camera", + CHARACTER = "character", + EXPRESSION = "expression", + OBJECT = "object", + SKYBOX = "skybox", + SHAPE = "shape", + SPLAT = "splat", +} diff --git a/frontend/libs/components/pagescene/src/lib/enums/ClipGroup.ts b/frontend/libs/components/pagescene/src/lib/enums/ClipGroup.ts new file mode 100644 index 0000000000..bae91bc0ba --- /dev/null +++ b/frontend/libs/components/pagescene/src/lib/enums/ClipGroup.ts @@ -0,0 +1,7 @@ +export enum ClipGroup { + CHARACTER = "character", + CAMERA = "camera", + GLOBAL_AUDIO = "global_audio", + OBJECT = "object", + PROMPT_TRAVEL = "prompt", +} diff --git a/frontend/libs/components/pagescene/src/lib/enums/EditorState.ts b/frontend/libs/components/pagescene/src/lib/enums/EditorState.ts new file mode 100644 index 0000000000..c86452f045 --- /dev/null +++ b/frontend/libs/components/pagescene/src/lib/enums/EditorState.ts @@ -0,0 +1,12 @@ +export enum EditorStates { + EDIT, + CAMERA_VIEW, +} + +export enum CameraAspectRatio { + HORIZONTAL_16_9 = "horizontal_16_9", + VERTICAL_9_16 = "vertical_9_16", + SQUARE_1_1 = "SQUARE_1_1", + HORIZONTAL_3_2 = "horizontal_3_2", + VERTICAL_2_3 = "vertical_2_3", +} diff --git a/frontend/libs/components/pagescene/src/lib/enums/index.ts b/frontend/libs/components/pagescene/src/lib/enums/index.ts new file mode 100644 index 0000000000..8e4116a013 --- /dev/null +++ b/frontend/libs/components/pagescene/src/lib/enums/index.ts @@ -0,0 +1,4 @@ +export * from "./AssetType"; +export * from "./AssetFilterOption"; +export * from "./ClipGroup"; +export * from "./EditorState"; diff --git a/frontend/libs/components/pagescene/src/lib/models/assets.ts b/frontend/libs/components/pagescene/src/lib/models/assets.ts new file mode 100644 index 0000000000..91bce42441 --- /dev/null +++ b/frontend/libs/components/pagescene/src/lib/models/assets.ts @@ -0,0 +1,23 @@ +import { AssetType } from "../enums"; + +export interface MediaItem { + version: number; + type: AssetType; + maybe_animation_type?: string; + media_type?: string; + media_id: string; + object_uuid?: string; + name: string; + description?: string; + publicBucketPath?: string; + length?: number; + thumbnail?: string; + isMine?: boolean; + isBookmarked?: boolean; + imageIndex?: number; +} + +export interface AudioMediaItem extends MediaItem { + category?: string; + isNew?: boolean; +} diff --git a/frontend/libs/components/pagescene/src/lib/models/index.ts b/frontend/libs/components/pagescene/src/lib/models/index.ts new file mode 100644 index 0000000000..4ccdc7c5c5 --- /dev/null +++ b/frontend/libs/components/pagescene/src/lib/models/index.ts @@ -0,0 +1 @@ +export * from "./assets";