@pascal-app/mcp 0.3.0 → 0.3.2

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 (40) hide show
  1. package/README.md +71 -0
  2. package/dist/bin/pascal-mcp.js +0 -0
  3. package/dist/bridge/scene-bridge.d.ts.map +1 -1
  4. package/dist/bridge/scene-bridge.js +6 -0
  5. package/dist/operations/scene-operations.js +1 -0
  6. package/dist/resources/constraints.js +1 -1
  7. package/dist/tools/construction-tools.d.ts +21 -21
  8. package/dist/tools/construction-tools.d.ts.map +1 -1
  9. package/dist/tools/construction-tools.js +45 -19
  10. package/dist/tools/create-level.d.ts +1 -1
  11. package/dist/tools/create-level.d.ts.map +1 -1
  12. package/dist/tools/create-level.js +5 -1
  13. package/dist/tools/create-wall.d.ts +2 -2
  14. package/dist/tools/create-wall.d.ts.map +1 -1
  15. package/dist/tools/create-wall.js +6 -2
  16. package/dist/tools/cut-opening.d.ts +3 -3
  17. package/dist/tools/cut-opening.d.ts.map +1 -1
  18. package/dist/tools/cut-opening.js +3 -2
  19. package/dist/tools/find-nodes.d.ts +2 -2
  20. package/dist/tools/find-nodes.js +1 -1
  21. package/dist/tools/measurement.d.ts +37 -0
  22. package/dist/tools/measurement.d.ts.map +1 -0
  23. package/dist/tools/measurement.js +94 -0
  24. package/dist/tools/photo-to-scene/photo-to-scene.d.ts +2 -2
  25. package/dist/tools/photo-to-scene/photo-to-scene.d.ts.map +1 -1
  26. package/dist/tools/photo-to-scene/photo-to-scene.js +9 -2
  27. package/dist/tools/place-item.d.ts +1 -1
  28. package/dist/tools/place-item.d.ts.map +1 -1
  29. package/dist/tools/place-item.js +2 -1
  30. package/dist/tools/room-tools.d.ts +8 -8
  31. package/dist/tools/room-tools.d.ts.map +1 -1
  32. package/dist/tools/room-tools.js +17 -7
  33. package/dist/tools/scene-query.js +2 -2
  34. package/examples/coordinate-conventions-demo.json +968 -0
  35. package/examples/coordinate-conventions-demo.md +68 -0
  36. package/examples/embed-in-agent.ts +82 -0
  37. package/examples/generate-apartment.md +242 -0
  38. package/examples/photo-to-scene.md +119 -0
  39. package/examples/renovate-from-photos.md +202 -0
  40. package/package.json +8 -6
package/README.md CHANGED
@@ -227,6 +227,77 @@ console.log(scene)
227
227
  See [`examples/embed-in-agent.ts`](./examples/embed-in-agent.ts) for a
228
228
  compilable version.
229
229
 
