import type { SceneGraph } from '@pascal-app/core/clone-scene-graph' import type { AnyNode, AnyNodeId, AnyNodeType } from '@pascal-app/core/schema' import type { ActiveSceneMeta, Patch, SceneBridge, ValidationResult } from '../bridge/scene-bridge' import type { ProjectCreateOptions, ProjectStatus, SceneEvent, SceneEventAppendOptions, SceneEventListOptions, SceneListOptions, SceneMeta, SceneMutateOptions, SceneSaveOptions, SceneStore, SceneWithGraph, } from '../storage/types' export type CreateSceneOperationsOptions = { bridge?: SceneBridge store?: SceneStore } export interface SceneOperations { readonly hasBridge: boolean readonly hasStore: boolean readonly hasSceneEvents: boolean readonly canAppendSceneEvents: boolean readonly canListSceneEvents: boolean readonly canCreateProject: boolean readonly canGetProjectStatus: boolean readonly storeBackend: SceneStore['backend'] | null setActiveScene(meta: ActiveSceneMeta): void getActiveScene(): ActiveSceneMeta | null clearActiveScene(): void loadDefault(): void setScene(nodes: Record, rootNodeIds: AnyNodeId[]): void exportJSON(): SceneGraph & { collections: Record } exportSceneGraph(): SceneGraph loadJSON(json: string | SceneGraph): void getNode(id: AnyNodeId): AnyNode | null getNodes(): Record getRootNodeIds(): AnyNodeId[] getChildren(parentId: AnyNodeId): AnyNode[] getAncestry(id: AnyNodeId): AnyNode[] findNodes(filter: { type?: AnyNodeType parentId?: AnyNodeId | null levelId?: AnyNodeId }): AnyNode[] resolveLevelId(id: AnyNodeId): AnyNodeId | null createNode(node: AnyNode, parentId?: AnyNodeId): AnyNodeId updateNode(id: AnyNodeId, data: Partial): void deleteNode(id: AnyNodeId, cascade?: boolean): string[] applyPatch(patches: Patch[]): { appliedOps: number deletedIds: AnyNodeId[] createdIds: AnyNodeId[] } undo(steps?: number): number redo(steps?: number): number validateScene(): ValidationResult flushDirty(): string[] getHistory(): { pastCount: number; futureCount: number } clearHistory(): void createProject(options: ProjectCreateOptions): Promise getProjectStatus(id: string): Promise saveScene(options: SceneSaveOptions): Promise loadStoredScene(id: string): Promise listScenes(options?: SceneListOptions): Promise deleteStoredScene(id: string, options?: SceneMutateOptions): Promise renameStoredScene(id: string, newName: string, options?: SceneMutateOptions): Promise appendSceneEvent(options: SceneEventAppendOptions): Promise listSceneEvents(id: string, options?: SceneEventListOptions): Promise } export function createSceneOperations(options: CreateSceneOperationsOptions): SceneOperations { return new SceneOperationsFacade(options) } class SceneOperationsFacade implements SceneOperations { readonly #bridge?: SceneBridge readonly #store?: SceneStore constructor(options: CreateSceneOperationsOptions) { this.#bridge = options.bridge this.#store = options.store } get hasBridge(): boolean { return this.#bridge !== undefined } get hasStore(): boolean { return this.#store !== undefined } get hasSceneEvents(): boolean { return this.canAppendSceneEvents && this.canListSceneEvents } get canAppendSceneEvents(): boolean { return typeof this.#store?.appendSceneEvent === 'function' } get canListSceneEvents(): boolean { return typeof this.#store?.listSceneEvents === 'function' } get canCreateProject(): boolean { return typeof this.#store?.createProject === 'function' } get canGetProjectStatus(): boolean { return typeof this.#store?.getProjectStatus === 'function' } get storeBackend(): SceneStore['backend'] | null { return this.#store?.backend ?? null } setActiveScene(meta: ActiveSceneMeta): void { this.requireBridge().setActiveScene(meta) } getActiveScene(): ActiveSceneMeta | null { return this.requireBridge().getActiveScene() } clearActiveScene(): void { this.requireBridge().clearActiveScene() } loadDefault(): void { this.requireBridge().loadDefault() } setScene(nodes: Record, rootNodeIds: AnyNodeId[]): void { this.requireBridge().setScene(nodes, rootNodeIds) } exportJSON(): SceneGraph & { collections: Record } { return this.requireBridge().exportJSON() } exportSceneGraph(): SceneGraph { const exported = this.exportJSON() return { nodes: exported.nodes, rootNodeIds: exported.rootNodeIds, collections: exported.collections as SceneGraph['collections'], } } loadJSON(json: string | SceneGraph): void { this.requireBridge().loadJSON(json) } getNode(id: AnyNodeId): AnyNode | null { return this.requireBridge().getNode(id) } getNodes(): Record { return this.requireBridge().getNodes() } getRootNodeIds(): AnyNodeId[] { return this.requireBridge().getRootNodeIds() } getChildren(parentId: AnyNodeId): AnyNode[] { return this.requireBridge().getChildren(parentId) } getAncestry(id: AnyNodeId): AnyNode[] { return this.requireBridge().getAncestry(id) } findNodes(filter: { type?: AnyNodeType parentId?: AnyNodeId | null levelId?: AnyNodeId }): AnyNode[] { return this.requireBridge().findNodes(filter) } resolveLevelId(id: AnyNodeId): AnyNodeId | null { return this.requireBridge().resolveLevelId(id) } createNode(node: AnyNode, parentId?: AnyNodeId): AnyNodeId { return this.requireBridge().createNode(node, parentId) } updateNode(id: AnyNodeId, data: Partial): void { this.requireBridge().updateNode(id, data) } deleteNode(id: AnyNodeId, cascade?: boolean): string[] { return this.requireBridge().deleteNode(id, cascade) } applyPatch(patches: Patch[]): { appliedOps: number deletedIds: AnyNodeId[] createdIds: AnyNodeId[] } { return this.requireBridge().applyPatch(patches) } undo(steps?: number): number { return this.requireBridge().undo(steps) } redo(steps?: number): number { return this.requireBridge().redo(steps) } validateScene(): ValidationResult { return this.requireBridge().validateScene() } flushDirty(): string[] { return this.requireBridge().flushDirty() } getHistory(): { pastCount: number; futureCount: number } { return this.requireBridge().getHistory() } clearHistory(): void { this.requireBridge().clearHistory() } async createProject(options: ProjectCreateOptions): Promise { const store = this.requireStore() if (!store.createProject) { throw new Error('create_project_unavailable') } return store.createProject(options) } async getProjectStatus(id: string): Promise { const store = this.requireStore() if (store.getProjectStatus) { return store.getProjectStatus(id) } const scene = await store.load(id) if (!scene) return null const editorUrl = scene.editorUrl ?? `/editor/${scene.id}` return { id: scene.id, projectId: scene.projectId ?? scene.id, name: scene.name, editorUrl, url: editorUrl, ownerId: scene.ownerId, thumbnailUrl: scene.thumbnailUrl, publishedVersion: scene.published === false ? null : scene.version, latestVersion: scene.version, draftVersion: null, browserVisibleVersion: scene.version, version: scene.version, isEmpty: scene.nodeCount === 0, sizeBytes: scene.sizeBytes, nodeCount: scene.nodeCount, graphHash: scene.graphHash ?? null, createdAt: scene.createdAt, updatedAt: scene.updatedAt, } } async saveScene(options: SceneSaveOptions): Promise { const store = this.requireStore() return store.save(options) } async loadStoredScene(id: string): Promise { const store = this.requireStore() return store.load(id) } async listScenes(options?: SceneListOptions): Promise { const store = this.requireStore() return store.list(options) } async deleteStoredScene(id: string, options?: SceneMutateOptions): Promise { const store = this.requireStore() return store.delete(id, options) } async renameStoredScene( id: string, newName: string, options?: SceneMutateOptions, ): Promise { const store = this.requireStore() return store.rename(id, newName, options) } async appendSceneEvent(options: SceneEventAppendOptions): Promise { const store = this.requireStore() if (!store.appendSceneEvent) return null return store.appendSceneEvent(options) } async listSceneEvents(id: string, options?: SceneEventListOptions): Promise { const store = this.requireStore() if (!store.listSceneEvents) { throw new Error('scene_events_unavailable') } return store.listSceneEvents(id, options) } private requireBridge(): SceneBridge { if (!this.#bridge) { throw new Error('scene_bridge_unavailable') } return this.#bridge } private requireStore(): SceneStore { if (!this.#store) { throw new Error('scene_store_unavailable') } return this.#store } }