@pascal-app/mcp 0.2.0

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