@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,591 +1,155 @@
1
- import { CeilingNode, DoorNode, ItemNode, SlabNode, WallNode, WindowNode, ZoneNode, } from '@pascal-app/core/schema';
2
- import { z } from 'zod';
3
- import { ADDITIVE_TOOL_ANNOTATIONS, READ_ONLY_TOOL_ANNOTATIONS } from './annotations.js';
4
- import { findCatalogItem, searchCatalogItems } from './asset-catalog.js';
5
- import { keepoutCoversPlanned, keepoutForPolygonEdge } from './door-clearance.js';
6
- import { ErrorCode, throwMcpError } from './errors.js';
7
- import { polygonArea, polygonBounds, wallLength, wallLocalXFromT } from './geometry.js';
8
- import { collectDoorKeepouts, collectOccupiedFootprints, findValidPlacement, itemPlanAabb, } from './layout-clearance.js';
9
- import { liveSyncOutput, persistencePayload, publishLiveSceneSnapshot, } from './live-sync.js';
10
- import { measurement } from './measurement.js';
11
- import { NodeIdSchema, Vec2Schema } from './schemas.js';
12
- const ROOM_TYPES = [
13
- 'bedroom',
14
- 'kitchen',
15
- 'bathroom',
16
- 'living',
17
- 'dining',
18
- 'hallway',
19
- 'entry',
20
- 'laundry',
21
- 'storage',
22
- ];
23
- export const searchAssetsInput = {
24
- query: z.string().min(1),
25
- category: z.string().optional(),
26
- };
27
- export const searchAssetsOutput = {
28
- results: z.array(z.record(z.string(), z.unknown())),
29
- total: z.number(),
30
- };
31
- export const createRoomInput = {
32
- levelId: NodeIdSchema,
33
- name: z.string().min(1),
34
- polygon: z.array(Vec2Schema).min(3),
35
- color: z.string().optional(),
36
- wallHeight: measurement('length', 'm', {
37
- positive: true,
38
- description: 'Wall height.',
39
- }).optional(),
40
- wallThickness: measurement('length', 'm', {
41
- positive: true,
42
- description: 'Wall thickness.',
43
- }).optional(),
44
- };
45
- export const createRoomOutput = {
46
- zoneId: z.string(),
47
- slabId: z.string(),
48
- ceilingId: z.string(),
49
- wallIds: z.array(z.string()),
50
- areaSqMeters: z.number(),
51
- ...liveSyncOutput,
52
- };
53
- export const addDoorInput = {
54
- wallId: NodeIdSchema,
55
- t: z.number().min(0).max(1).optional(),
56
- position: z.number().min(0).max(1).optional(),
57
- width: measurement('length', 'm', { positive: true, description: 'Door width.' }).optional(),
58
- height: measurement('length', 'm', { positive: true, description: 'Door height.' }).optional(),
59
- hingesSide: z.enum(['left', 'right']).optional(),
60
- swingDirection: z.enum(['inward', 'outward']).optional(),
61
- };
62
- export const addDoorOutput = {
63
- doorId: z.string(),
64
- localX: z.number(),
65
- t: z.number(),
66
- position: z.number(),
67
- wallLength: z.number(),
68
- clamped: z.boolean(),
69
- coordinateSystem: z.literal('wall-local-meters'),
70
- ...liveSyncOutput,
71
- };
72
- export const addWindowInput = {
73
- wallId: NodeIdSchema,
74
- t: z.number().min(0).max(1).optional(),
75
- position: z.number().min(0).max(1).optional(),
76
- width: measurement('length', 'm', { positive: true, description: 'Window width.' }).optional(),
77
- height: measurement('length', 'm', { positive: true, description: 'Window height.' }).optional(),
78
- sillHeight: measurement('length', 'm', {
79
- min: 0,
80
- description: 'Sill height above floor.',
81
- }).optional(),
82
- };
83
- export const addWindowOutput = {
84
- windowId: z.string(),
85
- localX: z.number(),
86
- t: z.number(),
87
- position: z.number(),
88
- wallLength: z.number(),
89
- clamped: z.boolean(),
90
- coordinateSystem: z.literal('wall-local-meters'),
91
- sillHeight: z.number(),
92
- ...liveSyncOutput,
93
- };
94
- export const furnishRoomInput = {
95
- levelId: NodeIdSchema.optional(),
96
- zoneId: NodeIdSchema.optional(),
97
- roomType: z.enum(ROOM_TYPES),
98
- polygon: z.array(Vec2Schema).min(3).optional(),
99
- doorWallIndex: z.number().int().min(0).optional(),
100
- };
101
- export const furnishRoomOutput = {
102
- placed: z.number(),
103
- itemIds: z.array(z.string()),
104
- skipped: z.array(z.string()),
105
- ...liveSyncOutput,
106
- };
1
+ import { isScriptedNode, scriptedObjectMeta } from '@pascal-app/core';
2
+ import { achievedChanges, addWallOpening, editedScriptLanguage, editedScriptParams, rebuiltOpeningResult, rescriptOpening, } from '@pascal-app/core/agent-operations';
3
+ import { addDoorOutput as addDoorResult, addDoorTool, addWindowOutput as addWindowResult, addWindowTool, isAgentRefusal, } from '@pascal-app/core/agent-tools';
4
+ import { GeometryReuseFields } from '@pascal-app/core/schema';
5
+ import { compileAndStore, readScript } from './add-object.js';
6
+ import { ADDITIVE_TOOL_ANNOTATIONS } from './annotations.js';
7
+ import { refusalResult, toolError } from './errors.js';
8
+ import { liveSyncOutput, persistencePayload, publishLiveSceneSnapshot } from './live-sync.js';
9
+ import { toPatches } from './shared-tools.js';
10
+ /** The contract's one result (core `addWallOpening`), and the live sync's note. */
11
+ export const addDoorOutput = { ...addDoorResult, ...liveSyncOutput };
12
+ export const addWindowOutput = { ...addWindowResult, ...liveSyncOutput };
107
13
  function textResult(payload) {
108
14
  return {
109
15
  content: [{ type: 'text', text: JSON.stringify(payload) }],
110
16
  structuredContent: payload,
111
17
  };
112
18
  }
