@pascal-app/mcp 1.0.1 → 1.0.4

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 (196) hide show
  1. package/README.md +47 -26
  2. package/dist/bin/pascal-mcp.js +8 -2
  3. package/dist/bridge/scene-bridge.d.ts +41 -6
  4. package/dist/bridge/scene-bridge.d.ts.map +1 -1
  5. package/dist/bridge/scene-bridge.js +230 -45
  6. package/dist/index.d.ts +3 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1 -0
  9. package/dist/operations/scene-operations.d.ts +23 -3
  10. package/dist/operations/scene-operations.d.ts.map +1 -1
  11. package/dist/operations/scene-operations.js +31 -1
  12. package/dist/prompts/from-brief.js +1 -1
  13. package/dist/prompts/iterate-on-feedback.js +1 -1
  14. package/dist/prompts/scene-guidance.js +3 -3
  15. package/dist/resources/agent-guide.d.ts.map +1 -1
  16. package/dist/resources/agent-guide.js +22 -1
  17. package/dist/resources/catalog-items.d.ts +4 -5
  18. package/dist/resources/catalog-items.d.ts.map +1 -1
  19. package/dist/resources/catalog-items.js +7 -9
  20. package/dist/resources/design-schema.d.ts +9 -0
  21. package/dist/resources/design-schema.d.ts.map +1 -0
  22. package/dist/resources/design-schema.js +21 -0
  23. package/dist/resources/index.d.ts +3 -1
  24. package/dist/resources/index.d.ts.map +1 -1
  25. package/dist/resources/index.js +5 -2
  26. package/dist/server.d.ts +19 -0
  27. package/dist/server.d.ts.map +1 -1
  28. package/dist/server.js +14 -6
  29. package/dist/storage/disk-artifact-store.d.ts +9 -0
  30. package/dist/storage/disk-artifact-store.d.ts.map +1 -0
  31. package/dist/storage/disk-artifact-store.js +46 -0
  32. package/dist/storage/index.d.ts +1 -0
  33. package/dist/storage/index.d.ts.map +1 -1
  34. package/dist/storage/index.js +1 -0
  35. package/dist/storage/sqlite-scene-store.d.ts +2 -2
  36. package/dist/storage/sqlite-scene-store.d.ts.map +1 -1
  37. package/dist/storage/sqlite-scene-store.js +4 -3
  38. package/dist/storage/types.d.ts +32 -3
  39. package/dist/storage/types.d.ts.map +1 -1
  40. package/dist/storage/types.js +12 -0
  41. package/dist/tools/add-column.d.ts +5 -0
  42. package/dist/tools/add-column.d.ts.map +1 -0
  43. package/dist/tools/add-column.js +58 -0
  44. package/dist/tools/add-object.d.ts +60 -0
  45. package/dist/tools/add-object.d.ts.map +1 -0
  46. package/dist/tools/add-object.js +156 -0
  47. package/dist/tools/apply-patch.d.ts +3 -0
  48. package/dist/tools/apply-patch.d.ts.map +1 -1
  49. package/dist/tools/apply-patch.js +28 -2
  50. package/dist/tools/asset-catalog.d.ts +9 -5
  51. package/dist/tools/asset-catalog.d.ts.map +1 -1
  52. package/dist/tools/asset-catalog.js +10 -12
  53. package/dist/tools/check-collisions.d.ts +1 -1
  54. package/dist/tools/check-collisions.d.ts.map +1 -1
  55. package/dist/tools/check-collisions.js +1 -2
  56. package/dist/tools/clear-scene.d.ts +9 -0
  57. package/dist/tools/clear-scene.d.ts.map +1 -0
  58. package/dist/tools/clear-scene.js +59 -0
  59. package/dist/tools/collections.d.ts +4 -0
  60. package/dist/tools/collections.d.ts.map +1 -0
  61. package/dist/tools/collections.js +52 -0
  62. package/dist/tools/construction-tools.d.ts +8 -41
  63. package/dist/tools/construction-tools.d.ts.map +1 -1
  64. package/dist/tools/construction-tools.js +120 -230
  65. package/dist/tools/create-unit.d.ts +2 -0
  66. package/dist/tools/create-unit.d.ts.map +1 -1
  67. package/dist/tools/describe-node.d.ts.map +1 -1
  68. package/dist/tools/describe-node.js +8 -6
  69. package/dist/tools/errors.d.ts +11 -0
  70. package/dist/tools/errors.d.ts.map +1 -1
  71. package/dist/tools/errors.js +10 -0
  72. package/dist/tools/find-nodes.d.ts +3 -19
  73. package/dist/tools/find-nodes.d.ts.map +1 -1
  74. package/dist/tools/find-nodes.js +150 -53
  75. package/dist/tools/geometry-script-worker.d.ts +2 -0
  76. package/dist/tools/geometry-script-worker.d.ts.map +1 -0
  77. package/dist/tools/geometry-script-worker.js +21 -0
  78. package/dist/tools/get-scene.d.ts +1 -0
  79. package/dist/tools/get-scene.d.ts.map +1 -1
  80. package/dist/tools/get-scene.js +3 -1
  81. package/dist/tools/honest-patch-guard.d.ts +2 -0
  82. package/dist/tools/honest-patch-guard.d.ts.map +1 -0
  83. package/dist/tools/honest-patch-guard.js +32 -0
  84. package/dist/tools/hosted-services.d.ts +4 -0
  85. package/dist/tools/hosted-services.d.ts.map +1 -0
  86. package/dist/tools/hosted-services.js +30 -0
  87. package/dist/tools/index.d.ts +10 -1
  88. package/dist/tools/index.d.ts.map +1 -1
  89. package/dist/tools/index.js +17 -26
  90. package/dist/tools/live-sync.d.ts +24 -5
  91. package/dist/tools/live-sync.d.ts.map +1 -1
  92. package/dist/tools/live-sync.js +47 -26
  93. package/dist/tools/local-geometry-scripts.d.ts +8 -0
  94. package/dist/tools/local-geometry-scripts.d.ts.map +1 -0
  95. package/dist/tools/local-geometry-scripts.js +67 -0
  96. package/dist/tools/measure.d.ts +6 -0
  97. package/dist/tools/measure.d.ts.map +1 -1
  98. package/dist/tools/measure.js +33 -65
  99. package/dist/tools/measurement.d.ts +1 -36
  100. package/dist/tools/measurement.d.ts.map +1 -1
  101. package/dist/tools/measurement.js +2 -94
  102. package/dist/tools/node-world-point.d.ts +37 -0
  103. package/dist/tools/node-world-point.d.ts.map +1 -0
  104. package/dist/tools/node-world-point.js +457 -0
  105. package/dist/tools/normalize-schema-dialect.d.ts.map +1 -1
  106. package/dist/tools/normalize-schema-dialect.js +20 -0
  107. package/dist/tools/patch-guards.d.ts +58 -0
  108. package/dist/tools/patch-guards.d.ts.map +1 -0
  109. package/dist/tools/patch-guards.js +253 -0
  110. package/dist/tools/photo-to-scene/photo-to-scene.d.ts.map +1 -1
  111. package/dist/tools/photo-to-scene/photo-to-scene.js +3 -2
  112. package/dist/tools/place-design.d.ts +34 -0
  113. package/dist/tools/place-design.d.ts.map +1 -0
  114. package/dist/tools/place-design.js +91 -0
  115. package/dist/tools/redo.d.ts +2 -0
  116. package/dist/tools/redo.d.ts.map +1 -1
  117. package/dist/tools/redo.js +2 -2
  118. package/dist/tools/room-tools.d.ts +50 -113
  119. package/dist/tools/room-tools.d.ts.map +1 -1
  120. package/dist/tools/room-tools.js +129 -565
  121. package/dist/tools/scene-lifecycle/create-project.d.ts.map +1 -1
  122. package/dist/tools/scene-lifecycle/create-project.js +26 -1
  123. package/dist/tools/scene-lifecycle/delete-scene.d.ts +2 -0
  124. package/dist/tools/scene-lifecycle/delete-scene.d.ts.map +1 -1
  125. package/dist/tools/scene-lifecycle/delete-scene.js +4 -3
  126. package/dist/tools/scene-lifecycle/save-scene.d.ts +1 -0
  127. package/dist/tools/scene-lifecycle/save-scene.d.ts.map +1 -1
  128. package/dist/tools/scene-lifecycle/save-scene.js +46 -6
  129. package/dist/tools/scene-lifecycle/test-utils.d.ts +2 -2
  130. package/dist/tools/scene-lifecycle/test-utils.d.ts.map +1 -1
  131. package/dist/tools/scene-lifecycle/test-utils.js +2 -1
  132. package/dist/tools/set-unit-members.d.ts +2 -0
  133. package/dist/tools/set-unit-members.d.ts.map +1 -1
  134. package/dist/tools/set-zone.d.ts +2 -0
  135. package/dist/tools/set-zone.d.ts.map +1 -1
  136. package/dist/tools/set-zone.js +2 -2
  137. package/dist/tools/shared-tools.d.ts +30 -0
  138. package/dist/tools/shared-tools.d.ts.map +1 -0
  139. package/dist/tools/shared-tools.js +361 -0
  140. package/dist/tools/structure-tools.d.ts +57 -0
  141. package/dist/tools/structure-tools.d.ts.map +1 -0
  142. package/dist/tools/structure-tools.js +57 -0
  143. package/dist/tools/undo.d.ts +2 -0
  144. package/dist/tools/undo.d.ts.map +1 -1
  145. package/dist/tools/undo.js +2 -2
  146. package/dist/tools/validate-design.d.ts +136 -0
  147. package/dist/tools/validate-design.d.ts.map +1 -0
  148. package/dist/tools/validate-design.js +28 -0
  149. package/dist/tools/validate-scene.d.ts +5 -0
  150. package/dist/tools/validate-scene.d.ts.map +1 -1
  151. package/dist/tools/validate-scene.js +8 -6
  152. package/dist/tools/variants/mutations.js +1 -1
  153. package/dist/tools/view-scene.d.ts +38 -0
  154. package/dist/tools/view-scene.d.ts.map +1 -0
  155. package/dist/tools/view-scene.js +63 -0
  156. package/dist/version.d.ts +5 -0
  157. package/dist/version.d.ts.map +1 -1
  158. package/dist/version.js +17 -3
  159. package/examples/embed-in-agent.ts +1 -1
  160. package/examples/generate-apartment.md +9 -11
  161. package/examples/photo-to-scene.md +1 -1
  162. package/examples/renovate-from-photos.md +3 -4
  163. package/package.json +12 -6
  164. package/dist/tools/create-level.d.ts +0 -21
  165. package/dist/tools/create-level.d.ts.map +0 -1
  166. package/dist/tools/create-level.js +0 -63
  167. package/dist/tools/create-wall.d.ts +0 -22
  168. package/dist/tools/create-wall.d.ts.map +0 -1
  169. package/dist/tools/create-wall.js +0 -57
  170. package/dist/tools/cut-opening.d.ts +0 -25
  171. package/dist/tools/cut-opening.d.ts.map +0 -1
  172. package/dist/tools/cut-opening.js +0 -61
  173. package/dist/tools/delete-node.d.ts +0 -19
  174. package/dist/tools/delete-node.d.ts.map +0 -1
  175. package/dist/tools/delete-node.js +0 -40
  176. package/dist/tools/door-clearance.d.ts +0 -135
  177. package/dist/tools/door-clearance.d.ts.map +0 -1
  178. package/dist/tools/door-clearance.js +0 -334
  179. package/dist/tools/duplicate-level.d.ts +0 -19
  180. package/dist/tools/duplicate-level.d.ts.map +0 -1
  181. package/dist/tools/duplicate-level.js +0 -60
  182. package/dist/tools/geometry.d.ts +0 -24
  183. package/dist/tools/geometry.d.ts.map +0 -1
  184. package/dist/tools/geometry.js +0 -94
  185. package/dist/tools/get-node.d.ts +0 -11
  186. package/dist/tools/get-node.d.ts.map +0 -1
  187. package/dist/tools/get-node.js +0 -29
  188. package/dist/tools/layout-clearance.d.ts +0 -105
  189. package/dist/tools/layout-clearance.d.ts.map +0 -1
  190. package/dist/tools/layout-clearance.js +0 -190
  191. package/dist/tools/place-item.d.ts +0 -22
  192. package/dist/tools/place-item.d.ts.map +0 -1
  193. package/dist/tools/place-item.js +0 -89
  194. package/dist/tools/scene-query.d.ts +0 -62
  195. package/dist/tools/scene-query.d.ts.map +0 -1
  196. package/dist/tools/scene-query.js +0 -670
