@pascal-app/mcp 1.0.5 → 1.0.7

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.
Files changed (87) hide show
  1. package/README.md +31 -10
  2. package/dist/bin/pascal-mcp.js +2 -0
  3. package/dist/bridge/agent-session.d.ts +2 -1
  4. package/dist/bridge/agent-session.d.ts.map +1 -1
  5. package/dist/bridge/agent-session.js +0 -1
  6. package/dist/bridge/scene-bridge.d.ts +21 -10
  7. package/dist/bridge/scene-bridge.d.ts.map +1 -1
  8. package/dist/bridge/scene-bridge.js +178 -194
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/lib/safe-fetch.d.ts +6 -0
  12. package/dist/lib/safe-fetch.d.ts.map +1 -1
  13. package/dist/lib/safe-fetch.js +135 -41
  14. package/dist/operations/scene-operations.d.ts +7 -0
  15. package/dist/operations/scene-operations.d.ts.map +1 -1
  16. package/dist/operations/scene-operations.js +4 -0
  17. package/dist/prompts/from-brief.js +1 -1
  18. package/dist/prompts/iterate-on-feedback.js +1 -1
  19. package/dist/prompts/renovation-from-photos.js +1 -1
  20. package/dist/prompts/scene-guidance.js +2 -2
  21. package/dist/resources/agent-guide.d.ts.map +1 -1
  22. package/dist/resources/agent-guide.js +8 -3
  23. package/dist/resources/catalog-items.d.ts +4 -1
  24. package/dist/resources/catalog-items.d.ts.map +1 -1
  25. package/dist/resources/catalog-items.js +14 -6
  26. package/dist/server.d.ts +9 -5
  27. package/dist/server.d.ts.map +1 -1
  28. package/dist/server.js +21 -5
  29. package/dist/templates/empty-studio.d.ts.map +1 -1
  30. package/dist/templates/empty-studio.js +2 -3
  31. package/dist/templates/garden-house.d.ts.map +1 -1
  32. package/dist/templates/garden-house.js +2 -3
  33. package/dist/templates/two-bedroom.d.ts.map +1 -1
  34. package/dist/templates/two-bedroom.js +2 -3
  35. package/dist/tools/add-column.d.ts.map +1 -1
  36. package/dist/tools/add-column.js +9 -6
  37. package/dist/tools/add-object.d.ts +44 -5
  38. package/dist/tools/add-object.d.ts.map +1 -1
  39. package/dist/tools/add-object.js +75 -21
  40. package/dist/tools/asset-catalog.d.ts +5 -6
  41. package/dist/tools/asset-catalog.d.ts.map +1 -1
  42. package/dist/tools/asset-catalog.js +1 -9
  43. package/dist/tools/author-scene.d.ts +8 -5
  44. package/dist/tools/author-scene.d.ts.map +1 -1
  45. package/dist/tools/author-scene.js +302 -51
  46. package/dist/tools/clear-scene.js +2 -2
  47. package/dist/tools/collections.d.ts.map +1 -1
  48. package/dist/tools/collections.js +8 -5
  49. package/dist/tools/honest-patch-guard.js +6 -5
  50. package/dist/tools/hosted-services.d.ts.map +1 -1
  51. package/dist/tools/hosted-services.js +2 -2
  52. package/dist/tools/index.d.ts +7 -2
  53. package/dist/tools/index.d.ts.map +1 -1
  54. package/dist/tools/index.js +3 -9
  55. package/dist/tools/local-geometry-scripts.d.ts.map +1 -1
  56. package/dist/tools/local-geometry-scripts.js +7 -2
  57. package/dist/tools/patch-guards.d.ts.map +1 -1
  58. package/dist/tools/patch-guards.js +24 -14
  59. package/dist/tools/project-files.d.ts.map +1 -1
  60. package/dist/tools/project-files.js +2 -2
  61. package/dist/tools/room-tools.d.ts +1 -0
  62. package/dist/tools/room-tools.d.ts.map +1 -1
  63. package/dist/tools/room-tools.js +11 -8
  64. package/dist/tools/scene-scope.d.ts +4 -0
  65. package/dist/tools/scene-scope.d.ts.map +1 -0
  66. package/dist/tools/scene-scope.js +4 -0
  67. package/dist/tools/shared-tools.d.ts +0 -2
  68. package/dist/tools/shared-tools.d.ts.map +1 -1
  69. package/dist/tools/shared-tools.js +146 -31
  70. package/dist/tools/templates/create-house-from-brief.js +1 -1
  71. package/dist/tools/view-scene.js +2 -2
  72. package/examples/coordinate-conventions-demo.md +1 -0
  73. package/examples/photo-to-scene.md +1 -0
  74. package/examples/renovate-from-photos.md +2 -0
  75. package/package.json +10 -10
  76. package/dist/tools/create-unit.d.ts +0 -29
  77. package/dist/tools/create-unit.d.ts.map +0 -1
  78. package/dist/tools/create-unit.js +0 -36
  79. package/dist/tools/list-units.d.ts +0 -38
  80. package/dist/tools/list-units.d.ts.map +0 -1
  81. package/dist/tools/list-units.js +0 -61
  82. package/dist/tools/set-unit-members.d.ts +0 -22
  83. package/dist/tools/set-unit-members.d.ts.map +0 -1
  84. package/dist/tools/set-unit-members.js +0 -43
  85. package/dist/tools/unit-members.d.ts +0 -4
  86. package/dist/tools/unit-members.d.ts.map +0 -1
  87. package/dist/tools/unit-members.js +0 -24
