@womp/spark-agent 5.2.2 → 5.3.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.
Files changed (58) hide show
  1. package/README.md +430 -423
  2. package/dist/agent-run.d.ts +1 -1
  3. package/dist/agent-tools.js +30 -12
  4. package/dist/capabilities/geometry.js +14 -14
  5. package/dist/capabilities/materials.d.ts +45 -0
  6. package/dist/capabilities/materials.js +89 -0
  7. package/dist/capabilities/openscad-instructions.js +77 -77
  8. package/dist/capabilities/operations.js +28 -28
  9. package/dist/capabilities/scene.d.ts +21 -0
  10. package/dist/capabilities/scene.js +82 -82
  11. package/dist/capabilities.d.ts +1 -0
  12. package/dist/capabilities.js +1 -0
  13. package/dist/error-diagnostics.d.ts +24 -0
  14. package/dist/error-diagnostics.js +53 -0
  15. package/dist/generated/materials-declarations.d.ts +1 -0
  16. package/dist/generated/materials-declarations.js +3 -0
  17. package/dist/geometry/schemas/backend-schema.json +3393 -3393
  18. package/dist/geometry/schemas/flowir-dialect.json +1023 -1023
  19. package/dist/geometry.js +6 -6
  20. package/dist/kakapo.d.ts +1 -0
  21. package/dist/kakapo.js +1 -0
  22. package/dist/materials/composition.d.ts +64 -0
  23. package/dist/materials/composition.js +121 -0
  24. package/dist/materials/graph.d.ts +79 -0
  25. package/dist/materials/graph.js +335 -0
  26. package/dist/materials/inspection.d.ts +40 -0
  27. package/dist/materials/inspection.js +54 -0
  28. package/dist/materials/recipes.d.ts +12 -0
  29. package/dist/materials/recipes.js +284 -0
  30. package/dist/materials/reference.d.ts +2 -0
  31. package/dist/materials/reference.js +97 -0
  32. package/dist/materials/shaping.d.ts +294 -0
  33. package/dist/materials/shaping.js +184 -0
  34. package/dist/materials/structure.d.ts +2 -0
  35. package/dist/materials/structure.js +12 -0
  36. package/dist/materials/surface-recipes.d.ts +78 -0
  37. package/dist/materials/surface-recipes.js +193 -0
  38. package/dist/materials/types.d.ts +222 -0
  39. package/dist/materials/types.js +1 -0
  40. package/dist/models.d.ts +7 -0
  41. package/dist/models.js +8 -0
  42. package/dist/pricing.js +5 -0
  43. package/dist/prompt.d.ts +1 -1
  44. package/dist/prompt.js +72 -72
  45. package/dist/scene-operations.d.ts +3 -2
  46. package/dist/scene-operations.js +19 -11
  47. package/dist/sdk-geometry-host.js +3 -1
  48. package/dist/sdk-materials-host.d.ts +9 -0
  49. package/dist/sdk-materials-host.js +360 -0
  50. package/dist/sdk-openscad-host.js +3 -1
  51. package/dist/sdk-scene-host.d.ts +6 -1
  52. package/dist/sdk-scene-host.js +19 -3
  53. package/dist/server/fixtures/materials/matte-blue.json +14 -14
  54. package/dist/server/fixtures/materials/polished-copper.json +14 -14
  55. package/dist/server/sqlite-store.js +37 -37
  56. package/dist/server/standalone.js +6 -1
  57. package/dist/ui.d.ts +50 -6
  58. package/package.json +139 -139
@@ -41,13 +41,13 @@ export async function recoverSceneOperation(options) {
41
41
  }
42
42
  return previous;
43
43
  }
