@pascal-app/mcp 1.0.6 → 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.
- package/README.md +31 -10
- package/dist/bin/pascal-mcp.js +2 -0
- package/dist/bridge/agent-session.d.ts +2 -1
- package/dist/bridge/agent-session.d.ts.map +1 -1
- package/dist/bridge/agent-session.js +0 -1
- package/dist/bridge/scene-bridge.d.ts +21 -10
- package/dist/bridge/scene-bridge.d.ts.map +1 -1
- package/dist/bridge/scene-bridge.js +178 -194
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/safe-fetch.d.ts +6 -0
- package/dist/lib/safe-fetch.d.ts.map +1 -1
- package/dist/lib/safe-fetch.js +135 -41
- package/dist/operations/scene-operations.d.ts +7 -0
- package/dist/operations/scene-operations.d.ts.map +1 -1
- package/dist/operations/scene-operations.js +4 -0
- package/dist/prompts/from-brief.js +1 -1
- package/dist/prompts/iterate-on-feedback.js +1 -1
- package/dist/prompts/renovation-from-photos.js +1 -1
- package/dist/prompts/scene-guidance.js +2 -2
- package/dist/resources/agent-guide.d.ts.map +1 -1
- package/dist/resources/agent-guide.js +8 -3
- package/dist/resources/catalog-items.d.ts +4 -1
- package/dist/resources/catalog-items.d.ts.map +1 -1
- package/dist/resources/catalog-items.js +14 -6
- package/dist/server.d.ts +9 -5
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +21 -5
- package/dist/templates/empty-studio.d.ts.map +1 -1
- package/dist/templates/empty-studio.js +2 -3
- package/dist/templates/garden-house.d.ts.map +1 -1
- package/dist/templates/garden-house.js +2 -3
- package/dist/templates/two-bedroom.d.ts.map +1 -1
- package/dist/templates/two-bedroom.js +2 -3
- package/dist/tools/add-column.d.ts.map +1 -1
- package/dist/tools/add-column.js +9 -6
- package/dist/tools/add-object.d.ts +44 -5
- package/dist/tools/add-object.d.ts.map +1 -1
- package/dist/tools/add-object.js +75 -21
- package/dist/tools/asset-catalog.d.ts +5 -6
- package/dist/tools/asset-catalog.d.ts.map +1 -1
- package/dist/tools/asset-catalog.js +1 -9
- package/dist/tools/author-scene.d.ts +8 -5
- package/dist/tools/author-scene.d.ts.map +1 -1
- package/dist/tools/author-scene.js +302 -51
- package/dist/tools/clear-scene.js +2 -2
- package/dist/tools/collections.d.ts.map +1 -1
- package/dist/tools/collections.js +8 -5
- package/dist/tools/honest-patch-guard.js +6 -5
- package/dist/tools/hosted-services.d.ts.map +1 -1
- package/dist/tools/hosted-services.js +2 -2
- package/dist/tools/index.d.ts +7 -2
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +3 -9
- package/dist/tools/local-geometry-scripts.d.ts.map +1 -1
- package/dist/tools/local-geometry-scripts.js +7 -2
- package/dist/tools/patch-guards.d.ts.map +1 -1
- package/dist/tools/patch-guards.js +24 -14
- package/dist/tools/project-files.d.ts.map +1 -1
- package/dist/tools/project-files.js +2 -2
- package/dist/tools/room-tools.d.ts +1 -0
- package/dist/tools/room-tools.d.ts.map +1 -1
- package/dist/tools/room-tools.js +11 -8
- package/dist/tools/scene-scope.d.ts +4 -0
- package/dist/tools/scene-scope.d.ts.map +1 -0
- package/dist/tools/scene-scope.js +4 -0
- package/dist/tools/shared-tools.d.ts +0 -2
- package/dist/tools/shared-tools.d.ts.map +1 -1
- package/dist/tools/shared-tools.js +146 -31
- package/dist/tools/templates/create-house-from-brief.js +1 -1
- package/dist/tools/view-scene.js +2 -2
- package/examples/coordinate-conventions-demo.md +1 -0
- package/examples/photo-to-scene.md +1 -0
- package/examples/renovate-from-photos.md +2 -0
- package/package.json +10 -10
- package/dist/tools/create-unit.d.ts +0 -29
- package/dist/tools/create-unit.d.ts.map +0 -1
- package/dist/tools/create-unit.js +0 -36
- package/dist/tools/list-units.d.ts +0 -38
- package/dist/tools/list-units.d.ts.map +0 -1
- package/dist/tools/list-units.js +0 -61
- package/dist/tools/set-unit-members.d.ts +0 -22
- package/dist/tools/set-unit-members.d.ts.map +0 -1
- package/dist/tools/set-unit-members.js +0 -43
- package/dist/tools/unit-members.d.ts +0 -4
- package/dist/tools/unit-members.d.ts.map +0 -1
- 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 {
|
|
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
|
|
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
|
-
*
|
|
13
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
58
|
-
rootNodeIds:
|
|
59
|
-
collections:
|
|
60
|
-
materials
|
|
61
|
-
...(state.hasExplicitPluginInstallState ||
|
|
62
|
-
? { 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
|
-
|
|
124
|
-
return node ?? null;
|
|
140
|
+
return this.runtime.reader().get(id) ?? null;
|
|
125
141
|
}
|
|
126
|
-
/** All nodes (
|
|
142
|
+
/** All nodes (a read-only view of the store's current nodes — do NOT mutate). */
|
|
127
143
|
getNodes() {
|
|
128
|
-
return
|
|
144
|
+
return this.runtime.reader().nodes;
|
|
129
145
|
}
|
|
130
146
|
getCollections() {
|
|
131
|
-
return
|
|
147
|
+
return this.runtime.reader().collections;
|
|
132
148
|
}
|
|
133
149
|
getMaterials() {
|
|
134
|
-
return
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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(
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
this.
|
|
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,
|
|
269
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
|
288
|
-
const node =
|
|
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
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
389
|
+
simCreated.add(res.data.id);
|
|
356
390
|
}
|
|
357
391
|
else if (p.op === 'update') {
|
|
358
|
-
if (!simAvailable
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
-
|
|
451
|
-
|
|
452
|
-
this.reconcileDependentStructure(nodes);
|
|
484
|
+
declareActionDependencies(this.runtime, beforeNodes, requested);
|
|
485
|
+
this.runtime.reconcile(beforeNodes);
|
|
453
486
|
});
|
|
454
|
-
const
|
|
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(
|
|
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
|
-
|
|
520
|
+
return this.write(() => runAsSettledSceneStep(this.store, () => this.deriveStructureInScope(levelIds)));
|
|
521
|
+
}
|
|
522
|
+
deriveStructureInScope(levelIds) {
|
|
523
|
+
pauseSceneHistory(this.store);
|
|
488
524
|
try {
|
|
489
|
-
|
|
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(
|
|
539
|
+
resumeSceneHistory(this.store);
|
|
500
540
|
}
|
|
501
541
|
}
|
|
502
542
|
/** Undo. Returns the number of steps actually undone. */
|
|
503
543
|
undo(steps = 1) {
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/lib/safe-fetch.d.ts
CHANGED
|
@@ -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,
|
|
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"}
|