@@ -1,21 +1,32 @@
1
1
  // Side-effect import MUST come first: installs RAF polyfill before core loads.
2
2
  import './node-shims.js';
3
- import { adjacentLevelId, applyStructureReconciliation, assertDerivedNodeWrites, changedStairOpeningOwners, floorOpeningTargets, HIDDEN_SITE_NOTE, pauseSceneHistory, pauseSpaceDetection, planNodeDeletion, planOwnedFloorOpenings, planWallDeletion, resumeSceneHistory, resumeSpaceDetection, runAsSingleSceneHistoryStep, stairDeckLevelId, structureChangeBatch, } from '@pascal-app/core';
3
+ import { applyStructureReconciliation, assertDerivedNodeWrites, HIDDEN_SITE_NOTE, pauseSceneHistory, pauseSpaceDetection, planNodeDeletion, planWallDeletion, resumeSceneHistory, resumeSpaceDetection, structureChangeBatch, } from '@pascal-app/core';
4
+ import { declareActionDependencies, liveSceneRuntime, materialize, runAsSettledSceneStep, } from '@pascal-app/core/runtime';
4
5
  import { AnyNode as AnyNodeSchema, generateId, parseNode, SceneMaterial, } from '@pascal-app/core/schema';
5
- // Per PLAN §0.6: `useScene` is the DEFAULT export from `@pascal-app/core/store`.
6
- import useScene from '@pascal-app/core/store';
7
6
  import { InMemoryAgentSession } from './agent-session.js';
8
7
  /**
9
- * Headless bridge to the `@pascal-app/core` Zustand store.
8
+ * Headless bridge to a `@pascal-app/core` scene store: the editor's own (the default), or a
9
+ * detached scene's (`@pascal-app/core/runtime`), whose host passes it as `runtime`.
10
10
  *
11
- * All mutation flows through the real core store so undo/redo works via Zundo.
12
- * No renderer is attached; `dirtyNodes` accumulates and can be drained via
13
- * `flushDirty()` for observability.
11
+ * All mutation flows through the real core store so undo/redo works via Zundo. Reads go through
12
+ * the runtime's reader (recorded on a detached scene), and writes run in its scope: its history
13
+ * environment, its id allocator, its disposal guard. No renderer is attached; `dirtyNodes`
14
+ * accumulates and can be drained via `flushDirty()` for observability.
14
15
  */
