@compilr-dev/sdk 0.37.0 → 0.38.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.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
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' | 'wedge' | 'group';
|
|
16
|
+
export type SceneObjectType = 'box' | 'sphere' | 'cylinder' | 'cone' | 'torus' | 'extrude' | 'wedge' | 'group' | 'label';
|
|
17
17
|
export declare const SCENE_OBJECT_TYPES: readonly SceneObjectType[];
|
|
18
18
|
/** The direction an extrude extends along (§17). */
|
|
19
19
|
export type ExtrudeAxis = 'x' | 'y' | 'z';
|
|
@@ -133,7 +133,32 @@ export interface GroupObject extends SceneObjectBase {
|
|
|
133
133
|
color?: never;
|
|
134
134
|
material?: never;
|
|
135
135
|
}
|
|
136
|
-
|
|
136
|
+
/** Which way a label turns (§19, D-L2). */
|
|
137
|
+
export type LabelFace = 'camera' | 'fixed';
|
|
138
|
+
export declare const SCENE_LABEL_FACES: readonly LabelFace[];
|
|
139
|
+
/**
|
|
140
|
+
* Visible text in the scene (§19). Drawn as an unlit textured plane, so it reads the same in
|
|
141
|
+
* shadow as in the sun and it survives into the GLB and into `scene_screenshot` — which a DOM
|
|
142
|
+
* overlay would not, and those are the two places the work leaves the app.
|
|
143
|
+
*
|
|
144
|
+
* `color` is the INK. `material` is rejected: roughness and metalness mean nothing for unlit
|
|
145
|
+
* text, and a half-faded label is a request nobody has.
|
|
146
|
+
*
|
|
147
|
+
* Parent it to name a thing — `group: "<id>"` — and it moves when that thing moves.
|
|
148
|
+
*/
|
|
149
|
+
export interface LabelObject extends SceneObjectBase {
|
|
150
|
+
type: 'label';
|
|
151
|
+
/** One line, 1–120 chars. A newline is rejected: use two labels. */
|
|
152
|
+
text: string;
|
|
153
|
+
/** Cap height in metres (the width follows the text). */
|
|
154
|
+
size?: number;
|
|
155
|
+
/** '#RRGGBB' plate behind the text. Omitted = transparent. */
|
|
156
|
+
background?: string;
|
|
157
|
+
/** 'camera' (default) turns to face the viewer; 'fixed' obeys `rotation`. */
|
|
158
|
+
face?: LabelFace;
|
|
159
|
+
material?: never;
|
|
160
|
+
}
|
|
161
|
+
export type SceneObject = BoxObject | SphereObject | CylinderObject | ConeObject | TorusObject | ExtrudeObject | WedgeObject | GroupObject | LabelObject;
|
|
137
162
|
export interface SceneCamera {
|
|
138
163
|
position: Vec3;
|
|
139
164
|
target: Vec3;
|
|
@@ -210,6 +235,24 @@ export declare const SCENE_MAX_SHADOW_LIGHTS = 2;
|
|
|
210
235
|
export declare const SCENE_LIGHT_INTENSITY_MAX = 10;
|
|
211
236
|
export declare const SCENE_MAX_GROUP_DEPTH = 8;
|
|
212
237
|
export declare const SCENE_NAME_MAX = 80;
|
|
238
|
+
/** Label text (§19). One line — long enough for a room name or a dimension, not a paragraph. */
|
|
239
|
+
export declare const SCENE_LABEL_TEXT_MAX = 120;
|
|
240
|
+
/** Label cap height in metres. */
|
|
241
|
+
export declare const SCENE_LABEL_SIZE_MIN = 0.02;
|
|
242
|
+
export declare const SCENE_LABEL_SIZE_MAX = 20;
|
|
243
|
+
export declare const SCENE_LABEL_SIZE_DEFAULT = 0.25;
|
|
244
|
+
/**
|
|
245
|
+
* Estimated advance per character, as a fraction of the cap height — used for a label's BOUNDS.
|
|
246
|
+
*
|
|
247
|
+
* ⚠️ An estimate on purpose. The real width comes from measuring the text in a 2D canvas, which
|
|
248
|
+
* this module cannot do (it runs in Node, in the CLI, and in tests with no DOM). The renderer
|
|
249
|
+
* measures and builds its plane from that; this number only decides how much room a label is
|
|
250
|
+
* given when the camera is fitted to the scene, where being a few centimetres out is invisible.
|
|
251
|
+
* Anything that must agree exactly uses `liftFor`, which depends on the height alone.
|
|
252
|
+
*/
|
|
253
|
+
export declare const SCENE_LABEL_ADVANCE = 0.62;
|
|
254
|
+
/** A label's plane in metres, [width, height] — the width estimated (see SCENE_LABEL_ADVANCE). */
|
|
255
|
+
export declare function labelPlaneSize(obj: LabelObject): Vec2;
|
|
213
256
|
export declare const SCENE_OPACITY_MIN = 0.05;
|
|
214
257
|
export declare const SCENE_ID_PATTERN: RegExp;
|
|
215
258
|
/** Object colours never read as state (README) — a neutral palette, filled by insertion index. */
|
package/dist/canvas/scene.js
CHANGED
|
@@ -23,8 +23,10 @@ export const SCENE_OBJECT_TYPES = [
|
|
|
23
23
|
'extrude',
|
|
24
24
|
'wedge',
|
|
25
25
|
'group',
|
|
26
|
+
'label',
|
|
26
27
|
];
|
|
27
28
|
export const SCENE_EXTRUDE_AXES = ['x', 'y', 'z'];
|
|
29
|
+
export const SCENE_LABEL_FACES = ['camera', 'fixed'];
|
|
28
30
|
// =============================================================================
|
|
29
31
|
// Limits and defaults (§2.3, §2.4, Q-1, Q-14)
|
|
30
32
|
// =============================================================================
|
|
@@ -56,6 +58,27 @@ export const SCENE_MAX_SHADOW_LIGHTS = 2;
|
|
|
56
58
|
export const SCENE_LIGHT_INTENSITY_MAX = 10;
|
|
57
59
|
export const SCENE_MAX_GROUP_DEPTH = 8;
|
|
58
60
|
export const SCENE_NAME_MAX = 80;
|
|
61
|
+
/** Label text (§19). One line — long enough for a room name or a dimension, not a paragraph. */
|
|
62
|
+
export const SCENE_LABEL_TEXT_MAX = 120;
|
|
63
|
+
/** Label cap height in metres. */
|
|
64
|
+
export const SCENE_LABEL_SIZE_MIN = 0.02;
|
|
65
|
+
export const SCENE_LABEL_SIZE_MAX = 20;
|
|
66
|
+
export const SCENE_LABEL_SIZE_DEFAULT = 0.25;
|
|
67
|
+
/**
|
|
68
|
+
* Estimated advance per character, as a fraction of the cap height — used for a label's BOUNDS.
|
|
69
|
+
*
|
|
70
|
+
* ⚠️ An estimate on purpose. The real width comes from measuring the text in a 2D canvas, which
|
|
71
|
+
* this module cannot do (it runs in Node, in the CLI, and in tests with no DOM). The renderer
|
|
72
|
+
* measures and builds its plane from that; this number only decides how much room a label is
|
|
73
|
+
* given when the camera is fitted to the scene, where being a few centimetres out is invisible.
|
|
74
|
+
* Anything that must agree exactly uses `liftFor`, which depends on the height alone.
|
|
75
|
+
*/
|
|
76
|
+
export const SCENE_LABEL_ADVANCE = 0.62;
|
|
77
|
+
/** A label's plane in metres, [width, height] — the width estimated (see SCENE_LABEL_ADVANCE). */
|
|
78
|
+
export function labelPlaneSize(obj) {
|
|
79
|
+
const h = obj.size ?? SCENE_LABEL_SIZE_DEFAULT;
|
|
80
|
+
return [Math.max(h * 0.5, obj.text.length * h * SCENE_LABEL_ADVANCE), h];
|
|
81
|
+
}
|
|
59
82
|
export const SCENE_OPACITY_MIN = 0.05;
|
|
60
83
|
export const SCENE_ID_PATTERN = /^[a-z0-9][a-z0-9_-]{0,47}$/;
|
|
61
84
|
/** Object colours never read as state (README) — a neutral palette, filled by insertion index. */
|
|
@@ -132,6 +155,8 @@ export function defaultObjectFor(type, id) {
|
|
|
132
155
|
return { ...base, type, size: [1, 1, 1] };
|
|
133
156
|
case 'group':
|
|
134
157
|
return { ...base, type };
|
|
158
|
+
case 'label':
|
|
159
|
+
return { ...base, type, text: 'Label' };
|
|
135
160
|
}
|
|
136
161
|
}
|
|
137
162
|
/** The stored form: pretty-printed, because agents read and cite it (§4.1). */
|
|
@@ -177,6 +202,7 @@ const TYPE_KEYS = {
|
|
|
177
202
|
extrude: ['points', 'height', 'axis'],
|
|
178
203
|
wedge: ['size', 'ridge'],
|
|
179
204
|
group: [],
|
|
205
|
+
label: ['text', 'size', 'background', 'face'],
|
|
180
206
|
};
|
|
181
207
|
/** How each type is described in errors (what fields it takes). */
|
|
182
208
|
const TYPE_SHAPE = {
|
|
@@ -188,6 +214,7 @@ const TYPE_SHAPE = {
|
|
|
188
214
|
extrude: 'extrude takes points [[a, b], …], height and optional axis',
|
|
189
215
|
wedge: 'wedge takes size [w, h, d] and optional ridge',
|
|
190
216
|
group: 'a group is a container: id, name, position, rotation, group, locked, tags only — colour its children',
|
|
217
|
+
label: 'label takes text and optional size (cap height in metres), background, face ("camera" or "fixed"); color is the ink and material is not allowed',
|
|
191
218
|
};
|
|
192
219
|
const MATERIAL_KEYS = new Set(['roughness', 'metalness', 'opacity']);
|
|
193
220
|
const HEX_RE = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
@@ -337,7 +364,8 @@ function validateObject(raw, i, errors) {
|
|
|
337
364
|
// Unknown keys: rejected, because they would round-trip silently and read as honoured.
|
|
338
365
|
const allowed = new Set([
|
|
339
366
|
...BASE_KEYS,
|
|
340
|
-
|
|
367
|
+
// A group has no surface at all; a label has ink (`color`) but no material (§19.2).
|
|
368
|
+
...(type === 'group' ? [] : type === 'label' ? ['color'] : SURFACE_KEYS),
|
|
341
369
|
...TYPE_KEYS[type],
|
|
342
370
|
]);
|
|
343
371
|
for (const k of Object.keys(raw)) {
|
|
@@ -386,7 +414,7 @@ function validateObject(raw, i, errors) {
|
|
|
386
414
|
}
|
|
387
415
|
}
|
|
388
416
|
}
|
|
389
|
-
if (type !== 'group' && raw.material !== undefined) {
|
|
417
|
+
if (type !== 'group' && type !== 'label' && raw.material !== undefined) {
|
|
390
418
|
if (!isRecord(raw.material)) {
|
|
391
419
|
errors.push(`${label}.material = ${show(raw.material)} — { roughness?, metalness?, opacity? }.`);
|
|
392
420
|
}
|
|
@@ -433,6 +461,29 @@ function validateObject(raw, i, errors) {
|
|
|
433
461
|
break;
|
|
434
462
|
case 'group':
|
|
435
463
|
break;
|
|
464
|
+
case 'label': {
|
|
465
|
+
if (typeof raw.text !== 'string' || raw.text.length < 1) {
|
|
466
|
+
errors.push(`${label}.text = ${show(raw.text)} — the words to show, 1–${String(SCENE_LABEL_TEXT_MAX)} chars.`);
|
|
467
|
+
}
|
|
468
|
+
else if (raw.text.length > SCENE_LABEL_TEXT_MAX) {
|
|
469
|
+
errors.push(`${label}.text is ${String(raw.text.length)} chars (max ${String(SCENE_LABEL_TEXT_MAX)}) — a label is one short line, not a paragraph.`);
|
|
470
|
+
}
|
|
471
|
+
else if (/[\n\r]/.test(raw.text)) {
|
|
472
|
+
errors.push(`${label}.text has a line break — a label is one line; use two labels.`);
|
|
473
|
+
}
|
|
474
|
+
if (raw.size !== undefined) {
|
|
475
|
+
checkNumber(raw.size, `${label}.size`, SCENE_LABEL_SIZE_MIN, SCENE_LABEL_SIZE_MAX, `size is the cap height in metres, ${fmtNum(SCENE_LABEL_SIZE_MIN)}–${fmtNum(SCENE_LABEL_SIZE_MAX)} (the width follows the text)`, errors);
|
|
476
|
+
}
|
|
477
|
+
if (raw.background !== undefined &&
|
|
478
|
+
(typeof raw.background !== 'string' || !HEX_RE.test(raw.background))) {
|
|
479
|
+
errors.push(`${label}.background = ${show(raw.background)} — colours are "#RGB" or "#RRGGBB" hex.`);
|
|
480
|
+
}
|
|
481
|
+
if (raw.face !== undefined &&
|
|
482
|
+
!SCENE_LABEL_FACES.includes(raw.face)) {
|
|
483
|
+
errors.push(`${label}.face = ${show(raw.face)} — "camera" (turns to face the viewer) or "fixed" (obeys rotation).`);
|
|
484
|
+
}
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
436
487
|
case 'torus': {
|
|
437
488
|
const rOk = checkNumber(raw.radius, `${label}.radius`, SCENE_RADIUS_MIN, SCENE_RADIUS_MAX, RADIUS_RULE, errors);
|
|
438
489
|
const maxTube = rOk ? raw.radius : SCENE_RADIUS_MAX;
|
|
@@ -779,6 +830,9 @@ export function liftFor(obj) {
|
|
|
779
830
|
}
|
|
780
831
|
case 'group':
|
|
781
832
|
return 0;
|
|
833
|
+
// Half the plane's height, so `y` is the bottom here too (D-L6).
|
|
834
|
+
case 'label':
|
|
835
|
+
return (obj.size ?? SCENE_LABEL_SIZE_DEFAULT) / 2;
|
|
782
836
|
}
|
|
783
837
|
}
|
|
784
838
|
/**
|
|
@@ -822,6 +876,12 @@ function extrudeBox(obj) {
|
|
|
822
876
|
/** Local AABB of the mesh around its own origin (the pivot). null for a group (no geometry). */
|
|
823
877
|
function localBox(obj) {
|
|
824
878
|
switch (obj.type) {
|
|
879
|
+
// A flat plane in x–y, centred on the pivot. Its depth is zero, and the width is the
|
|
880
|
+
// estimate (SCENE_LABEL_ADVANCE) — a label never decides a scene's extents on its own.
|
|
881
|
+
case 'label': {
|
|
882
|
+
const [w, h] = labelPlaneSize(obj);
|
|
883
|
+
return { min: [-w / 2, -h / 2, 0], max: [w / 2, h / 2, 0] };
|
|
884
|
+
}
|
|
825
885
|
case 'box':
|
|
826
886
|
case 'wedge': {
|
|
827
887
|
// A wedge's triangle spans the full width at its base and the full height at its apex,
|
package/dist/models/providers.js
CHANGED
|
@@ -40,7 +40,12 @@ export const PROVIDER_METADATA = {
|
|
|
40
40
|
displayName: 'Google AI (Gemini)',
|
|
41
41
|
description: 'Gemini models from Google',
|
|
42
42
|
endpoint: 'https://generativelanguage.googleapis.com/v1beta',
|
|
43
|
-
|
|
43
|
+
// Must be a variable `detectProviderFromEnv` actually reads (see ENV_PROVIDER_MAP in
|
|
44
|
+
// provider.ts). It said `GOOGLE_API_KEY` for months — a variable nothing anywhere reads —
|
|
45
|
+
// and that string is what the CLI prints in "export <var>=..." when the key is missing and
|
|
46
|
+
// in the /model detail pane, so the one user who followed it set a variable that was then
|
|
47
|
+
// ignored. `GEMINI_API_KEY` is also accepted as a fallback.
|
|
48
|
+
envVar: 'GOOGLE_AI_API_KEY',
|
|
44
49
|
keyUrl: 'https://aistudio.google.com/apikey',
|
|
45
50
|
requiresKey: true,
|
|
46
51
|
category: 'primary',
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { defineTool, createErrorResult } from '@compilr-dev/agents';
|
|
16
16
|
import { createSceneWriter } from '../scene-writer.js';
|
|
17
|
-
import { SCENE_MAX_BATCH, SCENE_MAX_LAYERS, SCENE_OBJECT_TYPES, describeScene, serializeScene, } from '../../canvas/scene.js';
|
|
17
|
+
import { SCENE_MAX_BATCH, SCENE_MAX_LAYERS, SCENE_OBJECT_TYPES, SCENE_LABEL_FACES, describeScene, serializeScene, } from '../../canvas/scene.js';
|
|
18
18
|
import { describeLayers, emptyLayers, undeclaredTags } from '../../canvas/scene-layers.js';
|
|
19
19
|
import { formatUserEditNotice, } from '../../canvas/scene-ops.js';
|
|
20
20
|
export const SCENE_TOOL_NAMES = [
|
|
@@ -200,7 +200,9 @@ const OBJECT_PROPERTIES = {
|
|
|
200
200
|
description: 'box (size [w,h,d]) · sphere (radius) · cylinder / cone (radius, height, optional segments) · torus (radius, tube; lies flat) · ' +
|
|
201
201
|
'extrude (points + height, optional axis) for walls, floors and profiles · wedge (size [w,h,d], optional ridge) — a ' +
|
|
202
202
|
'triangular prism for roofs · group — a container with no geometry: give it your own id and put parts in it with ' +
|
|
203
|
-
'group: "<id>"
|
|
203
|
+
'group: "<id>" · label (text) — VISIBLE TEXT in the scene, for room names, part names and ' +
|
|
204
|
+
'callouts; parent it with group: "<id>" to name a thing and it moves with it. ' +
|
|
205
|
+
'There is no "wall" type — use extrude.',
|
|
204
206
|
},
|
|
205
207
|
position: {
|
|
206
208
|
...VEC3,
|
|
@@ -226,6 +228,25 @@ const OBJECT_PROPERTIES = {
|
|
|
226
228
|
description: 'cylinder/cone: height in metres. extrude: how far it extends along its axis.',
|
|
227
229
|
},
|
|
228
230
|
tube: { type: 'number', description: 'torus only: tube radius (≤ radius).' },
|
|
231
|
+
text: {
|
|
232
|
+
type: 'string',
|
|
233
|
+
description: 'label only: the words to show (≤ 120, ONE line — no newline; use two labels). The scene is ' +
|
|
234
|
+
'otherwise unlabelled solids, so name the rooms and the parts: a screenshot with no text in ' +
|
|
235
|
+
'it cannot be read back by you or by anyone else.',
|
|
236
|
+
},
|
|
237
|
+
background: {
|
|
238
|
+
type: 'string',
|
|
239
|
+
description: 'label only: "#RRGGBB" plate behind the text. Omit for transparent — add one only where the ' +
|
|
240
|
+
'label sits over busy geometry.',
|
|
241
|
+
},
|
|
242
|
+
face: {
|
|
243
|
+
type: 'string',
|
|
244
|
+
enum: SCENE_LABEL_FACES,
|
|
245
|
+
description: 'label only: "camera" (default) turns the text to the viewer, for a callout. "fixed" obeys ' +
|
|
246
|
+
'rotation, for text that belongs to a surface — a room name lying on the floor needs ' +
|
|
247
|
+
'face: "fixed" with rotation [-90, 0, 0]. ⚠️ A GLB export bakes whichever way a "camera" ' +
|
|
248
|
+
'label happened to face.',
|
|
249
|
+
},
|
|
229
250
|
segments: {
|
|
230
251
|
type: 'integer',
|
|
231
252
|
minimum: 3,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@compilr-dev/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
4
4
|
"description": "Universal agent runtime for building AI-powered applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"node": ">=20.0.0"
|
|
83
83
|
},
|
|
84
84
|
"dependencies": {
|
|
85
|
-
"@compilr-dev/agents": "^0.
|
|
85
|
+
"@compilr-dev/agents": "^0.9.0",
|
|
86
86
|
"@compilr-dev/logger": "^0.1.0",
|
|
87
87
|
"ajv": "^6.14.0",
|
|
88
88
|
"yaml": "^2.8.4"
|