artcraft: move PageSceneStore + scene enums into @storyteller/ui-pagescene

The lib now owns the canonical PageSceneStore (Zustand), the
PageScene-specific enums (AssetType, AssetFilterOption, ClipGroup,
CameraAspectRatio, EditorStates), the common datastructures
(XYZ + Simple3DVector), and a minimal models surface (MediaItem +
AudioMediaItem -- enough for the store; richer model types stay in
apps/PageScene/models for now and migrate alongside their consumers).

Old artcraft locations become re-export shims so 56+ existing
importers keep working unchanged during the staged migration:

- apps/.../enums/{AssetType,AssetFilterOption,ClipGroup}.ts re-export
  from @storyteller/ui-pagescene. Single nominal identity for each
  enum across both lib and host -- avoids the "two AssetType types
  that look the same but TypeScript treats as distinct" trap when
  values flow across the boundary (e.g. ImageTo3DExperience passes
  AssetType.SPLAT into PageScene's addObject).
- apps/.../pages/PageScene/PageSceneStore.ts re-exports the store +
  types from the lib.

artcraft/tsconfig.json gains a `references` entry for
libs/components/pagescene so TypeScript resolves the package.
node_modules/@storyteller/ui-pagescene gets its workspace symlink
on first `npm install`; created manually here for the typecheck.

Verified: total typecheck error count unchanged (124, all pre-existing)
-- no new errors from the move.
This commit is contained in:
Tanish Manku
2026-05-08 13:25:37 -07:00
parent 4db39881ee
commit bd18c3ec64
15 changed files with 605 additions and 474 deletions
@@ -1,6 +1 @@
export enum AssetFilterOption {
ALL,
FEATURED,
MINE,
BOOKMARKED,
}
export { AssetFilterOption } from "@storyteller/ui-pagescene";
@@ -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";
@@ -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";
@@ -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<Camera>) => 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<PageSceneState>((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";
+3
View File
@@ -58,6 +58,9 @@
{
"path": "../../libs/components/pagedraw"
},
{
"path": "../../libs/components/pagescene"
},
{
"path": "../../libs/components/settings-modal"
},
@@ -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";
@@ -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<Camera>) => 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<PageSceneState>((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 }),
}));
@@ -0,0 +1,11 @@
export type XYZ = {
x: number;
y: number;
z: number;
};
export type Simple3DVector = {
position: XYZ;
rotation: XYZ;
scale: XYZ;
};
@@ -0,0 +1,6 @@
export enum AssetFilterOption {
ALL,
FEATURED,
MINE,
BOOKMARKED,
}
@@ -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",
}
@@ -0,0 +1,7 @@
export enum ClipGroup {
CHARACTER = "character",
CAMERA = "camera",
GLOBAL_AUDIO = "global_audio",
OBJECT = "object",
PROMPT_TRAVEL = "prompt",
}
@@ -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",
}
@@ -0,0 +1,4 @@
export * from "./AssetType";
export * from "./AssetFilterOption";
export * from "./ClipGroup";
export * from "./EditorState";
@@ -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;
}
@@ -0,0 +1 @@
export * from "./assets";