@womp/spark-agent 9.0.0 → 9.1.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/CHANGELOG.md +27 -0
- package/dist/capabilities/materials.d.ts +10 -1
- package/dist/capabilities/materials.js +30 -10
- package/dist/generated/materials-declarations.d.ts +1 -1
- package/dist/generated/materials-declarations.js +1 -1
- package/dist/generated/materials-flowir-declarations.d.ts +1 -0
- package/dist/generated/materials-flowir-declarations.js +3 -0
- package/dist/materials/flowir.d.ts +15 -0
- package/dist/materials/flowir.js +362 -0
- package/dist/materials/graph.d.ts +7 -0
- package/dist/materials/graph.js +13 -1
- package/dist/materials/lift.d.ts +3 -0
- package/dist/materials/lift.js +216 -0
- package/dist/materials/schemas/materials-dialect.json +690 -0
- package/dist/materials/types.d.ts +15 -0
- package/dist/sdk-materials-host.js +136 -10
- package/package.json +3 -1
|
@@ -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
|
-
|
|
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 (
|
|
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
|
|
230
|
-
|
|
231
|
-
let
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "9.1.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
|
},
|