15
16
  export class SceneBridge {
16
17
  /** `applyPatch` takes `upsert_material` patches, in the patch's one history step. */
17
18
  supportsMaterialUpserts = true;
18
19
  activeScene = null;
20
+ runtime;
21
+ constructor(options = {}) {
22
+ this.runtime = options.runtime ?? liveSceneRuntime;
23
+ }
24
+ get store() {
25
+ return this.runtime.store;
26
+ }
27
+ write(run) {
28
+ return this.runtime.run(run);
29
+ }
19
30
  /** What the agent's session has kept: its named moments, and what it made. */
20
31
  agentSession = new InMemoryAgentSession();
21
32
  /**
@@ -42,24 +53,30 @@ export class SceneBridge {
42
53
  /** Load initial state; if empty, creates default Site → Building → Level. */
43
54
  loadDefault() {
44
55
  this.agentSession.replace();
45
- useScene.getState().loadScene();
56
+ this.write(() => this.store.getState().loadScene());
46
57
  }
47
58
  /** Replace entire scene (undoable via Zundo). */
48
59
  setScene(nodes, rootNodeIds, extra) {
49
60
  this.agentSession.replace();
50
- useScene.getState().setScene(nodes, rootNodeIds, extra);
61
+ // The store takes its own copy: a detached scene freezes what it holds.
62
+ const owned = structuredClone(materialize({ nodes, rootNodeIds, extra }));
63
+ this.write(() => this.store.getState().setScene(owned.nodes, owned.rootNodeIds, owned.extra));
51
64
  }
52
65
  /** Full snapshot for export, including collections and the material palette. */
53
66
  exportJSON() {
54
- const state = useScene.getState();
67
+ const state = this.store.getState();
68
+ const reader = this.runtime.reader();
69
+ // Everything exported is read through the reader: materials and the plugin list included.
70
+ const materials = Object.fromEntries(reader.query({ entity: 'material' }).map((material) => [material.id, material]));
71
+ const installedPlugins = reader.installedPlugins();
55
72
  // Deep-clone so callers can't mutate store state directly.
56
73
  return JSON.parse(JSON.stringify({
57
- nodes: state.nodes,
58
- rootNodeIds: state.rootNodeIds,
59
- collections: state.collections ?? {},
60
- materials: state.materials ?? {},
61
- ...(state.hasExplicitPluginInstallState || state.installedPlugins.length > 0
62
- ? { installedPlugins: state.installedPlugins }
74
+ nodes: reader.nodes,
75
+ rootNodeIds: this.getRootNodeIds(),
76
+ collections: reader.collections ?? {},
77
+ materials,
78
+ ...(state.hasExplicitPluginInstallState || installedPlugins.length > 0
79
+ ? { installedPlugins }
63
80
  : {}),
64
81
  }));
65
82
  }
@@ -120,27 +137,34 @@ export class SceneBridge {
120
137
  }
121
138
  /** Read a single node, or `null` if not present. */
122
139
  getNode(id) {
123
- const node = useScene.getState().nodes[id];
124
- return node ?? null;
140
+ return this.runtime.reader().get(id) ?? null;
125
141
  }
126
- /** All nodes (live reference into the store — do NOT mutate). */
142
+ /** All nodes (a read-only view of the store's current nodes — do NOT mutate). */
127
143
  getNodes() {
128
- return useScene.getState().nodes;
144
+ return this.runtime.reader().nodes;
129
145
  }
130
146
  getCollections() {
131
- return useScene.getState().collections;
147
+ return this.runtime.reader().collections;
132
148
  }
133
149
  getMaterials() {
134
- return useScene.getState().materials;
150
+ return Object.fromEntries(this.runtime
151
+ .reader()
152
+ .query({ entity: 'material' })
153
+ .map((material) => [material.id, material]));
135
154
  }
136
155
  setCollections(collections) {
137
- if (useScene.getState().readOnly)
138
- return;
139
- useScene.setState({ collections });
156
+ this.write(() => {
157
+ if (this.store.getState().readOnly)
158
+ return;
159
+ this.store.setState({ collections: structuredClone(materialize(collections)) });
160
+ });
140
161
  }
141
162
  /** Root node IDs. */
142
163
  getRootNodeIds() {
143
- return useScene.getState().rootNodeIds;
164
+ return this.runtime
165
+ .reader()
166
+ .query({ entity: 'root' })
167
+ .map((node) => node.id);
144
168
  }
145
169
  /**
146
170
  * Resolve children via the flat `nodes` dict. Uses THREE fallbacks because
@@ -158,7 +182,7 @@ export class SceneBridge {
158
182
  * Results are de-duplicated by id, in flat-dict iteration order.
159
183
  */
160
184
  getChildren(parentId) {
161
- const nodes = useScene.getState().nodes;
185
+ const nodes = this.getNodes();
162
186
  const out = [];
163
187
  const seen = new Set();
164
188
  // Strategy 1: parentId scan.
@@ -201,7 +225,7 @@ export class SceneBridge {
201
225
  * unset (see the default-scene quirk documented on `getChildren`).
202
226
  */
203
227
  getAncestry(id) {
204
- const nodes = useScene.getState().nodes;
228
+ const nodes = this.getNodes();
205
229
  const out = [];
206
230
  let current = nodes[id];
207
231
  const seen = new Set();
@@ -223,16 +247,16 @@ export class SceneBridge {
223
247
  }
224
248
  /** Find all nodes matching the given filters (all filters ANDed). */
225
249
  findNodes(filter) {
226
- const nodes = useScene.getState().nodes;
250
+ // A query, so a detached scene records the selection rather than a read of every node.
251
+ const selected = this.runtime.reader().query({
252
+ entity: 'node',
253
+ where: {
254
+ ...(filter.type !== undefined && { type: filter.type }),
255
+ ...(filter.parentId !== undefined && { parentId: filter.parentId }),
256
+ },
257
+ });
227
258
  const out = [];
228
- for (const node of Object.values(nodes)) {
229
- if (filter.type !== undefined && node.type !== filter.type)
230
- continue;
231
- if (filter.parentId !== undefined) {
232
- const np = (node.parentId ?? null);
233
- if (np !== filter.parentId)
234
- continue;
235
- }
259
+ for (const node of selected) {
236
260
  if (filter.levelId !== undefined) {
237
261
  if (this.resolveLevelId(node.id) !== filter.levelId)
238
262
  continue;
@@ -255,25 +279,36 @@ export class SceneBridge {
255
279
  * `id`, generated by the schema default if they did `XxxNode.parse({...})`).
256
280
  * Returns the generated id.
257
281
  */
258
- createNode(node, parentId) {
259
- const before = useScene.getState().nodes;
260
- assertDerivedNodeWrites(before, { create: [{ node: parentId ? { ...node, parentId } : node }] });
261
- runAsSingleSceneHistoryStep(useScene, () => {
262
- useScene.getState().createNode(node, parentId);
263
- this.reconcileDependentStructure(before);
282
+ createNode(input, parentId) {
283
+ // The store takes its own copy: a detached scene freezes what it holds.
284
+ const node = structuredClone(materialize(input));
285
+ this.write(() => {
286
+ const before = this.store.getState().nodes;
287
+ assertDerivedNodeWrites(this.getNodes(), {
288
+ create: [{ node: parentId ? { ...node, parentId } : node }],
289
+ });
290
+ runAsSettledSceneStep(this.store, () => {
291
+ this.store.getState().createNode(node, parentId);
292
+ declareActionDependencies(this.runtime, before, { create: [{ node, parentId }] });
293
+ this.runtime.reconcile(before);
294
+ });
264
295
  });
265
296
  return node.id;
266
297
  }
267
298
  /** Update node fields (shallow merge through the core store). */
268
- updateNode(id, data) {
269
- if (!useScene.getState().nodes[id]) {
299
+ updateNode(id, input) {
300
+ const data = structuredClone(materialize(input));
301
+ if (!this.getNode(id)) {
270
302
  throw new Error(`node not found: ${id}`);
271
303
  }
272
- const before = useScene.getState().nodes;
273
- assertDerivedNodeWrites(before, { update: [{ id, data }] });
274
- runAsSingleSceneHistoryStep(useScene, () => {
275
- useScene.getState().updateNode(id, data);
276
- this.reconcileDependentStructure(before);
304
+ this.write(() => {
305
+ const before = this.store.getState().nodes;
306
+ assertDerivedNodeWrites(this.getNodes(), { update: [{ id, data }] });
307
+ runAsSettledSceneStep(this.store, () => {
308
+ this.store.getState().updateNode(id, data);
309
+ declareActionDependencies(this.runtime, before, { update: [{ id, data }] });
310
+ this.runtime.reconcile(before);
311
+ });
277
312
  });
278
313
  }
279
314
  /**
@@ -284,8 +319,8 @@ export class SceneBridge {
284
319
  * Returns the list of ids actually removed from the scene.
285
320
  */
286
321
  deleteNode(id, cascade = false) {
287
- const state = useScene.getState();
288
- const node = state.nodes[id];
322
+ const nodes = this.getNodes();
323
+ const node = nodes[id];
289
324
  if (!node) {
290
325
  throw new Error(`node not found: ${id}`);
291
326
  }
@@ -293,20 +328,14 @@ export class SceneBridge {
293
328
  if (!cascade && descendants.length > 1) {
294
329
  throw new Error(`node has ${descendants.length - 1} descendant(s); pass cascade: true to delete recursively`);
295
330
  }
296
- const before = new Set(Object.keys(state.nodes));
297
- const wallLevels = [
298
- ...new Set(descendants.flatMap((nodeId) => {
299
- const deleted = state.nodes[nodeId];
300
- return deleted?.type === 'wall' && deleted.parentId ? [deleted.parentId] : [];
301
- })),
302
- ];
303
- runAsSingleSceneHistoryStep(useScene, () => {
304
- useScene.getState().deleteNode(id);
305
- if (wallLevels.length)
306
- this.deriveStructure(wallLevels);
307
- this.reconcileDependentStructure(state.nodes);
308
- });
309
- const afterNodes = useScene.getState().nodes;
331
+ const beforeNodes = this.store.getState().nodes;
332
+ const before = new Set(Object.keys(beforeNodes));
333
+ this.write(() => runAsSettledSceneStep(this.store, () => {
334
+ this.store.getState().deleteNode(id);
335
+ declareActionDependencies(this.runtime, beforeNodes, { delete: [id] });
336
+ this.runtime.reconcile(beforeNodes);
337
+ }));
338
+ const afterNodes = this.store.getState().nodes;
310
339
  const removed = [];
311
340
  for (const prevId of before) {
312
341
  if (!(prevId in afterNodes))
@@ -320,14 +349,19 @@ export class SceneBridge {
320
349
  * validation failure without mutating state.
321
350
  */
322
351
  applyPatch(patches) {
323
- const state = useScene.getState();
324
- const nodes = state.nodes;
325
- if (state.readOnly)
352
+ return this.write(() => this.applyPatchInScope(structuredClone(materialize(patches))));
353
+ }
354
+ applyPatchInScope(patches) {
355
+ const nodes = this.getNodes();
356
+ const beforeNodes = this.store.getState().nodes;
357
+ if (this.store.getState().readOnly)
326
358
  throw new Error('scene_read_only');
327
359
  // Track synthesized state as we dry-run so later ops can reference
328
- // earlier-created ids and reflect earlier-deleted ids.
329
- const simAvailable = new Set(Object.keys(nodes));
360
+ // earlier-created ids and reflect earlier-deleted ids. Existence is read
361
+ // id by id: enumerating the scene would read every node of a detached one.
362
+ const simCreated = new Set();
330
363
  const simDeleted = new Set();
364
+ const simAvailable = (id) => simCreated.has(id) || (!simDeleted.has(id) && nodes[id] !== undefined);
331
365
  // Parsed create nodes keyed by patch index — so the apply phase can use the
332
366
  // Zod-normalised copy (which has a generated id if the caller omitted one)
333
367
  // instead of the unparsed input.
@@ -348,14 +382,14 @@ export class SceneBridge {
348
382
  if (!res.success) {
349
383
  throw new Error(`invalid patch: patches[${i}] create node failed schema: ${res.error.message}`);
350
384
  }
351
- if (p.parentId !== undefined && !simAvailable.has(p.parentId)) {
385
+ if (p.parentId !== undefined && !simAvailable(p.parentId)) {
352
386
  throw new Error(`invalid patch: patches[${i}] create parentId "${p.parentId}" not found`);
353
387
  }
354
388
  parsedCreateNodes.set(i, p.parentId ? { ...res.data, parentId: p.parentId } : res.data);
355
- simAvailable.add(res.data.id);
389
+ simCreated.add(res.data.id);
356
390
  }
357
391
  else if (p.op === 'update') {
358
- if (!simAvailable.has(p.id) || simDeleted.has(p.id)) {
392
+ if (!simAvailable(p.id) || simDeleted.has(p.id)) {
359
393
  throw new Error(`invalid patch: patches[${i}] update id "${p.id}" not found`);
360
394
  }
361
395
  if (!p.data || typeof p.data !== 'object') {
@@ -363,7 +397,7 @@ export class SceneBridge {
363
397
  }
364
398
  }
365
399
  else if (p.op === 'delete') {
366
- if (!simAvailable.has(p.id) || simDeleted.has(p.id)) {
400
+ if (!simAvailable(p.id) || simDeleted.has(p.id)) {
367
401
  throw new Error(`invalid patch: patches[${i}] delete id "${p.id}" not found`);
368
402
  }
369
403
  if (p.cascade === false) {
@@ -376,7 +410,7 @@ export class SceneBridge {
376
410
  throw new Error(`invalid patch: patches[${i}] delete "${p.id}" has descendants; pass cascade: true`);
377
411
  }
378
412
  }
379
- simAvailable.delete(p.id);
413
+ simCreated.delete(p.id);
380
414
  simDeleted.add(p.id);
381
415
  }
382
416
  else {
@@ -393,20 +427,17 @@ export class SceneBridge {
393
427
  const createOps = new Map();
394
428
  const updateOps = new Map();
395
429
  const deleteIds = new Set();
396
- const nextNodes = { ...nodes };
397
430
  for (const [index, patch] of patches.entries()) {
398
431
  if (patch.op === 'create') {
399
432
  const parsed = parsedCreateNodes.get(index);
400
433
  const node = { ...parsed, parentId: patch.parentId ?? parsed.parentId };
401
434
  createOps.set(node.id, { node, parentId: patch.parentId });
402
435
  deleteIds.delete(node.id);
403
- nextNodes[node.id] = node;
404
436
  }
405
437
  else if (patch.op === 'update') {
406
- nextNodes[patch.id] = { ...nextNodes[patch.id], ...patch.data };
407
438
  const created = createOps.get(patch.id);
408
439
  if (created) {
409
- created.node = nextNodes[patch.id];
440
+ created.node = { ...created.node, ...patch.data };
410
441
  if (patch.data.parentId !== undefined)
411
442
  created.parentId = patch.data.parentId;
412
443
  }
@@ -417,45 +448,47 @@ export class SceneBridge {
417
448
  deleteIds.add(patch.id);
418
449
  createOps.delete(patch.id);
419
450
  updateOps.delete(patch.id);
420
- delete nextNodes[patch.id];
421
451
  }
422
452
  }
423
453
  // Plan the entire deletion selection before applying anything. Interleaved
424
454
  // update operations must not turn an all-boundaries deletion into balconies.
425
- const planned = structureChangeBatch(planWallDeletion(nodes, {
426
- nodeIds: [...deleteIds],
427
- nextNodes,
428
- mintId: generateId,
429
- }).changes);
455
+ // Only a deletion plans: the planner reads the whole scene for what the
456
+ // deleted nodes hold.
457
+ const planned = structureChangeBatch(deleteIds.size
458
+ ? planWallDeletion(nodes, {
459
+ nodeIds: [...deleteIds],
460
+ nextNodes: patchedNodes(nodes, createOps, updateOps, deleteIds),
461
+ mintId: generateId,
462
+ }).changes
463
+ : []);
464
+ // What was asked for, before the deletion planner adds its own writes.
465
+ const requested = {
466
+ create: [...createOps.values()],
467
+ update: [...updateOps].map(([id, data]) => ({ id, data })),
468
+ delete: [...deleteIds],
469
+ };
430
470
  for (const { id, data } of planned.update)
431
471
  updateOps.set(id, { ...updateOps.get(id), ...data });
432
- const wallLevels = [
433
- ...new Set(planned.delete.flatMap((id) => {
434
- const node = nodes[id];
435
- return node?.type === 'wall' && node.parentId ? [node.parentId] : [];
436
- })),
437
- ];
438
- runAsSingleSceneHistoryStep(useScene, () => {
472
+ runAsSettledSceneStep(this.store, () => {
439
473
  pauseSpaceDetection();
440
474
  try {
441
475
  for (const material of materialUpserts.values())
442
- useScene.getState().addSceneMaterial(material);
443
- useScene.getState().createNodes([...createOps.values(), ...planned.create]);
444
- useScene.getState().updateNodes([...updateOps].map(([id, data]) => ({ id, data })));
445
- useScene.getState().deleteNodes(planned.delete);
476
+ this.store.getState().addSceneMaterial(material);
477
+ this.store.getState().createNodes([...createOps.values(), ...planned.create]);
478
+ this.store.getState().updateNodes([...updateOps].map(([id, data]) => ({ id, data })));
479
+ this.store.getState().deleteNodes(planned.delete);
446
480
  }
447
481
  finally {
448
482
  resumeSpaceDetection();
449
483
  }
450
- if (wallLevels.length)
451
- this.deriveStructure(wallLevels);
452
- this.reconcileDependentStructure(nodes);
484
+ declareActionDependencies(this.runtime, beforeNodes, requested);
485
+ this.runtime.reconcile(beforeNodes);
453
486
  });
454
- const createdIds = Object.keys(useScene.getState().nodes).filter((id) => !nodes[id]);
487
+ const postNodes = this.store.getState().nodes;
488
+ const createdIds = Object.keys(postNodes).filter((id) => !beforeNodes[id]);
455
489
  // Compute actual deleted ids by diffing pre/post snapshots.
456
- const postNodes = useScene.getState().nodes;
457
490
  const deletedIds = [];
458
- for (const prevId of Object.keys(nodes)) {
491
+ for (const prevId of Object.keys(beforeNodes)) {
459
492
  if (!(prevId in postNodes))
460
493
  deletedIds.push(prevId);
461
494
  }
@@ -484,9 +517,16 @@ export class SceneBridge {
484
517
  * before the triggering tool call rather than stranding derived nodes.
485
518
  */
486
519
  deriveStructure(levelIds) {
487
- pauseSceneHistory(useScene);
520
+ return this.write(() => runAsSettledSceneStep(this.store, () => this.deriveStructureInScope(levelIds)));
521
+ }
522
+ deriveStructureInScope(levelIds) {
523
+ pauseSceneHistory(this.store);
488
524
  try {
489
- const patches = applyStructureReconciliation(useScene, {
525
+ // The kernel reads through the runtime's reader; its writes go to the store.
526
+ const reading = {
527
+ getState: () => ({ ...this.store.getState(), nodes: this.getNodes() }),
528
+ };
529
+ const patches = applyStructureReconciliation(reading, {
490
530
  levelIds,
491
531
  mintId: (kind) => generateId(kind),
492
532
  });
@@ -496,22 +536,24 @@ export class SceneBridge {
496
536
  };
497
537
  }
498
538
  finally {
499
- resumeSceneHistory(useScene);
539
+ resumeSceneHistory(this.store);
500
540
  }
501
541
  }
502
542
  /** Undo. Returns the number of steps actually undone. */
503
543
  undo(steps = 1) {
504
- const before = useScene.temporal.getState().pastStates.length;
505
- useScene.temporal.getState().undo(steps);
506
- const after = useScene.temporal.getState().pastStates.length;
507
- return Math.max(0, before - after);
544
+ return this.write(() => {
545
+ const before = this.store.temporal.getState().pastStates.length;
546
+ this.store.temporal.getState().undo(steps);
547
+ return Math.max(0, before - this.store.temporal.getState().pastStates.length);
548
+ });
508
549
  }
509
550
  /** Redo. Returns the number of steps actually redone. */
510
551
  redo(steps = 1) {
511
- const before = useScene.temporal.getState().futureStates.length;
512
- useScene.temporal.getState().redo(steps);
513
- const after = useScene.temporal.getState().futureStates.length;
514
- return Math.max(0, before - after);
552
+ return this.write(() => {
553
+ const before = this.store.temporal.getState().futureStates.length;
554
+ this.store.temporal.getState().redo(steps);
555
+ return Math.max(0, before - this.store.temporal.getState().futureStates.length);
556
+ });
515
557
  }
516
558
  /**
517
559
  * Zod-validate every node in the scene. Reports one error per failed node,
@@ -520,7 +562,7 @@ export class SceneBridge {
520
562
  validateScene() {
521
563
  const errors = [];
522
564
  const warnings = [];
523
- const nodes = useScene.getState().nodes;
565
+ const nodes = this.getNodes();
524
566
  for (const [id, node] of Object.entries(nodes)) {
525
567
  if (node.type === 'site' && node.visible === false) {
526
568
  warnings.push({ nodeId: id, path: 'visible', message: HIDDEN_SITE_NOTE });
@@ -543,7 +585,7 @@ export class SceneBridge {
543
585
  * renderer (there is no renderer in MCP mode); useful for observability.
544
586
  */
545
587
  flushDirty() {
546
- const state = useScene.getState();
588
+ const state = this.store.getState();
547
589
  const ids = Array.from(state.dirtyNodes);
548
590
  for (const id of ids) {
549
591
  state.clearDirty(id);
@@ -552,7 +594,7 @@ export class SceneBridge {
552
594
  }
553
595
  /** Current temporal history pointers. */
554
596
  getHistory() {
555
- const t = useScene.temporal.getState();
597
+ const t = this.store.temporal.getState();
556
598
  return {
557
599
  pastCount: t.pastStates.length,
558
600
  futureCount: t.futureStates.length,
@@ -560,83 +602,14 @@ export class SceneBridge {
560
602
  }
561
603
  /** Clear the temporal undo/redo history. */
562
604
  clearHistory() {
563
- useScene.temporal.getState().clear();
605
+ this.store.temporal.getState().clear();
606
+ }
607
+ /** Runs `run` in the scene's scope: an operation planned here mints the runtime's ids. */
608
+ runInScene(run) {
609
+ return this.write(run);
564
610
  }
565
611
  runAsSingleHistoryStep(run) {
566
- return runAsSingleSceneHistoryStep(useScene, run);
567
- }
568
- reconcileDependentStructure(before) {
569
- const currentNodes = this.getNodes();
570
- const authoredInputChanged = [
571
- ...new Set([...Object.keys(before), ...Object.keys(currentNodes)]),
572
- ].some((id) => {
573
- if (before[id] === currentNodes[id])
574
- return false;
575
- const type = (currentNodes[id] ?? before[id])?.type;
576
- return (type === 'stair' ||
577
- type === 'stair-segment' ||
578
- type === 'elevator' ||
579
- type === 'level' ||
580
- type === 'building' ||
581
- type === 'slab' ||
582
- type === 'ceiling');
583
- });
584
- if (authoredInputChanged) {
585
- const ownerIds = changedStairOpeningOwners(before, currentNodes);
586
- for (const node of [...Object.values(before), ...Object.values(currentNodes)]) {
587
- if (node.type === 'elevator' &&
588
- (before[node.id] !== currentNodes[node.id] ||
589
- Object.values(currentNodes).some((level) => level.type === 'level' && before[level.id] !== level)))
590
- ownerIds.add(node.id);
591
- }
592
- const owned = planOwnedFloorOpenings(currentNodes, { ownerIds });
593
- if (owned.length) {
594
- pauseSpaceDetection();
595
- try {
596
- useScene.getState().applyNodeChanges(structureChangeBatch(owned));
597
- }
598
- finally {
599
- resumeSpaceDetection();
600
- }
601
- }
602
- }
603
- const current = this.getNodes();
604
- const levels = new Set();
605
- for (const id of new Set([...Object.keys(before), ...Object.keys(current)])) {
606
- if (before[id] === current[id])
607
- continue;
608
- for (const nodes of [before, current]) {
609
- const level = stairDeckLevelId(nodes, nodes[id]);
610
- if (level)
611
- levels.add(level);
612
- const node = nodes[id];
613
- if (node?.type === 'level') {
614
- levels.add(node.id);
615
- const lower = adjacentLevelId(nodes, node.id, -1);
616
- if (lower)
617
- levels.add(lower);
618
- }
619
- if (node?.type === 'stair')
620
- for (const opening of Object.values(nodes))
621
- if (opening.type === 'floor-opening' && opening.ownerId === node.id)
622
- for (const target of floorOpeningTargets(nodes, opening))
623
- levels.add(target.levelId);
624
- if (node?.type === 'floor-opening')
625
- for (const target of floorOpeningTargets(nodes, node))
626
- levels.add(target.levelId);
627
- if (node?.type === 'door' || node?.type === 'window') {
628
- const wall = nodes[node.parentId];
629
- if (wall?.type === 'wall' && wall.parentId)
630
- levels.add(wall.parentId);
631
- }
632
- else if ((node?.type === 'zone' || node?.type === 'slab' || node?.type === 'wall') &&
633
- node.parentId) {
634
- levels.add(node.parentId);
635
- }
636
- }
637
- }
638
- if (levels.size)
639
- this.deriveStructure([...levels]);
612
+ return this.write(() => runAsSettledSceneStep(this.store, run));
640
613
  }
641
614
  // ---- internal helpers ----
642
615
  /**
@@ -645,7 +618,7 @@ export class SceneBridge {
645
618
  * missing on a node.
646
619
  */
647
620
  _findParentByChildrenScan(id) {
648
- const nodes = useScene.getState().nodes;
621
+ const nodes = this.getNodes();
649
622
  for (const candidate of Object.values(nodes)) {
650
623
  if (!('children' in candidate && Array.isArray(candidate.children)))
651
624
  continue;
@@ -671,7 +644,7 @@ export class SceneBridge {
671
644
  * the SiteNode quirk and the default-scene parentId-unset case both work.
672
645
  */
673
646
  _collectDescendants(id) {
674
- const nodes = useScene.getState().nodes;
647
+ const nodes = this.getNodes();
675
648
  if (!nodes[id])
676
649
  return [];
677
650
  const out = [];
@@ -692,3 +665,14 @@ export class SceneBridge {
692
665
  return out;
693
666
  }
694
667
  }
668
+ /** The scene as a patch leaves it, for the deletion planner. */
669
+ function patchedNodes(nodes, creates, updates, deletes) {
670
+ const next = { ...nodes };
671
+ for (const [id, data] of updates)
672
+ next[id] = { ...next[id], ...data };
673
+ for (const [id, { node }] of creates)
674
+ next[id] = node;
675
+ for (const id of deletes)
676
+ delete next[id];
677
+ return next;
678
+ }
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { SceneBridge } from './bridge/scene-bridge.js';
2
2
  export { createSceneOperations, type SceneOperations } from './operations/index.js';
3
3
  export { type CreatePascalMcpServerOptions, createPascalMcpServer } from './server.js';
4
- export type { GeometryScriptHost, ScriptedKind } from './tools/add-object.js';
4
+ export type { GeometryScriptHost, ObjectMakerHost, PieceState, ScriptedKind, } from './tools/add-object.js';
5
5
  export { type AssetCatalog, builtInCatalog } from './tools/asset-catalog.js';
6
6
  export type { SceneViewCapture, SceneViewHost } from './tools/view-scene.js';
7
7
  export { version } from './version.js';
@@ -1 +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;AACnF,YAAY,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAC1E,OAAO,EAAE,KAAK,YAAY,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AACzE,YAAY,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AACzE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA"}
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;AACnF,YAAY,EACV,kBAAkB,EAClB,eAAe,EACf,UAAU,EACV,YAAY,GACb,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,KAAK,YAAY,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AACzE,YAAY,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AACzE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA"}
@@ -3,6 +3,12 @@ import { type IncomingMessage } from 'node:http';
3
3
  import { type LookupFunction } from 'node:net';
4
4
  /** True for any address a server-side fetch must not reach. Unparseable input is blocked. */
5
5
  export declare function isBlockedAddress(address: string): boolean;
6
+ /**
7
+ * Scheme, host and a truncated path only: query strings can carry presigned
8
+ * tokens, userinfo carries credentials. Non-http(s) URLs (`data:`,
9
+ * `javascript:`) carry their payload in the path, so they log scheme and host.
10
+ */
11
+ export declare function loggableUrl(url: URL): string;
6
12
  export type Resolver = (hostname: string) => Promise<LookupAddress[]>;
7
13
  /** A socket `lookup` that only ever answers with the given, already-checked addresses. */
8
14
  export declare function pinnedLookup(addresses: LookupAddress[]): LookupFunction;
@@ -1 +1 @@
1
- {"version":3,"file":"safe-fetch.d.ts","sourceRoot":"","sources":["../../src/lib/safe-fetch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAE7C,OAAO,EAA0B,KAAK,eAAe,EAAE,MAAM,WAAW,CAAA;AAExE,OAAO,EAAQ,KAAK,cAAc,EAAE,MAAM,UAAU,CAAA;AA2GpD,6FAA6F;AAC7F,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAYzD;AAkDD,MAAM,MAAM,QAAQ,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,aAAa,EAAE,CAAC,CAAA;AA2BrE,0FAA0F;AAC1F,wBAAgB,YAAY,CAAC,SAAS,EAAE,aAAa,EAAE,GAAG,cAAc,CAcvE;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,MAAM,EAAE,cAAc,CAAA;IACtB,MAAM,EAAE,WAAW,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,SAAS,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,iBAAiB,KAAK,OAAO,CAAC,eAAe,CAAC,CAAA;AAqBvF,MAAM,MAAM,aAAa,GAAG;IAAE,OAAO,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,SAAS,CAAA;CAAE,CAAA;AAOrE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,uCAAuC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,EAAE,CAAA;CACf,CAAA;AAOD;;;;GAIG;AACH,wBAAsB,SAAS,CAC7B,MAAM,EAAE,MAAM,EACd,IAAI,GAAE,gBAAqB,EAC3B,IAAI,GAAE,aAA2B,GAChC,OAAO,CAAC,eAAe,CAAC,CAwE1B"}
1
+ {"version":3,"file":"safe-fetch.d.ts","sourceRoot":"","sources":["../../src/lib/safe-fetch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAE7C,OAAO,EAA0B,KAAK,eAAe,EAAgB,MAAM,WAAW,CAAA;AAEtF,OAAO,EAAQ,KAAK,cAAc,EAAE,MAAM,UAAU,CAAA;AAoIpD,6FAA6F;AAC7F,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAYzD;AAgBD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CAS5C;AAiDD,MAAM,MAAM,QAAQ,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,aAAa,EAAE,CAAC,CAAA;AAsDrE,0FAA0F;AAC1F,wBAAgB,YAAY,CAAC,SAAS,EAAE,aAAa,EAAE,GAAG,cAAc,CAcvE;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,MAAM,EAAE,cAAc,CAAA;IACtB,MAAM,EAAE,WAAW,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,SAAS,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,iBAAiB,KAAK,OAAO,CAAC,eAAe,CAAC,CAAA;AAqBvF,MAAM,MAAM,aAAa,GAAG;IAAE,OAAO,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,SAAS,CAAA;CAAE,CAAA;AAOrE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,uCAAuC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,EAAE,CAAA;CACf,CAAA;AAOD;;;;GAIG;AACH,wBAAsB,SAAS,CAC7B,MAAM,EAAE,MAAM,EACd,IAAI,GAAE,gBAAqB,EAC3B,IAAI,GAAE,aAA2B,GAChC,OAAO,CAAC,eAAe,CAAC,CAsF1B"}