@pascal-app/mcp 1.0.0-beta.5 → 1.0.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 (108) hide show
  1. package/LICENSE +21 -0
  2. package/dist/bridge/scene-bridge.d.ts.map +1 -1
  3. package/dist/bridge/scene-bridge.js +2 -2
  4. package/dist/server.d.ts +11 -0
  5. package/dist/server.d.ts.map +1 -1
  6. package/dist/server.js +52 -0
  7. package/dist/tools/annotations.d.ts +28 -0
  8. package/dist/tools/annotations.d.ts.map +1 -0
  9. package/dist/tools/annotations.js +27 -0
  10. package/dist/tools/apply-patch.d.ts +7 -0
  11. package/dist/tools/apply-patch.d.ts.map +1 -1
  12. package/dist/tools/apply-patch.js +6 -2
  13. package/dist/tools/check-collisions.d.ts +59 -0
  14. package/dist/tools/check-collisions.d.ts.map +1 -1
  15. package/dist/tools/check-collisions.js +240 -19
  16. package/dist/tools/construction-tools.d.ts +23 -1
  17. package/dist/tools/construction-tools.d.ts.map +1 -1
  18. package/dist/tools/construction-tools.js +34 -8
  19. package/dist/tools/create-level.d.ts +7 -0
  20. package/dist/tools/create-level.d.ts.map +1 -1
  21. package/dist/tools/create-level.js +6 -3
  22. package/dist/tools/create-wall.d.ts +7 -0
  23. package/dist/tools/create-wall.d.ts.map +1 -1
  24. package/dist/tools/create-wall.js +6 -3
  25. package/dist/tools/cut-opening.d.ts +7 -0
  26. package/dist/tools/cut-opening.d.ts.map +1 -1
  27. package/dist/tools/cut-opening.js +6 -3
  28. package/dist/tools/delete-node.d.ts +7 -0
  29. package/dist/tools/delete-node.d.ts.map +1 -1
  30. package/dist/tools/delete-node.js +6 -3
  31. package/dist/tools/describe-node.d.ts.map +1 -1
  32. package/dist/tools/describe-node.js +2 -0
  33. package/dist/tools/door-clearance.d.ts +18 -2
  34. package/dist/tools/door-clearance.d.ts.map +1 -1
  35. package/dist/tools/door-clearance.js +101 -7
  36. package/dist/tools/duplicate-level.d.ts +7 -0
  37. package/dist/tools/duplicate-level.d.ts.map +1 -1
  38. package/dist/tools/duplicate-level.js +6 -2
  39. package/dist/tools/export-glb.d.ts.map +1 -1
  40. package/dist/tools/export-glb.js +4 -2
  41. package/dist/tools/export-json.d.ts.map +1 -1
  42. package/dist/tools/export-json.js +2 -0
  43. package/dist/tools/find-nodes.d.ts.map +1 -1
  44. package/dist/tools/find-nodes.js +2 -0
  45. package/dist/tools/get-node.d.ts.map +1 -1
  46. package/dist/tools/get-node.js +2 -0
  47. package/dist/tools/get-scene.d.ts.map +1 -1
  48. package/dist/tools/get-scene.js +2 -0
  49. package/dist/tools/layout-clearance.d.ts +2 -0
  50. package/dist/tools/layout-clearance.d.ts.map +1 -1
  51. package/dist/tools/layout-clearance.js +5 -1
  52. package/dist/tools/live-sync.d.ts +33 -3
  53. package/dist/tools/live-sync.d.ts.map +1 -1
  54. package/dist/tools/live-sync.js +36 -4
  55. package/dist/tools/measure.d.ts +1 -0
  56. package/dist/tools/measure.d.ts.map +1 -1
  57. package/dist/tools/measure.js +4 -0
  58. package/dist/tools/photo-to-scene/photo-to-scene.d.ts.map +1 -1
  59. package/dist/tools/photo-to-scene/photo-to-scene.js +2 -0
  60. package/dist/tools/place-item.d.ts +7 -0
  61. package/dist/tools/place-item.d.ts.map +1 -1
  62. package/dist/tools/place-item.js +6 -2
  63. package/dist/tools/redo.d.ts +7 -0
  64. package/dist/tools/redo.d.ts.map +1 -1
  65. package/dist/tools/redo.js +6 -4
  66. package/dist/tools/room-tools.d.ts +28 -0
  67. package/dist/tools/room-tools.d.ts.map +1 -1
  68. package/dist/tools/room-tools.js +20 -5
  69. package/dist/tools/scene-lifecycle/create-project.d.ts.map +1 -1
  70. package/dist/tools/scene-lifecycle/create-project.js +2 -0
  71. package/dist/tools/scene-lifecycle/delete-scene.d.ts.map +1 -1
  72. package/dist/tools/scene-lifecycle/delete-scene.js +2 -0
  73. package/dist/tools/scene-lifecycle/get-project-status.d.ts.map +1 -1
  74. package/dist/tools/scene-lifecycle/get-project-status.js +2 -0
  75. package/dist/tools/scene-lifecycle/list-scenes.d.ts.map +1 -1
  76. package/dist/tools/scene-lifecycle/list-scenes.js +2 -0
  77. package/dist/tools/scene-lifecycle/load-scene.d.ts.map +1 -1
  78. package/dist/tools/scene-lifecycle/load-scene.js +2 -0
  79. package/dist/tools/scene-lifecycle/rename-scene.d.ts.map +1 -1
  80. package/dist/tools/scene-lifecycle/rename-scene.js +2 -0
  81. package/dist/tools/scene-lifecycle/save-scene.d.ts.map +1 -1
  82. package/dist/tools/scene-lifecycle/save-scene.js +2 -0
  83. package/dist/tools/scene-lifecycle/test-utils.d.ts +5 -1
  84. package/dist/tools/scene-lifecycle/test-utils.d.ts.map +1 -1
  85. package/dist/tools/scene-lifecycle/test-utils.js +24 -0
  86. package/dist/tools/scene-query.d.ts.map +1 -1
  87. package/dist/tools/scene-query.js +6 -0
  88. package/dist/tools/set-zone.d.ts +7 -0
  89. package/dist/tools/set-zone.d.ts.map +1 -1
  90. package/dist/tools/set-zone.js +6 -3
  91. package/dist/tools/templates/create-from-template.d.ts.map +1 -1
  92. package/dist/tools/templates/create-from-template.js +2 -0
  93. package/dist/tools/templates/create-house-from-brief.d.ts.map +1 -1
  94. package/dist/tools/templates/create-house-from-brief.js +2 -0
  95. package/dist/tools/templates/list-templates.d.ts.map +1 -1
  96. package/dist/tools/templates/list-templates.js +2 -0
  97. package/dist/tools/undo.d.ts +7 -0
  98. package/dist/tools/undo.d.ts.map +1 -1
  99. package/dist/tools/undo.js +6 -4
  100. package/dist/tools/validate-scene.d.ts.map +1 -1
  101. package/dist/tools/validate-scene.js +2 -0
  102. package/dist/tools/variants/generate-variants.d.ts.map +1 -1
  103. package/dist/tools/variants/generate-variants.js +2 -0
  104. package/dist/tools/vision/analyze-floorplan-image.d.ts.map +1 -1
  105. package/dist/tools/vision/analyze-floorplan-image.js +2 -0
  106. package/dist/tools/vision/analyze-room-photo.d.ts.map +1 -1
  107. package/dist/tools/vision/analyze-room-photo.js +2 -0
  108. package/package.json +5 -5