44
- /** The host owns connection/target validation and execution exclusivity. Never replays an existing edit. */
44
+ /** Hosts may omit persistence; journal-backed hosts retain duplicate-operation protection. */
45
45
  export async function executeSceneOperation(options) {
46
46
  const { api, store, operation: identity } = options;
47
47
  const savesScene = options.saveToRenderer === true;
48
48
  await options.assertEditable();
49
- const previous = await store.getSceneOperation(identity.targetKey, identity.id);
50
- if (previous) {
49
+ const previous = await store?.getSceneOperation(identity.targetKey, identity.id);
50
+ if (previous && store) {
51
51
  if ((previous.save !== undefined) !== savesScene)
52
52
  throw new Error("Scene operation identity was reused with a different save policy");
53
53
  if (previous.status === "committed")
@@ -65,7 +65,7 @@ export async function executeSceneOperation(options) {
65
65
  ...identity, revision: 0, status: "started",
66
66
  ...(savesScene ? { save: { status: "unknown" } } : {}),
67
67
  };
68
- if (!await store.createSceneOperation(operation))
68
+ if (store && !await store.createSceneOperation(operation))
69
69
  throw new Error("Scene operation is already in progress");
70
70
  try {
71
71
  const edit = async (draft) => {
@@ -77,10 +77,12 @@ export async function executeSceneOperation(options) {
77
77
  save: savesScene,
78
78
  beforeCommit: async (changed) => {
79
79
  await options.assertEditable();
80
- operation = await store.saveSceneOperation({
80
+ operation = {
81
81
  ...operation, status: "prepared", changed,
82
82
  ...(savesScene && !changed ? { save: { status: "not_needed" } } : {}),
83
- });
83
+ };
84
+ if (store)
85
+ operation = await store.saveSceneOperation(operation);
84
86
  await options.assertEditable();
85
87
  if (changed)
86
88
  await options.prepareCommit?.(operation);
@@ -88,18 +90,23 @@ export async function executeSceneOperation(options) {
88
90
  });
89
91
  if (savesScene)
90
92
  operation = { ...operation, save: committed.save };
93
+ operation = complete(operation, "committed");
94
+ options.onOutcome?.(operation);
91
95
  if (committed.changed)
92
96
  options.onCommittedScene?.(committed.scene);
93
97
  }
94
98
  catch (error) {
95
- const status = error instanceof KakapoSceneCommitError ? error.outcome
96
- : "rolled_back";
99
+ const status = operation.status === "committed" ? "committed"
100
+ : error instanceof KakapoSceneCommitError ? error.outcome : "rolled_back";
97
101
  try {
98
- await store.saveSceneOperation({
102
+ operation = {
99
103
  ...(status === "rolled_back" ? complete(operation, status) : { ...operation, status }),
100
104
  error: error instanceof Error ? error.message : String(error),
101
105
  ...(operation.save && status === "rolled_back" ? { save: { status: "not_needed" } } : {}),
102
- });
106
+ };
107
+ options.onOutcome?.(operation);
108
+ if (store)
109
+ operation = await store.saveSceneOperation(operation);
103
110
  }
104
111
  catch {
105
112
  // Keep the original error; recovery can inspect the prepared record.
@@ -107,6 +114,7 @@ export async function executeSceneOperation(options) {
107
114
  throw error;
108
115
  }
109
116
  // A prepared record alone never proves an uncertain commit succeeded.
110
- operation = await store.saveSceneOperation(complete(operation, "committed"));
117
+ if (store)
118
+ operation = await store.saveSceneOperation(complete(operation, "committed"));
111
119
  return { value: resultValue(operation), operation };
112
120
  }
@@ -1,3 +1,4 @@
1
+ import { SceneOperationError } from "./sdk-scene-host.js";
1
2
  import { compileGeometry, migrateGraphExt, readEditableGeometry, } from "./geometry/compiler.js";
2
3
  import { graphExtSchema } from "./geometry/graph.js";
3
4
  import { boundedDiagnostics } from "./geometry/diagnostics.js";
@@ -264,7 +265,8 @@ export function createSdkGeometryHost(options) {
264
265
  }
265
266
  catch (error) {
266
267
  if (operationId !== undefined && target.graphId !== undefined) {
267
- const operation = await editor.getOperation(operationId);
268
+ const operation = error instanceof SceneOperationError
269
+ ? error.operation : await editor.getOperation(operationId);
268
270
  if (operation?.status === "rolled_back")
269
271
  await repairs.refund(String(target.graphId), operationId);
270
272
  }
@@ -0,0 +1,9 @@
1
+ import type { SparkInvocationContext } from "./capabilities.js";
2
+ import type { MaterialsHost } from "./capabilities/materials.js";
3
+ import type { SdkSceneEditor } from "./sdk-scene-host.js";
4
+ export type SdkMaterialsHostOptions = {
5
+ editor: SdkSceneEditor;
6
+ /** Host-owned procedural-material entitlement/availability check, run for reads and writes. */
7
+ assertAvailable(context: SparkInvocationContext): void | Promise<void>;
8
+ };
9
+ export declare function createSdkMaterialsHost(options: SdkMaterialsHostOptions): MaterialsHost;
@@ -0,0 +1,360 @@
1
+ import { sparkErrorDiagnostics } from "./error-diagnostics.js";
2
+ import { graphSchema, materialId, parseMaterialCatalog, validateMaterialGraph, writeSchema, value3, } from "./materials/graph.js";
3
+ function record(value, label) {
4
+ if (!value || typeof value !== "object" || Array.isArray(value))
5
+ throw new Error(`Invalid ${label}`);
6
+ return value;
7
+ }
8
+ function storage(source) {
9
+ const data = source.getSceneStorage();
10
+ return {
11
+ materials: record(data.materials ?? {}, "material storage"),
12
+ shaders: record(data.shaders ?? {}, "shader storage"),
13
+ };
14
+ }
15
+ function rawMaterial(source, id) {
16
+ return record(storage(source).materials[String(id)], `material ${id}`);
17
+ }
18
+ function layers(source, id) {
19
+ const data = storage(source);
20
+ const material = rawMaterial(source, id);
21
+ if (material.shaders === undefined)
22
+ return [];
23
+ if (!Array.isArray(material.shaders) || material.shaders.length > 8)
24
+ throw new Error(`Material ${id} has invalid shader slots`);
25
+ return material.shaders.map((value) => value === 0
26
+ ? null
27
+ : graphSchema.parse(data.shaders[String(materialId.parse(value))]));
28
+ }
29
+ function directUsers(source, id) {
30
+ return source
31
+ .listNodes()
32
+ .filter((node) => ("materialId" in node && node.materialId === id) ||
33
+ (node.kind === "curve" && node.points.some((p) => p.materialId === id)))
34
+ .map((node) => node.id);
35
+ }
36
+ function unusedId(entries) {
37
+ for (let id = 1; id <= 65535; id++)
38
+ if (!(String(id) in entries))
39
+ return id;
40
+ throw new Error("No free 16-bit shader IDs remain");
41
+ }
42
+ function freshReference(used) {
43
+ // Same reference IDs survive save/reopen; random IDs also avoid retired native cache entries.
44
+ for (;;) {
45
+ const bits = globalThis.crypto.getRandomValues(new Uint32Array(2));
46
+ const id = (bits[0] & 0x1fffff) * 4294967296 + bits[1];
47
+ if (id > 0 && !used.has(id)) {
48
+ used.add(id);
49
+ return id;
50
+ }
51
+ }
52
+ }
53
+ function allReferences(source) {
54
+ const data = storage(source);
55
+ const used = new Set();
56
+ for (const graph of Object.values(data.shaders)) {
57
+ const nodes = record(graph, "shader").nodes;
58
+ if (Array.isArray(nodes))
59
+ for (const node of nodes) {
60
+ if (node &&
61
+ typeof node === "object" &&
62
+ !Array.isArray(node) &&
63
+ typeof node.reference_id === "number")
64
+ used.add(node.reference_id);
65
+ }
66
+ }
67
+ for (const material of Object.values(data.materials)) {
68
+ const constants = record(record(material, "material").data, "material data").constants;
69
+ if (constants && typeof constants === "object")
70
+ for (const id of Object.keys(constants))
71
+ used.add(Number(id));
72
+ }
73
+ return used;
74
+ }
75
+ function patchEntry(draft, collection, id, value) {
76
+ if (!(collection in draft.getSceneStorage()))
77
+ draft.patchSceneStorage([{ op: "add", path: `/${collection}`, value: {} }]);
78
+ draft.patchSceneStorage([
79
+ { op: "add", path: `/${collection}/${id}`, value: value },
80
+ ]);
81
+ }
82
+ export function createSdkMaterialsHost(options) {
83
+ const initialized = new WeakSet();
84
+ const catalogue = async (api, context, refresh) => {
85
+ await options.assertAvailable(context);
86
+ context.signal.throwIfAborted();
87
+ if (refresh || !initialized.has(api)) {
88
+ await api.rpc("reload_textures", []);
89
+ context.signal.throwIfAborted();
90
+ initialized.add(api);
91
+ }
92
+ const nodes = parseMaterialCatalog(await api.rpc("get_procedural_nodes", []));
93
+ context.signal.throwIfAborted();
94
+ return nodes;
95
+ };
96
+ const snapshot = async (api, id) => {
97
+ const items = api.listMaterials().map((material) => ({
98
+ id: material.id,
99
+ shaderIds: material.shaderIds,
100
+ objectIds: directUsers(api, material.id),
101
+ }));
102
+ return {
103
+ materials: items,
104
+ ...(id === undefined
105
+ ? {}
106
+ : {
107
+ material: {
108
+ id,
109
+ data: record(rawMaterial(api, id).data, "material data"),
110
+ layers: layers(api, id),
111
+ },
112
+ }),
113
+ diagnostics: [
114
+ "Object IDs list direct assignments and curve points. Geometry Nodes/OpenSCAD can also refer to a material ID; existing-material edits affect all references.",
115
+ ],
116
+ };
117
+ };
118
+ return {
119
+ catalog: (settings, context) => options.editor.read(context, async (api) => {
120
+ const nodes = await catalogue(api, context, settings.refreshTextures === true);
121
+ const selected = {};
122
+ for (const op of settings.operations ?? []) {
123
+ if (!nodes[op])
124
+ throw new Error(`Material operation ${op} is unavailable`);
125
+ selected[op] = nodes[op];
126
+ }
127
+ return {
128
+ operations: Object.entries(nodes).map(([op, node]) => ({
129
+ op,
130
+ category: node.type,
131
+ title: node.title,
132
+ })),
133
+ nodes: selected,
134
+ textureCount: nodes.PredefinedTexture?.inputs[2]?.values?.length ?? 0,
135
+ ...(settings.includeTextures
136
+ ? {
137
+ textures: nodes.PredefinedTexture?.inputs[2]?.values ?? [],
138
+ textureMetadata: {
139
+ channels: "unknown",
140
+ colorSpace: "unknown",
141
+ normalConvention: "unknown",
142
+ reason: "The SDK texture catalogue supplies names, not channel or encoding metadata.",
143
+ },
144
+ }
145
+ : {}),
146
+ };
147
+ }),
148
+ read: (id, context) => options.editor.read(context, async (api) => {
149
+ await options.assertAvailable(context);
150
+ context.signal.throwIfAborted();
151
+ return snapshot(api, id === undefined ? undefined : materialId.parse(id));
152
+ }),
153
+ status: (context) => options.editor.read(context, async (api) => {
154
+ await options.assertAvailable(context);
155
+ context.signal.throwIfAborted();
156
+ const errors = [];
157
+ let remaining = null;
158
+ let pendingResources = null;
159
+ try {
160
+ const result = await api.rpc("get_procedural_materials_progress", []);
161
+ const data = record(typeof result === "string" ? JSON.parse(result) : result, "compiler progress");
162
+ if (typeof data.remaining !== "number" ||
163
+ !Number.isSafeInteger(data.remaining) || data.remaining < 0)
164
+ throw new Error("Invalid compiler progress counter");
165
+ remaining = data.remaining;
166
+ }
167
+ catch (error) {
168
+ context.signal.throwIfAborted();
169
+ errors.push({ source: "compiler-progress", message: sparkErrorDiagnostics(error).message });
170
+ }
171
+ context.signal.throwIfAborted();
172
+ await options.assertAvailable(context);
173
+ context.signal.throwIfAborted();
174
+ try {
175
+ const pending = record(await api.getPendingResources(), "pending resources");
176
+ const validated = {};
177
+ for (const [kind, names] of Object.entries(pending)) {
178
+ if (!Array.isArray(names) || !names.every((name) => typeof name === "string"))
179
+ throw new Error("Invalid pending resource list");
180
+ Object.defineProperty(validated, kind, { value: names, enumerable: true });
181
+ }
182
+ pendingResources = validated;
183
+ }
184
+ catch (error) {
185
+ context.signal.throwIfAborted();
186
+ errors.push({ source: "pending-resources", message: sparkErrorDiagnostics(error).message });
187
+ }
188
+ context.signal.throwIfAborted();
189
+ await options.assertAvailable(context);
190
+ context.signal.throwIfAborted();
191
+ return {
192
+ remaining,
193
+ pendingResources,
194
+ errors,
195
+ shaderDiagnostics: {
196
+ status: "unavailable",
197
+ reason: "The current SDK/native RPC exposes global queue length, not per-shader compiler results or errors bound to a scene revision.",
198
+ },
199
+ rendering: "unverified",
200
+ explanation: "A zero queue does not prove compilation, texture loading or the new rendered appearance succeeded. Inspect a scene screenshot.",
201
+ };
202
+ }),
203
+ write: async (input, context) => {
204
+ const request = writeSchema.parse(input);
205
+ await options.assertAvailable(context);
206
+ let resultId = request.materialId;
207
+ const edited = await options.editor.access({
208
+ capability: "materials",
209
+ operation: "write",
210
+ target: () => resultId === undefined ? {} : { materialId: resultId },
211
+ }, async (api, draft) => {
212
+ await options.assertAvailable(context);
213
+ context.signal.throwIfAborted();
214
+ if (request.materialId !== undefined &&
215
+ request.layers === undefined &&
216
+ request.constants === undefined) {
217
+ draft.getMaterial(request.materialId);
218
+ for (const id of request.objectIds ?? [])
219
+ draft.setNodeMaterial(id, request.materialId);
220
+ return {
221
+ materialId: request.materialId,
222
+ objectIds: directUsers(draft, request.materialId),
223
+ referenceIdChanges: {},
224
+ };
225
+ }
226
+ const catalog = await catalogue(api, context, false);
227
+ const sourceId = request.materialId ?? request.cloneFrom;
228
+ 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;
232
+ const used = allReferences(draft);
233
+ const refs = new Map();
234
+ for (const graph of graphs)
235
+ for (const node of graph?.nodes ?? [])
236
+ if (node.op === "Constant") {
237
+ const id = typeof node.reference_id === "number" && node.reference_id > 0
238
+ ? node.reference_id
239
+ : freshReference(used);
240
+ if (refs.has(id))
241
+ throw new Error(`Constant reference ${id} is duplicated across layers; give independent constants unique IDs`);
242
+ if (node.reference_id !== id)
243
+ rewriteGraphs = true;
244
+ node.reference_id = id;
245
+ used.add(id);
246
+ refs.set(id, node);
247
+ }
248
+ const sourceData = original
249
+ ? record(original.data, "material data")
250
+ : undefined;
251
+ const constants = record(sourceData?.constants ?? {}, "material constants");
252
+ const referenceIdChanges = {};
253
+ for (const [key, override] of Object.entries(request.constants ?? {})) {
254
+ const node = refs.get(Number(key));
255
+ if (!node)
256
+ throw new Error(`Constant reference ${key} does not exist in the resulting layers`);
257
+ if (override === null)
258
+ delete constants[key];
259
+ else
260
+ constants[key] = override;
261
+ }
262
+ for (const [reference, node] of refs) {
263
+ const key = String(reference);
264
+ // Replaced graphs can reuse IDs whose deleted overrides remain in the native cache.
265
+ if (request.constants?.[key] === null ||
266
+ (request.materialId !== undefined &&
267
+ request.layers !== undefined &&
268
+ constants[key] === undefined)) {
269
+ const id = freshReference(used);
270
+ rewriteGraphs = true;
271
+ node.reference_id = id;
272
+ referenceIdChanges[key] = id;
273
+ }
274
+ }
275
+ context.signal.throwIfAborted();
276
+ if (resultId === undefined)
277
+ resultId = materialId.parse(draft.createMaterial(sourceId === undefined
278
+ ? undefined
279
+ : api.getMaterial(sourceId).data, Array(8).fill(0)).id);
280
+ const target = rawMaterial(draft, resultId);
281
+ const data = sourceData ?? record(target.data, "material data");
282
+ // Keep complete override pairs; unoverridden replacement nodes use fresh references.
283
+ data.constants = constants;
284
+ const current = storage(draft);
285
+ const oldSlots = original && Array.isArray(original.shaders) ? original.shaders : [];
286
+ const slots = graphs.map((graph, slot) => {
287
+ if (graph === null)
288
+ return 0;
289
+ if (!rewriteGraphs)
290
+ return materialId.parse(oldSlots[slot]);
291
+ const oldId = oldSlots[slot];
292
+ const references = Object.values(current.materials).reduce((count, entry) => {
293
+ const ids = record(entry, "material").shaders;
294
+ return (count +
295
+ (Array.isArray(ids)
296
+ ? ids.filter((id) => id === oldId).length
297
+ : 0));
298
+ }, 0);
299
+ // A clone or shared shader gets independent storage; other materials keep their graph.
300
+ const shaderId = request.materialId !== undefined &&
301
+ typeof oldId === "number" &&
302
+ oldId > 0 &&
303
+ references === 1
304
+ ? materialId.parse(oldId)
305
+ : unusedId(current.shaders);
306
+ current.shaders[String(shaderId)] =
307
+ graph;
308
+ patchEntry(draft, "shaders", shaderId, graph);
309
+ return shaderId;
310
+ });
311
+ while (slots.length < 8)
312
+ slots.push(0);
313
+ patchEntry(draft, "materials", resultId, {
314
+ ...target,
315
+ ...(original ?? {}),
316
+ data,
317
+ shaders: slots,
318
+ });
319
+ if (request.cloneFrom !== undefined) {
320
+ const ext = draft.getSceneExt();
321
+ const metadata = record(ext.materialsExt ?? {}, "material metadata");
322
+ if (metadata[String(request.cloneFrom)] !== undefined) {
323
+ if (ext.materialsExt === undefined)
324
+ draft.patchSceneExt([
325
+ { op: "add", path: "/materialsExt", value: {} },
326
+ ]);
327
+ draft.patchSceneExt([
328
+ {
329
+ op: "add",
330
+ path: `/materialsExt/${resultId}`,
331
+ value: metadata[String(request.cloneFrom)],
332
+ },
333
+ ]);
334
+ }
335
+ }
336
+ for (const id of request.objectIds ?? [])
337
+ draft.setNodeMaterial(id, resultId);
338
+ return {
339
+ materialId: resultId,
340
+ objectIds: directUsers(draft, resultId),
341
+ referenceIdChanges,
342
+ parameters: graphs.flatMap(graph => graph?.nodes.filter(node => node.op === "Constant").map(node => {
343
+ const referenceId = node.reference_id;
344
+ const override = constants[String(referenceId)];
345
+ return { name: node.name ?? "", referenceId,
346
+ exposed: node.in3?.x !== undefined && node.in3.x > 0,
347
+ value: override?.value ?? node.in1 ?? value3(0),
348
+ color: override?.color ?? node.in2 ?? value3(0),
349
+ };
350
+ }) ?? []),
351
+ };
352
+ }, context);
353
+ return {
354
+ ...edited.value,
355
+ status: "applied",
356
+ rendering: "unverified",
357
+ };
358
+ },
359
+ };
360
+ }
@@ -1,3 +1,4 @@
1
+ import { SceneOperationError } from "./sdk-scene-host.js";
1
2
  import { z } from "zod";
2
3
  import { getOpenScadGroundedPosition, MAX_PRINTABILITY_REPAIR_ATTEMPTS, requestTurnOpenScadPrintability, toOpenScadCompileResponse, } from "./openscad.js";
3
4
  import { createGeometryRepairTracker, createRepairTracker, } from "./repair-state.js";
@@ -237,7 +238,8 @@ export function createSdkOpenScadHost(options) {
237
238
  }
238
239
  catch (error) {
239
240
  if (creating === false && operationId !== "") {
240
- const operation = await editor.getOperation(operationId);
241
+ const operation = error instanceof SceneOperationError
242
+ ? error.operation : await editor.getOperation(operationId);
241
243
  if (operation?.status === "rolled_back") {
242
244
  await geometryRepairs.refund(resolved.repairKey, operationId);
243
245
  await printabilityRepairs.refund(resolved.repairKey, operationId);
@@ -18,7 +18,7 @@ export type SdkSceneServices = {
18
18
  export type SdkSceneEditorOptions = {
19
19
  targetKey: string;
20
20
  projectId?: number;
21
- operations: SceneOperationStore;
21
+ operations?: SceneOperationStore;
22
22
  saveToRenderer?: boolean;
23
23
  withConnection<T>(context: SparkInvocationContext, action: (api: KakapoAPI, beginSettlement: () => void,
24
24
  /** Supply this when the host adds a stricter connection lifetime. */
@@ -34,6 +34,11 @@ export type SdkSceneEditIdentity = {
34
34
  operation: string;
35
35
  target?: Record<string, unknown> | (() => Record<string, unknown>);
36
36
  };
37
+ /** Carries this invocation's outcome without requiring a persistent journal. */
38
+ export declare class SceneOperationError extends Error {
39
+ readonly operation: SceneOperation;
40
+ constructor(operation: SceneOperation, cause: unknown);
41
+ }
37
42
  export type SdkSceneEditor = {
38
43
  getOperation(operationId: string): Promise<SceneOperation | undefined>;
39
44
  withConnection<T>(context: SparkInvocationContext, action: (api: KakapoAPI, beginSettlement: () => void) => T | Promise<T>): Promise<T>;
@@ -10,6 +10,15 @@ function assertSceneSaved(operation) {
10
10
  if (operation.save?.status === "unknown" || operation.save?.status === "requested")
11
11
  throw new Error("The scene edit was applied, but its save outcome is unknown. Inspect persistence before continuing; do not repeat the edit.");
12
12
  }
13
+ /** Carries this invocation's outcome without requiring a persistent journal. */
14
+ export class SceneOperationError extends Error {
15
+ operation;
16
+ constructor(operation, cause) {
17
+ super(cause instanceof Error ? cause.message : String(cause), { cause });
18
+ this.operation = operation;
19
+ this.name = "SceneOperationError";
20
+ }
21
+ }
13
22
  export function createSdkSceneDraft(api, scene, services) {
14
23
  let defaultMaterialId = scene
15
24
  .listMaterials()
@@ -65,7 +74,7 @@ export function createSdkSceneDraft(api, scene, services) {
65
74
  export function createSdkSceneEditor(options) {
66
75
  const ordinals = new WeakMap();
67
76
  return {
68
- getOperation: (operationId) => options.operations.getSceneOperation(options.targetKey, operationId),
77
+ getOperation: (operationId) => options.operations?.getSceneOperation(options.targetKey, operationId) ?? Promise.resolve(undefined),
69
78
  withConnection: (context, action) => options.withConnection(context, async (api, settle, active = context) => {
70
79
  active.signal.throwIfAborted();
71
80
  return action(api, settle);
@@ -102,6 +111,7 @@ export function createSdkSceneEditor(options) {
102
111
  let synchronized = false;
103
112
  let saveSettled = true;
104
113
  let committedScene;
114
+ let outcome;
105
115
  let hostDetails = {};
106
116
  const record = async (operation, details) => {
107
117
  hostDetails = { ...hostDetails, ...details };
@@ -121,7 +131,7 @@ export function createSdkSceneEditor(options) {
121
131
  result: operation.result,
122
132
  save: operation.save,
123
133
  error: operation.error,
124
- synchronized,
134
+ ...(options.operations ? { synchronized } : {}),
125
135
  }),
126
136
  });
127
137
  };
@@ -133,6 +143,7 @@ export function createSdkSceneEditor(options) {
133
143
  result = await executeSceneOperation({
134
144
  api,
135
145
  store: options.operations,
146
+ onOutcome: operation => { outcome = operation; },
136
147
  saveToRenderer: options.saveToRenderer,
137
148
  operation: {
138
149
  id,
@@ -157,7 +168,7 @@ export function createSdkSceneEditor(options) {
157
168
  catch (error) {
158
169
  // Receipt persistence must not replace the original SDK/tool error.
159
170
  try {
160
- const operation = await options.operations.getSceneOperation(options.targetKey, id);
171
+ const operation = outcome ?? await options.operations?.getSceneOperation(options.targetKey, id);
161
172
  if (operation) {
162
173
  synchronized = operation.status === "rolled_back";
163
174
  await record(operation);
@@ -204,6 +215,11 @@ export function createSdkSceneEditor(options) {
204
215
  }
205
216
  return result;
206
217
  }
218
+ catch (error) {
219
+ if (!options.operations && outcome)
220
+ throw new SceneOperationError(outcome, error);
221
+ throw error;
222
+ }
207
223
  finally {
208
224
  await releaseCommit?.(synchronized && saveSettled);
209
225
  }
@@ -1,14 +1,14 @@
1
- {
2
- "name": "Development: Matte Blue",
3
- "resource": {
4
- "data": {
5
- "color.x": 0.08,
6
- "color.y": 0.3,
7
- "color.z": 0.8,
8
- "roughness": 0.85,
9
- "metalness": 0,
10
- "index_of_refraction": 1.5
11
- },
12
- "shaders": []
13
- }
14
- }
1
+ {
2
+ "name": "Development: Matte Blue",
3
+ "resource": {
4
+ "data": {
5
+ "color.x": 0.08,
6
+ "color.y": 0.3,
7
+ "color.z": 0.8,
8
+ "roughness": 0.85,
9
+ "metalness": 0,
10
+ "index_of_refraction": 1.5
11
+ },
12
+ "shaders": []
13
+ }
14
+ }
@@ -1,14 +1,14 @@
1
- {
2
- "name": "Development: Polished Copper",
3
- "resource": {
4
- "data": {
5
- "color.x": 0.955,
6
- "color.y": 0.638,
7
- "color.z": 0.538,
8
- "roughness": 0.18,
9
- "metalness": 1,
10
- "index_of_refraction": 1.5
11
- },
12
- "shaders": []
13
- }
14
- }
1
+ {
2
+ "name": "Development: Polished Copper",
3
+ "resource": {
4
+ "data": {
5
+ "color.x": 0.955,
6
+ "color.y": 0.638,
7
+ "color.z": 0.538,
8
+ "roughness": 0.18,
9
+ "metalness": 1,
10
+ "index_of_refraction": 1.5
11
+ },
12
+ "shaders": []
13
+ }
14
+ }