@pascal-app/mcp 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/README.md +330 -0
- package/dist/bin/pascal-mcp.d.ts +3 -0
- package/dist/bin/pascal-mcp.d.ts.map +1 -0
- package/dist/bin/pascal-mcp.js +91 -0
- package/dist/bridge/index.d.ts +3 -0
- package/dist/bridge/index.d.ts.map +1 -0
- package/dist/bridge/index.js +1 -0
- package/dist/bridge/node-shims.d.ts +16 -0
- package/dist/bridge/node-shims.d.ts.map +1 -0
- package/dist/bridge/node-shims.js +25 -0
- package/dist/bridge/scene-bridge.d.ts +158 -0
- package/dist/bridge/scene-bridge.d.ts.map +1 -0
- package/dist/bridge/scene-bridge.js +482 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/lib/rehydrate-site-children.d.ts +12 -0
- package/dist/lib/rehydrate-site-children.d.ts.map +1 -0
- package/dist/lib/rehydrate-site-children.js +40 -0
- package/dist/lib/safe-fetch.d.ts +19 -0
- package/dist/lib/safe-fetch.d.ts.map +1 -0
- package/dist/lib/safe-fetch.js +221 -0
- package/dist/operations/index.d.ts +2 -0
- package/dist/operations/index.d.ts.map +1 -0
- package/dist/operations/index.js +1 -0
- package/dist/operations/scene-operations.d.ts +67 -0
- package/dist/operations/scene-operations.d.ts.map +1 -0
- package/dist/operations/scene-operations.js +192 -0
- package/dist/prompts/from-brief.d.ts +11 -0
- package/dist/prompts/from-brief.d.ts.map +1 -0
- package/dist/prompts/from-brief.js +48 -0
- package/dist/prompts/index.d.ts +10 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +14 -0
- package/dist/prompts/iterate-on-feedback.d.ts +11 -0
- package/dist/prompts/iterate-on-feedback.d.ts.map +1 -0
- package/dist/prompts/iterate-on-feedback.js +41 -0
- package/dist/prompts/renovation-from-photos.d.ts +24 -0
- package/dist/prompts/renovation-from-photos.d.ts.map +1 -0
- package/dist/prompts/renovation-from-photos.js +135 -0
- package/dist/prompts/scene-guidance.d.ts +2 -0
- package/dist/prompts/scene-guidance.d.ts.map +1 -0
- package/dist/prompts/scene-guidance.js +24 -0
- package/dist/resources/agent-guide.d.ts +5 -0
- package/dist/resources/agent-guide.d.ts.map +1 -0
- package/dist/resources/agent-guide.js +85 -0
- package/dist/resources/catalog-items.d.ts +10 -0
- package/dist/resources/catalog-items.d.ts.map +1 -0
- package/dist/resources/catalog-items.js +29 -0
- package/dist/resources/constraints.d.ts +9 -0
- package/dist/resources/constraints.d.ts.map +1 -0
- package/dist/resources/constraints.js +69 -0
- package/dist/resources/index.d.ts +15 -0
- package/dist/resources/index.d.ts.map +1 -0
- package/dist/resources/index.js +23 -0
- package/dist/resources/scene-current.d.ts +9 -0
- package/dist/resources/scene-current.d.ts.map +1 -0
- package/dist/resources/scene-current.js +20 -0
- package/dist/resources/scene-summary.d.ts +10 -0
- package/dist/resources/scene-summary.d.ts.map +1 -0
- package/dist/resources/scene-summary.js +196 -0
- package/dist/server.d.ts +14 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +18 -0
- package/dist/storage/index.d.ts +13 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +14 -0
- package/dist/storage/slug.d.ts +21 -0
- package/dist/storage/slug.d.ts.map +1 -0
- package/dist/storage/slug.js +62 -0
- package/dist/storage/sqlite-driver.d.ts +18 -0
- package/dist/storage/sqlite-driver.d.ts.map +1 -0
- package/dist/storage/sqlite-driver.js +32 -0
- package/dist/storage/sqlite-scene-store.d.ts +51 -0
- package/dist/storage/sqlite-scene-store.d.ts.map +1 -0
- package/dist/storage/sqlite-scene-store.js +526 -0
- package/dist/storage/types.d.ts +136 -0
- package/dist/storage/types.d.ts.map +1 -0
- package/dist/storage/types.js +28 -0
- package/dist/templates/empty-studio.d.ts +8 -0
- package/dist/templates/empty-studio.d.ts.map +1 -0
- package/dist/templates/empty-studio.js +230 -0
- package/dist/templates/garden-house.d.ts +8 -0
- package/dist/templates/garden-house.d.ts.map +1 -0
- package/dist/templates/garden-house.js +242 -0
- package/dist/templates/index.d.ts +23 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +20 -0
- package/dist/templates/two-bedroom.d.ts +8 -0
- package/dist/templates/two-bedroom.d.ts.map +1 -0
- package/dist/templates/two-bedroom.js +270 -0
- package/dist/tools/apply-patch.d.ts +25 -0
- package/dist/tools/apply-patch.d.ts.map +1 -0
- package/dist/tools/apply-patch.js +59 -0
- package/dist/tools/asset-catalog.d.ts +16 -0
- package/dist/tools/asset-catalog.d.ts.map +1 -0
- package/dist/tools/asset-catalog.js +308 -0
- package/dist/tools/check-collisions.d.ts +15 -0
- package/dist/tools/check-collisions.d.ts.map +1 -0
- package/dist/tools/check-collisions.js +61 -0
- package/dist/tools/construction-tools.d.ts +93 -0
- package/dist/tools/construction-tools.d.ts.map +1 -0
- package/dist/tools/construction-tools.js +369 -0
- package/dist/tools/create-level.d.ts +14 -0
- package/dist/tools/create-level.d.ts.map +1 -0
- package/dist/tools/create-level.js +48 -0
- package/dist/tools/create-wall.d.ts +15 -0
- package/dist/tools/create-wall.d.ts.map +1 -0
- package/dist/tools/create-wall.js +50 -0
- package/dist/tools/cut-opening.d.ts +18 -0
- package/dist/tools/cut-opening.d.ts.map +1 -0
- package/dist/tools/cut-opening.js +57 -0
- package/dist/tools/delete-node.d.ts +12 -0
- package/dist/tools/delete-node.d.ts.map +1 -0
- package/dist/tools/delete-node.js +37 -0
- package/dist/tools/describe-node.d.ts +17 -0
- package/dist/tools/describe-node.d.ts.map +1 -0
- package/dist/tools/describe-node.js +86 -0
- package/dist/tools/duplicate-level.d.ts +12 -0
- package/dist/tools/duplicate-level.d.ts.map +1 -0
- package/dist/tools/duplicate-level.js +56 -0
- package/dist/tools/errors.d.ts +20 -0
- package/dist/tools/errors.d.ts.map +1 -0
- package/dist/tools/errors.js +25 -0
- package/dist/tools/export-glb.d.ts +10 -0
- package/dist/tools/export-glb.d.ts.map +1 -0
- package/dist/tools/export-glb.js +24 -0
- package/dist/tools/export-json.d.ts +11 -0
- package/dist/tools/export-json.d.ts.map +1 -0
- package/dist/tools/export-json.js +23 -0
- package/dist/tools/find-nodes.d.ts +32 -0
- package/dist/tools/find-nodes.d.ts.map +1 -0
- package/dist/tools/find-nodes.js +105 -0
- package/dist/tools/geometry.d.ts +24 -0
- package/dist/tools/geometry.d.ts.map +1 -0
- package/dist/tools/geometry.js +94 -0
- package/dist/tools/get-node.d.ts +11 -0
- package/dist/tools/get-node.d.ts.map +1 -0
- package/dist/tools/get-node.js +27 -0
- package/dist/tools/get-scene.d.ts +11 -0
- package/dist/tools/get-scene.d.ts.map +1 -0
- package/dist/tools/get-scene.js +26 -0
- package/dist/tools/index.d.ts +12 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +64 -0
- package/dist/tools/live-sync.d.ts +11 -0
- package/dist/tools/live-sync.d.ts.map +1 -0
- package/dist/tools/live-sync.js +62 -0
- package/dist/tools/measure.d.ts +14 -0
- package/dist/tools/measure.d.ts.map +1 -0
- package/dist/tools/measure.js +137 -0
- package/dist/tools/photo-to-scene/index.d.ts +11 -0
- package/dist/tools/photo-to-scene/index.d.ts.map +1 -0
- package/dist/tools/photo-to-scene/index.js +11 -0
- package/dist/tools/photo-to-scene/photo-to-scene.d.ts +26 -0
- package/dist/tools/photo-to-scene/photo-to-scene.d.ts.map +1 -0
- package/dist/tools/photo-to-scene/photo-to-scene.js +323 -0
- package/dist/tools/place-item.d.ts +15 -0
- package/dist/tools/place-item.d.ts.map +1 -0
- package/dist/tools/place-item.js +84 -0
- package/dist/tools/redo.d.ts +11 -0
- package/dist/tools/redo.d.ts.map +1 -0
- package/dist/tools/redo.js +25 -0
- package/dist/tools/room-tools.d.ts +97 -0
- package/dist/tools/room-tools.d.ts.map +1 -0
- package/dist/tools/room-tools.js +510 -0
- package/dist/tools/scene-lifecycle/create-project.d.ts +33 -0
- package/dist/tools/scene-lifecycle/create-project.d.ts.map +1 -0
- package/dist/tools/scene-lifecycle/create-project.js +70 -0
- package/dist/tools/scene-lifecycle/delete-scene.d.ts +12 -0
- package/dist/tools/scene-lifecycle/delete-scene.d.ts.map +1 -0
- package/dist/tools/scene-lifecycle/delete-scene.js +42 -0
- package/dist/tools/scene-lifecycle/get-project-status.d.ts +31 -0
- package/dist/tools/scene-lifecycle/get-project-status.d.ts.map +1 -0
- package/dist/tools/scene-lifecycle/get-project-status.js +69 -0
- package/dist/tools/scene-lifecycle/index.d.ts +17 -0
- package/dist/tools/scene-lifecycle/index.d.ts.map +1 -0
- package/dist/tools/scene-lifecycle/index.js +29 -0
- package/dist/tools/scene-lifecycle/list-scenes.d.ts +27 -0
- package/dist/tools/scene-lifecycle/list-scenes.d.ts.map +1 -0
- package/dist/tools/scene-lifecycle/list-scenes.js +49 -0
- package/dist/tools/scene-lifecycle/load-scene.d.ts +31 -0
- package/dist/tools/scene-lifecycle/load-scene.d.ts.map +1 -0
- package/dist/tools/scene-lifecycle/load-scene.js +55 -0
- package/dist/tools/scene-lifecycle/metadata.d.ts +49 -0
- package/dist/tools/scene-lifecycle/metadata.d.ts.map +1 -0
- package/dist/tools/scene-lifecycle/metadata.js +68 -0
- package/dist/tools/scene-lifecycle/rename-scene.d.ts +22 -0
- package/dist/tools/scene-lifecycle/rename-scene.d.ts.map +1 -0
- package/dist/tools/scene-lifecycle/rename-scene.js +63 -0
- package/dist/tools/scene-lifecycle/save-scene.d.ts +42 -0
- package/dist/tools/scene-lifecycle/save-scene.d.ts.map +1 -0
- package/dist/tools/scene-lifecycle/save-scene.js +130 -0
- package/dist/tools/scene-lifecycle/test-utils.d.ts +39 -0
- package/dist/tools/scene-lifecycle/test-utils.d.ts.map +1 -0
- package/dist/tools/scene-lifecycle/test-utils.js +211 -0
- package/dist/tools/scene-query.d.ts +57 -0
- package/dist/tools/scene-query.d.ts.map +1 -0
- package/dist/tools/scene-query.js +645 -0
- package/dist/tools/schemas.d.ts +52 -0
- package/dist/tools/schemas.d.ts.map +1 -0
- package/dist/tools/schemas.js +42 -0
- package/dist/tools/set-zone.d.ts +14 -0
- package/dist/tools/set-zone.d.ts.map +1 -0
- package/dist/tools/set-zone.js +48 -0
- package/dist/tools/templates/create-from-template.d.ts +54 -0
- package/dist/tools/templates/create-from-template.d.ts.map +1 -0
- package/dist/tools/templates/create-from-template.js +145 -0
- package/dist/tools/templates/create-house-from-brief.d.ts +35 -0
- package/dist/tools/templates/create-house-from-brief.d.ts.map +1 -0
- package/dist/tools/templates/create-house-from-brief.js +170 -0
- package/dist/tools/templates/index.d.ts +14 -0
- package/dist/tools/templates/index.d.ts.map +1 -0
- package/dist/tools/templates/index.js +18 -0
- package/dist/tools/templates/list-templates.d.ts +18 -0
- package/dist/tools/templates/list-templates.d.ts.map +1 -0
- package/dist/tools/templates/list-templates.js +36 -0
- package/dist/tools/undo.d.ts +11 -0
- package/dist/tools/undo.d.ts.map +1 -0
- package/dist/tools/undo.js +25 -0
- package/dist/tools/validate-scene.d.ts +14 -0
- package/dist/tools/validate-scene.d.ts.map +1 -0
- package/dist/tools/validate-scene.js +24 -0
- package/dist/tools/variants/generate-variants.d.ts +30 -0
- package/dist/tools/variants/generate-variants.d.ts.map +1 -0
- package/dist/tools/variants/generate-variants.js +160 -0
- package/dist/tools/variants/index.d.ts +9 -0
- package/dist/tools/variants/index.d.ts.map +1 -0
- package/dist/tools/variants/index.js +9 -0
- package/dist/tools/variants/mutations.d.ts +18 -0
- package/dist/tools/variants/mutations.d.ts.map +1 -0
- package/dist/tools/variants/mutations.js +324 -0
- package/dist/tools/vision/analyze-floorplan-image.d.ts +33 -0
- package/dist/tools/vision/analyze-floorplan-image.d.ts.map +1 -0
- package/dist/tools/vision/analyze-floorplan-image.js +146 -0
- package/dist/tools/vision/analyze-room-photo.d.ts +29 -0
- package/dist/tools/vision/analyze-room-photo.d.ts.map +1 -0
- package/dist/tools/vision/analyze-room-photo.js +130 -0
- package/dist/tools/vision/index.d.ts +12 -0
- package/dist/tools/vision/index.d.ts.map +1 -0
- package/dist/tools/vision/index.js +14 -0
- package/dist/transports/http.d.ts +36 -0
- package/dist/transports/http.d.ts.map +1 -0
- package/dist/transports/http.js +203 -0
- package/dist/transports/stdio.d.ts +14 -0
- package/dist/transports/stdio.d.ts.map +1 -0
- package/dist/transports/stdio.js +16 -0
- package/package.json +86 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import './node-shims';
|
|
2
|
+
import type { SceneGraph } from '@pascal-app/core/clone-scene-graph';
|
|
3
|
+
import type { AnyNode } from '@pascal-app/core/schema';
|
|
4
|
+
import { type AnyNodeId, type AnyNodeType } from '@pascal-app/core/schema';
|
|
5
|
+
import type { SceneMeta } from '../storage/types';
|
|
6
|
+
export type ValidationError = {
|
|
7
|
+
nodeId: string;
|
|
8
|
+
path: string;
|
|
9
|
+
message: string;
|
|
10
|
+
};
|
|
11
|
+
export type ValidationResult = {
|
|
12
|
+
valid: boolean;
|
|
13
|
+
errors: ValidationError[];
|
|
14
|
+
};
|
|
15
|
+
export type CreatePatch = {
|
|
16
|
+
op: 'create';
|
|
17
|
+
node: AnyNode;
|
|
18
|
+
parentId?: AnyNodeId;
|
|
19
|
+
};
|
|
20
|
+
export type UpdatePatch = {
|
|
21
|
+
op: 'update';
|
|
22
|
+
id: AnyNodeId;
|
|
23
|
+
data: Partial<AnyNode>;
|
|
24
|
+
};
|
|
25
|
+
export type DeletePatch = {
|
|
26
|
+
op: 'delete';
|
|
27
|
+
id: AnyNodeId;
|
|
28
|
+
cascade?: boolean;
|
|
29
|
+
};
|
|
30
|
+
export type Patch = CreatePatch | UpdatePatch | DeletePatch;
|
|
31
|
+
export type ActiveSceneMeta = Pick<SceneMeta, 'id' | 'name' | 'projectId' | 'ownerId' | 'thumbnailUrl' | 'version'>;
|
|
32
|
+
/**
|
|
33
|
+
* Headless bridge to the `@pascal-app/core` Zustand store.
|
|
34
|
+
*
|
|
35
|
+
* All mutation flows through the real core store so undo/redo works via Zundo.
|
|
36
|
+
* No renderer is attached; `dirtyNodes` accumulates and can be drained via
|
|
37
|
+
* `flushDirty()` for observability.
|
|
38
|
+
*/
|
|
39
|
+
export declare class SceneBridge {
|
|
40
|
+
private activeScene;
|
|
41
|
+
/**
|
|
42
|
+
* Scene identity currently bound to this bridge. MCP tools use this to know
|
|
43
|
+
* which editor scene should receive live events after mutations.
|
|
44
|
+
*/
|
|
45
|
+
setActiveScene(meta: ActiveSceneMeta): void;
|
|
46
|
+
getActiveScene(): ActiveSceneMeta | null;
|
|
47
|
+
clearActiveScene(): void;
|
|
48
|
+
/** Load initial state; if empty, creates default Site → Building → Level. */
|
|
49
|
+
loadDefault(): void;
|
|
50
|
+
/** Replace entire scene (undoable via Zundo). */
|
|
51
|
+
setScene(nodes: Record<AnyNodeId, AnyNode>, rootNodeIds: AnyNodeId[]): void;
|
|
52
|
+
/** Full snapshot for export, including collections. */
|
|
53
|
+
exportJSON(): SceneGraph & {
|
|
54
|
+
collections: Record<string, unknown>;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Import. Accepts either a JSON string or a parsed SceneGraph object.
|
|
58
|
+
* Throws on invalid JSON, unexpected shape, or prototype-polluting keys.
|
|
59
|
+
*/
|
|
60
|
+
loadJSON(json: string | SceneGraph): void;
|
|
61
|
+
/** Read a single node, or `null` if not present. */
|
|
62
|
+
getNode(id: AnyNodeId): AnyNode | null;
|
|
63
|
+
/** All nodes (live reference into the store — do NOT mutate). */
|
|
64
|
+
getNodes(): Record<AnyNodeId, AnyNode>;
|
|
65
|
+
/** Root node IDs. */
|
|
66
|
+
getRootNodeIds(): AnyNodeId[];
|
|
67
|
+
/**
|
|
68
|
+
* Resolve children via the flat `nodes` dict. Uses THREE fallbacks because
|
|
69
|
+
* the codebase's parent-tracking is not uniform:
|
|
70
|
+
*
|
|
71
|
+
* 1. `node.parentId === parentId` (normal case post-store-mutation).
|
|
72
|
+
* 2. Parent has `children: string[]` of IDs (building, level, wall, ...).
|
|
73
|
+
* 3. Parent has `children: Array<node-object>` (the SiteNode quirk — see
|
|
74
|
+
* PLAN §0.7). We resolve each object to its flat-dict entry by `id`.
|
|
75
|
+
*
|
|
76
|
+
* The `loadScene()` default assembler skips the store mutation paths so the
|
|
77
|
+
* default site/building/level tree has `parentId === null` on every node —
|
|
78
|
+
* only the `children` arrays reflect the hierarchy.
|
|
79
|
+
*
|
|
80
|
+
* Results are de-duplicated by id, in flat-dict iteration order.
|
|
81
|
+
*/
|
|
82
|
+
getChildren(parentId: AnyNodeId): AnyNode[];
|
|
83
|
+
/**
|
|
84
|
+
* Walk up `parentId` chain; returns `[self, parent, grandparent, ...]`.
|
|
85
|
+
*
|
|
86
|
+
* Falls back to reverse-scanning `children` arrays when `parentId` is
|
|
87
|
+
* unset (see the default-scene quirk documented on `getChildren`).
|
|
88
|
+
*/
|
|
89
|
+
getAncestry(id: AnyNodeId): AnyNode[];
|
|
90
|
+
/** Find all nodes matching the given filters (all filters ANDed). */
|
|
91
|
+
findNodes(filter: {
|
|
92
|
+
type?: AnyNodeType;
|
|
93
|
+
parentId?: AnyNodeId | null;
|
|
94
|
+
levelId?: AnyNodeId;
|
|
95
|
+
}): AnyNode[];
|
|
96
|
+
/** Resolve the level-ancestor of a node, or `null` if none in the chain. */
|
|
97
|
+
resolveLevelId(id: AnyNodeId): AnyNodeId | null;
|
|
98
|
+
/**
|
|
99
|
+
* Create a node. Caller must pass an already-parsed `AnyNode` (with a valid
|
|
100
|
+
* `id`, generated by the schema default if they did `XxxNode.parse({...})`).
|
|
101
|
+
* Returns the generated id.
|
|
102
|
+
*/
|
|
103
|
+
createNode(node: AnyNode, parentId?: AnyNodeId): AnyNodeId;
|
|
104
|
+
/** Update node fields (shallow merge through the core store). */
|
|
105
|
+
updateNode(id: AnyNodeId, data: Partial<AnyNode>): void;
|
|
106
|
+
/**
|
|
107
|
+
* Delete a node. If the node has children and `cascade === false`, throws.
|
|
108
|
+
* If `cascade` is true (or undefined and no children), delegates to the core
|
|
109
|
+
* action which already recursively removes descendants.
|
|
110
|
+
*
|
|
111
|
+
* Returns the list of ids actually removed from the scene.
|
|
112
|
+
*/
|
|
113
|
+
deleteNode(id: AnyNodeId, cascade?: boolean): string[];
|
|
114
|
+
/**
|
|
115
|
+
* Atomic multi-op patch. Validates EVERY patch first (dry run); only if all
|
|
116
|
+
* pass does it apply in a single batch via `createNodes` / `updateNodes` /
|
|
117
|
+
* `deleteNodes`. Throws on any validation failure without mutating state.
|
|
118
|
+
*/
|
|
119
|
+
applyPatch(patches: Patch[]): {
|
|
120
|
+
appliedOps: number;
|
|
121
|
+
deletedIds: AnyNodeId[];
|
|
122
|
+
createdIds: AnyNodeId[];
|
|
123
|
+
};
|
|
124
|
+
/** Undo. Returns the number of steps actually undone. */
|
|
125
|
+
undo(steps?: number): number;
|
|
126
|
+
/** Redo. Returns the number of steps actually redone. */
|
|
127
|
+
redo(steps?: number): number;
|
|
128
|
+
/**
|
|
129
|
+
* Zod-validate every node in the scene. Reports one error per failed node,
|
|
130
|
+
* concatenating Zod issue paths.
|
|
131
|
+
*/
|
|
132
|
+
validateScene(): ValidationResult;
|
|
133
|
+
/**
|
|
134
|
+
* Drain the dirtyNodes set. Returns the ids that were present. No-op for
|
|
135
|
+
* renderer (there is no renderer in MCP mode); useful for observability.
|
|
136
|
+
*/
|
|
137
|
+
flushDirty(): string[];
|
|
138
|
+
/** Current temporal history pointers. */
|
|
139
|
+
getHistory(): {
|
|
140
|
+
pastCount: number;
|
|
141
|
+
futureCount: number;
|
|
142
|
+
};
|
|
143
|
+
/** Clear the temporal undo/redo history. */
|
|
144
|
+
clearHistory(): void;
|
|
145
|
+
/**
|
|
146
|
+
* Return the node whose `children` array (string or object form) contains
|
|
147
|
+
* the given id, or null if none. Used as a fallback when `parentId` is
|
|
148
|
+
* missing on a node.
|
|
149
|
+
*/
|
|
150
|
+
private _findParentByChildrenScan;
|
|
151
|
+
/**
|
|
152
|
+
* Collect ids of a node and all its descendants. Uses the same combined
|
|
153
|
+
* strategy as `getChildren` (parentId scan + children-array walk) so that
|
|
154
|
+
* the SiteNode quirk and the default-scene parentId-unset case both work.
|
|
155
|
+
*/
|
|
156
|
+
private _collectDescendants;
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=scene-bridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scene-bridge.d.ts","sourceRoot":"","sources":["../../src/bridge/scene-bridge.ts"],"names":[],"mappings":"AACA,OAAO,cAAc,CAAA;AAErB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAA;AACpE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAA;AACtD,OAAO,EAAE,KAAK,SAAS,EAA4B,KAAK,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAGpG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAEjD,MAAM,MAAM,eAAe,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAC/E,MAAM,MAAM,gBAAgB,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,eAAe,EAAE,CAAA;CAAE,CAAA;AAE5E,MAAM,MAAM,WAAW,GAAG;IAAE,EAAE,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,SAAS,CAAA;CAAE,CAAA;AAC/E,MAAM,MAAM,WAAW,GAAG;IAAE,EAAE,EAAE,QAAQ,CAAC;IAAC,EAAE,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAAA;CAAE,CAAA;AACjF,MAAM,MAAM,WAAW,GAAG;IAAE,EAAE,EAAE,QAAQ,CAAC;IAAC,EAAE,EAAE,SAAS,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAA;AAC5E,MAAM,MAAM,KAAK,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,CAAA;AAC3D,MAAM,MAAM,eAAe,GAAG,IAAI,CAChC,SAAS,EACT,IAAI,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,GAAG,cAAc,GAAG,SAAS,CACrE,CAAA;AAED;;;;;;GAMG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,WAAW,CAA+B;IAElD;;;OAGG;IACH,cAAc,CAAC,IAAI,EAAE,eAAe,GAAG,IAAI;IAW3C,cAAc,IAAI,eAAe,GAAG,IAAI;IAIxC,gBAAgB,IAAI,IAAI;IAIxB,6EAA6E;IAC7E,WAAW,IAAI,IAAI;IAInB,iDAAiD;IACjD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,EAAE,WAAW,EAAE,SAAS,EAAE,GAAG,IAAI;IAI3E,uDAAuD;IACvD,UAAU,IAAI,UAAU,GAAG;QAAE,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE;IAYnE;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI;IAuCzC,oDAAoD;IACpD,OAAO,CAAC,EAAE,EAAE,SAAS,GAAG,OAAO,GAAG,IAAI;IAKtC,iEAAiE;IACjE,QAAQ,IAAI,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC;IAItC,qBAAqB;IACrB,cAAc,IAAI,SAAS,EAAE;IAI7B;;;;;;;;;;;;;;OAcG;IACH,WAAW,CAAC,QAAQ,EAAE,SAAS,GAAG,OAAO,EAAE;IAuC3C;;;;;OAKG;IACH,WAAW,CAAC,EAAE,EAAE,SAAS,GAAG,OAAO,EAAE;IAqBrC,qEAAqE;IACrE,SAAS,CAAC,MAAM,EAAE;QAChB,IAAI,CAAC,EAAE,WAAW,CAAA;QAClB,QAAQ,CAAC,EAAE,SAAS,GAAG,IAAI,CAAA;QAC3B,OAAO,CAAC,EAAE,SAAS,CAAA;KACpB,GAAG,OAAO,EAAE;IAiBb,4EAA4E;IAC5E,cAAc,CAAC,EAAE,EAAE,SAAS,GAAG,SAAS,GAAG,IAAI;IAQ/C;;;;OAIG;IACH,UAAU,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS,GAAG,SAAS;IAK1D,iEAAiE;IACjE,UAAU,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI;IAOvD;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,EAAE,SAAS,EAAE,OAAO,UAAQ,GAAG,MAAM,EAAE;IAwBpD;;;;OAIG;IACH,UAAU,CAAC,OAAO,EAAE,KAAK,EAAE,GAAG;QAC5B,UAAU,EAAE,MAAM,CAAA;QAClB,UAAU,EAAE,SAAS,EAAE,CAAA;QACvB,UAAU,EAAE,SAAS,EAAE,CAAA;KACxB;IAkHD,yDAAyD;IACzD,IAAI,CAAC,KAAK,SAAI,GAAG,MAAM;IAOvB,yDAAyD;IACzD,IAAI,CAAC,KAAK,SAAI,GAAG,MAAM;IAOvB;;;OAGG;IACH,aAAa,IAAI,gBAAgB;IAiBjC;;;OAGG;IACH,UAAU,IAAI,MAAM,EAAE;IAStB,yCAAyC;IACzC,UAAU,IAAI;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE;IAQxD,4CAA4C;IAC5C,YAAY,IAAI,IAAI;IAMpB;;;;OAIG;IACH,OAAO,CAAC,yBAAyB;IAqBjC;;;;OAIG;IACH,OAAO,CAAC,mBAAmB;CAkB5B"}
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
// Side-effect import MUST come first: installs RAF polyfill before core loads.
|
|
2
|
+
import './node-shims';
|
|
3
|
+
import { AnyNode as AnyNodeSchema } from '@pascal-app/core/schema';
|
|
4
|
+
// Per PLAN §0.6: `useScene` is the DEFAULT export from `@pascal-app/core/store`.
|
|
5
|
+
import useScene from '@pascal-app/core/store';
|
|
6
|
+
/**
|
|
7
|
+
* Headless bridge to the `@pascal-app/core` Zustand store.
|
|
8
|
+
*
|
|
9
|
+
* All mutation flows through the real core store so undo/redo works via Zundo.
|
|
10
|
+
* No renderer is attached; `dirtyNodes` accumulates and can be drained via
|
|
11
|
+
* `flushDirty()` for observability.
|
|
12
|
+
*/
|
|
13
|
+
export class SceneBridge {
|
|
14
|
+
activeScene = null;
|
|
15
|
+
/**
|
|
16
|
+
* Scene identity currently bound to this bridge. MCP tools use this to know
|
|
17
|
+
* which editor scene should receive live events after mutations.
|
|
18
|
+
*/
|
|
19
|
+
setActiveScene(meta) {
|
|
20
|
+
this.activeScene = {
|
|
21
|
+
id: meta.id,
|
|
22
|
+
name: meta.name,
|
|
23
|
+
projectId: meta.projectId,
|
|
24
|
+
ownerId: meta.ownerId,
|
|
25
|
+
thumbnailUrl: meta.thumbnailUrl,
|
|
26
|
+
version: meta.version,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
getActiveScene() {
|
|
30
|
+
return this.activeScene;
|
|
31
|
+
}
|
|
32
|
+
clearActiveScene() {
|
|
33
|
+
this.activeScene = null;
|
|
34
|
+
}
|
|
35
|
+
/** Load initial state; if empty, creates default Site → Building → Level. */
|
|
36
|
+
loadDefault() {
|
|
37
|
+
useScene.getState().loadScene();
|
|
38
|
+
}
|
|
39
|
+
/** Replace entire scene (undoable via Zundo). */
|
|
40
|
+
setScene(nodes, rootNodeIds) {
|
|
41
|
+
useScene.getState().setScene(nodes, rootNodeIds);
|
|
42
|
+
}
|
|
43
|
+
/** Full snapshot for export, including collections. */
|
|
44
|
+
exportJSON() {
|
|
45
|
+
const state = useScene.getState();
|
|
46
|
+
// Deep-clone so callers can't mutate store state directly.
|
|
47
|
+
return JSON.parse(JSON.stringify({
|
|
48
|
+
nodes: state.nodes,
|
|
49
|
+
rootNodeIds: state.rootNodeIds,
|
|
50
|
+
collections: state.collections ?? {},
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Import. Accepts either a JSON string or a parsed SceneGraph object.
|
|
55
|
+
* Throws on invalid JSON, unexpected shape, or prototype-polluting keys.
|
|
56
|
+
*/
|
|
57
|
+
loadJSON(json) {
|
|
58
|
+
let parsed;
|
|
59
|
+
if (typeof json === 'string') {
|
|
60
|
+
try {
|
|
61
|
+
parsed = JSON.parse(json);
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
65
|
+
throw new Error(`invalid JSON: ${msg}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
parsed = json;
|
|
70
|
+
}
|
|
71
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
72
|
+
throw new Error('invalid scene: expected object with {nodes, rootNodeIds}');
|
|
73
|
+
}
|
|
74
|
+
const obj = parsed;
|
|
75
|
+
const nodes = obj.nodes;
|
|
76
|
+
const rootNodeIds = obj.rootNodeIds;
|
|
77
|
+
if (!nodes || typeof nodes !== 'object' || Array.isArray(nodes)) {
|
|
78
|
+
throw new Error('invalid scene: `nodes` must be an object');
|
|
79
|
+
}
|
|
80
|
+
if (!Array.isArray(rootNodeIds)) {
|
|
81
|
+
throw new Error('invalid scene: `rootNodeIds` must be an array');
|
|
82
|
+
}
|
|
83
|
+
// Reject prototype-polluting keys as top-level `nodes` keys.
|
|
84
|
+
const BANNED = new Set(['__proto__', 'constructor', 'prototype']);
|
|
85
|
+
for (const key of Object.keys(nodes)) {
|
|
86
|
+
if (BANNED.has(key)) {
|
|
87
|
+
throw new Error(`invalid scene: forbidden key "${key}" in nodes`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
this.setScene(nodes, rootNodeIds);
|
|
91
|
+
}
|
|
92
|
+
/** Read a single node, or `null` if not present. */
|
|
93
|
+
getNode(id) {
|
|
94
|
+
const node = useScene.getState().nodes[id];
|
|
95
|
+
return node ?? null;
|
|
96
|
+
}
|
|
97
|
+
/** All nodes (live reference into the store — do NOT mutate). */
|
|
98
|
+
getNodes() {
|
|
99
|
+
return useScene.getState().nodes;
|
|
100
|
+
}
|
|
101
|
+
/** Root node IDs. */
|
|
102
|
+
getRootNodeIds() {
|
|
103
|
+
return useScene.getState().rootNodeIds;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Resolve children via the flat `nodes` dict. Uses THREE fallbacks because
|
|
107
|
+
* the codebase's parent-tracking is not uniform:
|
|
108
|
+
*
|
|
109
|
+
* 1. `node.parentId === parentId` (normal case post-store-mutation).
|
|
110
|
+
* 2. Parent has `children: string[]` of IDs (building, level, wall, ...).
|
|
111
|
+
* 3. Parent has `children: Array<node-object>` (the SiteNode quirk — see
|
|
112
|
+
* PLAN §0.7). We resolve each object to its flat-dict entry by `id`.
|
|
113
|
+
*
|
|
114
|
+
* The `loadScene()` default assembler skips the store mutation paths so the
|
|
115
|
+
* default site/building/level tree has `parentId === null` on every node —
|
|
116
|
+
* only the `children` arrays reflect the hierarchy.
|
|
117
|
+
*
|
|
118
|
+
* Results are de-duplicated by id, in flat-dict iteration order.
|
|
119
|
+
*/
|
|
120
|
+
getChildren(parentId) {
|
|
121
|
+
const nodes = useScene.getState().nodes;
|
|
122
|
+
const out = [];
|
|
123
|
+
const seen = new Set();
|
|
124
|
+
// Strategy 1: parentId scan.
|
|
125
|
+
for (const node of Object.values(nodes)) {
|
|
126
|
+
if (node.parentId === parentId && !seen.has(node.id)) {
|
|
127
|
+
seen.add(node.id);
|
|
128
|
+
out.push(node);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Strategies 2 & 3: parent's own `children` field.
|
|
132
|
+
const parent = nodes[parentId];
|
|
133
|
+
if (parent && 'children' in parent && Array.isArray(parent.children)) {
|
|
134
|
+
for (const child of parent.children) {
|
|
135
|
+
let childId = null;
|
|
136
|
+
if (typeof child === 'string')
|
|
137
|
+
childId = child;
|
|
138
|
+
else if (child &&
|
|
139
|
+
typeof child === 'object' &&
|
|
140
|
+
'id' in child &&
|
|
141
|
+
typeof child.id === 'string') {
|
|
142
|
+
childId = child.id;
|
|
143
|
+
}
|
|
144
|
+
if (!childId)
|
|
145
|
+
continue;
|
|
146
|
+
const childNode = nodes[childId];
|
|
147
|
+
if (!childNode)
|
|
148
|
+
continue;
|
|
149
|
+
if (seen.has(childNode.id))
|
|
150
|
+
continue;
|
|
151
|
+
seen.add(childNode.id);
|
|
152
|
+
out.push(childNode);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Walk up `parentId` chain; returns `[self, parent, grandparent, ...]`.
|
|
159
|
+
*
|
|
160
|
+
* Falls back to reverse-scanning `children` arrays when `parentId` is
|
|
161
|
+
* unset (see the default-scene quirk documented on `getChildren`).
|
|
162
|
+
*/
|
|
163
|
+
getAncestry(id) {
|
|
164
|
+
const nodes = useScene.getState().nodes;
|
|
165
|
+
const out = [];
|
|
166
|
+
let current = nodes[id];
|
|
167
|
+
const seen = new Set();
|
|
168
|
+
while (current && !seen.has(current.id)) {
|
|
169
|
+
seen.add(current.id);
|
|
170
|
+
out.push(current);
|
|
171
|
+
const pid = current.parentId;
|
|
172
|
+
if (pid && nodes[pid]) {
|
|
173
|
+
current = nodes[pid];
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
// Fallback: scan for any node whose `children` includes this id.
|
|
177
|
+
const fallback = this._findParentByChildrenScan(current.id);
|
|
178
|
+
if (!fallback)
|
|
179
|
+
break;
|
|
180
|
+
current = fallback;
|
|
181
|
+
}
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
/** Find all nodes matching the given filters (all filters ANDed). */
|
|
185
|
+
findNodes(filter) {
|
|
186
|
+
const nodes = useScene.getState().nodes;
|
|
187
|
+
const out = [];
|
|
188
|
+
for (const node of Object.values(nodes)) {
|
|
189
|
+
if (filter.type !== undefined && node.type !== filter.type)
|
|
190
|
+
continue;
|
|
191
|
+
if (filter.parentId !== undefined) {
|
|
192
|
+
const np = (node.parentId ?? null);
|
|
193
|
+
if (np !== filter.parentId)
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (filter.levelId !== undefined) {
|
|
197
|
+
if (this.resolveLevelId(node.id) !== filter.levelId)
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
out.push(node);
|
|
201
|
+
}
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
/** Resolve the level-ancestor of a node, or `null` if none in the chain. */
|
|
205
|
+
resolveLevelId(id) {
|
|
206
|
+
const ancestry = this.getAncestry(id);
|
|
207
|
+
for (const node of ancestry) {
|
|
208
|
+
if (node.type === 'level')
|
|
209
|
+
return node.id;
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Create a node. Caller must pass an already-parsed `AnyNode` (with a valid
|
|
215
|
+
* `id`, generated by the schema default if they did `XxxNode.parse({...})`).
|
|
216
|
+
* Returns the generated id.
|
|
217
|
+
*/
|
|
218
|
+
createNode(node, parentId) {
|
|
219
|
+
useScene.getState().createNode(node, parentId);
|
|
220
|
+
return node.id;
|
|
221
|
+
}
|
|
222
|
+
/** Update node fields (shallow merge through the core store). */
|
|
223
|
+
updateNode(id, data) {
|
|
224
|
+
if (!useScene.getState().nodes[id]) {
|
|
225
|
+
throw new Error(`node not found: ${id}`);
|
|
226
|
+
}
|
|
227
|
+
useScene.getState().updateNode(id, data);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Delete a node. If the node has children and `cascade === false`, throws.
|
|
231
|
+
* If `cascade` is true (or undefined and no children), delegates to the core
|
|
232
|
+
* action which already recursively removes descendants.
|
|
233
|
+
*
|
|
234
|
+
* Returns the list of ids actually removed from the scene.
|
|
235
|
+
*/
|
|
236
|
+
deleteNode(id, cascade = false) {
|
|
237
|
+
const state = useScene.getState();
|
|
238
|
+
const node = state.nodes[id];
|
|
239
|
+
if (!node) {
|
|
240
|
+
throw new Error(`node not found: ${id}`);
|
|
241
|
+
}
|
|
242
|
+
const descendants = this._collectDescendants(id);
|
|
243
|
+
if (!cascade && descendants.length > 1) {
|
|
244
|
+
throw new Error(`node has ${descendants.length - 1} descendant(s); pass cascade: true to delete recursively`);
|
|
245
|
+
}
|
|
246
|
+
const before = new Set(Object.keys(state.nodes));
|
|
247
|
+
useScene.getState().deleteNode(id);
|
|
248
|
+
const afterNodes = useScene.getState().nodes;
|
|
249
|
+
const removed = [];
|
|
250
|
+
for (const prevId of before) {
|
|
251
|
+
if (!(prevId in afterNodes))
|
|
252
|
+
removed.push(prevId);
|
|
253
|
+
}
|
|
254
|
+
return removed;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Atomic multi-op patch. Validates EVERY patch first (dry run); only if all
|
|
258
|
+
* pass does it apply in a single batch via `createNodes` / `updateNodes` /
|
|
259
|
+
* `deleteNodes`. Throws on any validation failure without mutating state.
|
|
260
|
+
*/
|
|
261
|
+
applyPatch(patches) {
|
|
262
|
+
const state = useScene.getState();
|
|
263
|
+
const nodes = state.nodes;
|
|
264
|
+
// Track synthesized state as we dry-run so later ops can reference
|
|
265
|
+
// earlier-created ids and reflect earlier-deleted ids.
|
|
266
|
+
const simAvailable = new Set(Object.keys(nodes));
|
|
267
|
+
const simDeleted = new Set();
|
|
268
|
+
// Parsed create nodes keyed by patch index — so the apply phase can use the
|
|
269
|
+
// Zod-normalised copy (which has a generated id if the caller omitted one)
|
|
270
|
+
// instead of the unparsed input.
|
|
271
|
+
const parsedCreateNodes = new Map();
|
|
272
|
+
for (let i = 0; i < patches.length; i++) {
|
|
273
|
+
const p = patches[i];
|
|
274
|
+
if (!p)
|
|
275
|
+
throw new Error(`invalid patch: patches[${i}] is undefined`);
|
|
276
|
+
if (p.op === 'create') {
|
|
277
|
+
const res = AnyNodeSchema.safeParse(p.node);
|
|
278
|
+
if (!res.success) {
|
|
279
|
+
throw new Error(`invalid patch: patches[${i}] create node failed schema: ${res.error.message}`);
|
|
280
|
+
}
|
|
281
|
+
if (p.parentId !== undefined && !simAvailable.has(p.parentId)) {
|
|
282
|
+
throw new Error(`invalid patch: patches[${i}] create parentId "${p.parentId}" not found`);
|
|
283
|
+
}
|
|
284
|
+
parsedCreateNodes.set(i, res.data);
|
|
285
|
+
simAvailable.add(res.data.id);
|
|
286
|
+
}
|
|
287
|
+
else if (p.op === 'update') {
|
|
288
|
+
if (!simAvailable.has(p.id) || simDeleted.has(p.id)) {
|
|
289
|
+
throw new Error(`invalid patch: patches[${i}] update id "${p.id}" not found`);
|
|
290
|
+
}
|
|
291
|
+
if (!p.data || typeof p.data !== 'object') {
|
|
292
|
+
throw new Error(`invalid patch: patches[${i}] update data is not an object`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
else if (p.op === 'delete') {
|
|
296
|
+
if (!simAvailable.has(p.id) || simDeleted.has(p.id)) {
|
|
297
|
+
throw new Error(`invalid patch: patches[${i}] delete id "${p.id}" not found`);
|
|
298
|
+
}
|
|
299
|
+
if (p.cascade === false) {
|
|
300
|
+
// Only inspect the current store state — we don't simulate
|
|
301
|
+
// descendant additions during dry-run, because that would require
|
|
302
|
+
// building a full shadow tree. This matches the semantics of the
|
|
303
|
+
// single-op deleteNode guard.
|
|
304
|
+
const desc = this._collectDescendants(p.id);
|
|
305
|
+
if (desc.length > 1) {
|
|
306
|
+
throw new Error(`invalid patch: patches[${i}] delete "${p.id}" has descendants; pass cascade: true`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
simAvailable.delete(p.id);
|
|
310
|
+
simDeleted.add(p.id);
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
throw new Error(`invalid patch: patches[${i}] unknown op`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// Dry-run succeeded — apply in order, batching adjacent ops of the same
|
|
317
|
+
// op type so Zundo groups them tightly.
|
|
318
|
+
const createOps = [];
|
|
319
|
+
const updateOps = [];
|
|
320
|
+
const deleteIds = [];
|
|
321
|
+
const createdIds = [];
|
|
322
|
+
// Simple approach: queue by type, flush in original order by walking
|
|
323
|
+
// patches and interleaving flushes when the op type changes, so ids
|
|
324
|
+
// created/updated/deleted stay temporally consistent.
|
|
325
|
+
const flush = (kind) => {
|
|
326
|
+
if (kind !== 'create' && createOps.length > 0) {
|
|
327
|
+
useScene.getState().createNodes(createOps);
|
|
328
|
+
createOps.length = 0;
|
|
329
|
+
}
|
|
330
|
+
if (kind !== 'update' && updateOps.length > 0) {
|
|
331
|
+
useScene.getState().updateNodes(updateOps);
|
|
332
|
+
updateOps.length = 0;
|
|
333
|
+
}
|
|
334
|
+
if (kind !== 'delete' && deleteIds.length > 0) {
|
|
335
|
+
useScene.getState().deleteNodes(deleteIds);
|
|
336
|
+
deleteIds.length = 0;
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
for (let i = 0; i < patches.length; i++) {
|
|
340
|
+
const p = patches[i];
|
|
341
|
+
if (p.op === 'create') {
|
|
342
|
+
flush('create');
|
|
343
|
+
const parsedNode = parsedCreateNodes.get(i);
|
|
344
|
+
createOps.push({ node: parsedNode, parentId: p.parentId });
|
|
345
|
+
createdIds.push(parsedNode.id);
|
|
346
|
+
}
|
|
347
|
+
else if (p.op === 'update') {
|
|
348
|
+
flush('update');
|
|
349
|
+
updateOps.push({ id: p.id, data: p.data });
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
flush('delete');
|
|
353
|
+
deleteIds.push(p.id);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
flush('none');
|
|
357
|
+
// Compute actual deleted ids by diffing pre/post snapshots.
|
|
358
|
+
const postNodes = useScene.getState().nodes;
|
|
359
|
+
const deletedIds = [];
|
|
360
|
+
for (const prevId of Object.keys(nodes)) {
|
|
361
|
+
if (!(prevId in postNodes))
|
|
362
|
+
deletedIds.push(prevId);
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
appliedOps: patches.length,
|
|
366
|
+
deletedIds,
|
|
367
|
+
createdIds,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
/** Undo. Returns the number of steps actually undone. */
|
|
371
|
+
undo(steps = 1) {
|
|
372
|
+
const before = useScene.temporal.getState().pastStates.length;
|
|
373
|
+
useScene.temporal.getState().undo(steps);
|
|
374
|
+
const after = useScene.temporal.getState().pastStates.length;
|
|
375
|
+
return Math.max(0, before - after);
|
|
376
|
+
}
|
|
377
|
+
/** Redo. Returns the number of steps actually redone. */
|
|
378
|
+
redo(steps = 1) {
|
|
379
|
+
const before = useScene.temporal.getState().futureStates.length;
|
|
380
|
+
useScene.temporal.getState().redo(steps);
|
|
381
|
+
const after = useScene.temporal.getState().futureStates.length;
|
|
382
|
+
return Math.max(0, before - after);
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Zod-validate every node in the scene. Reports one error per failed node,
|
|
386
|
+
* concatenating Zod issue paths.
|
|
387
|
+
*/
|
|
388
|
+
validateScene() {
|
|
389
|
+
const errors = [];
|
|
390
|
+
const nodes = useScene.getState().nodes;
|
|
391
|
+
for (const [id, node] of Object.entries(nodes)) {
|
|
392
|
+
const res = AnyNodeSchema.safeParse(node);
|
|
393
|
+
if (res.success)
|
|
394
|
+
continue;
|
|
395
|
+
for (const issue of res.error.issues) {
|
|
396
|
+
errors.push({
|
|
397
|
+
nodeId: id,
|
|
398
|
+
path: issue.path.join('.'),
|
|
399
|
+
message: issue.message,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return { valid: errors.length === 0, errors };
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Drain the dirtyNodes set. Returns the ids that were present. No-op for
|
|
407
|
+
* renderer (there is no renderer in MCP mode); useful for observability.
|
|
408
|
+
*/
|
|
409
|
+
flushDirty() {
|
|
410
|
+
const state = useScene.getState();
|
|
411
|
+
const ids = Array.from(state.dirtyNodes);
|
|
412
|
+
for (const id of ids) {
|
|
413
|
+
state.clearDirty(id);
|
|
414
|
+
}
|
|
415
|
+
return ids;
|
|
416
|
+
}
|
|
417
|
+
/** Current temporal history pointers. */
|
|
418
|
+
getHistory() {
|
|
419
|
+
const t = useScene.temporal.getState();
|
|
420
|
+
return {
|
|
421
|
+
pastCount: t.pastStates.length,
|
|
422
|
+
futureCount: t.futureStates.length,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
/** Clear the temporal undo/redo history. */
|
|
426
|
+
clearHistory() {
|
|
427
|
+
useScene.temporal.getState().clear();
|
|
428
|
+
}
|
|
429
|
+
// ---- internal helpers ----
|
|
430
|
+
/**
|
|
431
|
+
* Return the node whose `children` array (string or object form) contains
|
|
432
|
+
* the given id, or null if none. Used as a fallback when `parentId` is
|
|
433
|
+
* missing on a node.
|
|
434
|
+
*/
|
|
435
|
+
_findParentByChildrenScan(id) {
|
|
436
|
+
const nodes = useScene.getState().nodes;
|
|
437
|
+
for (const candidate of Object.values(nodes)) {
|
|
438
|
+
if (!('children' in candidate && Array.isArray(candidate.children)))
|
|
439
|
+
continue;
|
|
440
|
+
for (const child of candidate.children) {
|
|
441
|
+
let childId = null;
|
|
442
|
+
if (typeof child === 'string')
|
|
443
|
+
childId = child;
|
|
444
|
+
else if (child &&
|
|
445
|
+
typeof child === 'object' &&
|
|
446
|
+
'id' in child &&
|
|
447
|
+
typeof child.id === 'string') {
|
|
448
|
+
childId = child.id;
|
|
449
|
+
}
|
|
450
|
+
if (childId === id)
|
|
451
|
+
return candidate;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Collect ids of a node and all its descendants. Uses the same combined
|
|
458
|
+
* strategy as `getChildren` (parentId scan + children-array walk) so that
|
|
459
|
+
* the SiteNode quirk and the default-scene parentId-unset case both work.
|
|
460
|
+
*/
|
|
461
|
+
_collectDescendants(id) {
|
|
462
|
+
const nodes = useScene.getState().nodes;
|
|
463
|
+
if (!nodes[id])
|
|
464
|
+
return [];
|
|
465
|
+
const out = [];
|
|
466
|
+
const stack = [id];
|
|
467
|
+
const seen = new Set();
|
|
468
|
+
// Precompute parent → child[] index from parentId only. `children` arrays
|
|
469
|
+
// are consulted on-the-fly via getChildren.
|
|
470
|
+
while (stack.length > 0) {
|
|
471
|
+
const curr = stack.pop();
|
|
472
|
+
if (seen.has(curr))
|
|
473
|
+
continue;
|
|
474
|
+
seen.add(curr);
|
|
475
|
+
out.push(curr);
|
|
476
|
+
const children = this.getChildren(curr);
|
|
477
|
+
for (const c of children)
|
|
478
|
+
stack.push(c.id);
|
|
479
|
+
}
|
|
480
|
+
return out;
|
|
481
|
+
}
|
|
482
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { SceneBridge } from './bridge/scene-bridge';
|
|
2
|
+
export { createSceneOperations, type SceneOperations } from './operations';
|
|
3
|
+
export { type CreatePascalMcpServerOptions, createPascalMcpServer } from './server';
|
|
4
|
+
export declare const version = "0.1.0";
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACnD,OAAO,EAAE,qBAAqB,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAA;AAC1E,OAAO,EAAE,KAAK,4BAA4B,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAA;AAEnF,eAAO,MAAM,OAAO,UAAU,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SceneGraph } from '@pascal-app/core/clone-scene-graph';
|
|
2
|
+
/**
|
|
3
|
+
* `cloneSceneGraph` normalises `SiteNode.children` to an array of node IDs,
|
|
4
|
+
* but core's `SiteNode` schema expects an array of embedded `BuildingNode` /
|
|
5
|
+
* `ItemNode` objects (see `packages/mcp/CROSS_CUTTING.md` §2). To keep the
|
|
6
|
+
* cloned graph validating against `AnyNode`, re-embed the site children from
|
|
7
|
+
* the flat dict.
|
|
8
|
+
*
|
|
9
|
+
* Pure: returns a new graph without mutating the input.
|
|
10
|
+
*/
|
|
11
|
+
export declare function rehydrateSiteChildren(graph: SceneGraph): SceneGraph;
|
|
12
|
+
//# sourceMappingURL=rehydrate-site-children.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rehydrate-site-children.d.ts","sourceRoot":"","sources":["../../src/lib/rehydrate-site-children.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAA;AAGpE;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,UAAU,GAAG,UAAU,CA2BnE"}
|