@womp/spark-agent 9.0.0 → 9.2.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.
@@ -101,9 +101,24 @@ export type MaterialWriteResult = {
101
101
  color: MaterialVector;
102
102
  exposed?: boolean;
103
103
  }[];
104
+ /** Parameter overrides reset by a FlowIR replacement. */
105
+ diagnostics?: string[];
104
106
  status: "applied";
105
107
  rendering: "unverified";
106
108
  };
109
+ export type MaterialProgramWrite = {
110
+ materialId?: number;
111
+ cloneFrom?: number;
112
+ layerIndex: number;
113
+ source: string;
114
+ objectIds?: number[];
115
+ };
116
+ export type MaterialProgramRead = {
117
+ materialId: number;
118
+ layerIndex: number;
119
+ source: string | null;
120
+ diagnostics: string[];
121
+ };
107
122
  export type MaterialNodeOptions = {
108
123
  op: string;
109
124
  name?: string;
@@ -1,5 +1,8 @@
1
1
  import { sparkErrorDiagnostics } from "./error-diagnostics.js";
2
- import { graphSchema, materialId, parseMaterialCatalog, validateMaterialGraph, writeSchema, value3, } from "./materials/graph.js";
2
+ import { graphSchema, materialId, parseMaterialCatalog, validateMaterialGraph, writeSchema, programWriteSchema, value3, } from "./materials/graph.js";
3
+ import { comparableMaterialGraph, compileMaterialFlowir } from "./materials/flowir.js";
4
+ import { liftMaterialFlowir } from "./materials/lift.js";
5
+ import { boundedDiagnostics } from "./geometry/diagnostics.js";
3
6
  function record(value, label) {
4
7
  if (!value || typeof value !== "object" || Array.isArray(value))
5
8
  throw new Error(`Invalid ${label}`);
@@ -115,7 +118,7 @@ export function createSdkMaterialsHost(options) {
115
118
  ],
116
119
  };
117
120
  };
