@compilr-dev/sdk 0.38.0 → 0.40.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.d.ts +6 -1
- package/dist/canvas/scene-ops.js +41 -0
- package/dist/canvas/scene.d.ts +19 -1
- package/dist/canvas/scene.js +23 -2
- package/dist/capabilities/packs.js +1 -0
- package/dist/platform/tools/scene-tools.d.ts +4 -1
- package/dist/platform/tools/scene-tools.js +79 -2
- package/dist/team/tool-config.js +1 -0
- package/package.json +1 -1
|
@@ -48,6 +48,11 @@ export type SceneOp =
|
|
|
48
48
|
op: 'layers';
|
|
49
49
|
layers: unknown;
|
|
50
50
|
}
|
|
51
|
+
/** Replace the lights (§20.5); `[]` restores the default studio light. */
|
|
52
|
+
| {
|
|
53
|
+
op: 'lights';
|
|
54
|
+
lights: unknown;
|
|
55
|
+
}
|
|
51
56
|
/** Whole-scene replace (scene_create, JSON import). */
|
|
52
57
|
| {
|
|
53
58
|
op: 'replace';
|
|
@@ -59,7 +64,7 @@ export type SceneEditSource = {
|
|
|
59
64
|
} | {
|
|
60
65
|
kind: 'user';
|
|
61
66
|
};
|
|
62
|
-
export type SceneChangeKind = 'moved' | 'resized' | 'rotated' | 'recoloured' | 'renamed' | 'removed' | 'added' | 'edited' | 'camera' | 'layers' | 'replaced';
|
|
67
|
+
export type SceneChangeKind = 'moved' | 'resized' | 'rotated' | 'recoloured' | 'renamed' | 'removed' | 'added' | 'edited' | 'camera' | 'layers' | 'lights' | 'replaced';
|
|
63
68
|
/** One journalled change (the writer stamps `rev`). */
|
|
64
69
|
export interface SceneChange {
|
|
65
70
|
rev: number;
|
package/dist/canvas/scene-ops.js
CHANGED
|
@@ -291,6 +291,43 @@ function setLayers(scene, layers) {
|
|
|
291
291
|
warnings: [],
|
|
292
292
|
};
|
|
293
293
|
}
|
|
294
|
+
/**
|
|
295
|
+
* Replace the lights.
|
|
296
|
+
*
|
|
297
|
+
* `[]` DELETES the field rather than storing an empty list, which is the difference between
|
|
298
|
+
* "the default studio light" and "no light at all" — an empty array would render a black scene
|
|
299
|
+
* and read in the JSON as if lighting had been configured.
|
|
300
|
+
*/
|
|
301
|
+
function setLights(scene, lights) {
|
|
302
|
+
if (!Array.isArray(lights)) {
|
|
303
|
+
return {
|
|
304
|
+
error: 'lights must be an array of { type: "sun" | "ambient", … } — or [] to go back to the default studio light.',
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
const probe = validateScene({ ...scene, lights: lights.length === 0 ? undefined : lights });
|
|
308
|
+
if (!probe.ok)
|
|
309
|
+
return { error: formatValidationErrors(probe.errors) };
|
|
310
|
+
const before = scene.lights ?? [];
|
|
311
|
+
const next = { ...scene };
|
|
312
|
+
if (lights.length === 0)
|
|
313
|
+
delete next.lights;
|
|
314
|
+
else
|
|
315
|
+
next.lights = probe.scene.lights;
|
|
316
|
+
const summary = lights.length === 0
|
|
317
|
+
? 'Lights cleared — back to the default studio light.'
|
|
318
|
+
: `Lights set: ${lights
|
|
319
|
+
.map((l) => isRecord(l) && typeof l.type === 'string'
|
|
320
|
+
? `${l.type} ${typeof l.intensity === 'number' ? String(l.intensity) : 'default'}`
|
|
321
|
+
: '?')
|
|
322
|
+
.join(', ')}.`;
|
|
323
|
+
return {
|
|
324
|
+
scene: next,
|
|
325
|
+
summary,
|
|
326
|
+
ids: [],
|
|
327
|
+
changes: [{ id: '', what: 'lights', from: before, to: lights }],
|
|
328
|
+
warnings: [],
|
|
329
|
+
};
|
|
330
|
+
}
|
|
294
331
|
function setCamera(scene, camera) {
|
|
295
332
|
if (isRecord(camera) && camera.fit === true) {
|
|
296
333
|
const rest = { ...scene };
|
|
@@ -335,6 +372,8 @@ export function applySceneOp(scene, op, opts) {
|
|
|
335
372
|
return setCamera(scene, op.camera);
|
|
336
373
|
case 'layers':
|
|
337
374
|
return setLayers(scene, op.layers);
|
|
375
|
+
case 'lights':
|
|
376
|
+
return setLights(scene, op.lights);
|
|
338
377
|
case 'replace': {
|
|
339
378
|
const v = validateScene(op.scene);
|
|
340
379
|
if (!v.ok)
|
|
@@ -431,6 +470,8 @@ function describeChange(c) {
|
|
|
431
470
|
return 'changed the camera';
|
|
432
471
|
case 'layers':
|
|
433
472
|
return 'changed which layers can be hidden';
|
|
473
|
+
case 'lights':
|
|
474
|
+
return 'changed the lighting';
|
|
434
475
|
case 'replaced':
|
|
435
476
|
return 'replaced the whole scene';
|
|
436
477
|
case 'edited':
|
package/dist/canvas/scene.d.ts
CHANGED
|
@@ -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}$/ */
|
|
@@ -260,6 +276,8 @@ export declare const SCENE_NEUTRAL_PALETTE: readonly string[];
|
|
|
260
276
|
/** Accessible names for the palette swatches, same order (Q-10). */
|
|
261
277
|
export declare const SCENE_PALETTE_NAMES: readonly string[];
|
|
262
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;
|
|
263
281
|
/** The reference camera, used for an empty scene. */
|
|
264
282
|
export declare const SCENE_DEFAULT_CAMERA: Readonly<SceneCamera>;
|
|
265
283
|
/** Vertical field of view of the viewer camera, degrees. */
|
package/dist/canvas/scene.js
CHANGED
|
@@ -105,7 +105,12 @@ export const SCENE_DEFAULT_MATERIAL = {
|
|
|
105
105
|
roughness: 0.78,
|
|
106
106
|
metalness: 0.02,
|
|
107
107
|
opacity: 1,
|
|
108
|
+
emissive: '#000000',
|
|
109
|
+
emissiveIntensity: 1,
|
|
110
|
+
wireframe: false,
|
|
108
111
|
};
|
|
112
|
+
/** `material.emissiveIntensity` range. Above ~5 everything clips to white. */
|
|
113
|
+
export const SCENE_EMISSIVE_INTENSITY_MAX = 5;
|
|
109
114
|
/** The reference camera, used for an empty scene. */
|
|
110
115
|
export const SCENE_DEFAULT_CAMERA = {
|
|
111
116
|
position: [7, 5.5, 8],
|
|
@@ -216,7 +221,14 @@ const TYPE_SHAPE = {
|
|
|
216
221
|
group: 'a group is a container: id, name, position, rotation, group, locked, tags only — colour its children',
|
|
217
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',
|
|
218
223
|
};
|
|
219
|
-
const MATERIAL_KEYS = new Set([
|
|
224
|
+
const MATERIAL_KEYS = new Set([
|
|
225
|
+
'roughness',
|
|
226
|
+
'metalness',
|
|
227
|
+
'opacity',
|
|
228
|
+
'emissive',
|
|
229
|
+
'emissiveIntensity',
|
|
230
|
+
'wireframe',
|
|
231
|
+
]);
|
|
220
232
|
const HEX_RE = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
221
233
|
/** Short, stable number rendering for messages and summaries. */
|
|
222
234
|
export function fmtNum(n) {
|
|
@@ -422,7 +434,7 @@ function validateObject(raw, i, errors) {
|
|
|
422
434
|
const m = raw.material;
|
|
423
435
|
for (const k of Object.keys(m)) {
|
|
424
436
|
if (!MATERIAL_KEYS.has(k)) {
|
|
425
|
-
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.`);
|
|
426
438
|
}
|
|
427
439
|
}
|
|
428
440
|
if (m.roughness !== undefined)
|
|
@@ -431,6 +443,15 @@ function validateObject(raw, i, errors) {
|
|
|
431
443
|
checkNumber(m.metalness, `${label}.material.metalness`, 0, 1, 'metalness is 0–1', errors);
|
|
432
444
|
if (m.opacity !== undefined)
|
|
433
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
|
+
}
|
|
434
455
|
}
|
|
435
456
|
}
|
|
436
457
|
// Type-specific dimensions.
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import type { PlatformContext, PlatformToolsConfig } from '../context.js';
|
|
16
16
|
import { type ISceneWriter } from '../scene-writer.js';
|
|
17
17
|
import { type SceneFile } from '../../canvas/scene.js';
|
|
18
|
-
export declare const SCENE_TOOL_NAMES: readonly ["scene_create", "scene_get", "scene_add_object", "scene_update_object", "scene_remove_object", "scene_set_camera", "scene_set_layers", "scene_screenshot"];
|
|
18
|
+
export declare const SCENE_TOOL_NAMES: readonly ["scene_create", "scene_get", "scene_add_object", "scene_update_object", "scene_remove_object", "scene_set_camera", "scene_set_layers", "scene_set_lights", "scene_screenshot"];
|
|
19
19
|
/**
|
|
20
20
|
* "sofa · box · Sofa" rows, indented under their group parent; `#tag` suffixes when `withTags`.
|
|
21
21
|
*
|
|
@@ -103,6 +103,9 @@ export declare function createSceneTools(config: PlatformToolsConfig, options?:
|
|
|
103
103
|
}> | import("@compilr-dev/agents").Tool<{
|
|
104
104
|
canvas_id: number;
|
|
105
105
|
layers?: unknown;
|
|
106
|
+
}> | import("@compilr-dev/agents").Tool<{
|
|
107
|
+
canvas_id: number;
|
|
108
|
+
lights?: unknown;
|
|
106
109
|
}> | import("@compilr-dev/agents").Tool<{
|
|
107
110
|
canvas_id: number;
|
|
108
111
|
width?: number;
|
|
@@ -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, SCENE_LABEL_FACES, describeScene, serializeScene, } from '../../canvas/scene.js';
|
|
17
|
+
import { SCENE_MAX_BATCH, SCENE_MAX_LAYERS, SCENE_OBJECT_TYPES, SCENE_MAX_LIGHTS, SCENE_MAX_SHADOW_LIGHTS, SCENE_LIGHT_INTENSITY_MAX, 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 = [
|
|
@@ -25,6 +25,7 @@ export const SCENE_TOOL_NAMES = [
|
|
|
25
25
|
'scene_remove_object',
|
|
26
26
|
'scene_set_camera',
|
|
27
27
|
'scene_set_layers',
|
|
28
|
+
'scene_set_lights',
|
|
28
29
|
'scene_screenshot',
|
|
29
30
|
];
|
|
30
31
|
const DEFAULT_SHOT = { width: 1280, height: 800 };
|
|
@@ -288,11 +289,36 @@ const OBJECT_PROPERTIES = {
|
|
|
288
289
|
},
|
|
289
290
|
material: {
|
|
290
291
|
type: 'object',
|
|
291
|
-
description: '{ roughness 0–1
|
|
292
|
+
description: 'Not on a group or a label. { roughness 0–1 (0 = mirror-smooth, 1 = matt) · metalness 0–1 ' +
|
|
293
|
+
'(metal is MIRROR, not colour: it shows the room, so it reads dark against a plain ' +
|
|
294
|
+
'background — use it for a few accents, not everything) · opacity 0.05–1 (a pane of glass, ' +
|
|
295
|
+
'a volume you want to see into) · emissive "#RRGGBB" + emissiveIntensity 0–5 · wireframe }.',
|
|
292
296
|
properties: {
|
|
293
297
|
roughness: { type: 'number' },
|
|
294
298
|
metalness: { type: 'number' },
|
|
295
299
|
opacity: { type: 'number' },
|
|
300
|
+
emissive: {
|
|
301
|
+
type: 'string',
|
|
302
|
+
description: 'A colour the object GIVES OFF, so it reads as live rather than lit: an indicator, an ' +
|
|
303
|
+
'active path, the step being explained. ⚠️ It brightens the object only — it casts no ' +
|
|
304
|
+
"light on its neighbours and draws no halo. Pair it with the object's own colour, and " +
|
|
305
|
+
'use it on a few things: everything glowing is nothing glowing.',
|
|
306
|
+
},
|
|
307
|
+
emissiveIntensity: {
|
|
308
|
+
type: 'number',
|
|
309
|
+
description: '0–5, default 1. Does nothing without emissive. ⚠️ MEASURED on a real scene: a sphere at ' +
|
|
310
|
+
'1.5 loses its shading and reads as a flat disc, and a ring at 2 washes to near-white and ' +
|
|
311
|
+
'loses its hue. Emission adds a flat colour OVER the shading, so the bigger or paler the ' +
|
|
312
|
+
'object, the sooner it stops looking like a solid. Stay at or below ~0.3 for anything ' +
|
|
313
|
+
'larger than a token and save the high end for small accents. Against a bright scene a ' +
|
|
314
|
+
'glow reads poorly whatever the number — dim the scene with scene_set_lights rather than ' +
|
|
315
|
+
'turning this up.',
|
|
316
|
+
},
|
|
317
|
+
wireframe: {
|
|
318
|
+
type: 'boolean',
|
|
319
|
+
description: 'Edges only. For the parts of a diagram that are LOGICAL rather than built — a boundary, ' +
|
|
320
|
+
'a planned phase, a region — beside solid objects that are real.',
|
|
321
|
+
},
|
|
296
322
|
},
|
|
297
323
|
},
|
|
298
324
|
locked: {
|
|
@@ -664,6 +690,56 @@ export function createSceneTools(config, options) {
|
|
|
664
690
|
},
|
|
665
691
|
});
|
|
666
692
|
// ---------------------------------------------------------------------------
|
|
693
|
+
const sceneSetLightsTool = defineTool({
|
|
694
|
+
name: 'scene_set_lights',
|
|
695
|
+
description: "Replace a scene's lighting. Until this existed, lights could only be set at creation, so " +
|
|
696
|
+
'dimming a finished scene meant rebuilding every object in it. ' +
|
|
697
|
+
'Two kinds: { type: "ambient", intensity } is the overall fill — it also sets how much of ' +
|
|
698
|
+
'the room glossy and metallic surfaces reflect, so dropping it to 0 makes a genuinely dark ' +
|
|
699
|
+
'scene — and { type: "sun", position: [x, y, z], intensity, castShadow } is a directional ' +
|
|
700
|
+
'light that can cast shadows. [] goes back to the default studio light (ambient fill + one ' +
|
|
701
|
+
'sun from above-right), which is what most scenes want. ' +
|
|
702
|
+
`Up to ${String(SCENE_MAX_LIGHTS)} lights, ${String(SCENE_MAX_SHADOW_LIGHTS)} of them casting shadows ` +
|
|
703
|
+
'(each shadow costs a render pass). ⚠️ Lighting a scene darker makes emissive objects stand ' +
|
|
704
|
+
'out; it also makes everything else harder to read, so change it for a reason.',
|
|
705
|
+
inputSchema: {
|
|
706
|
+
type: 'object',
|
|
707
|
+
properties: {
|
|
708
|
+
canvas_id: { type: 'number', description: 'The scene canvas id.' },
|
|
709
|
+
lights: {
|
|
710
|
+
type: 'array',
|
|
711
|
+
maxItems: SCENE_MAX_LIGHTS,
|
|
712
|
+
description: '[] restores the default studio light.',
|
|
713
|
+
items: {
|
|
714
|
+
type: 'object',
|
|
715
|
+
properties: {
|
|
716
|
+
type: { type: 'string', enum: ['sun', 'ambient'] },
|
|
717
|
+
position: { ...VEC3, description: 'sun only: where it shines FROM, in metres.' },
|
|
718
|
+
intensity: {
|
|
719
|
+
type: 'number',
|
|
720
|
+
description: `0–${String(SCENE_LIGHT_INTENSITY_MAX)}. The default studio light is ambient ~1 and sun ~1.6.`,
|
|
721
|
+
},
|
|
722
|
+
castShadow: { type: 'boolean', description: 'sun only. Default false.' },
|
|
723
|
+
},
|
|
724
|
+
required: ['type'],
|
|
725
|
+
},
|
|
726
|
+
},
|
|
727
|
+
},
|
|
728
|
+
required: ['canvas_id', 'lights'],
|
|
729
|
+
},
|
|
730
|
+
execute: async (input) => {
|
|
731
|
+
try {
|
|
732
|
+
if (!Array.isArray(input.lights)) {
|
|
733
|
+
return createErrorResult('lights must be an array of { type: "sun" | "ambient", … } — or [] to go back to the default studio light.');
|
|
734
|
+
}
|
|
735
|
+
return await applyAsAgent(input.canvas_id, [{ op: 'lights', lights: input.lights }], () => null);
|
|
736
|
+
}
|
|
737
|
+
catch (error) {
|
|
738
|
+
return createErrorResult(errorMessage('set lights', error));
|
|
739
|
+
}
|
|
740
|
+
},
|
|
741
|
+
});
|
|
742
|
+
// ---------------------------------------------------------------------------
|
|
667
743
|
const sceneScreenshotTool = defineTool({
|
|
668
744
|
name: 'scene_screenshot',
|
|
669
745
|
description: 'RENDER a 3D scene and SEE it — check proportions, overlaps, gaps and floating objects after ' +
|
|
@@ -717,6 +793,7 @@ export function createSceneTools(config, options) {
|
|
|
717
793
|
sceneRemoveObjectTool,
|
|
718
794
|
sceneSetCameraTool,
|
|
719
795
|
sceneSetLayersTool,
|
|
796
|
+
sceneSetLightsTool,
|
|
720
797
|
sceneScreenshotTool,
|
|
721
798
|
];
|
|
722
799
|
}
|
package/dist/team/tool-config.js
CHANGED