@compilr-dev/sdk 0.37.1 → 0.39.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';
|
|
@@ -21,10 +21,26 @@ export declare const SCENE_EXTRUDE_AXES: readonly ExtrudeAxis[];
|
|
|
21
21
|
export interface SceneMaterial {
|
|
22
22
|
/** 0..1, default 0.78 */
|
|
23
23
|
roughness?: number;
|
|
24
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* 0..1, default 0.02.
|
|
26
|
+
*
|
|
27
|
+
* ⚠️ Metal is MIRROR, not colour: a metal with nothing to reflect renders near-black. The
|
|
28
|
+
* viewer lights the scene with a neutral room environment so this means something; without
|
|
29
|
+
* one, raising it made objects darker, which is why it had looked like a dead knob.
|
|
30
|
+
*/
|
|
25
31
|
metalness?: number;
|
|
26
32
|
/** 0.05..1, default 1 (transparent when < 1) */
|
|
27
33
|
opacity?: number;
|
|
34
|
+
/**
|
|
35
|
+
* '#RRGGBB' the object gives off, default '#000000' (none). Emission is NOT light: it
|
|
36
|
+
* brightens the object itself and casts nothing on its neighbours, so it marks a thing as
|
|
37
|
+
* live or active — an indicator, a hot path, a selected step — rather than lighting a room.
|
|
38
|
+
*/
|
|
39
|
+
emissive?: string;
|
|
40
|
+
/** How strong the emission is. 0..5, default 1. Only means anything with `emissive` set. */
|
|
41
|
+
emissiveIntensity?: number;
|
|
42
|
+
/** Draw the edges only. Default false. For the parts of a diagram that are logical, not built. */
|
|
43
|
+
wireframe?: boolean;
|
|
28
44
|
}
|
|
29
45
|
export interface SceneObjectBase {
|
|
30
46
|
/** Unique in the scene; /^[a-z0-9][a-z0-9_-]{0,47}$/ */
|
|
@@ -133,7 +149,32 @@ export interface GroupObject extends SceneObjectBase {
|
|
|
133
149
|
color?: never;
|
|
134
150
|
material?: never;
|
|
135
151
|
}
|
|
136
|
-
|
|
152
|
+
/** Which way a label turns (§19, D-L2). */
|
|
153
|
+
export type LabelFace = 'camera' | 'fixed';
|
|
154
|
+
export declare const SCENE_LABEL_FACES: readonly LabelFace[];
|
|
155
|
+
/**
|
|
156
|
+
* Visible text in the scene (§19). Drawn as an unlit textured plane, so it reads the same in
|
|
157
|
+
* shadow as in the sun and it survives into the GLB and into `scene_screenshot` — which a DOM
|
|
158
|
+
* overlay would not, and those are the two places the work leaves the app.
|
|
159
|
+
*
|
|
160
|
+
* `color` is the INK. `material` is rejected: roughness and metalness mean nothing for unlit
|
|
161
|
+
* text, and a half-faded label is a request nobody has.
|
|
162
|
+
*
|
|
163
|
+
* Parent it to name a thing — `group: "<id>"` — and it moves when that thing moves.
|
|
164
|
+
*/
|
|
165
|
+
export interface LabelObject extends SceneObjectBase {
|
|
166
|
+
type: 'label';
|
|
167
|
+
/** One line, 1–120 chars. A newline is rejected: use two labels. */
|
|
168
|
+
text: string;
|
|
169
|
+
/** Cap height in metres (the width follows the text). */
|
|
170
|
+
size?: number;
|
|
171
|
+
/** '#RRGGBB' plate behind the text. Omitted = transparent. */
|
|
172
|
+
background?: string;
|
|
173
|
+
/** 'camera' (default) turns to face the viewer; 'fixed' obeys `rotation`. */
|
|
174
|
+
face?: LabelFace;
|
|
175
|
+
material?: never;
|
|
176
|
+
}
|
|
177
|
+
export type SceneObject = BoxObject | SphereObject | CylinderObject | ConeObject | TorusObject | ExtrudeObject | WedgeObject | GroupObject | LabelObject;
|
|
137
178
|
export interface SceneCamera {
|
|
138
179
|
position: Vec3;
|
|
139
180
|
target: Vec3;
|
|
@@ -210,6 +251,24 @@ export declare const SCENE_MAX_SHADOW_LIGHTS = 2;
|
|
|
210
251
|
export declare const SCENE_LIGHT_INTENSITY_MAX = 10;
|
|
211
252
|
export declare const SCENE_MAX_GROUP_DEPTH = 8;
|
|
212
253
|
export declare const SCENE_NAME_MAX = 80;
|
|
254
|
+
/** Label text (§19). One line — long enough for a room name or a dimension, not a paragraph. */
|
|
255
|
+
export declare const SCENE_LABEL_TEXT_MAX = 120;
|
|
256
|
+
/** Label cap height in metres. */
|
|
257
|
+
export declare const SCENE_LABEL_SIZE_MIN = 0.02;
|
|
258
|
+
export declare const SCENE_LABEL_SIZE_MAX = 20;
|
|
259
|
+
export declare const SCENE_LABEL_SIZE_DEFAULT = 0.25;
|
|
260
|
+
/**
|
|
261
|
+
* Estimated advance per character, as a fraction of the cap height — used for a label's BOUNDS.
|
|
262
|
+
*
|
|
263
|
+
* ⚠️ An estimate on purpose. The real width comes from measuring the text in a 2D canvas, which
|
|
264
|
+
* this module cannot do (it runs in Node, in the CLI, and in tests with no DOM). The renderer
|
|
265
|
+
* measures and builds its plane from that; this number only decides how much room a label is
|
|
266
|
+
* given when the camera is fitted to the scene, where being a few centimetres out is invisible.
|
|
267
|
+
* Anything that must agree exactly uses `liftFor`, which depends on the height alone.
|
|
268
|
+
*/
|
|
269
|
+
export declare const SCENE_LABEL_ADVANCE = 0.62;
|
|
270
|
+
/** A label's plane in metres, [width, height] — the width estimated (see SCENE_LABEL_ADVANCE). */
|
|
271
|
+
export declare function labelPlaneSize(obj: LabelObject): Vec2;
|
|
213
272
|
export declare const SCENE_OPACITY_MIN = 0.05;
|
|
214
273
|
export declare const SCENE_ID_PATTERN: RegExp;
|
|
215
274
|
/** Object colours never read as state (README) — a neutral palette, filled by insertion index. */
|
|
@@ -217,6 +276,8 @@ export declare const SCENE_NEUTRAL_PALETTE: readonly string[];
|
|
|
217
276
|
/** Accessible names for the palette swatches, same order (Q-10). */
|
|
218
277
|
export declare const SCENE_PALETTE_NAMES: readonly string[];
|
|
219
278
|
export declare const SCENE_DEFAULT_MATERIAL: Readonly<Required<SceneMaterial>>;
|
|
279
|
+
/** `material.emissiveIntensity` range. Above ~5 everything clips to white. */
|
|
280
|
+
export declare const SCENE_EMISSIVE_INTENSITY_MAX = 5;
|
|
220
281
|
/** The reference camera, used for an empty scene. */
|
|
221
282
|
export declare const SCENE_DEFAULT_CAMERA: Readonly<SceneCamera>;
|
|
222
283
|
/** Vertical field of view of the viewer camera, degrees. */
|
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. */
|
|
@@ -82,7 +105,12 @@ export const SCENE_DEFAULT_MATERIAL = {
|
|
|
82
105
|
roughness: 0.78,
|
|
83
106
|
metalness: 0.02,
|
|
84
107
|
opacity: 1,
|
|
108
|
+
emissive: '#000000',
|
|
109
|
+
emissiveIntensity: 1,
|
|
110
|
+
wireframe: false,
|
|
85
111
|
};
|
|
112
|
+
/** `material.emissiveIntensity` range. Above ~5 everything clips to white. */
|
|
113
|
+
export const SCENE_EMISSIVE_INTENSITY_MAX = 5;
|
|
86
114
|
/** The reference camera, used for an empty scene. */
|
|
87
115
|
export const SCENE_DEFAULT_CAMERA = {
|
|
88
116
|
position: [7, 5.5, 8],
|
|
@@ -132,6 +160,8 @@ export function defaultObjectFor(type, id) {
|
|
|
132
160
|
return { ...base, type, size: [1, 1, 1] };
|
|
133
161
|
case 'group':
|
|
134
162
|
return { ...base, type };
|
|
163
|
+
case 'label':
|
|
164
|
+
return { ...base, type, text: 'Label' };
|
|
135
165
|
}
|
|
136
166
|
}
|
|
137
167
|
/** The stored form: pretty-printed, because agents read and cite it (§4.1). */
|
|
@@ -177,6 +207,7 @@ const TYPE_KEYS = {
|
|
|
177
207
|
extrude: ['points', 'height', 'axis'],
|
|
178
208
|
wedge: ['size', 'ridge'],
|
|
179
209
|
group: [],
|
|
210
|
+
label: ['text', 'size', 'background', 'face'],
|
|
180
211
|
};
|
|
181
212
|
/** How each type is described in errors (what fields it takes). */
|
|
182
213
|
const TYPE_SHAPE = {
|
|
@@ -188,8 +219,16 @@ const TYPE_SHAPE = {
|
|
|
188
219
|
extrude: 'extrude takes points [[a, b], …], height and optional axis',
|
|
189
220
|
wedge: 'wedge takes size [w, h, d] and optional ridge',
|
|
190
221
|
group: 'a group is a container: id, name, position, rotation, group, locked, tags only — colour its children',
|
|
222
|
+
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
223
|
};
|
|
192
|
-
const MATERIAL_KEYS = new Set([
|
|
224
|
+
const MATERIAL_KEYS = new Set([
|
|
225
|
+
'roughness',
|
|
226
|
+
'metalness',
|
|
227
|
+
'opacity',
|
|
228
|
+
'emissive',
|
|
229
|
+
'emissiveIntensity',
|
|
230
|
+
'wireframe',
|
|
231
|
+
]);
|
|
193
232
|
const HEX_RE = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
194
233
|
/** Short, stable number rendering for messages and summaries. */
|
|
195
234
|
export function fmtNum(n) {
|
|
@@ -337,7 +376,8 @@ function validateObject(raw, i, errors) {
|
|
|
337
376
|
// Unknown keys: rejected, because they would round-trip silently and read as honoured.
|
|
338
377
|
const allowed = new Set([
|
|
339
378
|
...BASE_KEYS,
|
|
340
|
-
|
|
379
|
+
// A group has no surface at all; a label has ink (`color`) but no material (§19.2).
|
|
380
|
+
...(type === 'group' ? [] : type === 'label' ? ['color'] : SURFACE_KEYS),
|
|
341
381
|
...TYPE_KEYS[type],
|
|
342
382
|
]);
|
|
343
383
|
for (const k of Object.keys(raw)) {
|
|
@@ -386,7 +426,7 @@ function validateObject(raw, i, errors) {
|
|
|
386
426
|
}
|
|
387
427
|
}
|
|
388
428
|
}
|
|
389
|
-
if (type !== 'group' && raw.material !== undefined) {
|
|
429
|
+
if (type !== 'group' && type !== 'label' && raw.material !== undefined) {
|
|
390
430
|
if (!isRecord(raw.material)) {
|
|
391
431
|
errors.push(`${label}.material = ${show(raw.material)} — { roughness?, metalness?, opacity? }.`);
|
|
392
432
|
}
|
|
@@ -394,7 +434,7 @@ function validateObject(raw, i, errors) {
|
|
|
394
434
|
const m = raw.material;
|
|
395
435
|
for (const k of Object.keys(m)) {
|
|
396
436
|
if (!MATERIAL_KEYS.has(k)) {
|
|
397
|
-
errors.push(`${label}.material.${k} is not a material field — roughness, metalness, opacity.`);
|
|
437
|
+
errors.push(`${label}.material.${k} is not a material field — roughness, metalness, opacity, emissive, emissiveIntensity, wireframe.`);
|
|
398
438
|
}
|
|
399
439
|
}
|
|
400
440
|
if (m.roughness !== undefined)
|
|
@@ -403,6 +443,15 @@ function validateObject(raw, i, errors) {
|
|
|
403
443
|
checkNumber(m.metalness, `${label}.material.metalness`, 0, 1, 'metalness is 0–1', errors);
|
|
404
444
|
if (m.opacity !== undefined)
|
|
405
445
|
checkNumber(m.opacity, `${label}.material.opacity`, SCENE_OPACITY_MIN, 1, `opacity is ${fmtNum(SCENE_OPACITY_MIN)}–1`, errors);
|
|
446
|
+
if (m.emissive !== undefined &&
|
|
447
|
+
(typeof m.emissive !== 'string' || !HEX_RE.test(m.emissive))) {
|
|
448
|
+
errors.push(`${label}.material.emissive = ${show(m.emissive)} — a colour the object gives off, "#RGB" or "#RRGGBB" ("#000000" for none).`);
|
|
449
|
+
}
|
|
450
|
+
if (m.emissiveIntensity !== undefined)
|
|
451
|
+
checkNumber(m.emissiveIntensity, `${label}.material.emissiveIntensity`, 0, SCENE_EMISSIVE_INTENSITY_MAX, `emissiveIntensity is 0–${String(SCENE_EMISSIVE_INTENSITY_MAX)}, and only does anything with emissive set`, errors);
|
|
452
|
+
if (m.wireframe !== undefined && typeof m.wireframe !== 'boolean') {
|
|
453
|
+
errors.push(`${label}.material.wireframe = ${show(m.wireframe)} — true or false.`);
|
|
454
|
+
}
|
|
406
455
|
}
|
|
407
456
|
}
|
|
408
457
|
// Type-specific dimensions.
|
|
@@ -433,6 +482,29 @@ function validateObject(raw, i, errors) {
|
|
|
433
482
|
break;
|
|
434
483
|
case 'group':
|
|
435
484
|
break;
|
|
485
|
+
case 'label': {
|
|
486
|
+
if (typeof raw.text !== 'string' || raw.text.length < 1) {
|
|
487
|
+
errors.push(`${label}.text = ${show(raw.text)} — the words to show, 1–${String(SCENE_LABEL_TEXT_MAX)} chars.`);
|
|
488
|
+
}
|
|
489
|
+
else if (raw.text.length > SCENE_LABEL_TEXT_MAX) {
|
|
490
|
+
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.`);
|
|
491
|
+
}
|
|
492
|
+
else if (/[\n\r]/.test(raw.text)) {
|
|
493
|
+
errors.push(`${label}.text has a line break — a label is one line; use two labels.`);
|
|
494
|
+
}
|
|
495
|
+
if (raw.size !== undefined) {
|
|
496
|
+
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);
|
|
497
|
+
}
|
|
498
|
+
if (raw.background !== undefined &&
|
|
499
|
+
(typeof raw.background !== 'string' || !HEX_RE.test(raw.background))) {
|
|
500
|
+
errors.push(`${label}.background = ${show(raw.background)} — colours are "#RGB" or "#RRGGBB" hex.`);
|
|
501
|
+
}
|
|
502
|
+
if (raw.face !== undefined &&
|
|
503
|
+
!SCENE_LABEL_FACES.includes(raw.face)) {
|
|
504
|
+
errors.push(`${label}.face = ${show(raw.face)} — "camera" (turns to face the viewer) or "fixed" (obeys rotation).`);
|
|
505
|
+
}
|
|
506
|
+
break;
|
|
507
|
+
}
|
|
436
508
|
case 'torus': {
|
|
437
509
|
const rOk = checkNumber(raw.radius, `${label}.radius`, SCENE_RADIUS_MIN, SCENE_RADIUS_MAX, RADIUS_RULE, errors);
|
|
438
510
|
const maxTube = rOk ? raw.radius : SCENE_RADIUS_MAX;
|
|
@@ -779,6 +851,9 @@ export function liftFor(obj) {
|
|
|
779
851
|
}
|
|
780
852
|
case 'group':
|
|
781
853
|
return 0;
|
|
854
|
+
// Half the plane's height, so `y` is the bottom here too (D-L6).
|
|
855
|
+
case 'label':
|
|
856
|
+
return (obj.size ?? SCENE_LABEL_SIZE_DEFAULT) / 2;
|
|
782
857
|
}
|
|
783
858
|
}
|
|
784
859
|
/**
|
|
@@ -822,6 +897,12 @@ function extrudeBox(obj) {
|
|
|
822
897
|
/** Local AABB of the mesh around its own origin (the pivot). null for a group (no geometry). */
|
|
823
898
|
function localBox(obj) {
|
|
824
899
|
switch (obj.type) {
|
|
900
|
+
// A flat plane in x–y, centred on the pivot. Its depth is zero, and the width is the
|
|
901
|
+
// estimate (SCENE_LABEL_ADVANCE) — a label never decides a scene's extents on its own.
|
|
902
|
+
case 'label': {
|
|
903
|
+
const [w, h] = labelPlaneSize(obj);
|
|
904
|
+
return { min: [-w / 2, -h / 2, 0], max: [w / 2, h / 2, 0] };
|
|
905
|
+
}
|
|
825
906
|
case 'box':
|
|
826
907
|
case 'wedge': {
|
|
827
908
|
// 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,
|
|
@@ -267,11 +288,30 @@ const OBJECT_PROPERTIES = {
|
|
|
267
288
|
},
|
|
268
289
|
material: {
|
|
269
290
|
type: 'object',
|
|
270
|
-
description: '{ roughness 0–1
|
|
291
|
+
description: 'Not on a group or a label. { roughness 0–1 (0 = mirror-smooth, 1 = matt) · metalness 0–1 ' +
|
|
292
|
+
'(metal is MIRROR, not colour: it shows the room, so it reads dark against a plain ' +
|
|
293
|
+
'background — use it for a few accents, not everything) · opacity 0.05–1 (a pane of glass, ' +
|
|
294
|
+
'a volume you want to see into) · emissive "#RRGGBB" + emissiveIntensity 0–5 · wireframe }.',
|
|
271
295
|
properties: {
|
|
272
296
|
roughness: { type: 'number' },
|
|
273
297
|
metalness: { type: 'number' },
|
|
274
298
|
opacity: { type: 'number' },
|
|
299
|
+
emissive: {
|
|
300
|
+
type: 'string',
|
|
301
|
+
description: 'A colour the object GIVES OFF, so it reads as live rather than lit: an indicator, an ' +
|
|
302
|
+
'active path, the step being explained. ⚠️ It brightens the object only — it casts no ' +
|
|
303
|
+
"light on its neighbours and draws no halo. Pair it with the object's own colour, and " +
|
|
304
|
+
'use it on a few things: everything glowing is nothing glowing.',
|
|
305
|
+
},
|
|
306
|
+
emissiveIntensity: {
|
|
307
|
+
type: 'number',
|
|
308
|
+
description: '0–5, default 1. Does nothing without emissive. Past ~3 the colour clips to white.',
|
|
309
|
+
},
|
|
310
|
+
wireframe: {
|
|
311
|
+
type: 'boolean',
|
|
312
|
+
description: 'Edges only. For the parts of a diagram that are LOGICAL rather than built — a boundary, ' +
|
|
313
|
+
'a planned phase, a region — beside solid objects that are real.',
|
|
314
|
+
},
|
|
275
315
|
},
|
|
276
316
|
},
|
|
277
317
|
locked: {
|