@compilr-dev/sdk 0.30.0 → 0.31.1

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.
@@ -13,7 +13,17 @@ import { SCENE_ID_PATTERN, SCENE_MAX_BATCH, SCENE_OBJECT_TYPES, descendantsOf, f
13
13
  // =============================================================================
14
14
  // Helpers
15
15
  // =============================================================================
16
- const SIZE_KEYS = new Set(['size', 'radius', 'height', 'tube', 'points']);
16
+ /** Geometry fields — a change to any of them is journalled as "resized". */
17
+ const SIZE_KEYS = new Set([
18
+ 'size',
19
+ 'radius',
20
+ 'height',
21
+ 'tube',
22
+ 'points',
23
+ 'ridge',
24
+ 'segments',
25
+ 'axis',
26
+ ]);
17
27
  function sceneRef(opts) {
18
28
  return opts?.canvasId !== undefined ? `scene ${String(opts.canvasId)}` : 'this scene';
19
29
  }
@@ -54,15 +64,59 @@ function addObjects(scene, inputs, index, opts) {
54
64
  error: `A batch adds at most ${String(SCENE_MAX_BATCH)} objects (got ${String(inputs.length)}). Split it over several scene_add_object calls.`,
55
65
  };
56
66
  }
57
- const taken = new Set(scene.objects.map((o) => o.id));
58
- const added = [];
67
+ const existing = new Map(scene.objects.map((o) => [o.id, o]));
68
+ const taken = new Set(existing.keys());
69
+ const label = (i) => (inputs.length > 1 ? `objects[${String(i)}]` : 'The object');
70
+ // Explicit nulls are a caller's "not set" — dropped.
71
+ const objs = [];
59
72
  for (let i = 0; i < inputs.length; i++) {
60
73
  const raw = inputs[i];
61
- const at = inputs.length > 1 ? `objects[${String(i)}]` : 'The object';
62
74
  if (!isRecord(raw))
63
- return { error: `${at} must be a JSON object.` };
64
- // Explicit nulls are a caller's "not set" — dropped.
65
- const obj = Object.fromEntries(Object.entries(clone(raw)).filter(([, v]) => v !== null));
75
+ return { error: `${label(i)} must be a JSON object.` };
76
+ objs.push(Object.fromEntries(Object.entries(clone(raw)).filter(([, v]) => v !== null)));
77
+ }
78
+ // Caller-chosen ids first: they are reserved before any id is generated, so a generated id
79
+ // never takes a name the caller picked later in the same batch. A collision with a CHOSEN
80
+ // id is an error naming the existing object (never a silent "-2" — the caller would then
81
+ // reference the wrong object).
82
+ const chosenAt = new Map();
83
+ for (let i = 0; i < objs.length; i++) {
84
+ const id = objs[i].id;
85
+ if (id === undefined)
86
+ continue;
87
+ if (typeof id !== 'string' || !SCENE_ID_PATTERN.test(id)) {
88
+ return {
89
+ error: `${label(i)}.id = ${JSON.stringify(id)} — ids are 1–48 chars of lowercase letters, digits, "-" or "_", starting with a letter or digit. Omit id to have one generated from the name.`,
90
+ };
91
+ }
92
+ const prior = existing.get(id);
93
+ if (prior) {
94
+ const suggestion = generateSceneId(id, new Set([...taken, ...chosenAt.keys()]));
95
+ return {
96
+ error: `${label(i)}.id = "${id}" — ${sceneRef(opts)} already has an object with that id ` +
97
+ `(${prior.type}${prior.name ? ` "${prior.name}"` : ''}). Choose another id (e.g. "${suggestion}"), ` +
98
+ `change the existing one with scene_update_object id "${id}", or omit id to have one generated. scene_get lists ids.`,
99
+ };
100
+ }
101
+ const first = chosenAt.get(id);
102
+ if (first !== undefined) {
103
+ return {
104
+ error: `${label(i)}.id = "${id}" — objects[${String(first)}] in this batch already uses that id; ids must be unique.`,
105
+ };
106
+ }
107
+ chosenAt.set(id, i);
108
+ }
109
+ for (const id of chosenAt.keys())
110
+ taken.add(id);
111
+ /*
112
+ Parents (`group`) are NOT resolved here: the final validation of the whole op list does it
113
+ (validateGroups), so a batch may name a parent created anywhere in the same batch or in an
114
+ earlier op of the same apply — e.g. an undo that re-adds a removed subtree child-first.
115
+ */
116
+ const added = [];
117
+ for (let i = 0; i < objs.length; i++) {
118
+ const at = label(i);
119
+ const obj = objs[i];
66
120
  if (obj.type === 'wall') {
67
121
  return {
68
122
  error: `${at}: use type "extrude" with points [[x,z],…] and height for walls.`,
@@ -75,20 +129,11 @@ function addObjects(scene, inputs, index, opts) {
75
129
  };
76
130
  }
77
131
  if (obj.id === undefined) {
132
+ // Generated ids keep the P1 behaviour: slug of the name (or type), -2, -3… on collision.
78
133
  const base = typeof obj.name === 'string' && obj.name.trim() ? obj.name : obj.type;
79
134
  obj.id = generateSceneId(base, taken);
135
+ taken.add(obj.id);
80
136
  }
81
- else if (typeof obj.id !== 'string' || !SCENE_ID_PATTERN.test(obj.id)) {
82
- return {
83
- error: `${at}.id = ${JSON.stringify(obj.id)} — ids are 1–48 chars of lowercase letters, digits, "-" or "_". Omit id to have one generated from the name.`,
84
- };
85
- }
86
- else if (taken.has(obj.id)) {
87
- return {
88
- error: `An object "${obj.id}" already exists in ${sceneRef(opts)}. Choose another id, or omit id to have one generated (scene_get lists ids).`,
89
- };
90
- }
91
- taken.add(obj.id);
92
137
  added.push(obj);
93
138
  }
94
139
  const objects = [...scene.objects];
@@ -13,8 +13,11 @@
13
13
  export declare const SCENE_VERSION = 1;
14
14
  export type Vec3 = [number, number, number];
15
15
  export type Vec2 = [number, number];
16
- export type SceneObjectType = 'box' | 'sphere' | 'cylinder' | 'cone' | 'torus' | 'extrude';
16
+ export type SceneObjectType = 'box' | 'sphere' | 'cylinder' | 'cone' | 'torus' | 'extrude' | 'wedge' | 'group';
17
17
  export declare const SCENE_OBJECT_TYPES: readonly SceneObjectType[];
18
+ /** The direction an extrude extends along (§17). */
19
+ export type ExtrudeAxis = 'x' | 'y' | 'z';
20
+ export declare const SCENE_EXTRUDE_AXES: readonly ExtrudeAxis[];
18
21
  export interface SceneMaterial {
19
22
  /** 0..1, default 0.78 */
20
23
  roughness?: number;
@@ -35,7 +38,11 @@ export interface SceneObjectBase {
35
38
  rotation?: Vec3;
36
39
  /** '#RRGGBB'. Filled by the writer from the neutral palette when absent. */
37
40
  color?: string;
38
- /** Parent object id — position/rotation are relative to the parent's frame. */
41
+ /**
42
+ * Parent object id — position/rotation are relative to the parent's frame. A `group` is the
43
+ * intended container, but ANY object may be a parent (P1 scenes parent to shapes; kept for
44
+ * backward compatibility). No cycles, depth ≤ SCENE_MAX_GROUP_DEPTH.
45
+ */
39
46
  group?: string;
40
47
  material?: SceneMaterial;
41
48
  /** User-only lock: the inspector cannot move/remove it; agents still can (and are warned). */
@@ -50,15 +57,24 @@ export interface SphereObject extends SceneObjectBase {
50
57
  type: 'sphere';
51
58
  radius: number;
52
59
  }
60
+ /**
61
+ * `segments` (integer 3–64, optional): a faceted prism instead of a smooth round one. Omitted =
62
+ * smooth. Vertex k sits at angle θ = 2πk/segments with x = radius·sin θ, z = radius·cos θ
63
+ * (three.js CylinderGeometry/ConeGeometry, thetaStart 0) — so vertex 0 is on +z, and `radius`
64
+ * is the CIRCUMradius. A 4-segment cone is a pyramid with corners on the ±x/±z axes (rotate 45°
65
+ * about Y for sides parallel to the axes; its base side is radius·√2).
66
+ */
53
67
  export interface CylinderObject extends SceneObjectBase {
54
68
  type: 'cylinder';
55
69
  radius: number;
56
70
  height: number;
71
+ segments?: number;
57
72
  }
58
73
  export interface ConeObject extends SceneObjectBase {
59
74
  type: 'cone';
60
75
  radius: number;
61
76
  height: number;
77
+ segments?: number;
62
78
  }
63
79
  /** Lies flat (ring in the x–z plane). radius = ring radius, tube = tube radius. */
64
80
  export interface TorusObject extends SceneObjectBase {
@@ -66,13 +82,50 @@ export interface TorusObject extends SceneObjectBase {
66
82
  radius: number;
67
83
  tube: number;
68
84
  }
69
- /** An outline in plan (x, z), local to position, extruded upward by `height`. Walls and floors. */
85
+ /**
86
+ * An outline extruded by `height` along `axis` (default 'y'). Points are local to `position`;
87
+ * which world axes a point [a, b] maps to depends on the axis:
88
+ *
89
+ * | axis | point [a, b] is | the outline lies in | extends along |
90
+ * |------|-----------------|---------------------|----------------|
91
+ * | 'y' | [x, z] (plan) | the floor (x–z) | +y, 0 → height |
92
+ * | 'z' | [x, y] (side) | the x–y plane | +z, 0 → height |
93
+ * | 'x' | [z, y] (side) | the z–y plane | +x, 0 → height |
94
+ *
95
+ * For 'x'/'z' the SECOND coordinate is up (y) and the first is the horizontal one — a side
96
+ * profile (a gable, a roof section) pushed sideways. y-bottom rule: the mesh is lifted by
97
+ * −min(b) for 'x'/'z' (0 for 'y'), so the lowest point of the outline sits at `position.y`.
98
+ * The rotation pivot is the outline's [0, 0] at the start of the extrusion (lifted likewise).
99
+ */
70
100
  export interface ExtrudeObject extends SceneObjectBase {
71
101
  type: 'extrude';
72
102
  points: Vec2[];
73
103
  height: number;
104
+ axis?: ExtrudeAxis;
74
105
  }
75
- export type SceneObject = BoxObject | SphereObject | CylinderObject | ConeObject | TorusObject | ExtrudeObject;
106
+ /**
107
+ * A triangular prism (roofs): `size` [w, h, d]. The triangle is in the x–y plane and the ridge
108
+ * runs along z. Mesh-local (centred like a box, lift h/2): base corners (−w/2, −h/2) and
109
+ * (w/2, −h/2), apex (−w/2 + ridge·w, h/2); the prism spans z ∈ [−d/2, d/2]. `ridge` 0–1,
110
+ * default 0.5 (centred apex; 0 or 1 = a lean-to with a vertical side).
111
+ */
112
+ export interface WedgeObject extends SceneObjectBase {
113
+ type: 'wedge';
114
+ size: Vec3;
115
+ ridge?: number;
116
+ }
117
+ /**
118
+ * A transform node — no geometry, no size, no colour/material (rejected: colour its children).
119
+ * Children (`group: "<this id>"`) are positioned in its frame: its `position` is their origin
120
+ * (a group has no height, so its lift is 0 and child y = 0 is the group's y) and its rotation
121
+ * pivots there. Its bounds are the union of its descendants' (none while it is empty).
122
+ */
123
+ export interface GroupObject extends SceneObjectBase {
124
+ type: 'group';
125
+ color?: never;
126
+ material?: never;
127
+ }
128
+ export type SceneObject = BoxObject | SphereObject | CylinderObject | ConeObject | TorusObject | ExtrudeObject | WedgeObject | GroupObject;
76
129
  export interface SceneCamera {
77
130
  position: Vec3;
78
131
  target: Vec3;
@@ -111,6 +164,11 @@ export declare const SCENE_SIZE_MAX = 500;
111
164
  export declare const SCENE_RADIUS_MIN = 0.01;
112
165
  export declare const SCENE_RADIUS_MAX = 250;
113
166
  export declare const SCENE_TUBE_MIN = 0.005;
167
+ /** `segments` on cone/cylinder: integer range (§17). */
168
+ export declare const SCENE_SEGMENTS_MIN = 3;
169
+ export declare const SCENE_SEGMENTS_MAX = 64;
170
+ /** wedge `ridge` default: apex centred across the width. */
171
+ export declare const SCENE_WEDGE_RIDGE_DEFAULT = 0.5;
114
172
  export declare const SCENE_POINTS_MIN = 3;
115
173
  export declare const SCENE_POINTS_MAX = 256;
116
174
  export declare const SCENE_MAX_LIGHTS = 4;
@@ -178,17 +236,29 @@ export declare function normalizeAngle(deg: number): number;
178
236
  export declare function normalizeScene(scene: SceneFile): SceneFile;
179
237
  /**
180
238
  * Half-height from the object's BOTTOM (its `position.y`) to its geometric centre, where the
181
- * mesh sits. extrude is 0: its geometry already starts at the floor. ONE function — the
182
- * viewer, the fit and the GLB export must agree.
239
+ * mesh sits. extrude along y is 0: its geometry already starts at the floor; along x/z it is
240
+ * −min(profile y), so the profile's lowest point sits at position.y. A group has no height: 0.
241
+ * ONE function — the viewer, the fit and the GLB export must agree.
183
242
  */
184
243
  export declare function liftFor(obj: SceneObject): number;
185
244
  /**
186
245
  * World matrix of each object's mesh (row-major 4×4), keyed by id, with group parenting
187
- * applied. Assumes a validated scene (no cycles).
246
+ * applied. A `group` has no mesh and lift 0, so its entry is its frame: T(position) · R,
247
+ * rotating about its position. Assumes a validated scene (no cycles).
188
248
  */
189
249
  export declare function sceneWorldMatrices(scene: SceneFile): Map<string, number[]>;
190
- /** World-space AABB of all objects (conservative for rotated shapes). null when empty. */
250
+ /**
251
+ * World-space AABB of all objects (conservative for rotated shapes). null when nothing has
252
+ * geometry — an empty scene, or one holding only empty groups (the camera then uses the
253
+ * reference default, as for an empty scene).
254
+ */
191
255
  export declare function sceneBounds(scene: SceneFile): SceneBounds | null;
256
+ /**
257
+ * World AABB of one object and everything grouped under it — for a `group`, the union of its
258
+ * descendants (null while it holds no geometry); for a shape, its own mesh plus any children.
259
+ * null for an unknown id. (The viewer outlines a selected group's whole subtree with this.)
260
+ */
261
+ export declare function sceneObjectBounds(scene: SceneFile, id: string): SceneBounds | null;
192
262
  /** The reference view direction ([7, 5.5, 8], normalised). */
193
263
  export declare const SCENE_CAMERA_DIRECTION: Readonly<Vec3>;
194
264
  /**
@@ -199,7 +269,7 @@ export declare const SCENE_CAMERA_DIRECTION: Readonly<Vec3>;
199
269
  export declare function fitCamera(bounds: SceneBounds | null, fovDeg?: number): SceneCamera;
200
270
  /** The camera to view a scene with: its own, or the fit. */
201
271
  export declare function sceneCamera(scene: SceneFile): SceneCamera;
202
- /** One line: "12 objects, bounds 6.2 × 4 × 2.7 m" (W × D × H). */
272
+ /** One line: "12 objects (2 groups), bounds 6.2 × 4 × 2.7 m" (W × D × H). */
203
273
  export declare function describeScene(scene: SceneFile): string;
204
274
  /** The ids of an object's descendants (children, grandchildren, …), in scene order. */
205
275
  export declare function descendantsOf(scene: SceneFile, id: string): string[];
@@ -21,7 +21,10 @@ export const SCENE_OBJECT_TYPES = [
21
21
  'cone',
22
22
  'torus',
23
23
  'extrude',
24
+ 'wedge',
25
+ 'group',
24
26
  ];
27
+ export const SCENE_EXTRUDE_AXES = ['x', 'y', 'z'];
25
28
  // =============================================================================
26
29
  // Limits and defaults (§2.3, §2.4, Q-1, Q-14)
27
30
  // =============================================================================
@@ -35,6 +38,11 @@ export const SCENE_SIZE_MAX = 500;
35
38
  export const SCENE_RADIUS_MIN = 0.01;
36
39
  export const SCENE_RADIUS_MAX = 250;
37
40
  export const SCENE_TUBE_MIN = 0.005;
41
+ /** `segments` on cone/cylinder: integer range (§17). */
42
+ export const SCENE_SEGMENTS_MIN = 3;
43
+ export const SCENE_SEGMENTS_MAX = 64;
44
+ /** wedge `ridge` default: apex centred across the width. */
45
+ export const SCENE_WEDGE_RIDGE_DEFAULT = 0.5;
38
46
  export const SCENE_POINTS_MIN = 3;
39
47
  export const SCENE_POINTS_MAX = 256;
40
48
  export const SCENE_MAX_LIGHTS = 4;
@@ -114,6 +122,10 @@ export function defaultObjectFor(type, id) {
114
122
  ],
115
123
  height: 1,
116
124
  };
125
+ case 'wedge':
126
+ return { ...base, type, size: [1, 1, 1] };
127
+ case 'group':
128
+ return { ...base, type };
117
129
  }
118
130
  }
119
131
  /** The stored form: pretty-printed, because agents read and cite it (§4.1). */
@@ -147,33 +159,29 @@ export function utf8Bytes(s) {
147
159
  return n;
148
160
  }
149
161
  const TOP_KEYS = new Set(['version', 'name', 'units', 'camera', 'lights', 'objects']);
150
- const BASE_KEYS = [
151
- 'id',
152
- 'name',
153
- 'type',
154
- 'position',
155
- 'rotation',
156
- 'color',
157
- 'group',
158
- 'material',
159
- 'locked',
160
- ];
162
+ const BASE_KEYS = ['id', 'name', 'type', 'position', 'rotation', 'group', 'locked'];
163
+ /** Surface fields: every type but `group` (a group has no surface of its own). */
164
+ const SURFACE_KEYS = ['color', 'material'];
161
165
  const TYPE_KEYS = {
162
166
  box: ['size'],
163
167
  sphere: ['radius'],
164
- cylinder: ['radius', 'height'],
165
- cone: ['radius', 'height'],
168
+ cylinder: ['radius', 'height', 'segments'],
169
+ cone: ['radius', 'height', 'segments'],
166
170
  torus: ['radius', 'tube'],
167
- extrude: ['points', 'height'],
171
+ extrude: ['points', 'height', 'axis'],
172
+ wedge: ['size', 'ridge'],
173
+ group: [],
168
174
  };
169
175
  /** How each type is described in errors (what fields it takes). */
170
176
  const TYPE_SHAPE = {
171
177
  box: 'box takes size [w, h, d]',
172
178
  sphere: 'sphere takes radius',
173
- cylinder: 'cylinder takes radius and height',
174
- cone: 'cone takes radius and height',
179
+ cylinder: 'cylinder takes radius, height and optional segments',
180
+ cone: 'cone takes radius, height and optional segments',
175
181
  torus: 'torus takes radius and tube',
176
- extrude: 'extrude takes points [[x, z], …] and height',
182
+ extrude: 'extrude takes points [[a, b], …], height and optional axis',
183
+ wedge: 'wedge takes size [w, h, d] and optional ridge',
184
+ group: 'a group is a container: id, name, position, rotation, group, locked only — colour its children',
177
185
  };
178
186
  const MATERIAL_KEYS = new Set(['roughness', 'metalness', 'opacity']);
179
187
  const HEX_RE = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
@@ -321,7 +329,11 @@ function validateObject(raw, i, errors) {
321
329
  }
322
330
  const type = t;
323
331
  // Unknown keys: rejected, because they would round-trip silently and read as honoured.
324
- const allowed = new Set([...BASE_KEYS, ...TYPE_KEYS[type]]);
332
+ const allowed = new Set([
333
+ ...BASE_KEYS,
334
+ ...(type === 'group' ? [] : SURFACE_KEYS),
335
+ ...TYPE_KEYS[type],
336
+ ]);
325
337
  for (const k of Object.keys(raw)) {
326
338
  if (!allowed.has(k)) {
327
339
  errors.push(`${label}.${k} is not a ${type} field — ${TYPE_SHAPE[type]}.`);
@@ -339,7 +351,7 @@ function validateObject(raw, i, errors) {
339
351
  if (raw.rotation !== undefined) {
340
352
  checkVec(raw.rotation, `${label}.rotation`, 3, -Number.MAX_VALUE, Number.MAX_VALUE, 'rotation is degrees [x, y, z]; rotate about Y — x/z tilt breaks the floor rule', errors);
341
353
  }
342
- if (raw.color !== undefined) {
354
+ if (type !== 'group' && raw.color !== undefined) {
343
355
  if (typeof raw.color !== 'string' || !HEX_RE.test(raw.color)) {
344
356
  errors.push(`${label}.color = ${show(raw.color)} — colours are "#RGB" or "#RRGGBB" hex.`);
345
357
  }
@@ -350,7 +362,7 @@ function validateObject(raw, i, errors) {
350
362
  if (raw.locked !== undefined && typeof raw.locked !== 'boolean') {
351
363
  errors.push(`${label}.locked = ${show(raw.locked)} — true or false.`);
352
364
  }
353
- if (raw.material !== undefined) {
365
+ if (type !== 'group' && raw.material !== undefined) {
354
366
  if (!isRecord(raw.material)) {
355
367
  errors.push(`${label}.material = ${show(raw.material)} — { roughness?, metalness?, opacity? }.`);
356
368
  }
@@ -381,6 +393,21 @@ function validateObject(raw, i, errors) {
381
393
  case 'cone':
382
394
  checkNumber(raw.radius, `${label}.radius`, SCENE_RADIUS_MIN, SCENE_RADIUS_MAX, RADIUS_RULE, errors);
383
395
  checkNumber(raw.height, `${label}.height`, SCENE_SIZE_MIN, SCENE_SIZE_MAX, SIZE_RULE, errors);
396
+ if (raw.segments !== undefined) {
397
+ const rule = `segments is a whole number ${String(SCENE_SEGMENTS_MIN)}–${String(SCENE_SEGMENTS_MAX)} (4 on a cone = a pyramid); omit it for a smooth round ${type}`;
398
+ if (checkNumber(raw.segments, `${label}.segments`, SCENE_SEGMENTS_MIN, SCENE_SEGMENTS_MAX, rule, errors) &&
399
+ !Number.isInteger(raw.segments)) {
400
+ errors.push(`${label}.segments = ${fmtNum(raw.segments)} — ${rule}.`);
401
+ }
402
+ }
403
+ break;
404
+ case 'wedge':
405
+ checkVec(raw.size, `${label}.size`, 3, SCENE_SIZE_MIN, SCENE_SIZE_MAX, SIZE_RULE, errors);
406
+ if (raw.ridge !== undefined) {
407
+ checkNumber(raw.ridge, `${label}.ridge`, 0, 1, 'ridge is 0–1: where the apex sits across the width (0.5 = centred, 0 or 1 = a lean-to)', errors);
408
+ }
409
+ break;
410
+ case 'group':
384
411
  break;
385
412
  case 'torus': {
386
413
  const rOk = checkNumber(raw.radius, `${label}.radius`, SCENE_RADIUS_MIN, SCENE_RADIUS_MAX, RADIUS_RULE, errors);
@@ -388,15 +415,31 @@ function validateObject(raw, i, errors) {
388
415
  checkNumber(raw.tube, `${label}.tube`, SCENE_TUBE_MIN, maxTube, `tube is ${fmtNum(SCENE_TUBE_MIN)} m up to the ring radius (a tube wider than its ring self-intersects)`, errors);
389
416
  break;
390
417
  }
391
- case 'extrude':
418
+ case 'extrude': {
392
419
  checkNumber(raw.height, `${label}.height`, SCENE_SIZE_MIN, SCENE_SIZE_MAX, SIZE_RULE, errors);
393
- validatePoints(raw.points, `${label}.points`, errors);
420
+ let axis = 'y';
421
+ if (raw.axis !== undefined) {
422
+ if (typeof raw.axis !== 'string' ||
423
+ !SCENE_EXTRUDE_AXES.includes(raw.axis)) {
424
+ errors.push(`${label}.axis = ${show(raw.axis)} — "y" (default: a plan outline [x, z] rising vertically), "z" (a side profile [x, y] pushed along +z) or "x" (a side profile [z, y] pushed along +x).`);
425
+ }
426
+ else
427
+ axis = raw.axis;
428
+ }
429
+ validatePoints(raw.points, `${label}.points`, errors, axis);
394
430
  break;
431
+ }
395
432
  }
396
433
  }
397
- function validatePoints(v, path, errors) {
434
+ /** What an extrude point [a, b] means, per axis — for messages. */
435
+ const POINT_MEANING = {
436
+ y: '[x, z] plan points',
437
+ z: '[x, y] side-profile points (axis "z")',
438
+ x: '[z, y] side-profile points (axis "x")',
439
+ };
440
+ function validatePoints(v, path, errors, axis = 'y') {
398
441
  if (!Array.isArray(v)) {
399
- errors.push(`${path} = ${show(v)} — an array of [x, z] plan points.`);
442
+ errors.push(`${path} = ${show(v)} — an array of ${POINT_MEANING[axis]}.`);
400
443
  return;
401
444
  }
402
445
  const before = errors.length;
@@ -435,7 +478,7 @@ function validateGroups(objects, errors) {
435
478
  return;
436
479
  }
437
480
  if (!byId.has(o.group)) {
438
- errors.push(`${label}.group = "${o.group}" — no object with that id in the scene.`);
481
+ errors.push(`${label}.group = "${o.group}" — no object with that id in the scene. Add the parent first (type "group" with your own id), or create it in the same batch; scene_get lists ids.`);
439
482
  return;
440
483
  }
441
484
  // Walk up: cycle / depth.
@@ -614,10 +657,14 @@ export function normalizeScene(scene) {
614
657
  const out = JSON.parse(JSON.stringify(scene));
615
658
  out.objects = out.objects.map((o, i) => {
616
659
  const next = { ...o };
617
- next.color =
618
- next.color !== undefined
619
- ? normalizeColor(next.color)
620
- : SCENE_NEUTRAL_PALETTE[i % SCENE_NEUTRAL_PALETTE.length];
660
+ // A group has no surface: no colour to fill (the palette index stays the array index, so
661
+ // scenes without groups colour exactly as before).
662
+ if (next.type !== 'group') {
663
+ next.color =
664
+ next.color !== undefined
665
+ ? normalizeColor(next.color)
666
+ : SCENE_NEUTRAL_PALETTE[i % SCENE_NEUTRAL_PALETTE.length];
667
+ }
621
668
  if (next.rotation) {
622
669
  next.rotation = next.rotation.map(normalizeAngle);
623
670
  }
@@ -633,12 +680,14 @@ export function normalizeScene(scene) {
633
680
  // =============================================================================
634
681
  /**
635
682
  * Half-height from the object's BOTTOM (its `position.y`) to its geometric centre, where the
636
- * mesh sits. extrude is 0: its geometry already starts at the floor. ONE function — the
637
- * viewer, the fit and the GLB export must agree.
683
+ * mesh sits. extrude along y is 0: its geometry already starts at the floor; along x/z it is
684
+ * −min(profile y), so the profile's lowest point sits at position.y. A group has no height: 0.
685
+ * ONE function — the viewer, the fit and the GLB export must agree.
638
686
  */
639
687
  export function liftFor(obj) {
640
688
  switch (obj.type) {
641
689
  case 'box':
690
+ case 'wedge':
642
691
  return obj.size[1] / 2;
643
692
  case 'sphere':
644
693
  return obj.radius;
@@ -647,14 +696,61 @@ export function liftFor(obj) {
647
696
  return obj.height / 2;
648
697
  case 'torus':
649
698
  return obj.tube;
650
- case 'extrude':
699
+ case 'extrude': {
700
+ if ((obj.axis ?? 'y') === 'y')
701
+ return 0;
702
+ const lowest = Math.min(...obj.points.map((p) => p[1]));
703
+ return lowest === 0 ? 0 : -lowest; // no -0 in matrices/summaries
704
+ }
705
+ case 'group':
651
706
  return 0;
652
707
  }
653
708
  }
654
- /** Local AABB of the mesh around its own origin (the pivot). */
709
+ /**
710
+ * Extreme x / z of a faceted round shape: vertex k at θ = 2πk/n, x = r·sin θ, z = r·cos θ
711
+ * (three.js thetaStart 0). Smooth (no segments) → the circle's ±r.
712
+ */
713
+ function roundExtent(radius, segments) {
714
+ if (segments === undefined)
715
+ return { x: [-radius, radius], z: [-radius, radius] };
716
+ const xs = [];
717
+ const zs = [];
718
+ for (let k = 0; k < segments; k++) {
719
+ const t = (2 * Math.PI * k) / segments;
720
+ xs.push(radius * Math.sin(t));
721
+ zs.push(radius * Math.cos(t));
722
+ }
723
+ return { x: [Math.min(...xs), Math.max(...xs)], z: [Math.min(...zs), Math.max(...zs)] };
724
+ }
725
+ /**
726
+ * Mesh-local AABB of an extrude, per axis (see ExtrudeObject): the outline's [a, b] mapped to
727
+ * world axes, the extrusion 0 → height along the axis. Before the lift.
728
+ */
729
+ function extrudeBox(obj) {
730
+ const as = obj.points.map((p) => p[0]);
731
+ const bs = obj.points.map((p) => p[1]);
732
+ const [aMin, aMax, bMin, bMax] = [
733
+ Math.min(...as),
734
+ Math.max(...as),
735
+ Math.min(...bs),
736
+ Math.max(...bs),
737
+ ];
738
+ switch (obj.axis ?? 'y') {
739
+ case 'y': // [x, z] plan → (x, 0..h, z) — the reference mapping (§2.2)
740
+ return { min: [aMin, 0, bMin], max: [aMax, obj.height, bMax] };
741
+ case 'z': // [x, y] → (x, y, 0..h)
742
+ return { min: [aMin, bMin, 0], max: [aMax, bMax, obj.height] };
743
+ case 'x': // [z, y] → (0..h, y, z)
744
+ return { min: [0, bMin, aMin], max: [obj.height, bMax, aMax] };
745
+ }
746
+ }
747
+ /** Local AABB of the mesh around its own origin (the pivot). null for a group (no geometry). */
655
748
  function localBox(obj) {
656
749
  switch (obj.type) {
657
- case 'box': {
750
+ case 'box':
751
+ case 'wedge': {
752
+ // A wedge's triangle spans the full width at its base and the full height at its apex,
753
+ // so its AABB is exactly the box's, whatever the ridge.
658
754
  const [w, h, d] = obj.size;
659
755
  return { min: [-w / 2, -h / 2, -d / 2], max: [w / 2, h / 2, d / 2] };
660
756
  }
@@ -664,24 +760,21 @@ function localBox(obj) {
664
760
  max: [obj.radius, obj.radius, obj.radius],
665
761
  };
666
762
  case 'cylinder':
667
- case 'cone':
763
+ case 'cone': {
764
+ const e = roundExtent(obj.radius, obj.segments);
668
765
  return {
669
- min: [-obj.radius, -obj.height / 2, -obj.radius],
670
- max: [obj.radius, obj.height / 2, obj.radius],
766
+ min: [e.x[0], -obj.height / 2, e.z[0]],
767
+ max: [e.x[1], obj.height / 2, e.z[1]],
671
768
  };
769
+ }
672
770
  case 'torus': {
673
771
  const r = obj.radius + obj.tube;
674
772
  return { min: [-r, -obj.tube, -r], max: [r, obj.tube, r] };
675
773
  }
676
- case 'extrude': {
677
- // Plan (x, z) → world (x, y∈[0,h], z) — the reference mapping (§2.2).
678
- const xs = obj.points.map((p) => p[0]);
679
- const zs = obj.points.map((p) => p[1]);
680
- return {
681
- min: [Math.min(...xs), 0, Math.min(...zs)],
682
- max: [Math.max(...xs), obj.height, Math.max(...zs)],
683
- };
684
- }
774
+ case 'extrude':
775
+ return extrudeBox(obj);
776
+ case 'group':
777
+ return null;
685
778
  }
686
779
  }
687
780
  const IDENTITY = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
@@ -726,7 +819,8 @@ function objectFrame(obj) {
726
819
  }
727
820
  /**
728
821
  * World matrix of each object's mesh (row-major 4×4), keyed by id, with group parenting
729
- * applied. Assumes a validated scene (no cycles).
822
+ * applied. A `group` has no mesh and lift 0, so its entry is its frame: T(position) · R,
823
+ * rotating about its position. Assumes a validated scene (no cycles).
730
824
  */
731
825
  export function sceneWorldMatrices(scene) {
732
826
  const byId = new Map(scene.objects.map((o) => [o.id, o]));
@@ -746,16 +840,18 @@ export function sceneWorldMatrices(scene) {
746
840
  out.set(o.id, mul(frameOf(o), translate(0, liftFor(o), 0)));
747
841
  return out;
748
842
  }
749
- /** World-space AABB of all objects (conservative for rotated shapes). null when empty. */
750
- export function sceneBounds(scene) {
751
- if (scene.objects.length === 0)
752
- return null;
843
+ /** World AABB of the given objects' meshes. Groups add nothing; null when nothing has geometry. */
844
+ function boundsOf(scene, objects) {
753
845
  const mats = sceneWorldMatrices(scene);
754
846
  const min = [Infinity, Infinity, Infinity];
755
847
  const max = [-Infinity, -Infinity, -Infinity];
756
- for (const o of scene.objects) {
757
- const m = mats.get(o.id) ?? IDENTITY;
848
+ let any = false;
849
+ for (const o of objects) {
758
850
  const b = localBox(o);
851
+ if (!b)
852
+ continue;
853
+ any = true;
854
+ const m = mats.get(o.id) ?? IDENTITY;
759
855
  for (const cx of [b.min[0], b.max[0]])
760
856
  for (const cy of [b.min[1], b.max[1]])
761
857
  for (const cz of [b.min[2], b.max[2]]) {
@@ -768,7 +864,29 @@ export function sceneBounds(scene) {
768
864
  }
769
865
  }
770
866
  }
771
- return { min, max };
867
+ return any ? { min, max } : null;
868
+ }
869
+ /**
870
+ * World-space AABB of all objects (conservative for rotated shapes). null when nothing has
871
+ * geometry — an empty scene, or one holding only empty groups (the camera then uses the
872
+ * reference default, as for an empty scene).
873
+ */
874
+ export function sceneBounds(scene) {
875
+ if (scene.objects.length === 0)
876
+ return null;
877
+ return boundsOf(scene, scene.objects);
878
+ }
879
+ /**
880
+ * World AABB of one object and everything grouped under it — for a `group`, the union of its
881
+ * descendants (null while it holds no geometry); for a shape, its own mesh plus any children.
882
+ * null for an unknown id. (The viewer outlines a selected group's whole subtree with this.)
883
+ */
884
+ export function sceneObjectBounds(scene, id) {
885
+ const self = scene.objects.find((o) => o.id === id);
886
+ if (!self)
887
+ return null;
888
+ const members = new Set([id, ...descendantsOf(scene, id)]);
889
+ return boundsOf(scene, scene.objects.filter((o) => members.has(o.id)));
772
890
  }
773
891
  /** The reference view direction ([7, 5.5, 8], normalised). */
774
892
  export const SCENE_CAMERA_DIRECTION = (() => {
@@ -802,11 +920,13 @@ export function fitCamera(bounds, fovDeg = SCENE_CAMERA_FOV) {
802
920
  export function sceneCamera(scene) {
803
921
  return scene.camera ?? fitCamera(sceneBounds(scene));
804
922
  }
805
- /** One line: "12 objects, bounds 6.2 × 4 × 2.7 m" (W × D × H). */
923
+ /** One line: "12 objects (2 groups), bounds 6.2 × 4 × 2.7 m" (W × D × H). */
806
924
  export function describeScene(scene) {
807
925
  const n = scene.objects.length;
926
+ const g = scene.objects.filter((o) => o.type === 'group').length;
808
927
  const b = sceneBounds(scene);
809
- const count = `${String(n)} object${n === 1 ? '' : 's'}`;
928
+ const groups = g > 0 ? ` (${String(g)} group${g === 1 ? '' : 's'})` : '';
929
+ const count = `${String(n)} object${n === 1 ? '' : 's'}${groups}`;
810
930
  if (!b)
811
931
  return count;
812
932
  const w = b.max[0] - b.min[0];
@@ -285,11 +285,17 @@ export const CAPABILITY_PACKS = {
285
285
  promptModules: ['platform-tool-hints'],
286
286
  promptSnippet: "3D scenes are JSON, not code. Units metres; position.y is the object's BOTTOM (0 = on the floor); " +
287
287
  'rotation in degrees (rotate about Y). Types: box(size w,h,d), sphere(radius), cylinder/cone(radius, ' +
288
- 'height), torus(radius, tube; flat), extrude(points [[x,z],…], height) for walls and floors. Read with ' +
289
- 'scene_get before editing — the user edits too. Add up to 50 objects per scene_add_object call ' +
290
- '(objects: [...]). Keep neutral colours unless asked. After building, scene_screenshot to check.',
291
- estimatedPromptTokens: 120,
292
- estimatedToolTokens: 1400,
288
+ 'height, segments?), torus(radius, tube; flat), extrude(points, height, axis?) for walls and floors, ' +
289
+ 'wedge(size, ridge?), group (a container, no geometry). BUILD HIERARCHICALLY: give groups your own ids ' +
290
+ 'and nest the parts, e.g. house → walls, roof, doors, furniture — one scene_add_object batch per group ' +
291
+ '(objects: [...], ≤ 50), the group first, its parts with group: "<id>"; children are positioned in the ' +
292
+ "group's frame. Roofs: wedge (gable; ridge along z, rotate 90° for x), cone with segments 4 (pyramid/hip), " +
293
+ 'or extrude with axis "x"/"z" (a side profile pushed horizontally). Read with scene_get before editing — ' +
294
+ 'the user edits too. Keep neutral colours unless asked. After building, scene_screenshot to check.',
295
+ // ≈ chars / 4: the snippet is ~910 chars; the seven schemas serialise to ~12 KB (§17 added
296
+ // group/wedge/segments/axis/ridge and the axis mapping, ~3.7 KB).
297
+ estimatedPromptTokens: 230,
298
+ estimatedToolTokens: 3000,
293
299
  },
294
300
  plans: {
295
301
  id: 'plans',
package/dist/index.d.ts CHANGED
@@ -96,8 +96,8 @@ export { defineTool, createSuccessResult, createErrorResult, mergeHooks, createL
96
96
  export { classifyAgentError, isApiKeyError } from './errors/classify.js';
97
97
  export type { AgentErrorCategory, AgentErrorInfo } from './errors/classify.js';
98
98
  export { validateControlManifest, seedValues } from './canvas/index.js';
99
- export { validateScene, normalizeScene, serializeScene, emptyScene, sceneBounds, fitCamera, liftFor, describeScene, applySceneOp, applySceneOps, formatUserEditNotice, SCENE_MAX_OBJECTS, SCENE_MAX_BYTES, SCENE_MAX_BATCH, HTML_CANVAS_TYPES, } from './canvas/index.js';
100
- export type { HtmlCanvasType, SceneFile, SceneObject, SceneObjectType, SceneCamera, SceneLight, SceneMaterial, SceneOp, SceneChange, SceneEditSource, } from './canvas/index.js';
99
+ export { validateScene, normalizeScene, serializeScene, emptyScene, sceneBounds, sceneObjectBounds, fitCamera, liftFor, describeScene, applySceneOp, applySceneOps, formatUserEditNotice, SCENE_MAX_OBJECTS, SCENE_MAX_BYTES, SCENE_MAX_BATCH, HTML_CANVAS_TYPES, } from './canvas/index.js';
100
+ export type { HtmlCanvasType, SceneFile, SceneObject, SceneObjectType, ExtrudeAxis, GroupObject, WedgeObject, SceneCamera, SceneLight, SceneMaterial, SceneOp, SceneChange, SceneEditSource, } from './canvas/index.js';
101
101
  export type { ISceneWriter, SceneReadResult, SceneApplyResult, SceneCreateResult, SceneLastEdit, } from './platform/index.js';
102
102
  export type { CanvasType, ParamValue, Control, ControlManifest, CanvasRecord, CanvasSummary, CreateCanvasInput, UpdateCanvasInput, SliderControl, NumberControl, ToggleControl, SelectControl, ColorControl, TextControl, ManifestValidationResult, } from './canvas/index.js';
103
103
  export type { Tool, HooksConfig, AgentEvent, Message, LLMProvider, AnchorInput, ToolExecutionResult, AgentRunResult, PermissionHandler, PermissionHandlerResponse, ToolPermission, AgentTypeConfig, GuardrailTriggeredHandler, BeforeLLMHookResult, BeforeToolHook, BeforeToolHookResult, AfterToolHook, AgentState, AgentConfig, SessionInfo, Anchor, AnchorScope, AnchorClearOptions, AnchorPriority, AnchorQueryOptions, FileAccessType, FileAccess, GuardrailResult, GuardrailContext, MCPClient, MCPToolDefinition, } from '@compilr-dev/agents';
package/dist/index.js CHANGED
@@ -208,7 +208,7 @@ export { classifyAgentError, isApiKeyError } from './errors/classify.js';
208
208
  // Also exposed at the renderer-safe subpath `@compilr-dev/sdk/canvas`.
209
209
  export { validateControlManifest, seedValues } from './canvas/index.js';
210
210
  // 3D scenes (the `scene` canvas type) — contract + pure ops. Also at `@compilr-dev/sdk/canvas`.
211
- export { validateScene, normalizeScene, serializeScene, emptyScene, sceneBounds, fitCamera, liftFor, describeScene, applySceneOp, applySceneOps, formatUserEditNotice, SCENE_MAX_OBJECTS, SCENE_MAX_BYTES, SCENE_MAX_BATCH, HTML_CANVAS_TYPES, } from './canvas/index.js';
211
+ export { validateScene, normalizeScene, serializeScene, emptyScene, sceneBounds, sceneObjectBounds, fitCamera, liftFor, describeScene, applySceneOp, applySceneOps, formatUserEditNotice, SCENE_MAX_OBJECTS, SCENE_MAX_BYTES, SCENE_MAX_BATCH, HTML_CANVAS_TYPES, } from './canvas/index.js';
212
212
  // =============================================================================
213
213
  // Shared Permission Defaults & Utilities
214
214
  // =============================================================================
@@ -34,8 +34,8 @@ export const MODEL_REGISTRY = [
34
34
  contextWindow: 200000,
35
35
  },
36
36
  {
37
- id: 'claude-sonnet-5',
38
- displayName: 'Sonnet 5',
37
+ id: 'claude-sonnet-5-5',
38
+ displayName: 'Sonnet 5.5',
39
39
  description: 'Balanced (recommended)',
40
40
  provider: 'claude',
41
41
  supportsImages: true,
@@ -44,6 +44,7 @@ export const MODEL_REGISTRY = [
44
44
  thinkingFormat: 'claude',
45
45
  status: 'supported',
46
46
  contextWindow: 1000000,
47
+ notes: 'Adaptive thinking (default effort high); $2/$10 per MTok, same as Sonnet 5',
47
48
  },
48
49
  {
49
50
  id: 'claude-opus-5-5',
@@ -71,6 +72,18 @@ export const MODEL_REGISTRY = [
71
72
  notes: 'Flagship: adaptive thinking always on; premium pricing ($10/$50 per MTok)',
72
73
  },
73
74
  // Legacy Claude models (still supported)
75
+ {
76
+ id: 'claude-sonnet-5',
77
+ displayName: 'Sonnet 5',
78
+ description: 'Previous generation',
79
+ provider: 'claude',
80
+ supportsImages: true,
81
+ supportsTools: true,
82
+ thinkingFormat: 'claude',
83
+ status: 'supported',
84
+ contextWindow: 1000000,
85
+ notes: 'Legacy — consider upgrading to Sonnet 5.5 (same price)',
86
+ },
74
87
  {
75
88
  id: 'claude-opus-5',
76
89
  displayName: 'Opus 5',
@@ -147,17 +160,6 @@ export const MODEL_REGISTRY = [
147
160
  // Gemini Models (Google)
148
161
  // ---------------------------------------------------------------------------
149
162
  // Gemini 2.x - Supported
150
- {
151
- id: 'gemini-2.0-flash',
152
- displayName: 'Gemini 2.0 Flash',
153
- description: 'Fast, no thinking',
154
- provider: 'gemini',
155
- supportsImages: true,
156
- thinkingFormat: 'none',
157
- status: 'supported',
158
- contextWindow: 1000000,
159
- notes: 'Fast, no thinking blocks',
160
- },
161
163
  {
162
164
  id: 'gemini-2.5-flash-lite',
163
165
  displayName: 'Gemini 2.5 Flash Lite',
@@ -40,11 +40,31 @@ export function sceneOutline(scene) {
40
40
  list.push(o);
41
41
  byParent.set(key, list);
42
42
  }
43
+ /** The type column, with what changes its shape: "group (3 inside)", "cone (4 sides)", … */
44
+ const typeOf = (o) => {
45
+ switch (o.type) {
46
+ case 'group': {
47
+ const n = byParent.get(o.id)?.length ?? 0;
48
+ return n === 0 ? 'group (empty)' : `group (${String(n)} inside)`;
49
+ }
50
+ case 'cone':
51
+ case 'cylinder':
52
+ return o.segments !== undefined ? `${o.type} (${String(o.segments)} sides)` : o.type;
53
+ case 'extrude':
54
+ return o.axis !== undefined && o.axis !== 'y' ? `extrude (along ${o.axis})` : o.type;
55
+ case 'wedge':
56
+ return o.ridge !== undefined && o.ridge !== 0.5
57
+ ? `wedge (ridge ${String(o.ridge)})`
58
+ : o.type;
59
+ default:
60
+ return o.type;
61
+ }
62
+ };
43
63
  const rows = [];
44
64
  const walk = (parent, depth) => {
45
65
  for (const o of byParent.get(parent) ?? []) {
46
66
  const lock = o.locked ? ' · locked' : '';
47
- rows.push(`${' '.repeat(depth)}${o.id} · ${o.type}${o.name ? ` · ${o.name}` : ''}${lock}`);
67
+ rows.push(`${' '.repeat(depth)}${o.id} · ${typeOf(o)}${o.name ? ` · ${o.name}` : ''}${lock}`);
48
68
  if (depth < 8)
49
69
  walk(o.id, depth + 1);
50
70
  }
@@ -88,8 +108,10 @@ const OBJECT_PROPERTIES = {
88
108
  type: {
89
109
  type: 'string',
90
110
  enum: SCENE_OBJECT_TYPES,
91
- description: 'box (size [w,h,d]) · sphere (radius) · cylinder / cone (radius, height) · torus (radius, tube; lies flat) · ' +
92
- 'extrude (points [[x,z],…] + height) for walls and floors. There is no "wall" type — use extrude.',
111
+ description: 'box (size [w,h,d]) · sphere (radius) · cylinder / cone (radius, height, optional segments) · torus (radius, tube; lies flat) · ' +
112
+ 'extrude (points + height, optional axis) for walls, floors and profiles · wedge (size [w,h,d], optional ridge) — a ' +
113
+ 'triangular prism for roofs · group — a container with no geometry: give it your own id and put parts in it with ' +
114
+ 'group: "<id>". There is no "wall" type — use extrude.',
93
115
  },
94
116
  position: {
95
117
  ...VEC3,
@@ -97,17 +119,49 @@ const OBJECT_PROPERTIES = {
97
119
  },
98
120
  id: {
99
121
  type: 'string',
100
- description: 'Optional stable id (lowercase, digits, - or _; ≤ 48). Omit to generate one from name.',
122
+ description: 'Your own id (lowercase letters, digits, - or _; ≤ 48), e.g. "house", "roof-left". Choose ids for ' +
123
+ 'groups so later objects — even in the same batch — can say group: "<id>". Must be new: an id already ' +
124
+ 'in the scene is an error (never renamed). Omit to generate one from name.',
101
125
  },
102
126
  name: { type: 'string', description: 'Display name (≤ 80 chars), e.g. "Sofa".' },
103
- size: { ...VEC3, description: 'box only: [width, height, depth] in metres (0.01–500).' },
104
- radius: { type: 'number', description: 'sphere/cylinder/cone/torus: radius in metres.' },
105
- height: { type: 'number', description: 'cylinder/cone/extrude: height in metres.' },
127
+ size: {
128
+ ...VEC3,
129
+ description: 'box / wedge: [width (x), height (y), depth (z)] in metres (0.01–500).',
130
+ },
131
+ radius: {
132
+ type: 'number',
133
+ description: 'sphere/cylinder/cone/torus: radius in metres. With segments it is the corner (circum-) radius.',
134
+ },
135
+ height: {
136
+ type: 'number',
137
+ description: 'cylinder/cone: height in metres. extrude: how far it extends along its axis.',
138
+ },
106
139
  tube: { type: 'number', description: 'torus only: tube radius (≤ radius).' },
140
+ segments: {
141
+ type: 'integer',
142
+ minimum: 3,
143
+ maximum: 64,
144
+ description: 'cone/cylinder only: number of flat sides (3–64); omit for smooth. cone + 4 = a pyramid (hip roof) — ' +
145
+ 'its corners point along ±x/±z, so add rotation [0, 45, 0] for sides parallel to the axes (side = radius·√2).',
146
+ },
107
147
  points: {
108
148
  type: 'array',
109
149
  items: { type: 'array', items: { type: 'number' }, minItems: 2, maxItems: 2 },
110
- description: 'extrude only: plan outline [[x, z], …] local to position, 3–256 points, not self-intersecting.',
150
+ description: 'extrude only: outline [[a, b], …] local to position, 3–256 points, not self-intersecting. ' +
151
+ 'axis "y" (default): [x, z] plan points, rising 0 → height. axis "z": [x, y] side profile ' +
152
+ '(b is up), pushed 0 → height along +z. axis "x": [z, y] side profile (b is up), pushed 0 → height along +x. ' +
153
+ "The profile's lowest point sits at position.y.",
154
+ },
155
+ axis: {
156
+ type: 'string',
157
+ enum: ['x', 'y', 'z'],
158
+ description: 'extrude only: the direction it extends. "y" (default) = a plan outline rising vertically (walls, floors); ' +
159
+ '"x" / "z" = a side profile (a gable, a roof section) extended horizontally — see points.',
160
+ },
161
+ ridge: {
162
+ type: 'number',
163
+ description: 'wedge only: where the apex sits across the width, 0–1 (default 0.5 = centred; 0 or 1 = a lean-to). ' +
164
+ 'The triangle is in the x–y plane; the ridge runs along z (the depth). Rotate [0, 90, 0] for a ridge along x.',
111
165
  },
112
166
  rotation: {
113
167
  ...VEC3,
@@ -115,15 +169,16 @@ const OBJECT_PROPERTIES = {
115
169
  },
116
170
  color: {
117
171
  type: 'string',
118
- description: '"#RRGGBB". Omit for the neutral palette — keep neutral colours unless asked.',
172
+ description: '"#RRGGBB". Omit for the neutral palette — keep neutral colours unless asked. Not on a group.',
119
173
  },
120
174
  group: {
121
175
  type: 'string',
122
- description: "Parent object id. position/rotation become relative to the parent (y = 0 is the parent's bottom).",
176
+ description: 'Parent id — normally a type "group" container, created earlier or in the same batch. position/rotation ' +
177
+ "become relative to the parent (y = 0 is the group's y); moving the group moves its children.",
123
178
  },
124
179
  material: {
125
180
  type: 'object',
126
- description: '{ roughness 0–1, metalness 0–1, opacity 0.05–1 }.',
181
+ description: '{ roughness 0–1, metalness 0–1, opacity 0.05–1 }. Not on a group.',
127
182
  properties: {
128
183
  roughness: { type: 'number' },
129
184
  metalness: { type: 'number' },
@@ -257,7 +312,9 @@ export function createSceneTools(config, options) {
257
312
  name: 'scene_add_object',
258
313
  description: 'Add one object to a 3D scene, or up to 50 at once with objects: [...] (atomic: all or none). ' +
259
314
  "Units are metres; position.y is the object's BOTTOM (0 = on the floor); rotation in degrees about Y. " +
260
- 'Returns the new id(s) — generated from name when id is omitted.',
315
+ 'Build hierarchically: a type "group" with your own id (e.g. "house", then "walls", "roof" inside it), ' +
316
+ 'and its parts with group: "<id>" — a batch may name a group it creates itself. ' +
317
+ 'Returns the new id(s) — yours, or generated from name when id is omitted.',
261
318
  inputSchema: {
262
319
  type: 'object',
263
320
  properties: {
@@ -315,7 +372,9 @@ export function createSceneTools(config, options) {
315
372
  id: { type: 'string', description: 'The object id to change.' },
316
373
  patch: {
317
374
  type: 'object',
318
- description: 'Fields to set, e.g. { "position": [1.2, 0, -0.4], "color": "#8FA3B0" }. Same fields as scene_add_object.',
375
+ description: 'Fields to set, e.g. { "position": [1.2, 0, -0.4], "color": "#8FA3B0" }. Same fields as scene_add_object: ' +
376
+ 'size, radius, height, tube, points, segments (cone/cylinder), axis (extrude), ridge (wedge), group ' +
377
+ '(move it into a group; null = back to the root), name, rotation, color/material (not on a group), locked.',
319
378
  },
320
379
  },
321
380
  required: ['canvas_id', 'id', 'patch'],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compilr-dev/sdk",
3
- "version": "0.30.0",
3
+ "version": "0.31.1",
4
4
  "description": "Universal agent runtime for building AI-powered applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -81,7 +81,7 @@
81
81
  "node": ">=20.0.0"
82
82
  },
83
83
  "dependencies": {
84
- "@compilr-dev/agents": "^0.6.10",
84
+ "@compilr-dev/agents": "^0.6.11",
85
85
  "@compilr-dev/logger": "^0.1.0",
86
86
  "ajv": "^6.14.0",
87
87
  "yaml": "^2.8.4"