mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-25 18:49:38 +00:00
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:
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
Reference in New Issue
Block a user