@@ -1,670 +0,0 @@
1
- import { DEFAULT_LEVEL_HEIGHT, getStoredLevelHeight, getWallPlaneTop, resolveStairTotalRise, resolveWallEffectiveHeight, } from '@pascal-app/core';
2
- import { computeWallSlabSupport } from '@pascal-app/core/spatial-grid';
3
- import { z } from 'zod';
4
- import { READ_ONLY_TOOL_ANNOTATIONS } from './annotations.js';
5
- import { distance2D, pointInPolygon, polygonArea, polygonContainsPolygon, wallLength, } from './geometry.js';
6
- import { layoutIssuesFromScene } from './layout-clearance.js';
7
- import { NodeIdSchema } from './schemas.js';
8
- export const levelScopedInput = {
9
- levelId: NodeIdSchema.optional(),
10
- };
11
- const jsonObject = z.record(z.string(), z.unknown());
12
- export const listLevelsOutput = {
13
- activeSceneId: z.string().nullable(),
14
- levelCount: z.number(),
15
- occupiedStoryCount: z.number(),
16
- supportLevelCount: z.number(),
17
- roofLevelIds: z.array(z.string()),
18
- levels: z.array(jsonObject),
19
- };
20
- export const getLevelSummaryOutput = {
21
- levelId: z.string(),
22
- levelName: z.string().optional(),
23
- floorIndex: z.number(),
24
- role: z.string(),
25
- metadataRole: z.string().nullable(),
26
- isOccupiedStory: z.boolean(),
27
- isSupportLevel: z.boolean(),
28
- referenceLevelId: z.string().nullable(),
29
- counts: jsonObject,
30
- walls: z.array(jsonObject),
31
- zones: z.array(jsonObject),
32
- items: z.array(jsonObject),
33
- slabs: z.array(jsonObject),
34
- ceilings: z.array(jsonObject),
35
- };
36
- export const getWallsOutput = {
37
- levelId: z.string(),
38
- walls: z.array(jsonObject),
39
- };
40
- export const getZonesOutput = {
41
- levelId: z.string(),
42
- zones: z.array(jsonObject),
43
- };
44
- export const verifySceneOutput = {
45
- ok: z.boolean(),
46
- valid: z.boolean(),
47
- levelCount: z.number(),
48
- occupiedStoryCount: z.number(),
49
- supportLevelCount: z.number(),
50
- roofLevelIds: z.array(z.string()),
51
- activeSceneId: z.string().nullable(),
52
- levels: z.array(jsonObject),
53
- emptyLevelIds: z.array(z.string()),
54
- issues: z.array(z.string()),
55
- hasIssues: z.boolean(),
56
- };
57
- function textResult(payload) {
58
- return {
59
- content: [{ type: 'text', text: JSON.stringify(payload) }],
60
- structuredContent: payload,
61
- };
62
- }
63
- function getLevels(bridge) {
64
- return bridge.findNodes({ type: 'level' }).sort((a, b) => {
65
- const aa = a.type === 'level' ? a.level : 0;
66
- const bb = b.type === 'level' ? b.level : 0;
67
- return aa - bb;
68
- });
69
- }
70
- function getDefaultLevelId(bridge, requested) {
71
- if (requested)
72
- return requested;
73
- const level = getLevels(bridge)[0];
74
- return level?.id ?? null;
75
- }
76
- function nodesOnLevel(bridge, levelId) {
77
- return Object.values(bridge.getNodes()).filter((node) => node.id !== levelId && bridge.resolveLevelId(node.id) === levelId);
78
- }
79
- export function resolveReportedWallHeight(bridge, wall) {
80
- const levelId = bridge.resolveLevelId(wall.id);
81
- // Covering-clamped plane for plane-bound walls; explicit heights pass
82
- // through resolveWallEffectiveHeight untouched.
83
- const planeTop = levelId
84
- ? getWallPlaneTop(wall, levelId, bridge.getNodes())
85
- : DEFAULT_LEVEL_HEIGHT;
86
- const levelNodes = levelId ? nodesOnLevel(bridge, levelId) : [];
87
- const slabs = levelNodes.filter((node) => node.type === 'slab');
88
- const walls = levelNodes.filter((node) => node.type === 'wall');
89
- const support = computeWallSlabSupport(wall, slabs, walls, wall.supportSlabId);
90
- return resolveWallEffectiveHeight(wall, planeTop, support.elevation);
91
- }
92
- function metadataRecord(node) {
93
- return typeof node.metadata === 'object' && node.metadata !== null
94
- ? node.metadata
95
- : null;
96
- }
97
- function metadataString(node, key) {
98
- const value = metadataRecord(node)?.[key];
99
- return typeof value === 'string' ? value : undefined;
100
- }
101
- function occupiedContentCount(counts) {
102
- return (counts.walls +
103
- counts.zones +
104
- counts.doors +
105
- counts.windows +
106
- counts.items +
107
- counts.slabs +
108
- counts.ceilings +
109
- counts.stairs);
110
- }
111
- function classifyLevel(level, counts) {
112
- const metadataRole = metadataString(level, 'role');
113
- if (metadataRole === 'roof')
114
- return 'roof';
115
- if (metadataRole === 'support')
116
- return 'support';
117
- if (counts.roofs > 0 && occupiedContentCount(counts) === 0)
118
- return 'roof';
119
- return 'occupied';
120
- }
121
- function openingSummaries(bridge, wallId) {
122
- return bridge
123
- .getChildren(wallId)
124
- .filter((child) => child.type === 'door' || child.type === 'window')
125
- .map((child) => ({
126
- id: child.id,
127
- type: child.type,
128
- position: child.position,
129
- width: child.width,
130
- height: child.height,
131
- }));
132
- }
133
- function wallSummary(bridge, wall) {
134
- if (wall.type !== 'wall')
135
- return null;
136
- const length = distance2D(wall.start, wall.end);
137
- const resolvedHeight = resolveReportedWallHeight(bridge, wall);
138
- return {
139
- id: wall.id,
140
- name: wall.name,
141
- start: wall.start,
142
- end: wall.end,
143
- length: Math.round(length * 100) / 100,
144
- height: wall.height,
145
- resolvedHeight,
146
- heightIsExplicit: wall.height !== undefined,
147
- thickness: wall.thickness,
148
- openings: openingSummaries(bridge, wall.id),
149
- };
150
- }
151
- function zoneSummary(zone) {
152
- if (zone.type !== 'zone')
153
- return null;
154
- const xs = zone.polygon.map((p) => p[0]);
155
- const zs = zone.polygon.map((p) => p[1]);
156
- return {
157
- id: zone.id,
158
- name: zone.name,
159
- color: zone.color,
160
- polygon: zone.polygon,
161
- areaSqMeters: Math.round(polygonArea(zone.polygon) * 100) / 100,
162
- bounds: {
163
- width: Math.round((Math.max(...xs) - Math.min(...xs)) * 100) / 100,
164
- depth: Math.round((Math.max(...zs) - Math.min(...zs)) * 100) / 100,
165
- },
166
- };
167
- }
168
- function itemSummary(item) {
169
- if (item.type !== 'item')
170
- return null;
171
- return {
172
- id: item.id,
173
- name: item.name ?? item.asset.name,
174
- parentId: item.parentId,
175
- position: item.position,
176
- rotation: item.rotation,
177
- asset: {
178
- id: item.asset.id,
179
- name: item.asset.name,
180
- category: item.asset.category,
181
- dimensions: item.asset.dimensions,
182
- attachTo: item.asset.attachTo ?? null,
183
- },
184
- };
185
- }
186
- function rotateXZ(x, z, angle) {
187
- const cos = Math.cos(angle);
188
- const sin = Math.sin(angle);
189
- return [x * cos + z * sin, -x * sin + z * cos];
190
- }
191
- function toWorldPlanPoint(stair, localX, localZ) {
192
- const [worldX, worldZ] = rotateXZ(localX, localZ, stair.rotation ?? 0);
193
- return [stair.position[0] + worldX, stair.position[2] + worldZ];
194
- }
195
- function computeSegmentTransforms(segments) {
196
- const transforms = [];
197
- let currentX = 0;
198
- let currentY = 0;
199
- let currentZ = 0;
200
- let currentRot = 0;
201
- for (let index = 0; index < segments.length; index++) {
202
- const segment = segments[index];
203
- if (!segment)
204
- continue;
205
- if (index === 0) {
206
- transforms.push({ position: [currentX, currentY, currentZ], rotation: currentRot });
207
- continue;
208
- }
209
- const previous = segments[index - 1];
210
- if (!previous)
211
- continue;
212
- let attachX = 0;
213
- let attachZ = 0;
214
- let rotationDelta = 0;
215
- switch (segment.attachmentSide) {
216
- case 'front':
217
- attachZ = previous.length;
218
- break;
219
- case 'left':
220
- attachX = previous.width / 2;
221
- attachZ = previous.length / 2;
222
- rotationDelta = Math.PI / 2;
223
- break;
224
- case 'right':
225
- attachX = -previous.width / 2;
226
- attachZ = previous.length / 2;
227
- rotationDelta = -Math.PI / 2;
228
- break;
229
- }
230
- const [deltaX, deltaZ] = rotateXZ(attachX, attachZ, currentRot);
231
- currentX += deltaX;
232
- currentY += previous.height;
233
- currentZ += deltaZ;
234
- currentRot += rotationDelta;
235
- transforms.push({ position: [currentX, currentY, currentZ], rotation: currentRot });
236
- }
237
- return transforms;
238
- }
239
- function stairFootprintPolygons(bridge, stair) {
240
- if (stair.stairType === 'curved' || stair.stairType === 'spiral') {
241
- const radius = Math.max(0.05, stair.innerRadius ?? 0.9) + Math.max(stair.width ?? 1, 0.4);
242
- return [
243
- Array.from({ length: 24 }).map((_, index) => {
244
- const angle = (index / 24) * Math.PI * 2;
245
- return toWorldPlanPoint(stair, Math.cos(angle) * radius, Math.sin(angle) * radius);
246
- }),
247
- ];
248
- }
249
- const nodes = bridge.getNodes();
250
- const segments = (stair.children ?? [])
251
- .map((childId) => nodes[childId])
252
- .filter((node) => node?.type === 'stair-segment');
253
- const usableSegments = segments.length > 0
254
- ? segments
255
- : [
256
- {
257
- width: stair.width ?? 1,
258
- length: 3,
259
- height: resolveStairTotalRise(stair, nodes),
260
- stepCount: stair.stepCount ?? 10,
261
- attachmentSide: 'front',
262
- },
263
- ];
264
- const transforms = computeSegmentTransforms(usableSegments);
265
- return usableSegments.map((segment, index) => {
266
- const transform = transforms[index] ?? {
267
- position: [0, 0, 0],
268
- rotation: 0,
269
- };
270
- const halfWidth = segment.width / 2;
271
- const corners = [
272
- [-halfWidth, 0],
273
- [halfWidth, 0],
274
- [halfWidth, segment.length],
275
- [-halfWidth, segment.length],
276
- ];
277
- return corners.map(([localX, localZ]) => {
278
- const [rx, rz] = rotateXZ(localX, localZ, transform.rotation);
279
- return toWorldPlanPoint(stair, transform.position[0] + rx, transform.position[2] + rz);
280
- });
281
- });
282
- }
283
- function wallSamplePoints(wall) {
284
- return [0.25, 0.5, 0.75].map((t) => [
285
- wall.start[0] + (wall.end[0] - wall.start[0]) * t,
286
- wall.start[1] + (wall.end[1] - wall.start[1]) * t,
287
- ]);
288
- }
289
- function getLevelNumber(levelId, nodes) {
290
- if (!levelId)
291
- return undefined;
292
- const node = nodes[levelId];
293
- return node?.type === 'level' ? node.level : undefined;
294
- }
295
- function targetLevelIdsForStair(bridge, stair) {
296
- const nodes = bridge.getNodes();
297
- const parentLevelId = bridge.resolveLevelId(stair.id);
298
- const fromLevelId = (stair.fromLevelId ?? parentLevelId);
299
- const toLevelId = (stair.toLevelId ?? fromLevelId);
300
- const fromLevel = getLevelNumber(fromLevelId, nodes);
301
- const toLevel = getLevelNumber(toLevelId, nodes);
302
- if (fromLevel === undefined || toLevel === undefined) {
303
- return toLevelId ? [toLevelId] : [];
304
- }
305
- const minLevel = Math.min(fromLevel, toLevel);
306
- const maxLevel = Math.max(fromLevel, toLevel);
307
- return getLevels(bridge)
308
- .filter((level) => level.type === 'level' && level.level > minLevel && level.level <= maxLevel)
309
- .map((level) => level.id);
310
- }
311
- function holeBelongsToStair(surface, holeIndex, stairId) {
312
- const metadata = surface.holeMetadata?.[holeIndex];
313
- return metadata?.source === 'stair' && metadata.stairId === stairId;
314
- }
315
- function parentListsChild(parent, childId) {
316
- if (!('children' in parent && Array.isArray(parent.children)))
317
- return false;
318
- return parent.children.some((child) => {
319
- if (typeof child === 'string')
320
- return child === childId;
321
- return (child !== null &&
322
- typeof child === 'object' &&
323
- 'id' in child &&
324
- child.id === childId);
325
- });
326
- }
327
- function levelSummary(bridge, levelId) {
328
- const level = bridge.getNode(levelId);
329
- if (level?.type !== 'level') {
330
- throw new Error(`Level not found: ${levelId}`);
331
- }
332
- const nodes = nodesOnLevel(bridge, levelId);
333
- const walls = nodes
334
- .map((n) => wallSummary(bridge, n))
335
- .filter((n) => !!n);
336
- const zones = nodes.map(zoneSummary).filter((n) => !!n);
337
- const items = nodes.map(itemSummary).filter((n) => !!n);
338
- const slabs = nodes
339
- .filter((node) => node.type === 'slab')
340
- .map((node) => ({
341
- id: node.id,
342
- polygon: node.polygon,
343
- holes: node.holes ?? [],
344
- holeMetadata: node.holeMetadata ?? [],
345
- elevation: node.elevation,
346
- }));
347
- const ceilings = nodes
348
- .filter((node) => node.type === 'ceiling')
349
- .map((node) => ({
350
- id: node.id,
351
- polygon: node.polygon,
352
- holes: node.holes ?? [],
353
- holeMetadata: node.holeMetadata ?? [],
354
- height: node.height,
355
- }));
356
- const doors = nodes.filter((node) => node.type === 'door');
357
- const windows = nodes.filter((node) => node.type === 'window');
358
- const roofs = nodes.filter((node) => node.type === 'roof');
359
- const stairs = nodes.filter((node) => node.type === 'stair');
360
- const counts = {
361
- walls: walls.length,
362
- zones: zones.length,
363
- doors: doors.length,
364
- windows: windows.length,
365
- items: items.length,
366
- slabs: slabs.length,
367
- ceilings: ceilings.length,
368
- roofs: roofs.length,
369
- stairs: stairs.length,
370
- };
371
- const role = classifyLevel(level, counts);
372
- const metadataRole = metadataString(level, 'role') ?? null;
373
- const referenceLevelId = metadataString(level, 'referenceLevelId') ?? null;
374
- return {
375
- levelId,
376
- levelName: level.name,
377
- floorIndex: level.level,
378
- role,
379
- metadataRole,
380
- isOccupiedStory: role === 'occupied',
381
- isSupportLevel: role !== 'occupied',
382
- referenceLevelId,
383
- counts,
384
- walls,
385
- zones,
386
- items,
387
- slabs,
388
- ceilings,
389
- };
390
- }
391
- export function registerListLevels(server, bridge) {
392
- server.registerTool('list_levels', {
393
- title: 'List levels',
394
- description: 'List all levels in the current scene with ids, names, floor indices, and child counts.',
395
- inputSchema: {},
396
- outputSchema: listLevelsOutput,
397
- annotations: READ_ONLY_TOOL_ANNOTATIONS,
398
- }, async () => {
399
- const activeScene = bridge.getActiveScene();
400
- const levels = getLevels(bridge).map((level) => {
401
- const summary = levelSummary(bridge, level.id);
402
- return {
403
- id: level.id,
404
- name: level.name,
405
- floorIndex: level.type === 'level' ? level.level : 0,
406
- parentId: level.parentId,
407
- role: summary.role,
408
- metadataRole: summary.metadataRole,
409
- isOccupiedStory: summary.isOccupiedStory,
410
- isSupportLevel: summary.isSupportLevel,
411
- referenceLevelId: summary.referenceLevelId,
412
- childCount: bridge.getChildren(level.id).length,
413
- };
414
- });
415
- const occupiedStoryCount = levels.filter((level) => level.isOccupiedStory).length;
416
- const roofLevelIds = levels
417
- .filter((level) => level.role === 'roof')
418
- .map((level) => level.id);
419
- return textResult({
420
- activeSceneId: activeScene?.id ?? null,
421
- levelCount: levels.length,
422
- occupiedStoryCount,
423
- supportLevelCount: levels.length - occupiedStoryCount,
424
- roofLevelIds,
425
- levels,
426
- });
427
- });
428
- }
429
- export function registerGetLevelSummary(server, bridge) {
430
- server.registerTool('get_level_summary', {
431
- title: 'Get level summary',
432
- description: 'Get a compact model-friendly summary of one level: counts plus walls, zones, slabs, ceilings, and items. Omit levelId to use the first level.',
433
- inputSchema: levelScopedInput,
434
- outputSchema: getLevelSummaryOutput,
435
- annotations: READ_ONLY_TOOL_ANNOTATIONS,
436
- }, async ({ levelId }) => {
437
- const resolved = getDefaultLevelId(bridge, levelId);
438
- if (!resolved)
439
- throw new Error('No level exists in the scene');
440
- return textResult(levelSummary(bridge, resolved));
441
- });
442
- }
443
- export function registerGetWalls(server, bridge) {
444
- server.registerTool('get_walls', {
445
- title: 'Get walls',
446
- description: 'Get walls on a level with start/end coordinates, length, height, thickness, and child doors/windows. Omit levelId to use the first level.',
447
- inputSchema: levelScopedInput,
448
- outputSchema: getWallsOutput,
449
- annotations: READ_ONLY_TOOL_ANNOTATIONS,
450
- }, async ({ levelId }) => {
451
- const resolved = getDefaultLevelId(bridge, levelId);
452
- if (!resolved)
453
- throw new Error('No level exists in the scene');
454
- return textResult({
455
- levelId: resolved,
456
- walls: levelSummary(bridge, resolved).walls,
457
- });
458
- });
459
- }
460
- export function registerGetZones(server, bridge) {
461
- server.registerTool('get_zones', {
462
- title: 'Get zones',
463
- description: 'Get room/zone polygons on a level with names, colors, bounds, and approximate areas. Omit levelId to use the first level.',
464
- inputSchema: levelScopedInput,
465
- outputSchema: getZonesOutput,
466
- annotations: READ_ONLY_TOOL_ANNOTATIONS,
467
- }, async ({ levelId }) => {
468
- const resolved = getDefaultLevelId(bridge, levelId);
469
- if (!resolved)
470
- throw new Error('No level exists in the scene');
471
- return textResult({
472
- levelId: resolved,
473
- zones: levelSummary(bridge, resolved).zones,
474
- });
475
- });
476
- }
477
- export function registerVerifyScene(server, bridge) {
478
- server.registerTool('verify_scene', {
479
- title: 'Verify scene',
480
- description: 'High-level self-check after complex edits. Returns validation status, per-level room/content counts, empty levels, and practical layout issues.',
481
- inputSchema: {},
482
- outputSchema: verifySceneOutput,
483
- annotations: READ_ONLY_TOOL_ANNOTATIONS,
484
- }, async () => {
485
- const validation = bridge.validateScene();
486
- const levels = getLevels(bridge).map((level) => {
487
- const summary = levelSummary(bridge, level.id);
488
- const totalContent = Object.values(summary.counts).reduce((sum, count) => sum + (typeof count === 'number' ? count : 0), 0);
489
- return {
490
- levelId: level.id,
491
- levelName: level.name ?? `Level ${summary.floorIndex}`,
492
- floorIndex: summary.floorIndex,
493
- role: summary.role,
494
- metadataRole: summary.metadataRole,
495
- isOccupiedStory: summary.isOccupiedStory,
496
- isSupportLevel: summary.isSupportLevel,
497
- referenceLevelId: summary.referenceLevelId,
498
- isEmpty: totalContent === 0,
499
- content: summary.counts,
500
- };
501
- });
502
- const issues = [];
503
- const occupiedStoryCount = levels.filter((level) => level.isOccupiedStory).length;
504
- const roofLevelIds = levels
505
- .filter((level) => level.role === 'roof')
506
- .map((level) => level.levelId);
507
- const emptyLevelIds = levels.filter((level) => level.isEmpty).map((level) => level.levelId);
508
- if (emptyLevelIds.length > 0) {
509
- issues.push(`Empty level(s): ${emptyLevelIds.join(', ')}`);
510
- }
511
- for (const level of levels) {
512
- const occupiedContent = occupiedContentCount(level.content);
513
- if (level.role === 'roof') {
514
- if (level.content.roofs === 0) {
515
- issues.push(`${level.levelName} is a roof support level but has no roof geometry; add or move roof geometry there rather than deleting the support level to satisfy story count`);
516
- }
517
- if (occupiedContent > 0) {
518
- issues.push(`${level.levelName} is a roof support level but contains occupied-story content; move rooms, walls, stairs, slabs, ceilings, and items to an occupied story and keep the roof level for roof geometry only`);
519
- }
520
- if (level.referenceLevelId) {
521
- const referenceLevel = bridge.getNode(level.referenceLevelId);
522
- if (referenceLevel?.type === 'level' && level.floorIndex <= referenceLevel.level) {
523
- issues.push(`${level.levelName} roof support level should be above its reference occupied level ${referenceLevel.name ?? referenceLevel.id}`);
524
- }
525
- }
526
- continue;
527
- }
528
- if (level.content.walls > 0 && level.content.zones === 0) {
529
- issues.push(`${level.levelName} has walls but no zones/rooms`);
530
- }
531
- if (level.content.zones > 0 && level.content.slabs === 0) {
532
- issues.push(`${level.levelName} has zones but no slabs/floors`);
533
- }
534
- if (level.content.zones > 0 && level.content.ceilings === 0) {
535
- issues.push(`${level.levelName} has zones but no ceilings`);
536
- }
537
- if (level.content.walls > 0 && level.content.doors === 0) {
538
- issues.push(`${level.levelName} has walls but no doors`);
539
- }
540
- if (level.content.roofs > 0 &&
541
- (level.content.walls > 0 || level.content.zones > 0 || level.content.stairs > 0)) {
542
- issues.push(`${level.levelName} mixes roof geometry with occupied-level content; place roofs on a dedicated roof level for solo/exploded level views`);
543
- }
544
- }
545
- const hasMultipleOccupiedStories = occupiedStoryCount > 1;
546
- if (hasMultipleOccupiedStories) {
547
- for (const level of getLevels(bridge)) {
548
- if (level.type !== 'level')
549
- continue;
550
- const summary = levels.find((entry) => entry.levelId === level.id);
551
- if (!summary?.isOccupiedStory)
552
- continue;
553
- const expectedHeight = getStoredLevelHeight(level);
554
- for (const wall of nodesOnLevel(bridge, level.id).filter((node) => node.type === 'wall')) {
555
- const wallHeight = resolveReportedWallHeight(bridge, wall);
556
- if (wallHeight > expectedHeight + 0.25) {
557
- issues.push(`Wall ${wall.name ?? wall.id} on ${level.name ?? level.id} is ${wallHeight}m high; multi-story exterior walls should be split into level-owned story walls`);
558
- }
559
- }
560
- }
561
- }
562
- for (const node of Object.values(bridge.getNodes())) {
563
- if (node.type === 'door' || node.type === 'window') {
564
- const parent = node.parentId ? bridge.getNode(node.parentId) : null;
565
- if (parent?.type !== 'wall') {
566
- issues.push(`${node.type} ${node.id} is not parented to a wall`);
567
- continue;
568
- }
569
- if (node.wallId !== parent.id) {
570
- issues.push(`${node.type} ${node.id} has wallId ${node.wallId ?? 'unset'} but is parented to wall ${parent.id}`);
571
- }
572
- if (!parentListsChild(parent, node.id)) {
573
- issues.push(`${node.type} ${node.id} is not listed in wall ${parent.id} children`);
574
- }
575
- const length = wallLength(parent);
576
- const width = node.width ?? (node.type === 'door' ? 0.9 : 1.5);
577
- const height = node.height ?? (node.type === 'door' ? 2.1 : 1.5);
578
- const localX = node.position[0];
579
- if (localX - width / 2 < -0.01 || localX + width / 2 > length + 0.01) {
580
- issues.push(`${node.type} ${node.id} extends outside wall ${parent.id}`);
581
- }
582
- const wallHeight = resolveReportedWallHeight(bridge, parent);
583
- const bottom = node.position[1] - height / 2;
584
- const top = node.position[1] + height / 2;
585
- if (bottom < -0.01 || top > wallHeight + 0.01) {
586
- issues.push(`${node.type} ${node.id} vertical bounds [${bottom.toFixed(2)}, ${top.toFixed(2)}] exceed wall ${parent.id} height ${wallHeight.toFixed(2)}m`);
587
- }
588
- }
589
- }
590
- for (const stair of Object.values(bridge.getNodes()).filter((node) => node.type === 'stair')) {
591
- const sourceLevelId = bridge.resolveLevelId(stair.id);
592
- if (sourceLevelId) {
593
- const sourceLevel = bridge.getNode(sourceLevelId);
594
- const footprints = stairFootprintPolygons(bridge, stair);
595
- const sourceSlabs = nodesOnLevel(bridge, sourceLevelId).filter((node) => node.type === 'slab');
596
- if (sourceSlabs.length > 0) {
597
- const outsideFootprints = footprints.filter((footprint) => !sourceSlabs.some((slab) => polygonContainsPolygon(slab.polygon, footprint)));
598
- if (outsideFootprints.length > 0) {
599
- issues.push(`Stair ${stair.name ?? stair.id} footprint extends outside source floor slab on ${sourceLevel?.name ?? sourceLevelId}`);
600
- }
601
- }
602
- const obstructingWalls = nodesOnLevel(bridge, sourceLevelId)
603
- .filter((node) => node.type === 'wall')
604
- .filter((wall) => footprints.some((footprint) => wallSamplePoints(wall).some((point) => pointInPolygon(point, footprint, false))));
605
- for (const wall of obstructingWalls) {
606
- issues.push(`Wall ${wall.name ?? wall.id} obstructs stair ${stair.name ?? stair.id} on ${sourceLevel?.name ?? sourceLevelId}`);
607
- }
608
- }
609
- if ((stair.slabOpeningMode ?? 'none') === 'destination') {
610
- const targetLevelIds = targetLevelIdsForStair(bridge, stair);
611
- if (targetLevelIds.length === 0) {
612
- issues.push(`Stair ${stair.name ?? stair.id} requests a slab opening but has no target level`);
613
- }
614
- for (const targetLevelId of targetLevelIds) {
615
- const targetLevel = bridge.getNode(targetLevelId);
616
- const targetSlabs = nodesOnLevel(bridge, targetLevelId).filter((node) => node.type === 'slab');
617
- if (targetSlabs.length === 0) {
618
- issues.push(`Stair ${stair.name ?? stair.id} targets ${targetLevel?.name ?? targetLevelId} but it has no slab`);
619
- continue;
620
- }
621
- const matchingHoles = targetSlabs.flatMap((slab) => (slab.holes ?? [])
622
- .map((hole, index) => ({ slab, hole, index }))
623
- .filter((entry) => holeBelongsToStair(entry.slab, entry.index, stair.id)));
624
- if (matchingHoles.length === 0) {
625
- issues.push(`Stair ${stair.name ?? stair.id} has no destination slab opening on ${targetLevel?.name ?? targetLevelId}`);
626
- continue;
627
- }
628
- for (const { slab, hole } of matchingHoles) {
629
- if (!polygonContainsPolygon(slab.polygon, hole)) {
630
- issues.push(`Stair ${stair.name ?? stair.id} opening extends outside slab ${slab.name ?? slab.id}`);
631
- }
632
- }
633
- }
634
- }
635
- }
636
- if (!validation.valid) {
637
- for (const error of validation.errors.slice(0, 5)) {
638
- issues.push(`Schema: ${error.nodeId}.${error.path} ${error.message}`);
639
- }
640
- if (validation.errors.length > 5) {
641
- issues.push(`Schema: ${validation.errors.length - 5} additional validation errors`);
642
- }
643
- }
644
- // Door keep-outs + item–item footprint overlaps (rotation-aware).
645
- for (const layoutIssue of layoutIssuesFromScene(Object.values(bridge.getNodes()))) {
646
- issues.push(layoutIssue);
647
- }
648
- const payload = {
649
- ok: true,
650
- valid: validation.valid,
651
- levelCount: levels.length,
652
- occupiedStoryCount,
653
- supportLevelCount: levels.length - occupiedStoryCount,
654
- roofLevelIds,
655
- activeSceneId: bridge.getActiveScene()?.id ?? null,
656
- levels,
657
- emptyLevelIds,
658
- issues,
659
- hasIssues: issues.length > 0,
660
- };
661
- return textResult(payload);
662
- });
663
- }
664
- export function registerSceneQueryTools(server, bridge) {
665
- registerListLevels(server, bridge);
666
- registerGetLevelSummary(server, bridge);
667
- registerGetWalls(server, bridge);
668
- registerGetZones(server, bridge);
669
- registerVerifyScene(server, bridge);
670
- }