@compilr-dev/sdk 0.30.0 → 0.31.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.
- package/dist/canvas/scene-ops.js +63 -18
- package/dist/canvas/scene.d.ts +79 -9
- package/dist/canvas/scene.js +176 -56
- package/dist/capabilities/packs.js +11 -5
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/platform/tools/scene-tools.js +72 -13
- package/package.json +1 -1
package/dist/canvas/scene-ops.js
CHANGED
|
@@ -13,7 +13,17 @@ import { SCENE_ID_PATTERN, SCENE_MAX_BATCH, SCENE_OBJECT_TYPES, descendantsOf, f
|
|
|
13
13
|
// =============================================================================
|
|
14
14
|
// Helpers
|
|
15
15
|
// =============================================================================
|
|
16
|
-
|
|
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
|
|
58
|
-
const
|
|
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: `${
|
|
64
|
-
|
|
65
|
-
|
|
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];
|
package/dist/canvas/scene.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
182
|
-
*
|
|
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.
|
|
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
|
-
/**
|
|
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[];
|
package/dist/canvas/scene.js
CHANGED
|
@@ -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
|
-
|
|
152
|
-
|
|
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
|
|
174
|
-
cone: 'cone takes radius and
|
|
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 [[
|
|
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([
|
|
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
|
-
|
|
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
|
-
|
|
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 [
|
|
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
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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
|
|
637
|
-
*
|
|
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
|
-
/**
|
|
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: [
|
|
670
|
-
max: [
|
|
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
|
-
|
|
678
|
-
|
|
679
|
-
|
|
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.
|
|
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
|
|
750
|
-
|
|
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
|
-
|
|
757
|
-
|
|
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
|
|
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
|
|
289
|
-
'
|
|
290
|
-
'
|
|
291
|
-
|
|
292
|
-
|
|
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
|
// =============================================================================
|
|
@@ -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
|
|
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
|
|
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: '
|
|
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: {
|
|
104
|
-
|
|
105
|
-
|
|
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:
|
|
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:
|
|
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
|
-
'
|
|
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'],
|