@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.
- package/README.md +430 -423
- package/dist/agent-run.d.ts +1 -1
- package/dist/agent-tools.js +30 -12
- package/dist/capabilities/geometry.js +14 -14
- package/dist/capabilities/materials.d.ts +45 -0
- package/dist/capabilities/materials.js +89 -0
- package/dist/capabilities/openscad-instructions.js +77 -77
- package/dist/capabilities/operations.js +28 -28
- package/dist/capabilities/scene.d.ts +21 -0
- package/dist/capabilities/scene.js +82 -82
- package/dist/capabilities.d.ts +1 -0
- package/dist/capabilities.js +1 -0
- package/dist/error-diagnostics.d.ts +24 -0
- package/dist/error-diagnostics.js +53 -0
- package/dist/generated/materials-declarations.d.ts +1 -0
- package/dist/generated/materials-declarations.js +3 -0
- package/dist/geometry/schemas/backend-schema.json +3393 -3393
- package/dist/geometry/schemas/flowir-dialect.json +1023 -1023
- package/dist/geometry.js +6 -6
- package/dist/kakapo.d.ts +1 -0
- package/dist/kakapo.js +1 -0
- package/dist/materials/composition.d.ts +64 -0
- package/dist/materials/composition.js +121 -0
- package/dist/materials/graph.d.ts +79 -0
- package/dist/materials/graph.js +335 -0
- package/dist/materials/inspection.d.ts +40 -0
- package/dist/materials/inspection.js +54 -0
- package/dist/materials/recipes.d.ts +12 -0
- package/dist/materials/recipes.js +284 -0
- package/dist/materials/reference.d.ts +2 -0
- package/dist/materials/reference.js +97 -0
- package/dist/materials/shaping.d.ts +294 -0
- package/dist/materials/shaping.js +184 -0
- package/dist/materials/structure.d.ts +2 -0
- package/dist/materials/structure.js +12 -0
- package/dist/materials/surface-recipes.d.ts +78 -0
- package/dist/materials/surface-recipes.js +193 -0
- package/dist/materials/types.d.ts +222 -0
- package/dist/materials/types.js +1 -0
- package/dist/models.d.ts +7 -0
- package/dist/models.js +8 -0
- package/dist/pricing.js +5 -0
- package/dist/prompt.d.ts +1 -1
- package/dist/prompt.js +72 -72
- package/dist/scene-operations.d.ts +3 -2
- package/dist/scene-operations.js +19 -11
- package/dist/sdk-geometry-host.js +3 -1
- package/dist/sdk-materials-host.d.ts +9 -0
- package/dist/sdk-materials-host.js +360 -0
- package/dist/sdk-openscad-host.js +3 -1
- package/dist/sdk-scene-host.d.ts +6 -1
- package/dist/sdk-scene-host.js +19 -3
- package/dist/server/fixtures/materials/matte-blue.json +14 -14
- package/dist/server/fixtures/materials/polished-copper.json +14 -14
- package/dist/server/sqlite-store.js +37 -37
- package/dist/server/standalone.js +6 -1
- package/dist/ui.d.ts +50 -6
- package/package.json +139 -139
package/dist/scene-operations.js
CHANGED
|
@@ -41,13 +41,13 @@ export async function recoverSceneOperation(options) {
|
|
|
41
41
|
}
|
|
42
42
|
return previous;
|
|
43
43
|
}
|
|
44
|
-
/**
|
|
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
|
|
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 =
|
|
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 =
|
|
96
|
-
: "rolled_back";
|
|
99
|
+
const status = operation.status === "committed" ? "committed"
|
|
100
|
+
: error instanceof KakapoSceneCommitError ? error.outcome : "rolled_back";
|
|
97
101
|
try {
|
|
98
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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);
|
package/dist/sdk-scene-host.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export type SdkSceneServices = {
|
|
|
18
18
|
export type SdkSceneEditorOptions = {
|
|
19
19
|
targetKey: string;
|
|
20
20
|
projectId?: number;
|
|
21
|
-
operations
|
|
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>;
|
package/dist/sdk-scene-host.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
+
}
|