@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 an optional field; id/type rejected. */
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;
@@ -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 and nested objects replaced whole
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':
@@ -280,6 +280,7 @@ export const CAPABILITY_PACKS = {
280
280
  'scene_remove_object',
281
281
  'scene_set_camera',
282
282
  'scene_set_layers',
283
+ 'scene_set_lights',
283
284
  'scene_screenshot',
284
285
  ],
285
286
  readOnly: false,
@@ -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. Past ~3 the colour clips to white.',
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) and material are replaced whole; null clears an optional field (e.g. group). ' +
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
  }
@@ -315,6 +315,7 @@ export const TOOL_GROUPS = {
315
315
  'scene_remove_object',
316
316
  'scene_set_camera',
317
317
  'scene_set_layers',
318
+ 'scene_set_lights',
318
319
  'scene_screenshot',
319
320
  ],
320
321
  readOnly: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compilr-dev/sdk",
3
- "version": "0.39.0",
3
+ "version": "0.41.0",
4
4
  "description": "Universal agent runtime for building AI-powered applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",