@@ -16,6 +16,13 @@ export declare const createStoryShellInput: {
16
16
  ceilingMaterialPreset: z.ZodOptional<z.ZodString>;
17
17
  };
18
18
  export declare const createStoryShellOutput: {
19
+ persistence: z.ZodOptional<z.ZodObject<{
20
+ status: z.ZodEnum<{
21
+ unbound: "unbound";
22
+ events_unsupported: "events_unsupported";
23
+ }>;
24
+ warning: z.ZodString;
25
+ }, z.core.$strip>>;
19
26
  levelId: z.ZodString;
20
27
  wallIds: z.ZodArray<z.ZodString>;
21
28
  slabId: z.ZodNullable<z.ZodString>;
@@ -33,13 +40,14 @@ export declare const createRoofInput: {
33
40
  width: z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>;
34
41
  depth: z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>;
35
42
  roofType: z.ZodDefault<z.ZodEnum<{
43
+ gable: "gable";
36
44
  flat: "flat";
37
45
  hip: "hip";
38
- gable: "gable";
39
46
  shed: "shed";
40
47
  gambrel: "gambrel";
41
48
  dutch: "dutch";
42
49
  mansard: "mansard";
50
+ conical: "conical";
43
51
  }>>;
44
52
  pitch: z.ZodDefault<z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>>;
45
53
  wallHeight: z.ZodDefault<z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>>;
@@ -49,6 +57,13 @@ export declare const createRoofInput: {
49
57
  name: z.ZodOptional<z.ZodString>;
50
58
  };
51
59
  export declare const createRoofOutput: {
60
+ persistence: z.ZodOptional<z.ZodObject<{
61
+ status: z.ZodEnum<{
62
+ unbound: "unbound";
63
+ events_unsupported: "events_unsupported";
64
+ }>;
65
+ warning: z.ZodString;
66
+ }, z.core.$strip>>;
52
67
  referenceLevelId: z.ZodString;
53
68
  roofLevelId: z.ZodString;
54
69
  createdRoofLevelId: z.ZodNullable<z.ZodString>;
@@ -83,6 +98,13 @@ export declare const createStairBetweenLevelsInput: {
83
98
  name: z.ZodOptional<z.ZodString>;
84
99
  };
85
100
  export declare const createStairBetweenLevelsOutput: {
101
+ persistence: z.ZodOptional<z.ZodObject<{
102
+ status: z.ZodEnum<{
103
+ unbound: "unbound";
104
+ events_unsupported: "events_unsupported";
105
+ }>;
106
+ warning: z.ZodString;
107
+ }, z.core.$strip>>;
86
108
  stairId: z.ZodString;
87
109
  stairSegmentId: z.ZodString;
88
110
  destinationSlabId: z.ZodNullable<z.ZodString>;
@@ -1 +1 @@
1
- {"version":3,"file":"construction-tools.d.ts","sourceRoot":"","sources":["../../src/tools/construction-tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAcxE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAQpD,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;CAsBjC,CAAA;AAED,eAAO,MAAM,sBAAsB;;;;;;CAMlC,CAAA;AAED,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;CA0B3B,CAAA;AAED,eAAO,MAAM,gBAAgB;;;;;;CAM5B,CAAA;AAED,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;CAiCzC,CAAA;AAED,eAAO,MAAM,8BAA8B;;;;;;CAM1C,CAAA;AA4GD,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAmU1F"}
1
+ {"version":3,"file":"construction-tools.d.ts","sourceRoot":"","sources":["../../src/tools/construction-tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAcxE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAkBpD,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;CAsBjC,CAAA;AAED,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;CAOlC,CAAA;AAED,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;CA0B3B,CAAA;AAED,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;CAO5B,CAAA;AAED,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;CAiCzC,CAAA;AAED,eAAO,MAAM,8BAA8B;;;;;;;;;;;;;CAO1C,CAAA;AA4GD,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAgV1F"}
@@ -1,10 +1,20 @@
1
1
  import { resolveStairTotalRise } from '@pascal-app/core';
2
2
  import { CeilingNode, getActiveRoofHeight, LevelNode, RoofNode, RoofSegmentNode, SlabNode, StairNode, StairSegmentNode, WallNode, } from '@pascal-app/core/schema';
3
3
  import { z } from 'zod';
4
- import { publishLiveSceneSnapshot } from './live-sync';
4
+ import { ADDITIVE_TOOL_ANNOTATIONS, DESTRUCTIVE_TOOL_ANNOTATIONS } from './annotations';
5
+ import { liveSyncOutput, persistencePayload, publishLiveSceneSnapshot } from './live-sync';
5
6
  import { measurement } from './measurement';
6
7
  import { NodeIdSchema, Vec2Schema, Vec3Schema } from './schemas';
7
- const ROOF_TYPES = ['hip', 'gable', 'shed', 'gambrel', 'dutch', 'mansard', 'flat'];
8
+ const ROOF_TYPES = [
9
+ 'hip',
10
+ 'gable',
11
+ 'shed',
12
+ 'gambrel',
13
+ 'dutch',
14
+ 'mansard',
15
+ 'flat',
16
+ 'conical',
17
+ ];
8
18
  const RAILING_MODES = ['none', 'left', 'right', 'both'];
9
19
  export const createStoryShellInput = {
10
20
  levelId: NodeIdSchema,
@@ -35,6 +45,7 @@ export const createStoryShellOutput = {
35
45
  slabId: z.string().nullable(),
36
46
  ceilingId: z.string().nullable(),
37
47
  createdIds: z.array(z.string()),
48
+ ...liveSyncOutput,
38
49
  };
39
50
  export const createRoofInput = {
40
51
  levelId: NodeIdSchema,
@@ -67,6 +78,7 @@ export const createRoofOutput = {
67
78
  createdRoofLevelId: z.string().nullable(),
68
79
  roofId: z.string(),
69
80
  roofSegmentId: z.string(),
81
+ ...liveSyncOutput,
70
82
  };
71
83
  export const createStairBetweenLevelsInput = {
72
84
  fromLevelId: NodeIdSchema,
@@ -108,6 +120,7 @@ export const createStairBetweenLevelsOutput = {
108
120
  destinationSlabId: z.string().nullable(),
109
121
  sourceCeilingId: z.string().nullable(),
110
122
  openingPolygon: z.array(Vec2Schema),
123
+ ...liveSyncOutput,
111
124
  };
112
125
  function textResult(payload) {
113
126
  return {
@@ -190,6 +203,7 @@ export function registerConstructionTools(server, bridge) {
190
203
  description: 'Create one level-owned building shell from a footprint: perimeter walls plus optional slab and ceiling. Use once per story; do not make first-floor walls span multiple stories.',
191
204
  inputSchema: createStoryShellInput,
192
205
  outputSchema: createStoryShellOutput,
206
+ annotations: ADDITIVE_TOOL_ANNOTATIONS,
193
207
  }, async ({ levelId, footprint, wallHeight, wallThickness, createSlab, createCeiling, slabElevation, ceilingHeight, namePrefix, wallMaterialPreset, slabMaterialPreset, ceilingMaterialPreset, }) => {
194
208
  const level = assertNode(bridge, levelId, 'level');
195
209
  if (isRoofLevel(level)) {
@@ -246,13 +260,14 @@ export function registerConstructionTools(server, bridge) {
246
260
  patches.push({ op: 'create', node: ceiling, parentId: levelId });
247
261
  }
248
262
  const result = bridge.applyPatch(patches);
249
- await publishLiveSceneSnapshot(bridge, 'create_story_shell');
263
+ const persistence = await publishLiveSceneSnapshot(bridge, 'create_story_shell');
250
264
  return textResult({
251
265
  levelId,
252
266
  wallIds,
253
267
  slabId,
254
268
  ceilingId,
255
269
  createdIds: result.createdIds,
270
+ ...persistencePayload(persistence),
256
271
  });
257
272
  });
258
273
  server.registerTool('create_roof', {
@@ -260,10 +275,18 @@ export function registerConstructionTools(server, bridge) {
260
275
  description: 'Create a roof container with one roof segment. By default creates a dedicated roof level above the reference level so exploded/solo level views can isolate the roof.',
261
276
  inputSchema: createRoofInput,
262
277
  outputSchema: createRoofOutput,
278
+ annotations: ADDITIVE_TOOL_ANNOTATIONS,
263
279
  }, async ({ levelId, roofLevelId, useDedicatedRoofLevel, roofLevelLabel, roofLevelElevation, roofLevelHeight, center, width, depth, roofType, pitch, wallHeight, wallThickness, overhang, materialPreset, name, }) => {
280
+ const effectiveWidth = roofType === 'conical' ? Math.max(width, depth) : width;
281
+ const effectiveDepth = roofType === 'conical' ? effectiveWidth : depth;
264
282
  // Peak height is derived from pitch + footprint + type; we still
265
283
  // need it to size the auto-generated roof level container below.
266
- const peakHeight = getActiveRoofHeight({ roofType, pitch, width, depth });
284
+ const peakHeight = getActiveRoofHeight({
285
+ roofType,
286
+ pitch,
287
+ width: effectiveWidth,
288
+ depth: effectiveDepth,
289
+ });
267
290
  const referenceLevel = assertNode(bridge, levelId, 'level');
268
291
  const patches = [];
269
292
  let targetRoofLevelId = levelId;
@@ -294,8 +317,8 @@ export function registerConstructionTools(server, bridge) {
294
317
  }
295
318
  const segment = RoofSegmentNode.parse({
296
319
  roofType,
297
- width,
298
- depth,
320
+ width: effectiveWidth,
321
+ depth: effectiveDepth,
299
322
  wallHeight,
300
323
  pitch,
301
324
  wallThickness,
@@ -317,13 +340,14 @@ export function registerConstructionTools(server, bridge) {
317
340
  { op: 'create', node: roof, parentId: targetRoofLevelId },
318
341
  { op: 'create', node: segment, parentId: roof.id },
319
342
  ]);
320
- await publishLiveSceneSnapshot(bridge, 'create_roof');
343
+ const persistence = await publishLiveSceneSnapshot(bridge, 'create_roof');
321
344
  return textResult({
322
345
  referenceLevelId: levelId,
323
346
  roofLevelId: targetRoofLevelId,
324
347
  createdRoofLevelId,
325
348
  roofId: roof.id,
326
349
  roofSegmentId: segment.id,
350
+ ...persistencePayload(persistence),
327
351
  });
328
352
  });
329
353
  server.registerTool('create_stair_between_levels', {
@@ -331,6 +355,7 @@ export function registerConstructionTools(server, bridge) {
331
355
  description: 'Create a straight stair and a single rectangular manual opening in the destination slab/source ceiling. This disables stair auto-opening mode to avoid duplicate or irregular holes.',
332
356
  inputSchema: createStairBetweenLevelsInput,
333
357
  outputSchema: createStairBetweenLevelsOutput,
358
+ annotations: DESTRUCTIVE_TOOL_ANNOTATIONS,
334
359
  }, async ({ fromLevelId, toLevelId, position, rotation, width, runLength, totalRise, stepCount, railingMode, destinationSlabId, sourceCeilingId, createDestinationSlabOpening, createSourceCeilingOpening, openingWidth, openingLength, openingOffset, openingCenter, openingRotation, materialPreset, name, }) => {
335
360
  const fromLevel = assertNode(bridge, fromLevelId, 'level');
336
361
  const toLevel = assertNode(bridge, toLevelId, 'level');
@@ -408,13 +433,14 @@ export function registerConstructionTools(server, bridge) {
408
433
  });
409
434
  }
410
435
  bridge.applyPatch(patches);
411
- await publishLiveSceneSnapshot(bridge, 'create_stair_between_levels');
436
+ const persistence = await publishLiveSceneSnapshot(bridge, 'create_stair_between_levels');
412
437
  return textResult({
413
438
  stairId: stair.id,
414
439
  stairSegmentId: segment.id,
415
440
  destinationSlabId: destinationSlab?.id ?? null,
416
441
  sourceCeilingId: sourceCeiling?.id ?? null,
417
442
  openingPolygon,
443
+ ...persistencePayload(persistence),
418
444
  });
419
445
  });
420
446
  }
@@ -8,6 +8,13 @@ export declare const createLevelInput: {
8
8
  label: z.ZodOptional<z.ZodString>;
9
9
  };
10
10
  export declare const createLevelOutput: {
11
+ persistence: z.ZodOptional<z.ZodObject<{
12
+ status: z.ZodEnum<{
13
+ unbound: "unbound";
14
+ events_unsupported: "events_unsupported";
15
+ }>;
16
+ warning: z.ZodString;
17
+ }, z.core.$strip>>;
11
18
  levelId: z.ZodString;
12
19
  };
13
20
  export declare function registerCreateLevel(server: McpServer, bridge: SceneOperations): void;
@@ -1 +1 @@
1
- {"version":3,"file":"create-level.d.ts","sourceRoot":"","sources":["../../src/tools/create-level.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAIxE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAMpD,eAAO,MAAM,gBAAgB;;;;;CAW5B,CAAA;AAED,eAAO,MAAM,iBAAiB;;CAE7B,CAAA;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CA+CpF"}
1
+ {"version":3,"file":"create-level.d.ts","sourceRoot":"","sources":["../../src/tools/create-level.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAIxE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAOpD,eAAO,MAAM,gBAAgB;;;;;CAW5B,CAAA;AAED,eAAO,MAAM,iBAAiB;;;;;;;;;CAG7B,CAAA;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAgDpF"}
@@ -1,8 +1,9 @@
1
1
  import { DEFAULT_LEVEL_HEIGHT } from '@pascal-app/core';
2
2
  import { LevelNode } from '@pascal-app/core/schema';
3
3
  import { z } from 'zod';
4
+ import { ADDITIVE_TOOL_ANNOTATIONS } from './annotations';
4
5
  import { ErrorCode, throwMcpError } from './errors';
5
- import { publishLiveSceneSnapshot } from './live-sync';
6
+ import { liveSyncOutput, persistencePayload, publishLiveSceneSnapshot } from './live-sync';
6
7
  import { measurement } from './measurement';
7
8
  import { NodeIdSchema } from './schemas';
8
9
  export const createLevelInput = {
@@ -19,6 +20,7 @@ export const createLevelInput = {
19
20
  };
20
21
  export const createLevelOutput = {
21
22
  levelId: z.string(),
23
+ ...liveSyncOutput,
22
24
  };
23
25
  export function registerCreateLevel(server, bridge) {
24
26
  server.registerTool('create_level', {
@@ -26,6 +28,7 @@ export function registerCreateLevel(server, bridge) {
26
28
  description: "Append a new level above the given building's current top level. height is stored as the level's floor-to-floor storey height.",
27
29
  inputSchema: createLevelInput,
28
30
  outputSchema: createLevelOutput,
31
+ annotations: ADDITIVE_TOOL_ANNOTATIONS,
29
32
  }, async ({ buildingId, height, label }) => {
30
33
  const parent = bridge.getNode(buildingId);
31
34
  if (!parent) {
@@ -50,8 +53,8 @@ export function registerCreateLevel(server, bridge) {
50
53
  ...(label !== undefined ? { name: label } : {}),
51
54
  });
52
55
  const id = bridge.createNode(levelNode, buildingId);
53
- await publishLiveSceneSnapshot(bridge, 'create_level');
54
- const payload = { levelId: id };
56
+ const persistence = await publishLiveSceneSnapshot(bridge, 'create_level');
57
+ const payload = { levelId: id, ...persistencePayload(persistence) };
55
58
  return {
56
59
  content: [{ type: 'text', text: JSON.stringify(payload) }],
57
60
  structuredContent: payload,
@@ -9,6 +9,13 @@ export declare const createWallInput: {
9
9
  height: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>>;
10
10
  };
11
11
  export declare const createWallOutput: {
12
+ persistence: z.ZodOptional<z.ZodObject<{
13
+ status: z.ZodEnum<{
14
+ unbound: "unbound";
15
+ events_unsupported: "events_unsupported";
16
+ }>;
17
+ warning: z.ZodString;
18
+ }, z.core.$strip>>;
12
19
  wallId: z.ZodString;
13
20
  };
14
21
  export declare function registerCreateWall(server: McpServer, bridge: SceneOperations): void;
@@ -1 +1 @@
1
- {"version":3,"file":"create-wall.d.ts","sourceRoot":"","sources":["../../src/tools/create-wall.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAGxE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAMpD,eAAO,MAAM,eAAe;;;;;;CAS3B,CAAA;AAED,eAAO,MAAM,gBAAgB;;CAE5B,CAAA;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAgDnF"}
1
+ {"version":3,"file":"create-wall.d.ts","sourceRoot":"","sources":["../../src/tools/create-wall.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAGxE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAOpD,eAAO,MAAM,eAAe;;;;;;CAS3B,CAAA;AAED,eAAO,MAAM,gBAAgB;;;;;;;;;CAG5B,CAAA;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAiDnF"}
@@ -1,7 +1,8 @@
1
1
  import { WallNode } from '@pascal-app/core/schema';
2
2
  import { z } from 'zod';
3
+ import { ADDITIVE_TOOL_ANNOTATIONS } from './annotations';
3
4
  import { ErrorCode, throwMcpError } from './errors';
4
- import { publishLiveSceneSnapshot } from './live-sync';
5
+ import { liveSyncOutput, persistencePayload, publishLiveSceneSnapshot } from './live-sync';
5
6
  import { measurement } from './measurement';
6
7
  import { NodeIdSchema, Vec2Schema } from './schemas';
7
8
  export const createWallInput = {
@@ -16,6 +17,7 @@ export const createWallInput = {
16
17
  };
17
18
  export const createWallOutput = {
18
19
  wallId: z.string(),
20
+ ...liveSyncOutput,
19
21
  };
20
22
  export function registerCreateWall(server, bridge) {
21
23
  server.registerTool('create_wall', {
@@ -23,6 +25,7 @@ export function registerCreateWall(server, bridge) {
23
25
  description: 'Create a new wall on the given level between two 2D points. Thickness and height default to the core library defaults when omitted.',
24
26
  inputSchema: createWallInput,
25
27
  outputSchema: createWallOutput,
28
+ annotations: ADDITIVE_TOOL_ANNOTATIONS,
26
29
  }, async ({ levelId, start, end, thickness, height }) => {
27
30
  const parent = bridge.getNode(levelId);
28
31
  if (!parent) {
@@ -44,8 +47,8 @@ export function registerCreateWall(server, bridge) {
44
47
  ...(height !== undefined ? { height } : {}),
45
48
  });
46
49
  const id = bridge.createNode(wall, levelId);
47
- await publishLiveSceneSnapshot(bridge, 'create_wall');
48
- const payload = { wallId: id };
50
+ const persistence = await publishLiveSceneSnapshot(bridge, 'create_wall');
51
+ const payload = { wallId: id, ...persistencePayload(persistence) };
49
52
  return {
50
53
  content: [{ type: 'text', text: JSON.stringify(payload) }],
51
54
  structuredContent: payload,
@@ -12,6 +12,13 @@ export declare const cutOpeningInput: {
12
12
  height: z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>;
13
13
  };
14
14
  export declare const cutOpeningOutput: {
15
+ persistence: z.ZodOptional<z.ZodObject<{
16
+ status: z.ZodEnum<{
17
+ unbound: "unbound";
18
+ events_unsupported: "events_unsupported";
19
+ }>;
20
+ warning: z.ZodString;
21
+ }, z.core.$strip>>;
15
22
  openingId: z.ZodString;
16
23
  };
17
24
  export declare function registerCutOpening(server: McpServer, bridge: SceneOperations): void;
@@ -1 +1 @@
1
- {"version":3,"file":"cut-opening.d.ts","sourceRoot":"","sources":["../../src/tools/cut-opening.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAGxE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAOpD,eAAO,MAAM,eAAe;;;;;;;;;CAM3B,CAAA;AAED,eAAO,MAAM,gBAAgB;;CAE5B,CAAA;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAyDnF"}
1
+ {"version":3,"file":"cut-opening.d.ts","sourceRoot":"","sources":["../../src/tools/cut-opening.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAGxE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAQpD,eAAO,MAAM,eAAe;;;;;;;;;CAM3B,CAAA;AAED,eAAO,MAAM,gBAAgB;;;;;;;;;CAG5B,CAAA;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CA0DnF"}
@@ -1,8 +1,9 @@
1
1
  import { DoorNode, WindowNode } from '@pascal-app/core/schema';
2
2
  import { z } from 'zod';
3
+ import { ADDITIVE_TOOL_ANNOTATIONS } from './annotations';
3
4
  import { ErrorCode, throwMcpError } from './errors';
4
5
  import { wallLength, wallLocalXFromT } from './geometry';
5
- import { publishLiveSceneSnapshot } from './live-sync';
6
+ import { liveSyncOutput, persistencePayload, publishLiveSceneSnapshot } from './live-sync';
6
7
  import { measurement } from './measurement';
7
8
  import { NodeIdSchema } from './schemas';
8
9
  export const cutOpeningInput = {
@@ -14,6 +15,7 @@ export const cutOpeningInput = {
14
15
  };
15
16
  export const cutOpeningOutput = {
16
17
  openingId: z.string(),
18
+ ...liveSyncOutput,
17
19
  };
18
20
  export function registerCutOpening(server, bridge) {
19
21
  server.registerTool('cut_opening', {
@@ -21,6 +23,7 @@ export function registerCutOpening(server, bridge) {
21
23
  description: 'Cut a door or window opening into an existing wall. position is a parametric 0..1 offset along the wall centreline.',
22
24
  inputSchema: cutOpeningInput,
23
25
  outputSchema: cutOpeningOutput,
26
+ annotations: ADDITIVE_TOOL_ANNOTATIONS,
24
27
  }, async ({ wallId, type, position, width, height }) => {
25
28
  const wall = bridge.getNode(wallId);
26
29
  if (!wall) {
@@ -48,8 +51,8 @@ export function registerCutOpening(server, bridge) {
48
51
  position: [base.position[0], 0.9 + height / 2, 0],
49
52
  });
50
53
  const id = bridge.createNode(opening, wallId);
51
- await publishLiveSceneSnapshot(bridge, 'cut_opening');
52
- const payload = { openingId: id };
54
+ const persistence = await publishLiveSceneSnapshot(bridge, 'cut_opening');
55
+ const payload = { openingId: id, ...persistencePayload(persistence) };
53
56
  return {
54
57
  content: [{ type: 'text', text: JSON.stringify(payload) }],
55
58
  structuredContent: payload,
@@ -6,6 +6,13 @@ export declare const deleteNodeInput: {
6
6
  cascade: z.ZodOptional<z.ZodBoolean>;
7
7
  };
8
8
  export declare const deleteNodeOutput: {
9
+ persistence: z.ZodOptional<z.ZodObject<{
10
+ status: z.ZodEnum<{
11
+ unbound: "unbound";
12
+ events_unsupported: "events_unsupported";
13
+ }>;
14
+ warning: z.ZodString;
15
+ }, z.core.$strip>>;
9
16
  deletedIds: z.ZodArray<z.ZodString>;
10
17
  };
11
18
  export declare function registerDeleteNode(server: McpServer, bridge: SceneOperations): void;
@@ -1 +1 @@
1
- {"version":3,"file":"delete-node.d.ts","sourceRoot":"","sources":["../../src/tools/delete-node.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAExE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAKpD,eAAO,MAAM,eAAe;;;CAG3B,CAAA;AAED,eAAO,MAAM,gBAAgB;;CAE5B,CAAA;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CA6BnF"}
1
+ {"version":3,"file":"delete-node.d.ts","sourceRoot":"","sources":["../../src/tools/delete-node.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAExE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAMpD,eAAO,MAAM,eAAe;;;CAG3B,CAAA;AAED,eAAO,MAAM,gBAAgB;;;;;;;;;CAG5B,CAAA;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CA8BnF"}
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
+ import { DESTRUCTIVE_TOOL_ANNOTATIONS } from './annotations';
2
3
  import { ErrorCode, throwMcpError } from './errors';
3
- import { publishLiveSceneSnapshot } from './live-sync';
4
+ import { liveSyncOutput, persistencePayload, publishLiveSceneSnapshot } from './live-sync';
4
5
  import { NodeIdSchema } from './schemas';
5
6
  export const deleteNodeInput = {
6
7
  id: NodeIdSchema,
@@ -8,6 +9,7 @@ export const deleteNodeInput = {
8
9
  };
9
10
  export const deleteNodeOutput = {
10
11
  deletedIds: z.array(z.string()),
12
+ ...liveSyncOutput,
11
13
  };
12
14
  export function registerDeleteNode(server, bridge) {
13
15
  server.registerTool('delete_node', {
@@ -15,6 +17,7 @@ export function registerDeleteNode(server, bridge) {
15
17
  description: 'Delete a node. If it has children, pass `cascade: true` to delete descendants recursively.',
16
18
  inputSchema: deleteNodeInput,
17
19
  outputSchema: deleteNodeOutput,
20
+ annotations: DESTRUCTIVE_TOOL_ANNOTATIONS,
18
21
  }, async ({ id, cascade }) => {
19
22
  const node = bridge.getNode(id);
20
23
  if (!node) {
@@ -22,8 +25,8 @@ export function registerDeleteNode(server, bridge) {
22
25
  }
23
26
  try {
24
27
  const removed = bridge.deleteNode(id, cascade ?? false);
25
- await publishLiveSceneSnapshot(bridge, 'delete_node');
26
- const payload = { deletedIds: removed };
28
+ const persistence = await publishLiveSceneSnapshot(bridge, 'delete_node');
29
+ const payload = { deletedIds: removed, ...persistencePayload(persistence) };
27
30
  return {
28
31
  content: [{ type: 'text', text: JSON.stringify(payload) }],
29
32
  structuredContent: payload,
@@ -1 +1 @@
1
- {"version":3,"file":"describe-node.d.ts","sourceRoot":"","sources":["../../src/tools/describe-node.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAGxE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAKpD,eAAO,MAAM,iBAAiB;;CAE7B,CAAA;AAED,eAAO,MAAM,kBAAkB;;;;;;;;CAQ9B,CAAA;AA0CD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAgDrF"}
1
+ {"version":3,"file":"describe-node.d.ts","sourceRoot":"","sources":["../../src/tools/describe-node.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAGxE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAMpD,eAAO,MAAM,iBAAiB;;CAE7B,CAAA;AAED,eAAO,MAAM,kBAAkB;;;;;;;;CAQ9B,CAAA;AA0CD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAiDrF"}
@@ -1,5 +1,6 @@
1
1
  import { resolveCeilingHeight } from '@pascal-app/core';
2
2
  import { z } from 'zod';
3
+ import { READ_ONLY_TOOL_ANNOTATIONS } from './annotations';
3
4
  import { ErrorCode, throwMcpError } from './errors';
4
5
  import { resolveReportedWallHeight } from './scene-query';
5
6
  import { NodeIdSchema } from './schemas';
@@ -60,6 +61,7 @@ export function registerDescribeNode(server, bridge) {
60
61
  description: 'Return a structured summary of a node including its ancestry, children IDs, key properties, and a short human description.',
61
62
  inputSchema: describeNodeInput,
62
63
  outputSchema: describeNodeOutput,
64
+ annotations: READ_ONLY_TOOL_ANNOTATIONS,
63
65
  }, async ({ id }) => {
64
66
  const node = bridge.getNode(id);
65
67
  if (!node) {
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * See docs/layout-clearance-error-log.md for pitfalls (levels, gap sign, scale).
8
8
  */
9
- import type { AnyNode } from '@pascal-app/core/schema';
9
+ import { type AnyNode } from '@pascal-app/core/schema';
10
10
  import { type Vec2 } from './geometry';
11
11
  export type PlanAabb = {
12
12
  minX: number;
@@ -14,6 +14,17 @@ export type PlanAabb = {
14
14
  minZ: number;
15
15
  maxZ: number;
16
16
  };
17
+ export type ItemFootprintFailureReason = 'missing_dimensions' | 'non_finite_position' | 'non_finite_rotation' | 'non_finite_dimensions' | 'non_positive_plan_dimensions' | 'non_finite_scale' | 'zero_plan_scale' | 'non_planar_rotation' | 'unsupported_attachment';
18
+ export type ItemFootprintInspection = {
19
+ ok: true;
20
+ aabb: PlanAabb;
21
+ sourceDimensions: [number, number, number];
22
+ effectiveDimensions: [number, number, number];
23
+ rotationY: number;
24
+ } | {
25
+ ok: false;
26
+ reason: ItemFootprintFailureReason;
27
+ };
17
28
  export type DoorKeepout = {
18
29
  doorId: string;
19
30
  wallId: string;
@@ -63,8 +74,13 @@ export declare function resolveNodeLevelId(nodeId: string, byId: Map<string, Any
63
74
  * Prefer scaled dimensions when the node is available.
64
75
  */
65
76
  export declare function itemPlanAabb(position: [number, number, number] | number[], dimensions: [number, number, number] | number[] | undefined, rotationYRad?: number): PlanAabb;
66
- /** Footprint for a scene item node (uses getScaledDimensions). */
77
+ /** Scaled plan footprint used by legacy placement and door-clearance callers. */
67
78
  export declare function itemNodePlanAabb(node: AnyNode): PlanAabb | null;
79
+ export declare function inspectItemPlanFootprint(node: Extract<AnyNode, {
80
+ type: 'item';
81
+ }>, options?: {
82
+ floorOnly?: boolean;
83
+ }): ItemFootprintInspection;
68
84
  /**
69
85
  * Build a rectangular keep-out around a wall door, extruded perpendicular to the wall
70
86
  * on both faces so either swing side is protected.
@@ -1 +1 @@
1
- {"version":3,"file":"door-clearance.d.ts","sourceRoot":"","sources":["../../src/tools/door-clearance.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"door-clearance.d.ts","sourceRoot":"","sources":["../../src/tools/door-clearance.ts"],"names":[],"mappings":"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"}
@@ -34,12 +34,31 @@ export function resolveNodeLevelId(nodeId, byId) {
34
34
  if (current.type === 'level')
35
35
  return current.id;
36
36
  const parentId = current.parentId;
37
- if (!parentId)
38
- return null;
39
- current = byId.get(parentId);
37
+ if (parentId && byId.has(parentId)) {
38
+ current = byId.get(parentId);
39
+ continue;
40
+ }
41
+ current = findParentByChildren(current.id, byId);
40
42
  }
41
43
  return null;
42
44
  }
45
+ function findParentByChildren(nodeId, byId) {
46
+ for (const candidate of byId.values()) {
47
+ if (!('children' in candidate) || !Array.isArray(candidate.children))
48
+ continue;
49
+ const containsNode = candidate.children.some((child) => {
50
+ if (typeof child === 'string')
51
+ return child === nodeId;
52
+ return (child !== null &&
53
+ typeof child === 'object' &&
54
+ 'id' in child &&
55
+ child.id === nodeId);
56
+ });
57
+ if (containsNode)
58
+ return candidate;
59
+ }
60
+ return undefined;
61
+ }
43
62
  /**
44
63
  * Axis-aligned item footprint in plan (x/z), rotation-aware.
45
64
  * Prefer scaled dimensions when the node is available.
@@ -59,13 +78,88 @@ export function itemPlanAabb(position, dimensions, rotationYRad = 0) {
59
78
  maxZ: z + halfD,
60
79
  };
61
80
  }
62
- /** Footprint for a scene item node (uses getScaledDimensions). */
81
+ /** Scaled plan footprint used by legacy placement and door-clearance callers. */
63
82
  export function itemNodePlanAabb(node) {
64
83
  if (node.type !== 'item')
65
84
  return null;
66
- const [w, , d] = getScaledDimensions(node);
67
- const rotY = Array.isArray(node.rotation) ? (node.rotation[1] ?? 0) : 0;
68
- return itemPlanAabb(node.position, [w, 0, d], rotY);
85
+ if (!Array.isArray(node.asset.dimensions))
86
+ return null;
87
+ const [width, height, depth] = getScaledDimensions(node);
88
+ const rotationY = Array.isArray(node.rotation) ? (node.rotation[1] ?? 0) : 0;
89
+ if (![node.position[0], node.position[2], width, height, depth, rotationY].every(Number.isFinite)) {
90
+ return null;
91
+ }
92
+ return itemPlanAabb(node.position, [Math.abs(width), Math.abs(height), Math.abs(depth)], rotationY);
93
+ }
94
+ export function inspectItemPlanFootprint(node, options) {
95
+ if (options?.floorOnly &&
96
+ (node.asset.attachTo === 'wall' ||
97
+ node.asset.attachTo === 'wall-side' ||
98
+ node.asset.attachTo === 'ceiling')) {
99
+ return { ok: false, reason: 'unsupported_attachment' };
100
+ }
101
+ const position = node.position;
102
+ if (!Array.isArray(position)) {
103
+ return { ok: false, reason: 'non_finite_position' };
104
+ }
105
+ const x = position[0];
106
+ const y = position[1];
107
+ const z = position[2];
108
+ if (!(Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z))) {
109
+ return { ok: false, reason: 'non_finite_position' };
110
+ }
111
+ const rotation = Array.isArray(node.rotation) ? node.rotation : [0, 0, 0];
112
+ const rotationX = rotation[0] ?? Number.NaN;
113
+ const rotationY = rotation[1] ?? Number.NaN;
114
+ const rotationZ = rotation[2] ?? Number.NaN;
115
+ if (!(Number.isFinite(rotationX) && Number.isFinite(rotationY) && Number.isFinite(rotationZ))) {
116
+ return { ok: false, reason: 'non_finite_rotation' };
117
+ }
118
+ if (Math.abs(rotationX) > 1e-6 || Math.abs(rotationZ) > 1e-6) {
119
+ return { ok: false, reason: 'non_planar_rotation' };
120
+ }
121
+ const dimensions = node.asset.dimensions;
122
+ if (!Array.isArray(dimensions) || dimensions.length !== 3) {
123
+ return { ok: false, reason: 'missing_dimensions' };
124
+ }
125
+ const width = dimensions[0] ?? Number.NaN;
126
+ const height = dimensions[1] ?? Number.NaN;
127
+ const depth = dimensions[2] ?? Number.NaN;
128
+ if (!(Number.isFinite(width) && Number.isFinite(height) && Number.isFinite(depth))) {
129
+ return { ok: false, reason: 'non_finite_dimensions' };
130
+ }
131
+ if (width <= 0 || depth <= 0) {
132
+ return { ok: false, reason: 'non_positive_plan_dimensions' };
133
+ }
134
+ const scale = Array.isArray(node.scale) ? node.scale : [1, 1, 1];
135
+ const scaleX = scale[0] ?? Number.NaN;
136
+ const scaleY = scale[1] ?? Number.NaN;
137
+ const scaleZ = scale[2] ?? Number.NaN;
138
+ if (!(Number.isFinite(scaleX) && Number.isFinite(scaleY) && Number.isFinite(scaleZ))) {
139
+ return { ok: false, reason: 'non_finite_scale' };
140
+ }
141
+ if (scaleX === 0 || scaleZ === 0) {
142
+ return { ok: false, reason: 'zero_plan_scale' };
143
+ }
144
+ const [scaledWidth, scaledHeight, scaledDepth] = getScaledDimensions(node);
145
+ const effectiveDimensions = [
146
+ Math.abs(scaledWidth),
147
+ Math.abs(scaledHeight),
148
+ Math.abs(scaledDepth),
149
+ ];
150
+ if (!effectiveDimensions.every(Number.isFinite)) {
151
+ return { ok: false, reason: 'non_finite_dimensions' };
152
+ }
153
+ if (effectiveDimensions[0] <= 0 || effectiveDimensions[2] <= 0) {
154
+ return { ok: false, reason: 'non_positive_plan_dimensions' };
155
+ }
156
+ return {
157
+ ok: true,
158
+ aabb: itemPlanAabb(node.position, effectiveDimensions, rotationY),
159
+ sourceDimensions: [width, height, depth],
160
+ effectiveDimensions,
161
+ rotationY,
162
+ };
69
163
  }
70
164
  /**
71
165
  * Build a rectangular keep-out around a wall door, extruded perpendicular to the wall