@compilr-dev/sdk 0.39.0 → 0.41.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.
|
@@ -25,7 +25,7 @@ export type SceneOp =
|
|
|
25
25
|
objects: SceneObjectInput[];
|
|
26
26
|
index?: number;
|
|
27
27
|
}
|
|
28
|
-
/** Shallow merge; arrays replaced whole; `null` clears
|
|
28
|
+
/** Shallow merge; arrays replaced whole; `material` MERGES; `null` clears; id/type rejected. */
|
|
29
29
|
| {
|
|
30
30
|
op: 'update';
|
|
31
31
|
id: string;
|
|
@@ -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
|
@@ -171,6 +171,30 @@ function showVal(v) {
|
|
|
171
171
|
return fmtVec(v);
|
|
172
172
|
return JSON.stringify(v);
|
|
173
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* `material` MERGES, where every other object-valued field is replaced whole.
|
|
176
|
+
*
|
|
177
|
+
* ⚠️ IT USED TO REPLACE, AND THAT COST SOMEONE A SURFACE. A material is a bag of independent
|
|
178
|
+
* knobs — roughness, metalness, opacity, emissive, wireframe — so "make this glow" is naturally
|
|
179
|
+
* `{ material: { emissive: "#f59e0b" } }`, and under replace that ALSO reset the metalness and
|
|
180
|
+
* roughness that were there. Reported from real use: a ring patched to glow quietly lost its
|
|
181
|
+
* metal finish, and the agent only caught it because it re-read the scene. Nothing errored, and
|
|
182
|
+
* the patch did exactly what it said.
|
|
183
|
+
*
|
|
184
|
+
* Arrays still replace whole (a position is one value, not three independent ones). To clear a
|
|
185
|
+
* single knob, pass it as `null`; to clear the whole material, pass `material: null`, which is
|
|
186
|
+
* the same rule one level up.
|
|
187
|
+
*/
|
|
188
|
+
function mergeMaterial(before, patch) {
|
|
189
|
+
const base = isRecord(before) ? { ...before } : {};
|
|
190
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
191
|
+
if (v === null)
|
|
192
|
+
Reflect.deleteProperty(base, k);
|
|
193
|
+
else
|
|
194
|
+
base[k] = clone(v);
|
|
195
|
+
}
|
|
196
|
+
return base;
|
|
197
|
+
}
|
|
174
198
|
function updateObject(scene, id, patch, opts) {
|
|
175
199
|
const idx = scene.objects.findIndex((o) => o.id === id);
|
|
176
200
|
if (idx < 0)
|
|
@@ -205,8 +229,10 @@ function updateObject(scene, id, patch, opts) {
|
|
|
205
229
|
const from = before[k];
|
|
206
230
|
if (v === null)
|
|
207
231
|
Reflect.deleteProperty(next, k);
|
|
232
|
+
else if (k === 'material' && isRecord(v))
|
|
233
|
+
next[k] = mergeMaterial(before[k], v);
|
|
208
234
|
else
|
|
209
|
-
next[k] = clone(v); // shallow merge: arrays
|
|
235
|
+
next[k] = clone(v); // shallow merge: arrays replaced whole
|
|
210
236
|
const to = next[k];
|
|
211
237
|
if (JSON.stringify(from) === JSON.stringify(to))
|
|
212
238
|
continue;
|
|
@@ -291,6 +317,43 @@ function setLayers(scene, layers) {
|
|
|
291
317
|
warnings: [],
|
|
292
318
|
};
|
|
293
319
|
}
|
|
320
|
+
/**
|
|
321
|
+
* Replace the lights.
|
|
322
|
+
*
|
|
323
|
+
* `[]` DELETES the field rather than storing an empty list, which is the difference between
|
|
324
|
+
* "the default studio light" and "no light at all" — an empty array would render a black scene
|
|
325
|
+
* and read in the JSON as if lighting had been configured.
|
|
326
|
+
*/
|
|
327
|
+
function setLights(scene, lights) {
|
|
328
|
+
if (!Array.isArray(lights)) {
|
|
329
|
+
return {
|
|
330
|
+
error: 'lights must be an array of { type: "sun" | "ambient", … } — or [] to go back to the default studio light.',
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
const probe = validateScene({ ...scene, lights: lights.length === 0 ? undefined : lights });
|
|
334
|
+
if (!probe.ok)
|
|
335
|
+
return { error: formatValidationErrors(probe.errors) };
|
|
336
|
+
const before = scene.lights ?? [];
|
|
337
|
+
const next = { ...scene };
|
|
338
|
+
if (lights.length === 0)
|
|
339
|
+
delete next.lights;
|
|
340
|
+
else
|
|
341
|
+
next.lights = probe.scene.lights;
|
|
342
|
+
const summary = lights.length === 0
|
|
343
|
+
? 'Lights cleared — back to the default studio light.'
|
|
344
|
+
: `Lights set: ${lights
|
|
345
|
+
.map((l) => isRecord(l) && typeof l.type === 'string'
|
|
346
|
+
? `${l.type} ${typeof l.intensity === 'number' ? String(l.intensity) : 'default'}`
|
|
347
|
+
: '?')
|
|
348
|
+
.join(', ')}.`;
|
|
349
|
+
return {
|
|
350
|
+
scene: next,
|
|
351
|
+
summary,
|
|
352
|
+
ids: [],
|
|
353
|
+
changes: [{ id: '', what: 'lights', from: before, to: lights }],
|
|
354
|
+
warnings: [],
|
|
355
|
+
};
|
|
356
|
+
}
|
|
294
357
|
function setCamera(scene, camera) {
|
|
295
358
|
if (isRecord(camera) && camera.fit === true) {
|
|
296
359
|
const rest = { ...scene };
|
|
@@ -335,6 +398,8 @@ export function applySceneOp(scene, op, opts) {
|
|
|
335
398
|
return setCamera(scene, op.camera);
|
|
336
399
|
case 'layers':
|
|
337
400
|
return setLayers(scene, op.layers);
|
|
401
|
+
case 'lights':
|
|
402
|
+
return setLights(scene, op.lights);
|
|
338
403
|
case 'replace': {
|
|
339
404
|
const v = validateScene(op.scene);
|
|
340
405
|
if (!v.ok)
|
|
@@ -431,6 +496,8 @@ function describeChange(c) {
|
|
|
431
496
|
return 'changed the camera';
|
|
432
497
|
case 'layers':
|
|
433
498
|
return 'changed which layers can be hidden';
|
|
499
|
+
case 'lights':
|
|
500
|
+
return 'changed the lighting';
|
|
434
501
|
case 'replaced':
|
|
435
502
|
return 'replaced the whole scene';
|
|
436
503
|
case 'edited':
|
|
@@ -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 };
|
|
@@ -232,7 +233,10 @@ const OBJECT_PROPERTIES = {
|
|
|
232
233
|
type: 'string',
|
|
233
234
|
description: 'label only: the words to show (≤ 120, ONE line — no newline; use two labels). The scene is ' +
|
|
234
235
|
'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
|
+
'it cannot be read back by you or by anyone else. ⚠️ SIZE IT AGAINST WHAT IT NAMES: the text ' +
|
|
237
|
+
'is in METRES like everything else, so the 0.25 default is right for a desk and unreadable ' +
|
|
238
|
+
'across a building. Roughly a tenth of the thing it labels — a 4 m wall wants ~0.4, a whole ' +
|
|
239
|
+
'floor plan wants more. A label you have to zoom in to read is one nobody reads.',
|
|
236
240
|
},
|
|
237
241
|
background: {
|
|
238
242
|
type: 'string',
|
|
@@ -305,7 +309,13 @@ const OBJECT_PROPERTIES = {
|
|
|
305
309
|
},
|
|
306
310
|
emissiveIntensity: {
|
|
307
311
|
type: 'number',
|
|
308
|
-
description: '0–5, default 1. Does nothing without emissive.
|
|
312
|
+
description: '0–5, default 1. Does nothing without emissive. ⚠️ MEASURED on a real scene: a sphere at ' +
|
|
313
|
+
'1.5 loses its shading and reads as a flat disc, and a ring at 2 washes to near-white and ' +
|
|
314
|
+
'loses its hue. Emission adds a flat colour OVER the shading, so the bigger or paler the ' +
|
|
315
|
+
'object, the sooner it stops looking like a solid. Stay at or below ~0.3 for anything ' +
|
|
316
|
+
'larger than a token and save the high end for small accents. Against a bright scene a ' +
|
|
317
|
+
'glow reads poorly whatever the number — dim the scene with scene_set_lights rather than ' +
|
|
318
|
+
'turning this up.',
|
|
309
319
|
},
|
|
310
320
|
wireframe: {
|
|
311
321
|
type: 'boolean',
|
|
@@ -531,7 +541,10 @@ export function createSceneTools(config, options) {
|
|
|
531
541
|
const sceneUpdateObjectTool = defineTool({
|
|
532
542
|
name: 'scene_update_object',
|
|
533
543
|
description: 'Change fields of one object in a 3D scene. patch is a SHALLOW merge — arrays (position, size, ' +
|
|
534
|
-
'points, rotation)
|
|
544
|
+
'points, rotation) are replaced whole, since a position is one value and not three independent ' +
|
|
545
|
+
'ones. MATERIAL IS THE EXCEPTION: it merges, so { material: { emissive: "#f59e0b" } } adds a glow ' +
|
|
546
|
+
'and leaves the roughness, metalness and opacity that were already there. null clears an optional ' +
|
|
547
|
+
'field (group, or material itself); a null INSIDE material clears that one knob. ' +
|
|
535
548
|
'id and type cannot change: remove the object and add it again. Read ids with scene_get first.',
|
|
536
549
|
inputSchema: {
|
|
537
550
|
type: 'object',
|
|
@@ -683,6 +696,56 @@ export function createSceneTools(config, options) {
|
|
|
683
696
|
},
|
|
684
697
|
});
|
|
685
698
|
// ---------------------------------------------------------------------------
|
|
699
|
+
const sceneSetLightsTool = defineTool({
|
|
700
|
+
name: 'scene_set_lights',
|
|
701
|
+
description: "Replace a scene's lighting. Until this existed, lights could only be set at creation, so " +
|
|
702
|
+
'dimming a finished scene meant rebuilding every object in it. ' +
|
|
703
|
+
'Two kinds: { type: "ambient", intensity } is the overall fill — it also sets how much of ' +
|
|
704
|
+
'the room glossy and metallic surfaces reflect, so dropping it to 0 makes a genuinely dark ' +
|
|
705
|
+
'scene — and { type: "sun", position: [x, y, z], intensity, castShadow } is a directional ' +
|
|
706
|
+
'light that can cast shadows. [] goes back to the default studio light (ambient fill + one ' +
|
|
707
|
+
'sun from above-right), which is what most scenes want. ' +
|
|
708
|
+
`Up to ${String(SCENE_MAX_LIGHTS)} lights, ${String(SCENE_MAX_SHADOW_LIGHTS)} of them casting shadows ` +
|
|
709
|
+
'(each shadow costs a render pass). ⚠️ Lighting a scene darker makes emissive objects stand ' +
|
|
710
|
+
'out; it also makes everything else harder to read, so change it for a reason.',
|
|
711
|
+
inputSchema: {
|
|
712
|
+
type: 'object',
|
|
713
|
+
properties: {
|
|
714
|
+
canvas_id: { type: 'number', description: 'The scene canvas id.' },
|
|
715
|
+
lights: {
|
|
716
|
+
type: 'array',
|
|
717
|
+
maxItems: SCENE_MAX_LIGHTS,
|
|
718
|
+
description: '[] restores the default studio light.',
|
|
719
|
+
items: {
|
|
720
|
+
type: 'object',
|
|
721
|
+
properties: {
|
|
722
|
+
type: { type: 'string', enum: ['sun', 'ambient'] },
|
|
723
|
+
position: { ...VEC3, description: 'sun only: where it shines FROM, in metres.' },
|
|
724
|
+
intensity: {
|
|
725
|
+
type: 'number',
|
|
726
|
+
description: `0–${String(SCENE_LIGHT_INTENSITY_MAX)}. The default studio light is ambient ~1 and sun ~1.6.`,
|
|
727
|
+
},
|
|
728
|
+
castShadow: { type: 'boolean', description: 'sun only. Default false.' },
|
|
729
|
+
},
|
|
730
|
+
required: ['type'],
|
|
731
|
+
},
|
|
732
|
+
},
|
|
733
|
+
},
|
|
734
|
+
required: ['canvas_id', 'lights'],
|
|
735
|
+
},
|
|
736
|
+
execute: async (input) => {
|
|
737
|
+
try {
|
|
738
|
+
if (!Array.isArray(input.lights)) {
|
|
739
|
+
return createErrorResult('lights must be an array of { type: "sun" | "ambient", … } — or [] to go back to the default studio light.');
|
|
740
|
+
}
|
|
741
|
+
return await applyAsAgent(input.canvas_id, [{ op: 'lights', lights: input.lights }], () => null);
|
|
742
|
+
}
|
|
743
|
+
catch (error) {
|
|
744
|
+
return createErrorResult(errorMessage('set lights', error));
|
|
745
|
+
}
|
|
746
|
+
},
|
|
747
|
+
});
|
|
748
|
+
// ---------------------------------------------------------------------------
|
|
686
749
|
const sceneScreenshotTool = defineTool({
|
|
687
750
|
name: 'scene_screenshot',
|
|
688
751
|
description: 'RENDER a 3D scene and SEE it — check proportions, overlaps, gaps and floating objects after ' +
|
|
@@ -736,6 +799,7 @@ export function createSceneTools(config, options) {
|
|
|
736
799
|
sceneRemoveObjectTool,
|
|
737
800
|
sceneSetCameraTool,
|
|
738
801
|
sceneSetLayersTool,
|
|
802
|
+
sceneSetLightsTool,
|
|
739
803
|
sceneScreenshotTool,
|
|
740
804
|
];
|
|
741
805
|
}
|
package/dist/team/tool-config.js
CHANGED