230
+ ## Coordinate conventions
231
+
232
+ Pascal is a **right-handed** scene where **X and Z form the ground plane and Y
233
+ is up**. Lengths are in **metres**; rotations are **radians**, stored as Euler
234
+ `[x, y, z]` tuples.
235
+
236
+ **Plan → world.** Every 2-D point you pass is a level/building-local
237
+ ground-plane coordinate
238
+ `[x, z]` — this includes `wall.start` / `wall.end` and the `polygon` / `holes`
239
+ arrays of `slab`, `zone`, and `ceiling`. With the default identity building
240
+ transform, it appears in world space as:
241
+
242
+ ```
243
+ [x, z] → (x, y, z) // the 2nd component is world Z (depth), not "up"
244
+ ```
245
+
246
+ There is no sign flip in the stored convention: tooling consumes the second
247
+ component as world Z directly. The vertical `y` starts from the owning level's
248
+ stacked height as computed by the level system from accumulated level heights,
249
+ plus the element's own height; slabs additionally carry an absolute
250
+ `elevation`.
251
+
252
+ **Heads-up when you compute coordinates outside the editor.** Pascal's
253
+ viewports apply their own rotations on top of the world axes: the 2-D plan
254
+ panel rotates its content by the user's view rotation (north-aligned = 0°,
255
+ `FLOORPLAN_VIEW_ROTATION_DEG` baseline, north = world −Z), and
256
+ the 3-D "top-down" snap preserves the camera's current azimuth, so when invoked
257
+ from the iso default position, world and screen axes are offset by ~45° until
258
+ you orbit to an axis-aligned view. So a layout authored as if
259
+ *"Y = north, viewed top-down"* — common in land surveys, north-up site plans,
260
+ and 2-D plotting libraries — will arrive **rotated** relative to its source
261
+ when viewed in Pascal (and possibly further reflected, depending on which
262
+ viewport and camera state you're in). The editor's own 2-D and 3-D tools are
263
+ internally consistent with their stored coordinates, so this only affects
264
+ geometry authored programmatically. To verify orientation before trusting
265
+ externally-computed coordinates, place a scaled guide image at known anchor
266
+ points and check alignment; apply whatever rotation (or reflection) your
267
+ authoring side needs to match.
268
+
269
+ A worked demonstration of all of this — axis-aligned baseline, the rotated
270
+ 30° example below, and a paired "page-intent vs world-result" L for the
271
+ external-coordinate gotcha — lives in
272
+ [`examples/coordinate-conventions-demo.md`](./examples/coordinate-conventions-demo.md)
273
+ and [`examples/coordinate-conventions-demo.json`](./examples/coordinate-conventions-demo.json).
274
+ Load the JSON with
275
+ `pascal-mcp --stdio --scene examples/coordinate-conventions-demo.json`.
276
+
277
+ **Example — a 6 × 4 m slab rotated 30° about its first corner** (coordinates
278
+ rounded to 3 dp; sides ≈ 6 m / 4 m; not axis-aligned, so the mapping is
279
+ actually exercised):
280
+
281
+ ```json
282
+ {
283
+ "op": "create",
284
+ "parentId": "<levelId>",
285
+ "node": {
286
+ "type": "slab",
287
+ "elevation": 0.0,
288
+ "polygon": [[0, 0], [5.196, 3.0], [3.196, 6.464], [-2.0, 3.464]]
289
+ }
290
+ }
291
+ ```
292
+
293
+ This lands flat on the ground (Y = 0), about 6 m along a heading 30° off the +X
294
+ axis and 4 m along its perpendicular — i.e. occupying world (x, z) directly.
295
+
296
+ One separate gotcha: wall-attached coordinates are wall-local, not plan
297
+ coordinates. Stored door/window `position[0]`, and `place_item` `position[0]`
298
+ when the target is a wall, are metres along the wall; wall-attached rotations
299
+ are wall-local too.
300
+
230
301
  ## Tools
231
302
 
232
303
  All tools validate their inputs and outputs with Zod. Mutation tools are
File without changes
@@ -1 +1 @@
1
- {"version":3,"file":"scene-bridge.d.ts","sourceRoot":"","sources":["../../src/bridge/scene-bridge.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"scene-bridge.d.ts","sourceRoot":"","sources":["../../src/bridge/scene-bridge.ts"],"names":[],"mappings":"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"}
@@ -48,6 +48,9 @@ export class SceneBridge {
48
48
  nodes: state.nodes,
49
49
  rootNodeIds: state.rootNodeIds,
50
50
  collections: state.collections ?? {},
51
+ ...(state.hasExplicitPluginInstallState || state.installedPlugins.length > 0
52
+ ? { installedPlugins: state.installedPlugins }
53
+ : {}),
51
54
  }));
52
55
  }
53
56
  /**
@@ -88,6 +91,9 @@ export class SceneBridge {
88
91
  }
89
92
  }
90
93
  this.setScene(nodes, rootNodeIds);
94
+ if (Array.isArray(obj.installedPlugins)) {
95
+ useScene.getState().setInstalledPlugins(obj.installedPlugins.filter((id) => typeof id === 'string'), { explicit: true });
96
+ }
91
97
  }
92
98
  /** Read a single node, or `null` if not present. */
93
99
  getNode(id) {
@@ -56,6 +56,7 @@ class SceneOperationsFacade {
56
56
  nodes: exported.nodes,
57
57
  rootNodeIds: exported.rootNodeIds,
58
58
  collections: exported.collections,
59
+ installedPlugins: exported.installedPlugins,
59
60
  };
60
61
  }