113
- function assertLevel(bridge, levelId) {
114
- const level = bridge.getNode(levelId);
115
- if (!level)
116
- throwMcpError(ErrorCode.InvalidParams, `Level not found: ${levelId}`);
117
- if (level.type !== 'level') {
118
- throwMcpError(ErrorCode.InvalidParams, `Node ${levelId} is a ${level.type}, expected level`);
119
- }
120
- if (typeof level.metadata === 'object' &&
121
- level.metadata !== null &&
122
- 'role' in level.metadata &&
123
- level.metadata.role === 'roof') {
124
- throwMcpError(ErrorCode.InvalidParams, `Roof support level ${levelId} is not an occupied story; create rooms or furnishings on an occupied level instead`);
125
- }
126
- return level;
127
- }
128
- function assertWall(bridge, wallId) {
129
- const wall = bridge.getNode(wallId);
130
- if (!wall)
131
- throwMcpError(ErrorCode.InvalidParams, `Wall not found: ${wallId}`);
132
- if (wall.type !== 'wall') {
133
- throwMcpError(ErrorCode.InvalidParams, `Node ${wallId} is a ${wall.type}, expected wall`);
134
- }
135
- return wall;
136
- }
137
- function inferRoomGeometry(bridge, levelId, polygon, zoneId) {
138
- if (levelId && polygon)
139
- return { levelId, polygon };
140
- if (!zoneId) {
141
- throwMcpError(ErrorCode.InvalidParams, 'Provide either levelId + polygon or zoneId so the room can be furnished');
142
- }
143
- const zone = bridge.getNode(zoneId);
144
- if (!zone)
145
- throwMcpError(ErrorCode.InvalidParams, `Zone not found: ${zoneId}`);
146
- if (zone.type !== 'zone') {
147
- throwMcpError(ErrorCode.InvalidParams, `Node ${zoneId} is a ${zone.type}, expected zone`);
148
- }
149
- const inferredLevelId = levelId ?? zone.parentId ?? undefined;
150
- if (!inferredLevelId) {
151
- throwMcpError(ErrorCode.InvalidParams, `Zone ${zoneId} is missing a parent level`);
152
- }
153
- return {
154
- levelId: inferredLevelId,
155
- polygon: polygon ?? zone.polygon,
156
- };
157
- }
158
- function resolveWallT(toolName, t, position) {
159
- const resolved = t ?? position;
160
- if (resolved === undefined) {
161
- throwMcpError(ErrorCode.InvalidParams, `${toolName} requires t or position in the 0..1 range`);
162
- }
163
- return resolved;
164
- }
165
- function makeItemAsset(asset) {
166
- return {
167
- id: asset.id,
168
- name: asset.name,
169
- category: asset.category,
170
- thumbnail: asset.thumbnail ?? '',
171
- src: asset.src,
172
- dimensions: asset.dimensions ?? [1, 1, 1],
173
- offset: asset.offset ?? [0, 0, 0],
174
- rotation: asset.rotation ?? [0, 0, 0],
175
- scale: asset.scale ?? [1, 1, 1],
176
- ...(asset.attachTo ? { attachTo: asset.attachTo } : {}),
177
- ...(asset.tags ? { tags: asset.tags } : {}),
178
- ...(asset.surface ? { surface: asset.surface } : {}),
179
- ...(asset.interactive ? { interactive: asset.interactive } : {}),
180
- };
181
- }
182
- function buildRoomPlacements(roomType, polygon, doorWallIndex = 0) {
183
- const bounds = polygonBounds(polygon);
184
- const n = polygon.length;
185
- const backIdx = (doorWallIndex + Math.floor(n / 2)) % n;
186
- const backStart = polygon[backIdx];
187
- const backEnd = polygon[(backIdx + 1) % n];
188
- const backMidX = (backStart[0] + backEnd[0]) / 2;
189
- const backMidZ = (backStart[1] + backEnd[1]) / 2;
190
- const inwardX = bounds.centerX - backMidX;
191
- const inwardZ = bounds.centerZ - backMidZ;
192
- const inwardLen = Math.sqrt(inwardX * inwardX + inwardZ * inwardZ) || 1;
193
- const inX = inwardX / inwardLen;
194
- const inZ = inwardZ / inwardLen;
195
- const facingRot = (Math.atan2(inX, inZ) * 180) / Math.PI;
196
- const alongX = backEnd[0] - backStart[0];
197
- const alongZ = backEnd[1] - backStart[1];
198
- const alongLen = Math.sqrt(alongX * alongX + alongZ * alongZ) || 1;
199
- const ax = alongX / alongLen;
200
- const az = alongZ / alongLen;
201
- const backPos = (inset, lateral = 0) => [
202
- backMidX + inX * inset + ax * lateral,
203
- backMidZ + inZ * inset + az * lateral,
204
- ];
205
- const sideIdx = (doorWallIndex + 1) % n;
206
- const sideStart = polygon[sideIdx];
207
- const sideEnd = polygon[(sideIdx + 1) % n];
208
- const sideMidX = (sideStart[0] + sideEnd[0]) / 2;
209
- const sideMidZ = (sideStart[1] + sideEnd[1]) / 2;
210
- const sideInX = bounds.centerX - sideMidX;
211
- const sideInZ = bounds.centerZ - sideMidZ;
212
- const sideInLen = Math.sqrt(sideInX * sideInX + sideInZ * sideInZ) || 1;
213
- const snX = sideInX / sideInLen;
214
- const snZ = sideInZ / sideInLen;
215
- const sideRot = (Math.atan2(snX, snZ) * 180) / Math.PI;
216
- const sideAlongX = sideEnd[0] - sideStart[0];
217
- const sideAlongZ = sideEnd[1] - sideStart[1];
218
- const sideAlongLen = Math.sqrt(sideAlongX * sideAlongX + sideAlongZ * sideAlongZ) || 1;
219
- const sax = sideAlongX / sideAlongLen;
220
- const saz = sideAlongZ / sideAlongLen;
221
- const sidePos = (inset, lateral = 0) => [
222
- sideMidX + snX * inset + sax * lateral,
223
- sideMidZ + snZ * inset + saz * lateral,
224
- ];
225
- const placements = [];
226
- const area = polygonArea(polygon);
227
- const addBack = (assetId, inset, lateral = 0, rotationDeg = facingRot) => {
228
- const [x, z] = backPos(inset, lateral);
229
- placements.push({
230
- assetId,
231
- x,
232
- z,
233
- rotationDeg,
234
- along: { x: ax, z: az },
235
- inward: { x: inX, z: inZ },
19
+ /**
20
+ * `add_door` / `add_window` with a nodeId: rebuild that opening from new code,
21
+ * or its stored script with new params, through the shared operation.
22
+ */
23
+ async function rebuildOpening(kind, bridge, host, input) {
24
+ if (!host)
25
+ return toolError('This Pascal server cannot run geometry scripts.', {
26
+ code: 'scripts_unavailable',
236
27
  });
237
- };
238
- const addSide = (assetId, inset, lateral = 0, rotationDeg = sideRot) => {
239
- const [x, z] = sidePos(inset, lateral);
240
- placements.push({
241
- assetId,
242
- x,
243
- z,
244
- rotationDeg,
245
- along: { x: sax, z: saz },
246
- inward: { x: snX, z: snZ },
28
+ const scene = bridge.getActiveScene();
29
+ if (!scene)
30
+ return toolError('Open or save a scene first.', { code: 'no_active_scene' });
31
+ const nodes = bridge.getNodes();
32
+ let outcome;
33
+ try {
34
+ const code = input.code ?? (await readScript(host, scene.id, bridge, input.nodeId));
35
+ const previous = nodes[input.nodeId];
36
+ const language = editedScriptLanguage(previous, input);
37
+ const params = editedScriptParams(previous, input.params);
38
+ const compiled = await compileAndStore(host, scene.id, { code, language, params }, kind, {
39
+ nodeId: input.nodeId,
40
+ metadata: {
41
+ ...(isScriptedNode(previous) ? scriptedObjectMeta(previous) : {}),
42
+ ...GeometryReuseFields.parse(input),
43
+ },
247
44
  });
248
- };
249
- switch (roomType) {
250
- case 'bedroom': {
251
- const bedId = Math.max(bounds.width, bounds.depth) >= 3.2 ? 'double-bed' : 'single-bed';
252
- const bed = findCatalogItem(bedId);
253
- const [bedW = 2, , bedD = 2.5] = bed?.dimensions ?? [];
254
- addBack(bedId, bedD / 2 + 0.1);
255
- if (alongLen > bedW + 1.1) {
256
- addBack('bedside-table', 0.35, -(bedW / 2 + 0.35));
257
- addBack('bedside-table', 0.35, bedW / 2 + 0.35);
258
- }
259
- if (area >= 10)
260
- addSide('dresser', 0.55, sideAlongLen * 0.22);
261
- if (area >= 13)
262
- addSide('closet', 0.6, -sideAlongLen * 0.22);
263
- break;
264
- }
265
- case 'kitchen':
266
- addBack(alongLen >= 2.6 ? 'kitchen' : 'kitchen-counter', 0.55);
267
- if (alongLen >= 3.5)
268
- addBack('stove', 0.55, alongLen / 2 - 0.65);
269
- addSide('fridge', 0.6, sideAlongLen * 0.25);
270
- break;
271
- case 'bathroom':
272
- addBack('toilet', 0.55, alongLen * 0.25);
273
- addBack('bathroom-sink', 0.8, -alongLen * 0.2);
274
- if (area >= 6.5)
275
- addSide('bathtub', 0.85);
276
- else
277
- placements.push({ assetId: 'shower-square', x: bounds.centerX, z: bounds.centerZ });
278
- break;
279
- case 'living': {
280
- addBack('sofa', 0.9);
281
- addBack('coffee-table', 2.1);
282
- addSide('livingroom-chair', 0.85, -sideAlongLen * 0.18);
283
- // TV faces the sofa from the door wall: use door-wall axes so smart
284
- // re-place nudges into the room (along wall / inward), not world X/Z.
285
- const doorIdx = doorWallIndex % n;
286
- const doorStart = polygon[doorIdx];
287
- const doorEnd = polygon[(doorIdx + 1) % n];
288
- const doorMidX = (doorStart[0] + doorEnd[0]) / 2;
289
- const doorMidZ = (doorStart[1] + doorEnd[1]) / 2;
290
- // Door-wall inward is opposite of "back wall" inward (into room from door).
291
- const doorInX = -inX;
292
- const doorInZ = -inZ;
293
- placements.push({
294
- assetId: 'tv-stand',
295
- x: doorMidX + doorInX * 0.35,
296
- z: doorMidZ + doorInZ * 0.35,
297
- rotationDeg: facingRot + 180,
298
- along: { x: ax, z: az },
299
- inward: { x: doorInX, z: doorInZ },
300
- });
301
- break;
302
- }
303
- case 'dining':
304
- placements.push({ assetId: 'dining-table', x: bounds.centerX, z: bounds.centerZ });
305
- placements.push({ assetId: 'dining-chair', x: bounds.centerX, z: bounds.centerZ - 0.85 });
306
- placements.push({
307
- assetId: 'dining-chair',
308
- x: bounds.centerX,
309
- z: bounds.centerZ + 0.85,
310
- rotationDeg: 180,
311
- });
312
- if (Math.min(bounds.width, bounds.depth) >= 3) {
313
- placements.push({
314
- assetId: 'dining-chair',
315
- x: bounds.centerX - 0.85,
316
- z: bounds.centerZ,
317
- rotationDeg: 270,
318
- });
319
- placements.push({
320
- assetId: 'dining-chair',
321
- x: bounds.centerX + 0.85,
322
- z: bounds.centerZ,
323
- rotationDeg: 90,
324
- });
325
- }
326
- break;
327
- case 'laundry':
328
- addBack('washing-machine', 0.6, -0.55);
329
- addBack('drying-rack', 0.65, 0.65);
330
- break;
331
- case 'entry':
332
- case 'hallway':
333
- if (Math.min(bounds.width, bounds.depth) >= 1.4)
334
- addSide('coat-rack', 0.35);
335
- break;
336
- case 'storage':
337
- addBack('closet', 0.6);
338
- break;
45
+ outcome = rescriptOpening(nodes, { ...input, compiled }, { activeLevelId: null });
339
46
  }
340
- return { placements, bounds };
341
- }
342
- export function registerSearchAssets(server) {
343
- server.registerTool('search_assets', {
344
- title: 'Search assets',
345
- description: 'Search the built-in MCP item catalog by keyword. Call before place_item when you need a valid catalogItemId.',
346
- inputSchema: searchAssetsInput,
347
- outputSchema: searchAssetsOutput,
348
- annotations: READ_ONLY_TOOL_ANNOTATIONS,
349
- }, async ({ query, category }) => {
350
- const results = searchCatalogItems({ query, category }).map((item) => ({
351
- id: item.id,
352
- name: item.name,
353
- category: item.category,
354
- tags: item.tags ?? [],
355
- dimensions: item.dimensions,
356
- attachTo: item.attachTo ?? null,
357
- }));
358
- return textResult({ results, total: results.length });
359
- });
360
- }
361
- export function registerCreateRoom(server, bridge) {
362
- server.registerTool('create_room', {
363
- title: 'Create room',
364
- description: 'Create a room on a level: zone, slab, ceiling, and one wall per polygon edge. Returns wallIds in polygon edge order.',
365
- inputSchema: createRoomInput,
366
- outputSchema: createRoomOutput,
367
- annotations: ADDITIVE_TOOL_ANNOTATIONS,
368
- }, async ({ levelId, name, polygon, color, wallHeight, wallThickness }) => {
369
- assertLevel(bridge, levelId);
370
- const points = polygon;
371
- const zone = ZoneNode.parse({
372
- name,
373
- polygon: points,
374
- color: color ?? '#60a5fa',
375
- metadata: { mcpTool: 'create_room' },
376
- });
377
- const slab = SlabNode.parse({ polygon: points, metadata: { mcpTool: 'create_room' } });
378
- const ceiling = CeilingNode.parse({ polygon: points, metadata: { mcpTool: 'create_room' } });
379
- const walls = points.map((start, index) => WallNode.parse({
380
- name: `${name} wall ${index + 1}`,
381
- start,
382
- end: points[(index + 1) % points.length],
383
- ...(wallHeight !== undefined ? { height: wallHeight } : {}),
384
- ...(wallThickness !== undefined ? { thickness: wallThickness } : {}),
385
- metadata: { mcpTool: 'create_room', roomName: name, edgeIndex: index },
386
- }));
387
- bridge.applyPatch([
388
- { op: 'create', node: zone, parentId: levelId },
389
- { op: 'create', node: slab, parentId: levelId },
390
- { op: 'create', node: ceiling, parentId: levelId },
391
- ...walls.map((wall) => ({
392
- op: 'create',
393
- node: wall,
394
- parentId: levelId,
395
- })),
396
- ]);
397
- const persistence = await publishLiveSceneSnapshot(bridge, 'create_room');
398
- return textResult({
399
- zoneId: zone.id,
400
- slabId: slab.id,
401
- ceilingId: ceiling.id,
402
- wallIds: walls.map((wall) => wall.id),
403
- areaSqMeters: Math.round(polygonArea(points) * 100) / 100,
404
- ...persistencePayload(persistence),
47
+ catch (error) {
48
+ if (isAgentRefusal(error))
49
+ return refusalResult(error);
50
+ return toolError(error instanceof Error ? error.message : String(error), {
51
+ code: 'script_failed',
405
52
  });
53
+ }
54
+ const achieved = outcome.changes
55
+ ? achievedChanges(nodes, outcome.changes)
56
+ : achievedChanges(nodes, {});
57
+ if (outcome.changes)
58
+ bridge.applyPatch(toPatches(outcome.changes));
59
+ const persistence = await publishLiveSceneSnapshot(bridge, `add_${kind}`);
60
+ return textResult({
61
+ ...rebuiltOpeningResult(bridge.getNodes(), input.nodeId, outcome.result, achieved),
62
+ ...persistencePayload(persistence),
406
63
  });
407
64
  }
408
- export function registerAddDoor(server, bridge) {
409
- server.registerTool('add_door', {
410
- title: 'Add door',
411
- description: 'Add a door to an existing wall. t/position is 0..1 along the wall: 0 = start, 0.5 = center, 1 = end.',
412
- inputSchema: addDoorInput,
65
+ /** A door or window passed `code`: compiled and stored the way add_object does, or the tool's error. */
66
+ async function compileOpeningScript(kind, bridge, host, input) {
67
+ if (!input.code)
68
+ return {};
69
+ if (!host)
70
+ return {
71
+ error: toolError('This Pascal server cannot run geometry scripts; use the fields.', {
72
+ code: 'scripts_unavailable',
73
+ }),
74
+ };
75
+ const scene = bridge.getActiveScene();
76
+ if (!scene)
77
+ return { error: toolError('Open or save a scene first.', { code: 'no_active_scene' }) };
78
+ try {
79
+ return {
80
+ script: await compileAndStore(host, scene.id, { code: input.code, language: input.language ?? 'three', params: input.params }, kind, { metadata: input }),
81
+ };
82
+ }
83
+ catch (error) {
84
+ if (isAgentRefusal(error))
85
+ return { error: refusalResult(error) };
86
+ return {
87
+ error: toolError(error instanceof Error ? error.message : String(error), {
88
+ code: 'script_failed',
89
+ }),
90
+ };
91
+ }
92
+ }
93
+ export function registerAddDoor(server, bridge, geometryScripts) {
94
+ server.registerTool(addDoorTool.name, {
95
+ title: addDoorTool.title,
96
+ description: addDoorTool.description,
97
+ inputSchema: addDoorTool.input,
413
98
  outputSchema: addDoorOutput,
414
99
  annotations: ADDITIVE_TOOL_ANNOTATIONS,
415
- }, async ({ wallId, t, position, width = 0.9, height = 2.1, hingesSide, swingDirection }) => {
416
- const wall = assertWall(bridge, wallId);
417
- const length = wallLength(wall);
418
- if (length < width) {
419
- throwMcpError(ErrorCode.InvalidParams, `Wall ${wallId} is ${length.toFixed(2)}m long, too short for a ${width.toFixed(2)}m door`);
100
+ }, async (input) => {
101
+ if (input.nodeId)
102
+ return rebuildOpening('door', bridge, geometryScripts, { ...input, nodeId: input.nodeId });
103
+ const compiled = await compileOpeningScript('door', bridge, geometryScripts, input);
104
+ if ('error' in compiled)
105
+ return compiled.error;
106
+ let opening;
107
+ try {
108
+ opening = addWallOpening(bridge.getNodes(), {
109
+ kind: 'door',
110
+ ...input,
111
+ compiled: compiled.script,
112
+ });
420
113
  }
421
- const wallT = resolveWallT('add_door', t, position);
422
- const localX = wallLocalXFromT(wall, wallT, width);
423
- const door = DoorNode.parse({
424
- wallId,
425
- parentId: wallId,
426
- position: [localX, height / 2, 0],
427
- width,
428
- height,
429
- ...(hingesSide ? { hingesSide } : {}),
430
- ...(swingDirection ? { swingDirection } : {}),
431
- });
432
- const id = bridge.createNode(door, wallId);
114
+ catch (error) {
115
+ return refusalResult(error);
116
+ }
117
+ bridge.applyPatch(toPatches(opening.changes));
433
118
  const persistence = await publishLiveSceneSnapshot(bridge, 'add_door');
434
- return textResult({
435
- doorId: id,
436
- localX,
437
- t: wallT,
438
- position: wallT,
439
- wallLength: length,
440
- clamped: Math.abs(localX - wallT * length) > 1e-9,
441
- coordinateSystem: 'wall-local-meters',
442
- ...persistencePayload(persistence),
443
- });
119
+ return textResult({ ...opening.result, ...persistencePayload(persistence) });
444
120
  });
445
121
  }
446
- export function registerAddWindow(server, bridge) {
447
- server.registerTool('add_window', {
448
- title: 'Add window',
449
- description: 'Add a window to an existing wall. t/position is 0..1 along the wall; sillHeight is the height from floor to window bottom.',
450
- inputSchema: addWindowInput,
122
+ export function registerAddWindow(server, bridge, geometryScripts) {
123
+ server.registerTool(addWindowTool.name, {
124
+ title: addWindowTool.title,
125
+ description: addWindowTool.description,
126
+ inputSchema: addWindowTool.input,
451
127
  outputSchema: addWindowOutput,
452
128
  annotations: ADDITIVE_TOOL_ANNOTATIONS,
453
- }, async ({ wallId, t, position, width = 1.5, height = 1.5, sillHeight = 0.9 }) => {
454
- const wall = assertWall(bridge, wallId);
455
- const length = wallLength(wall);
456
- if (length < width) {
457
- throwMcpError(ErrorCode.InvalidParams, `Wall ${wallId} is ${length.toFixed(2)}m long, too short for a ${width.toFixed(2)}m window`);
458
- }
459
- const wallT = resolveWallT('add_window', t, position);
460
- const localX = wallLocalXFromT(wall, wallT, width);
461
- const windowNode = WindowNode.parse({
462
- wallId,
463
- parentId: wallId,
464
- position: [localX, sillHeight + height / 2, 0],
465
- width,
466
- height,
467
- });
468
- const id = bridge.createNode(windowNode, wallId);
469
- const persistence = await publishLiveSceneSnapshot(bridge, 'add_window');
470
- return textResult({
471
- windowId: id,
472
- localX,
473
- t: wallT,
474
- position: wallT,
475
- wallLength: length,
476
- clamped: Math.abs(localX - wallT * length) > 1e-9,
477
- coordinateSystem: 'wall-local-meters',
478
- sillHeight,
479
- ...persistencePayload(persistence),
480
- });
481
- });
482
- }
483
- export function registerFurnishRoom(server, bridge) {
484
- server.registerTool('furnish_room', {
485
- title: 'Furnish room',
486
- description: 'Place furniture for a room type (levelId+polygon or zoneId). Skips or nudges poses that block door clear zones or overlap existing items (rotation-aware). Parent floor items to the level.',
487
- inputSchema: furnishRoomInput,
488
- outputSchema: furnishRoomOutput,
489
- annotations: ADDITIVE_TOOL_ANNOTATIONS,
490
- }, async ({ levelId, zoneId, roomType, polygon, doorWallIndex }) => {
491
- const room = inferRoomGeometry(bridge, levelId, polygon, zoneId);
492
- assertLevel(bridge, room.levelId);
493
- const points = room.polygon;
494
- const resolvedDoorWallIndex = doorWallIndex ?? 0;
495
- const { placements, bounds } = buildRoomPlacements(roomType, points, resolvedDoorWallIndex);
496
- const skipped = [];
497
- const items = [];
498
- const allNodes = Object.values(bridge.getNodes());
499
- // Doors on THIS level only (stacked floors must not interact in plan).
500
- const existingKeepouts = collectDoorKeepouts(allNodes, { levelId: room.levelId });
501
- const doorKeepoutAabbs = existingKeepouts.map((k) => k.aabb);
502
- // Always protect this room's door-wall edge when no keep-out already covers it
503
- // (other rooms may already have doors elsewhere on the same level).
504
- const planned = keepoutForPolygonEdge(points, resolvedDoorWallIndex, {
505
- t: 0.5,
506
- width: 0.9,
507
- });
508
- if (planned &&
509
- !doorKeepoutAabbs.some((existing) => keepoutCoversPlanned(existing, planned))) {
510
- doorKeepoutAabbs.push(planned);
511
- }
512
- // Existing floor items on this level + footprints we place in this batch.
513
- const occupied = collectOccupiedFootprints(allNodes, {
514
- levelId: room.levelId,
515
- floorOnly: true,
516
- }).map((f) => f.aabb);
517
- const roomBounds = {
518
- minX: bounds.minX,
519
- maxX: bounds.maxX,
520
- minZ: bounds.minZ,
521
- maxZ: bounds.maxZ,
522
- };
523
- for (const placement of placements) {
524
- const asset = findCatalogItem(placement.assetId);
525
- if (!asset) {
526
- skipped.push(`${placement.assetId}: asset not found`);
527
- continue;
528
- }
529
- const primary = {
530
- x: placement.x,
531
- z: placement.z,
532
- rotationDeg: placement.rotationDeg ?? 0,
533
- };
534
- const resolved = findValidPlacement({
535
- primary,
536
- dimensions: asset.dimensions,
537
- doorKeepouts: doorKeepoutAabbs,
538
- occupied,
539
- roomBounds,
540
- along: placement.along,
541
- inward: placement.inward,
129
+ }, async (input) => {
130
+ if (input.nodeId)
131
+ return rebuildOpening('window', bridge, geometryScripts, { ...input, nodeId: input.nodeId });
132
+ const compiled = await compileOpeningScript('window', bridge, geometryScripts, input);
133
+ if ('error' in compiled)
134
+ return compiled.error;
135
+ let opening;
136
+ try {
137
+ opening = addWallOpening(bridge.getNodes(), {
138
+ kind: 'window',
139
+ ...input,
140
+ compiled: compiled.script,
542
141
  });
543
- if (!resolved.candidate) {
544
- const reason = resolved.reason === 'blocks_door_clearance'
545
- ? 'blocks door clearance'
546
- : resolved.reason === 'outside_bounds'
547
- ? 'outside room bounds'
548
- : 'overlaps another item';
549
- skipped.push(`${asset.id}: ${reason}`);
550
- continue;
551
- }
552
- const { x, z, rotationDeg } = resolved.candidate;
553
- const rotRad = (rotationDeg * Math.PI) / 180;
554
- const planAabb = itemPlanAabb([x, 0, z], asset.dimensions, rotRad);
555
- occupied.push(planAabb);
556
- items.push(ItemNode.parse({
557
- name: asset.name,
558
- position: [x, 0, z],
559
- rotation: [0, rotRad, 0],
560
- asset: makeItemAsset(asset),
561
- metadata: {
562
- mcpTool: 'furnish_room',
563
- roomType,
564
- ...(x !== primary.x || z !== primary.z ? { placementAdjusted: true } : {}),
565
- },
566
- }));
567
142
  }
568
- let persistence = 'published';
569
- if (items.length > 0) {
570
- bridge.applyPatch(items.map((item) => ({
571
- op: 'create',
572
- node: item,
573
- parentId: room.levelId,
574
- })));
575
- persistence = await publishLiveSceneSnapshot(bridge, 'furnish_room');
143
+ catch (error) {
144
+ return refusalResult(error);
576
145
  }
577
- return textResult({
578
- placed: items.length,
579
- itemIds: items.map((item) => item.id),
580
- skipped,
581
- ...persistencePayload(persistence),
582
- });
146
+ bridge.applyPatch(toPatches(opening.changes));
147
+ const persistence = await publishLiveSceneSnapshot(bridge, 'add_window');
148
+ return textResult({ ...opening.result, ...persistencePayload(persistence) });
583
149
  });
584
150
  }
585
- export function registerRoomTools(server, bridge) {
586
- registerSearchAssets(server);
587
- registerCreateRoom(server, bridge);
588
- registerAddDoor(server, bridge);
589
- registerAddWindow(server, bridge);
590
- registerFurnishRoom(server, bridge);
151
+ /** add_door and add_window; create_room and furnish_room are shared tools (shared-tools.ts). */
152
+ export function registerRoomTools(server, bridge, geometryScripts) {
153
+ registerAddDoor(server, bridge, geometryScripts);
154
+ registerAddWindow(server, bridge, geometryScripts);
591
155
  }