118
- return {
121
+ const host = {
119
122
  catalog: (settings, context) => options.editor.read(context, async (api) => {
120
123
  const nodes = await catalogue(api, context, settings.refreshTextures === true);
121
124
  const selected = {};
@@ -200,7 +203,7 @@ export function createSdkMaterialsHost(options) {
200
203
  explanation: "A zero queue does not prove compilation, texture loading or the new rendered appearance succeeded. Inspect a scene screenshot.",
201
204
  };
202
205
  }),
203
- write: async (input, context) => {
206
+ write: async (input, context, program) => {
204
207
  const request = writeSchema.parse(input);
205
208
  await options.assertAvailable(context);
206
209
  let resultId = request.materialId;
@@ -211,7 +214,8 @@ export function createSdkMaterialsHost(options) {
211
214
  }, async (api, draft) => {
212
215
  await options.assertAvailable(context);
213
216
  context.signal.throwIfAborted();
214
- if (request.materialId !== undefined &&
217
+ if (program === undefined &&
218
+ request.materialId !== undefined &&
215
219
  request.layers === undefined &&
216
220
  request.constants === undefined) {
217
221
  draft.getMaterial(request.materialId);
@@ -226,12 +230,58 @@ export function createSdkMaterialsHost(options) {
226
230
  const catalog = await catalogue(api, context, false);
227
231
  const sourceId = request.materialId ?? request.cloneFrom;
228
232
  const original = sourceId === undefined ? undefined : rawMaterial(draft, sourceId);
229
- const graphs = (request.layers ??
230
- (sourceId === undefined ? [] : layers(draft, sourceId))).map((graph) => graph === null ? null : validateMaterialGraph(graph, catalog));
231
- let rewriteGraphs = request.layers !== undefined || request.materialId === undefined;
233
+ const sourceLayers = sourceId === undefined ? [] : layers(draft, sourceId);
234
+ const proposedLayers = program ? [...sourceLayers] : request.layers ?? sourceLayers;
235
+ let programSymbols;
236
+ const programDiagnostics = [];
237
+ if (program) {
238
+ while (proposedLayers.length <= program.layerIndex)
239
+ proposedLayers.push(null);
240
+ const compiled = compileMaterialFlowir(program.source, catalog);
241
+ const replacement = compiled.graph;
242
+ programSymbols = compiled.symbols;
243
+ const previous = sourceLayers[program.layerIndex];
244
+ for (const node of replacement.nodes) {
245
+ const matches = previous?.nodes.filter(candidate => candidate.op === node.op && candidate.name === node.name) ?? [];
246
+ const old = matches.length === 1 ? matches[0] : undefined;
247
+ if (!old || node.name === undefined)
248
+ continue;
249
+ if (old.location !== undefined)
250
+ node.location = old.location;
251
+ if (old.editor_flags !== undefined)
252
+ node.editor_flags = old.editor_flags;
253
+ if (node.op !== "Constant")
254
+ continue;
255
+ const exposure = node.in3.x;
256
+ const oldExposure = old.in3?.x ?? 0;
257
+ const nodeSlot = replacement.connections.find(edge => edge.input_node === replacement.nodes.indexOf(node))?.input_slot;
258
+ const oldSlot = previous?.connections.find(edge => edge.input_node === previous.nodes.indexOf(old))?.input_slot;
259
+ const vectorWidget = exposure === 1 && oldExposure === 2 && oldSlot === 1;
260
+ const compatible = (nodeSlot === oldSlot && exposure === oldExposure) || vectorWidget;
261
+ if (compatible) {
262
+ if (vectorWidget) {
263
+ node.in1 = node.in2;
264
+ delete node.in2;
265
+ for (const edge of replacement.connections)
266
+ if (edge.input_node === replacement.nodes.indexOf(node) && edge.input_slot === 2)
267
+ edge.input_slot = 1;
268
+ }
269
+ if (old.reference_id !== undefined)
270
+ node.reference_id = old.reference_id;
271
+ if (old.in3 !== undefined)
272
+ node.in3 = old.in3;
273
+ }
274
+ }
275
+ for (const option of previous?.nodes.filter(node => node.op === "Options") ?? [])
276
+ replacement.nodes.push(option);
277
+ proposedLayers[program.layerIndex] = replacement;
278
+ }
279
+ const graphs = proposedLayers.map((graph, slot) => graph === null ? null : program && slot !== program.layerIndex ? graph : validateMaterialGraph(graph, catalog));
280
+ let rewriteGraphs = program !== undefined || request.layers !== undefined || request.materialId === undefined;
232
281
  const used = allReferences(draft);
233
282
  const refs = new Map();
234
- for (const graph of graphs)
283
+ const changedGraphs = program && request.materialId !== undefined ? [graphs[program.layerIndex]] : graphs;
284
+ for (const graph of changedGraphs)
235
285
  for (const node of graph?.nodes ?? [])
236
286
  if (node.op === "Constant") {
237
287
  const id = typeof node.reference_id === "number" && node.reference_id > 0
@@ -249,6 +299,18 @@ export function createSdkMaterialsHost(options) {
249
299
  ? record(original.data, "material data")
250
300
  : undefined;
251
301
  const constants = record(sourceData?.constants ?? {}, "material constants");
302
+ if (program) {
303
+ const retained = new Set(graphs.flatMap(graph => graph?.nodes.filter(node => node.op === "Constant").map(node => String(node.reference_id)) ?? []));
304
+ for (const node of sourceLayers[program.layerIndex]?.nodes ?? []) {
305
+ if (node.op !== "Constant" || retained.has(String(node.reference_id)))
306
+ continue;
307
+ const key = String(node.reference_id);
308
+ if (constants[key] !== undefined) {
309
+ delete constants[key];
310
+ programDiagnostics.push(`Reset override for ${String(node.name ?? key)}: parameter removed or type changed.`);
311
+ }
312
+ }
313
+ }
252
314
  const referenceIdChanges = {};
253
315
  for (const [key, override] of Object.entries(request.constants ?? {})) {
254
316
  const node = refs.get(Number(key));
@@ -264,7 +326,7 @@ export function createSdkMaterialsHost(options) {
264
326
  // Replaced graphs can reuse IDs whose deleted overrides remain in the native cache.
265
327
  if (request.constants?.[key] === null ||
266
328
  (request.materialId !== undefined &&
267
- request.layers !== undefined &&
329
+ (request.layers !== undefined || program !== undefined) &&
268
330
  constants[key] === undefined)) {
269
331
  const id = freshReference(used);
270
332
  rewriteGraphs = true;
@@ -286,6 +348,8 @@ export function createSdkMaterialsHost(options) {
286
348
  const slots = graphs.map((graph, slot) => {
287
349
  if (graph === null)
288
350
  return 0;
351
+ if (program && request.materialId !== undefined && slot !== program.layerIndex)
352
+ return materialId.parse(oldSlots[slot]);
289
353
  if (!rewriteGraphs)
290
354
  return materialId.parse(oldSlots[slot]);
291
355
  const oldId = oldSlots[slot];
@@ -310,6 +374,30 @@ export function createSdkMaterialsHost(options) {
310
374
  });
311
375
  while (slots.length < 8)
312
376
  slots.push(0);
377
+ if (program) {
378
+ const ext = draft.getSceneExt();
379
+ if (!ext.shadersExt)
380
+ draft.patchSceneExt([{ op: "add", path: "/shadersExt", value: {} }], { returnScene: false });
381
+ const previousMetadata = ext.shadersExt;
382
+ if (request.cloneFrom !== undefined)
383
+ for (let slot = 0; slot < slots.length; slot++) {
384
+ if (slot === program.layerIndex || !slots[slot])
385
+ continue;
386
+ const originalShader = oldSlots[slot];
387
+ const metadata = previousMetadata?.[String(originalShader)];
388
+ if (metadata !== undefined)
389
+ draft.patchSceneExt([{ op: "add", path: `/shadersExt/${slots[slot]}`, value: metadata }], { returnScene: false });
390
+ }
391
+ draft.patchSceneExt([{
392
+ op: "add",
393
+ path: `/shadersExt/${slots[program.layerIndex]}`,
394
+ value: {
395
+ ...record(previousMetadata?.[String(oldSlots[program.layerIndex])] ?? {}, "shader metadata"),
396
+ flowirSource: program.source,
397
+ flowirSymbols: programSymbols,
398
+ },
399
+ }], { returnScene: false });
400
+ }
313
401
  patchEntry(draft, "materials", resultId, {
314
402
  ...target,
315
403
  ...(original ?? {}),
@@ -339,7 +427,8 @@ export function createSdkMaterialsHost(options) {
339
427
  materialId: resultId,
340
428
  objectIds: directUsers(draft, resultId),
341
429
  referenceIdChanges,
342
- parameters: graphs.flatMap(graph => graph?.nodes.filter(node => node.op === "Constant").map(node => {
430
+ ...(program ? { diagnostics: boundedDiagnostics(programDiagnostics) } : {}),
431
+ parameters: (program ? changedGraphs : graphs).flatMap(graph => graph?.nodes.filter(node => node.op === "Constant").map(node => {
343
432
  const referenceId = node.reference_id;
344
433
  const override = constants[String(referenceId)];
345
434
  return { name: node.name ?? "", referenceId,
@@ -356,5 +445,42 @@ export function createSdkMaterialsHost(options) {
356
445
  rendering: "unverified",
357
446
  };
358
447
  },
448
+ writeProgram: async (input, context) => {
449
+ const request = programWriteSchema.parse(input);
450
+ return host.write({
451
+ ...(request.materialId === undefined ? {} : { materialId: request.materialId }),
452
+ ...(request.cloneFrom === undefined ? {} : { cloneFrom: request.cloneFrom }),
453
+ ...(request.objectIds === undefined ? {} : { objectIds: request.objectIds }),
454
+ }, context, request);
455
+ },
456
+ readProgram: async (material, layerIndex, context) => options.editor.read(context, async (api) => {
457
+ await options.assertAvailable(context);
458
+ context.signal.throwIfAborted();
459
+ const id = materialId.parse(material);
460
+ if (!Number.isInteger(layerIndex) || layerIndex < 0 || layerIndex > 7)
461
+ throw new Error("layerIndex must be within 0..7");
462
+ const materialData = rawMaterial(api, id);
463
+ const shaderId = Array.isArray(materialData.shaders) ? materialData.shaders[layerIndex] : undefined;
464
+ if (typeof shaderId !== "number" || shaderId <= 0)
465
+ return { materialId: id, layerIndex, source: null, diagnostics: ["This layer is empty."] };
466
+ const metadata = api.getSceneExt().shadersExt?.[String(shaderId)];
467
+ const source = metadata?.flowirSource;
468
+ const catalog = await catalogue(api, context, false);
469
+ const actual = layers(api, id)[layerIndex];
470
+ if (!actual)
471
+ return { materialId: id, layerIndex, source: null, diagnostics: ["This layer is empty."] };
472
+ try {
473
+ if (typeof source === "string") {
474
+ const rebuilt = compileMaterialFlowir(source, catalog).graph;
475
+ if (comparableMaterialGraph(rebuilt) === comparableMaterialGraph(actual))
476
+ return { materialId: id, layerIndex, source, diagnostics: [] };
477
+ }
478
+ return { materialId: id, layerIndex, source: liftMaterialFlowir(actual, catalog), diagnostics: [] };
479
+ }
480
+ catch (error) {
481
+ return { materialId: id, layerIndex, source: null, diagnostics: boundedDiagnostics([sparkErrorDiagnostics(error).message]) };
482
+ }
483
+ }),
359
484
  };
485
+ return host;
360
486
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@womp/spark-agent",
3
- "version": "9.0.0",
3
+ "version": "9.2.0",
4
4
  "description": "Portable skills-based AI agent with local and durable chat transports for Womp",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -74,6 +74,7 @@
74
74
  "dist/**/*.d.ts",
75
75
  "dist/server/fixtures/**/*.json",
76
76
  "dist/geometry/schemas/**/*.json",
77
+ "dist/materials/schemas/**/*.json",
77
78
  "README.md",
78
79
  "CHANGELOG.md"
79
80
  ],
@@ -105,6 +106,7 @@
105
106
  "web:build": "yarn build && tsc --noEmit -p web/tsconfig.json && vite build --config web/vite.config.ts",
106
107
  "test": "yarn build && tsx --tsconfig tsconfig.json --test test/**/*.test.ts && node --input-type=module -e \"await Promise.all([import('@womp/spark-agent'), import('@womp/spark-agent/ui'), import('@womp/spark-agent/client'), import('@womp/spark-agent/server'), import('@womp/spark-agent/express'), import('@womp/spark-agent/kakapo'), import('@womp/spark-agent/geometry'), import('@womp/spark-agent/openscad')])\"",
107
108
  "test:live": "tsx --tsconfig tsconfig.json test/womp-scene-api.live.ts",
109
+ "test:live:materials": "tsx --tsconfig tsconfig.json test/materials-flowir.live.ts",
108
110
  "prepack": "yarn build",
109
111
  "release:pack": "node scripts/npm-release.mjs pack"
110
112
  },