61
62
  loadJSON(json) {
@@ -15,7 +15,7 @@ const EMPTY_MITER_DATA = {
15
15
  };
16
16
  function buildPayload(bridge, levelId) {
17
17
  const level = bridge.getNode(levelId);
18
- if (!level || level.type !== 'level') {
18
+ if (level?.type !== 'level') {
19
19
  return {
20
20
  error: 'level_not_found',
21
21
  levelId,
@@ -4,12 +4,12 @@ import type { SceneOperations } from '../operations';
4
4
  export declare const createStoryShellInput: {
5
5
  levelId: z.ZodString;
6
6
  footprint: z.ZodArray<z.ZodArray<z.ZodNumber>>;
7
- wallHeight: z.ZodDefault<z.ZodNumber>;
8
- wallThickness: z.ZodDefault<z.ZodNumber>;
7
+ wallHeight: z.ZodDefault<z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>>;
8
+ wallThickness: z.ZodDefault<z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>>;
9
9
  createSlab: z.ZodDefault<z.ZodBoolean>;
10
10
  createCeiling: z.ZodDefault<z.ZodBoolean>;
11
- slabElevation: z.ZodDefault<z.ZodNumber>;
12
- ceilingHeight: z.ZodOptional<z.ZodNumber>;
11
+ slabElevation: z.ZodDefault<z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>>;
12
+ ceilingHeight: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>>;
13
13
  namePrefix: z.ZodOptional<z.ZodString>;
14
14
  wallMaterialPreset: z.ZodOptional<z.ZodString>;
15
15
  slabMaterialPreset: z.ZodOptional<z.ZodString>;
@@ -28,10 +28,10 @@ export declare const createRoofInput: {
28
28
  useDedicatedRoofLevel: z.ZodDefault<z.ZodBoolean>;
29
29
  roofLevelLabel: z.ZodDefault<z.ZodString>;
30
30
  roofLevelElevation: z.ZodOptional<z.ZodNumber>;
31
- roofLevelHeight: z.ZodOptional<z.ZodNumber>;
31
+ roofLevelHeight: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>>;
32
32
  center: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
33
- width: z.ZodNumber;
34
- depth: z.ZodNumber;
33
+ width: z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>;
34
+ depth: z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>;
35
35
  roofType: z.ZodDefault<z.ZodEnum<{
36
36
  flat: "flat";
37
37
  hip: "hip";
@@ -41,10 +41,10 @@ export declare const createRoofInput: {
41
41
  dutch: "dutch";
42
42
  mansard: "mansard";
43
43
  }>>;
44
- pitch: z.ZodDefault<z.ZodNumber>;
45
- wallHeight: z.ZodDefault<z.ZodNumber>;
46
- wallThickness: z.ZodDefault<z.ZodNumber>;
47
- overhang: z.ZodDefault<z.ZodNumber>;
44
+ pitch: z.ZodDefault<z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>>;
45
+ wallHeight: z.ZodDefault<z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>>;
46
+ wallThickness: z.ZodDefault<z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>>;
47
+ overhang: z.ZodDefault<z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>>;
48
48
  materialPreset: z.ZodOptional<z.ZodString>;
49
49
  name: z.ZodOptional<z.ZodString>;
50
50
  };
@@ -59,26 +59,26 @@ export declare const createStairBetweenLevelsInput: {
59
59
  fromLevelId: z.ZodString;
60
60
  toLevelId: z.ZodString;
61
61
  position: z.ZodArray<z.ZodNumber>;
62
- rotation: z.ZodDefault<z.ZodNumber>;
63
- width: z.ZodDefault<z.ZodNumber>;
64
- runLength: z.ZodDefault<z.ZodNumber>;
65
- totalRise: z.ZodDefault<z.ZodNumber>;
62
+ rotation: z.ZodDefault<z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>>;
63
+ width: z.ZodDefault<z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>>;
64
+ runLength: z.ZodDefault<z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>>;
65
+ totalRise: z.ZodDefault<z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>>;
66
66
  stepCount: z.ZodDefault<z.ZodNumber>;
67
67
  railingMode: z.ZodDefault<z.ZodEnum<{
68
68
  none: "none";
69
- left: "left";
70
- right: "right";
71
69
  both: "both";
70
+ right: "right";
71
+ left: "left";
72
72
  }>>;
73
73
  destinationSlabId: z.ZodOptional<z.ZodString>;
74
74
  sourceCeilingId: z.ZodOptional<z.ZodString>;
75
75
  createDestinationSlabOpening: z.ZodDefault<z.ZodBoolean>;
76
76
  createSourceCeilingOpening: z.ZodDefault<z.ZodBoolean>;
77
- openingWidth: z.ZodOptional<z.ZodNumber>;
78
- openingLength: z.ZodOptional<z.ZodNumber>;
79
- openingOffset: z.ZodDefault<z.ZodNumber>;
77
+ openingWidth: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>>;
78
+ openingLength: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>>;
79
+ openingOffset: z.ZodDefault<z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>>;
80
80
  openingCenter: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
81
- openingRotation: z.ZodOptional<z.ZodNumber>;
81
+ openingRotation: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>>;
82
82
  materialPreset: z.ZodOptional<z.ZodString>;
83
83
  name: z.ZodOptional<z.ZodString>;
84
84
  };
@@ -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;AAaxE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAOpD,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;CAajC,CAAA;AAED,eAAO,MAAM,sBAAsB;;;;;;CAMlC,CAAA;AAED,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;CAiB3B,CAAA;AAED,eAAO,MAAM,gBAAgB;;;;;;CAM5B,CAAA;AAED,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;CAqBzC,CAAA;AAED,eAAO,MAAM,8BAA8B;;;;;;CAM1C,CAAA;AA4GD,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAiT1F"}
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;AAaxE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAQpD,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;CAqBjC,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,CAiT1F"}
@@ -1,18 +1,25 @@
1
1
  import { CeilingNode, getActiveRoofHeight, LevelNode, RoofNode, RoofSegmentNode, SlabNode, StairNode, StairSegmentNode, WallNode, } from '@pascal-app/core/schema';
2
2
  import { z } from 'zod';
3
3
  import { publishLiveSceneSnapshot } from './live-sync';
4
+ import { measurement } from './measurement';
4
5
  import { NodeIdSchema, Vec2Schema, Vec3Schema } from './schemas';
5
6
  const ROOF_TYPES = ['hip', 'gable', 'shed', 'gambrel', 'dutch', 'mansard', 'flat'];
6
7
  const RAILING_MODES = ['none', 'left', 'right', 'both'];
7
8
  export const createStoryShellInput = {
8
9
  levelId: NodeIdSchema,
9
10
  footprint: z.array(Vec2Schema).min(3),
10
- wallHeight: z.number().positive().default(2.8),
11
- wallThickness: z.number().positive().default(0.16),
11
+ wallHeight: measurement('length', 'm', { positive: true, description: 'Wall height.' }).default(2.8),
12
+ wallThickness: measurement('length', 'm', {
13
+ positive: true,
14
+ description: 'Wall thickness.',
15
+ }).default(0.16),
12
16
  createSlab: z.boolean().default(true),
13
17
  createCeiling: z.boolean().default(true),
14
- slabElevation: z.number().default(0.1),
15
- ceilingHeight: z.number().positive().optional(),
18
+ slabElevation: measurement('length', 'm', { description: 'Slab elevation.' }).default(0.1),
19
+ ceilingHeight: measurement('length', 'm', {
20
+ positive: true,
21
+ description: 'Ceiling height.',
22
+ }).optional(),
16
23
  namePrefix: z.string().optional(),
17
24
  wallMaterialPreset: z.string().optional(),
18
25
  slabMaterialPreset: z.string().optional(),
@@ -30,16 +37,23 @@ export const createRoofInput = {
30
37
  roofLevelId: NodeIdSchema.optional(),
31
38
  useDedicatedRoofLevel: z.boolean().default(true),
32
39
  roofLevelLabel: z.string().default('Roof'),
40
+ // A level ordinal (story index), not a length — kept numeric.
33
41
  roofLevelElevation: z.number().optional(),
34
- roofLevelHeight: z.number().positive().optional(),
42
+ roofLevelHeight: measurement('length', 'm', {
43
+ positive: true,
44
+ description: 'Roof level height.',
45
+ }).optional(),
35
46
  center: Vec3Schema.optional(),
36
- width: z.number().positive(),
37
- depth: z.number().positive(),
47
+ width: measurement('length', 'm', { positive: true, description: 'Roof width.' }),
48
+ depth: measurement('length', 'm', { positive: true, description: 'Roof depth.' }),
38
49
  roofType: z.enum(ROOF_TYPES).default('hip'),
39
- pitch: z.number().min(0).max(85).default(35),
40
- wallHeight: z.number().min(0).default(0.35),
41
- wallThickness: z.number().positive().default(0.16),
42
- overhang: z.number().min(0).default(0.45),
50
+ pitch: measurement('angle', 'deg', { min: 0, max: 85, description: 'Roof pitch.' }).default(35),
51
+ wallHeight: measurement('length', 'm', { min: 0, description: 'Knee-wall height.' }).default(0.35),
52
+ wallThickness: measurement('length', 'm', {
53
+ positive: true,
54
+ description: 'Wall thickness.',
55
+ }).default(0.16),
56
+ overhang: measurement('length', 'm', { min: 0, description: 'Eave overhang.' }).default(0.45),
43
57
  materialPreset: z.string().optional(),
44
58
  name: z.string().optional(),
45
59
  };
@@ -54,21 +68,33 @@ export const createStairBetweenLevelsInput = {
54
68
  fromLevelId: NodeIdSchema,
55
69
  toLevelId: NodeIdSchema,
56
70
  position: Vec3Schema,
57
- rotation: z.number().default(0),
58
- width: z.number().positive().default(1),
59
- runLength: z.number().positive().default(3),
60
- totalRise: z.number().positive().default(2.8),
71
+ rotation: measurement('angle', 'rad', { description: 'Y-axis rotation.' }).default(0),
72
+ width: measurement('length', 'm', { positive: true, description: 'Stair width.' }).default(1),
73
+ runLength: measurement('length', 'm', {
74
+ positive: true,
75
+ description: 'Horizontal run length.',
76
+ }).default(3),
77
+ totalRise: measurement('length', 'm', {
78
+ positive: true,
79
+ description: 'Total vertical rise.',
80
+ }).default(2.8),
61
81
  stepCount: z.number().int().positive().default(14),
62
82
  railingMode: z.enum(RAILING_MODES).default('both'),
63
83
  destinationSlabId: NodeIdSchema.optional(),
64
84
  sourceCeilingId: NodeIdSchema.optional(),
65
85
  createDestinationSlabOpening: z.boolean().default(true),
66
86
  createSourceCeilingOpening: z.boolean().default(true),
67
- openingWidth: z.number().positive().optional(),
68
- openingLength: z.number().positive().optional(),
69
- openingOffset: z.number().min(0).default(0.15),
87
+ openingWidth: measurement('length', 'm', {
88
+ positive: true,
89
+ description: 'Floor opening width.',
90
+ }).optional(),
91
+ openingLength: measurement('length', 'm', {
92
+ positive: true,
93
+ description: 'Floor opening length.',
94
+ }).optional(),
95
+ openingOffset: measurement('length', 'm', { min: 0, description: 'Opening offset.' }).default(0),
70
96
  openingCenter: Vec2Schema.optional(),
71
- openingRotation: z.number().optional(),
97
+ openingRotation: measurement('angle', 'rad', { description: 'Opening rotation.' }).optional(),
72
98
  materialPreset: z.string().optional(),
73
99
  name: z.string().optional(),
74
100
  };
@@ -4,7 +4,7 @@ import type { SceneOperations } from '../operations';
4
4
  export declare const createLevelInput: {
5
5
  buildingId: z.ZodString;
6
6
  elevation: z.ZodOptional<z.ZodNumber>;
7
- height: z.ZodOptional<z.ZodNumber>;
7
+ height: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>>;
8
8
  label: z.ZodOptional<z.ZodString>;
9
9
  };
10
10
  export declare const createLevelOutput: {
@@ -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;AAGxE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAKpD,eAAO,MAAM,gBAAgB;;;;;CAK5B,CAAA;AAED,eAAO,MAAM,iBAAiB;;CAE7B,CAAA;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CA0CpF"}
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;AAGxE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAMpD,eAAO,MAAM,gBAAgB;;;;;CAQ5B,CAAA;AAED,eAAO,MAAM,iBAAiB;;CAE7B,CAAA;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CA0CpF"}
@@ -2,11 +2,15 @@ import { LevelNode } from '@pascal-app/core/schema';
2
2
  import { z } from 'zod';
3
3
  import { ErrorCode, throwMcpError } from './errors';
4
4
  import { publishLiveSceneSnapshot } from './live-sync';
5
+ import { measurement } from './measurement';
5
6
  import { NodeIdSchema } from './schemas';
6
7
  export const createLevelInput = {
7
8
  buildingId: NodeIdSchema,
8
9
  elevation: z.number().optional(),
9
- height: z.number().optional(),
10
+ height: measurement('length', 'm', {
11
+ min: 0,
12
+ description: 'Level height (stored in metadata).',
13
+ }).optional(),
10
14
  label: z.string().optional(),
11
15
  };
12
16
  export const createLevelOutput = {
@@ -5,8 +5,8 @@ export declare const createWallInput: {
5
5
  levelId: z.ZodString;
6
6
  start: z.ZodArray<z.ZodNumber>;
7
7
  end: z.ZodArray<z.ZodNumber>;
8
- thickness: z.ZodOptional<z.ZodNumber>;
9
- height: z.ZodOptional<z.ZodNumber>;
8
+ thickness: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>>;
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
12
  wallId: z.ZodString;
@@ -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;AAKpD,eAAO,MAAM,eAAe;;;;;;CAM3B,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;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"}
@@ -2,13 +2,17 @@ import { WallNode } from '@pascal-app/core/schema';
2
2
  import { z } from 'zod';
3
3
  import { ErrorCode, throwMcpError } from './errors';
4
4
  import { publishLiveSceneSnapshot } from './live-sync';
5
+ import { measurement } from './measurement';
5
6
  import { NodeIdSchema, Vec2Schema } from './schemas';
6
7
  export const createWallInput = {
7
8
  levelId: NodeIdSchema,
8
9
  start: Vec2Schema,
9
10
  end: Vec2Schema,
10
- thickness: z.number().positive().optional(),
11
- height: z.number().positive().optional(),
11
+ thickness: measurement('length', 'm', {
12
+ positive: true,
13
+ description: 'Wall thickness.',
14
+ }).optional(),
15
+ height: measurement('length', 'm', { positive: true, description: 'Wall height.' }).optional(),
12
16
  };
13
17
  export const createWallOutput = {
14
18
  wallId: z.string(),
@@ -4,12 +4,12 @@ import type { SceneOperations } from '../operations';
4
4
  export declare const cutOpeningInput: {
5
5
  wallId: z.ZodString;
6
6
  type: z.ZodEnum<{
7
- window: "window";
8
7
  door: "door";
8
+ window: "window";
9
9
  }>;
10
10
  position: z.ZodNumber;
11
- width: z.ZodNumber;
12
- height: z.ZodNumber;
11
+ width: z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>;
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
15
  openingId: z.ZodString;
@@ -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;AAMpD,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;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"}
@@ -3,13 +3,14 @@ import { z } from 'zod';
3
3
  import { ErrorCode, throwMcpError } from './errors';
4
4
  import { wallLength, wallLocalXFromT } from './geometry';
5
5
  import { publishLiveSceneSnapshot } from './live-sync';
6
+ import { measurement } from './measurement';
6
7
  import { NodeIdSchema } from './schemas';
7
8
  export const cutOpeningInput = {
8
9
  wallId: NodeIdSchema,
9
10
  type: z.enum(['door', 'window']),
10
11
  position: z.number().min(0).max(1),
11
- width: z.number().positive(),
12
- height: z.number().positive(),
12
+ width: measurement('length', 'm', { positive: true, description: 'Opening width.' }),
13
+ height: measurement('length', 'm', { positive: true, description: 'Opening height.' }),
13
14
  };
14
15
  export const cutOpeningOutput = {
15
16
  openingId: z.string(),
@@ -8,9 +8,10 @@ export declare const findNodesInput: {
8
8
  level: "level";
9
9
  wall: "wall";
10
10
  fence: "fence";
11
+ slab: "slab";
12
+ door: "door";
11
13
  item: "item";
12
14
  zone: "zone";
13
- slab: "slab";
14
15
  stair: "stair";
15
16
  ceiling: "ceiling";
16
17
  roof: "roof";
@@ -19,7 +20,6 @@ export declare const findNodesInput: {
19
20
  scan: "scan";
20
21
  guide: "guide";
21
22
  window: "window";
22
- door: "door";
23
23
  }>>;
24
24
  parentId: z.ZodOptional<z.ZodString>;
25
25
  levelId: z.ZodOptional<z.ZodString>;
@@ -79,7 +79,7 @@ export function registerFindNodes(server, bridge) {
79
79
  // Zone-polygon filter: point-in-polygon on a representative 2D point.
80
80
  if (zoneId) {
81
81
  const zone = bridge.getNode(zoneId);
82
- if (!zone || zone.type !== 'zone') {
82
+ if (zone?.type !== 'zone') {
83
83
  // Unknown zoneId → return empty list rather than throw; matches
84
84
  // typical "filter" semantics.
85
85
  results = [];
@@ -0,0 +1,37 @@
1
+ import { type Kind } from '@pascal-app/lingo';
2
+ import { z } from 'zod';
3
+ /**
4
+ * A zod field for a measurement tool argument that a model may emit as a bare
5
+ * number OR as natural language. `measurement('length', 'm')` accepts `0.15`,
6
+ * `"6 in"`, `"180cm"`, `"2 ft 3 in"`; `measurement('angle', 'deg')` accepts
7
+ * `45`, `"45°"`, `"1.57rad"`, `"0.25 turn"`. The value is canonicalized (via
8
+ * `@pascal-app/lingo`) to a number in `unit` — the exact unit the tool handler
9
+ * already expects — so no handler change is needed. `min`/`max` (in `unit`)
10
+ * reject out-of-range values with a model-readable message.
11
+ *
12
+ * The emitted JSON Schema is `number | string`, so the model is free to answer
13
+ * in whatever unit it is thinking in; AI SDK v6 applies the transform and
14
+ * forwards the canonical number to the tool executor.
15
+ *
16
+ * Tool-boundary safety: genuinely ambiguous separators (`"1,234"`, which could
17
+ * mean 1234 or 1.234) are rejected rather than silently absorbed, so a European
18
+ * decimal never becomes a 1000× value.
19
+ *
20
+ * NOTE: intentionally duplicated as `editor/packages/mcp/src/tools/measurement.ts`
21
+ * (MCP tools) and `packages/ai/src/tools/scene/measurement.ts` (AI-chat tools).
22
+ * The two tool stacks live in different packages — one inside the `editor`
23
+ * submodule — so they can't share a module. Keep the two copies identical; the
24
+ * clean long-term dedup is a zod-field adapter exported from `@pascal-app/lingo`.
25
+ */
26
+ export interface MeasurementOptions {
27
+ /** Semantic description (e.g. "Wall thickness"). The natural-language note is appended. */
28
+ description?: string;
29
+ /** Upper bound, in `unit`. */
30
+ max?: number;
31
+ /** Inclusive lower bound, in `unit`. */
32
+ min?: number;
33
+ /** Require the value to be strictly greater than 0 (for non-zero dimensions). */
34
+ positive?: boolean;
35
+ }
36
+ export declare function measurement(kind: Kind, unit: string, opts?: MeasurementOptions): z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>;
37
+ //# sourceMappingURL=measurement.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"measurement.d.ts","sourceRoot":"","sources":["../../src/tools/measurement.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,IAAI,EAAiB,MAAM,mBAAmB,CAAA;AAC5D,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,MAAM,WAAW,kBAAkB;IACjC,2FAA2F;IAC3F,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,8BAA8B;IAC9B,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,wCAAwC;IACxC,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,iFAAiF;IACjF,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AA6CD,wBAAgB,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,kBAAuB,uGAoDlF"}
@@ -0,0 +1,94 @@
1
+ import { parseQuantity } from '@pascal-app/lingo';
2
+ import { z } from 'zod';
3
+ function unitNoun(unit) {
4
+ switch (unit) {
5
+ case 'm':
6
+ return 'meters';
7
+ case 'deg':
8
+ return 'degrees';
9
+ case 'rad':
10
+ return 'radians';
11
+ default:
12
+ return unit;
13
+ }
14
+ }
15
+ function naturalLanguageNote(kind, unit) {
16
+ let examples;
17
+ if (kind === 'angle') {
18
+ // Lead with an example in the field's own unit so a bare number is read
19
+ // the way the model intends (a bare "45" in a radians field is 45 rad).
20
+ examples =
21
+ unit === 'rad' ? '1.5708, "90°", "1.57rad", "0.25 turn"' : '45, "45°", "1.57rad", "0.25 turn"';
22
+ }
23
+ else {
24
+ examples = '0.9, "6 ft", "180cm", "2 ft 3 in"';
25
+ }
26
+ return `Accepts a number (${unitNoun(unit)}) or a natural-language string; other units are converted. e.g. ${examples}.`;
27
+ }
28
+ function boundsNote(unit, min, max, positive) {
29
+ if (positive) {
30
+ return max === undefined
31
+ ? ` Must be greater than 0 ${unit}.`
32
+ : ` Greater than 0, up to ${max} ${unit}.`;
33
+ }
34
+ if (min !== undefined && max !== undefined)
35
+ return ` Range ${min}–${max} ${unit}.`;
36
+ if (min !== undefined)
37
+ return ` Minimum ${min} ${unit}.`;
38
+ if (max !== undefined)
39
+ return ` Maximum ${max} ${unit}.`;
40
+ return '';
41
+ }
42
+ export function measurement(kind, unit, opts = {}) {
43
+ const { min, max, positive = false } = opts;
44
+ const description = [
45
+ opts.description,
46
+ naturalLanguageNote(kind, unit) + boundsNote(unit, min, max, positive),
47
+ ]
48
+ .filter(Boolean)
49
+ .join(' ');
50
+ return z
51
+ .union([z.number(), z.string()])
52
+ .transform((val, ctx) => {
53
+ let value;
54
+ if (typeof val === 'number') {
55
+ value = val;
56
+ }
57
+ else {
58
+ const result = parseQuantity(val, {
59
+ kind,
60
+ unit,
61
+ strictness: 'forgiving',
62
+ // A genuinely ambiguous separator ("1,234") must not silently become
63
+ // a 1000× value at a tool boundary — fail so the model self-corrects.
64
+ escalate: { AMBIGUOUS_NUMBER: 'error' },
65
+ });
66
+ if (!result.ok) {
67
+ ctx.addIssue({
68
+ code: 'custom',
69
+ message: result.issues[0]?.message ?? `Could not read "${val}" as a ${kind}.`,
70
+ });
71
+ return z.NEVER;
72
+ }
73
+ value = result.quantity.to(unit).value;
74
+ }
75
+ if (!Number.isFinite(value)) {
76
+ ctx.addIssue({ code: 'custom', message: `Value must be a finite ${kind} in ${unit}.` });
77
+ return z.NEVER;
78
+ }
79
+ if (positive && value <= 0) {
80
+ ctx.addIssue({ code: 'custom', message: `Must be greater than 0 ${unit} (got ${value}).` });
81
+ return z.NEVER;
82
+ }
83
+ if (min !== undefined && value < min) {
84
+ ctx.addIssue({ code: 'custom', message: `Must be at least ${min} ${unit} (got ${value}).` });
85
+ return z.NEVER;
86
+ }
87
+ if (max !== undefined && value > max) {
88
+ ctx.addIssue({ code: 'custom', message: `Must be at most ${max} ${unit} (got ${value}).` });
89
+ return z.NEVER;
90
+ }
91
+ return value;
92
+ })
93
+ .describe(description);
94
+ }
@@ -10,8 +10,8 @@ export declare const photoToSceneInput: {
10
10
  scaleHint: z.ZodOptional<z.ZodString>;
11
11
  name: z.ZodDefault<z.ZodString>;
12
12
  save: z.ZodDefault<z.ZodBoolean>;
13
- defaultWallThickness: z.ZodDefault<z.ZodNumber>;
14
- defaultWallHeight: z.ZodDefault<z.ZodNumber>;
13
+ defaultWallThickness: z.ZodDefault<z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>>;
14
+ defaultWallHeight: z.ZodDefault<z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>>;
15
15
  };
16
16
  export declare const photoToSceneOutput: {
17
17
  sceneId: z.ZodOptional<z.ZodString>;
@@ -1 +1 @@
1
- {"version":3,"file":"photo-to-scene.d.ts","sourceRoot":"","sources":["../../../src/tools/photo-to-scene/photo-to-scene.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAYxE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAGvD;;;GAGG;AACH,eAAO,MAAM,iBAAiB;;;;;;;CAO7B,CAAA;AAED,eAAO,MAAM,kBAAkB;;;;;;;;CAQ9B,CAAA;AAiTD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CA6ErF"}
1
+ {"version":3,"file":"photo-to-scene.d.ts","sourceRoot":"","sources":["../../../src/tools/photo-to-scene/photo-to-scene.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAYxE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAIvD;;;GAGG;AACH,eAAO,MAAM,iBAAiB;;;;;;;CAa7B,CAAA;AAED,eAAO,MAAM,kBAAkB;;;;;;;;CAQ9B,CAAA;AAiTD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CA6ErF"}