@womp/spark-agent 4.4.1 → 5.0.1
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 +23 -2
- package/dist/capabilities/geometry.d.ts +0 -3
- package/dist/capabilities/geometry.js +7 -8
- package/dist/capabilities/openscad-instructions.d.ts +1 -1
- package/dist/capabilities/openscad-instructions.js +80 -81
- package/dist/capabilities/openscad.d.ts +0 -3
- package/dist/capabilities/openscad.js +1 -2
- package/dist/capabilities/scene.d.ts +2 -6
- package/dist/capabilities/scene.js +44 -45
- package/dist/generated/geometry-declarations.d.ts +1 -1
- package/dist/generated/geometry-declarations.js +1 -1
- package/dist/generated/openscad-declarations.d.ts +1 -1
- package/dist/generated/openscad-declarations.js +1 -1
- package/dist/generated/scene-declarations.d.ts +1 -1
- package/dist/generated/scene-declarations.js +1 -1
- package/dist/native-executor.js +2 -2
- package/dist/openscad.d.ts +1 -2
- package/dist/openscad.js +1 -19
- package/dist/quickjs-executor.js +1 -1
- package/dist/repair-state.d.ts +2 -0
- package/dist/repair-state.js +15 -0
- package/dist/runtime.d.ts +1 -5
- package/dist/runtime.js +8 -14
- package/dist/scene-environment.js +1 -1
- package/dist/scene-health.d.ts +1 -2
- package/dist/scene-health.js +0 -17
- package/dist/scene-operations.d.ts +5 -5
- package/dist/scene-operations.js +24 -43
- package/dist/sdk-geometry-host.d.ts +2 -2
- package/dist/sdk-geometry-host.js +44 -61
- package/dist/sdk-openscad-host.js +63 -102
- package/dist/sdk-scene-health-host.d.ts +2 -1
- package/dist/sdk-scene-health-host.js +54 -4
- package/dist/sdk-scene-host.d.ts +20 -5
- package/dist/sdk-scene-host.js +34 -31
- package/dist/sdk-scene-screenshot.d.ts +1 -1
- package/dist/sdk-scene-screenshot.js +4 -3
- package/dist/server/scene-access.d.ts +4 -0
- package/dist/server/scene-access.js +40 -0
- package/dist/server/standalone.js +24 -19
- package/dist/womp-scene-api.d.ts +15 -0
- package/dist/womp-scene-api.js +18 -1
- package/package.json +139 -139
- package/dist/server/scene-initialization.d.ts +0 -3
- package/dist/server/scene-initialization.js +0 -44
package/dist/scene-operations.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { KakapoSceneCommitError } from "@womp/kakapo-sdk";
|
|
2
|
-
import { metadataObject } from "./scene-metadata.js";
|
|
3
2
|
function savedResult(value) {
|
|
4
3
|
if (value === undefined)
|
|
5
4
|
return { empty: true };
|
|
@@ -11,9 +10,9 @@ function savedResult(value) {
|
|
|
11
10
|
return;
|
|
12
11
|
if (typeof item !== "object" || item === null ||
|
|
13
12
|
(!Array.isArray(item) && Object.getPrototypeOf(item) !== Object.prototype && Object.getPrototypeOf(item) !== null))
|
|
14
|
-
throw new Error("scene.
|
|
13
|
+
throw new Error("scene.access must return JSON data or undefined");
|
|
15
14
|
if (active.has(item))
|
|
16
|
-
throw new Error("scene.
|
|
15
|
+
throw new Error("scene.access cannot return cyclic data");
|
|
17
16
|
active.add(item);
|
|
18
17
|
for (const nested of Object.values(item))
|
|
19
18
|
validate(nested);
|
|
@@ -27,26 +26,17 @@ function resultValue(operation) {
|
|
|
27
26
|
throw new Error("The saved scene operation result is missing");
|
|
28
27
|
return ("value" in operation.result ? structuredClone(operation.result.value) : undefined);
|
|
29
28
|
}
|
|
30
|
-
function committedIds(ext) {
|
|
31
|
-
const ids = metadataObject(ext.spark).committedSceneEdits;
|
|
32
|
-
return Array.isArray(ids) ? ids.filter((id) => typeof id === "string") : [];
|
|
33
|
-
}
|
|
34
29
|
function complete(operation, status) {
|
|
35
30
|
return { ...operation, status, completedAt: operation.completedAt ?? new Date().toISOString() };
|
|
36
31
|
}
|
|
37
|
-
/**
|
|
32
|
+
/** Recover acknowledged/no-op results without replaying or refetching an uncertain edit. */
|
|
38
33
|
export async function recoverSceneOperation(options) {
|
|
39
34
|
await options.assertActive();
|
|
40
35
|
const previous = await options.store.getSceneOperation(options.targetKey, options.id);
|
|
41
36
|
if (!previous || previous.status === "committed" || previous.status === "rolled_back")
|
|
42
37
|
return previous;
|
|
43
|
-
await options.api.refreshScene();
|
|
44
|
-
const ext = options.api.getSceneExt();
|
|
45
|
-
const revision = await options.api.getSceneRevision();
|
|
46
38
|
await options.assertActive();
|
|
47
|
-
|
|
48
|
-
previous.resultRevision === revision && committedIds(ext).includes(previous.id);
|
|
49
|
-
if ((previous.changed === false || matches) && previous.result) {
|
|
39
|
+
if (previous.changed === false && previous.result) {
|
|
50
40
|
return options.store.saveSceneOperation(complete(previous, "committed"));
|
|
51
41
|
}
|
|
52
42
|
return previous;
|
|
@@ -58,19 +48,17 @@ export async function executeSceneOperation(options) {
|
|
|
58
48
|
await options.assertEditable();
|
|
59
49
|
const previous = await store.getSceneOperation(identity.targetKey, identity.id);
|
|
60
50
|
if (previous) {
|
|
61
|
-
if (previous.expectedRevision !== identity.expectedRevision)
|
|
62
|
-
throw new Error("Scene operation identity was reused with a different revision");
|
|
63
51
|
if ((previous.save !== undefined) !== savesScene)
|
|
64
52
|
throw new Error("Scene operation identity was reused with a different save policy");
|
|
65
53
|
if (previous.status === "committed")
|
|
66
|
-
return { value: resultValue(previous), operation: previous };
|
|
54
|
+
return { value: resultValue(previous), operation: previous, reused: true };
|
|
67
55
|
if (previous.status === "rolled_back")
|
|
68
56
|
throw new Error(previous.error ?? "Scene operation was rolled back");
|
|
69
57
|
const recovered = await recoverSceneOperation({
|
|
70
58
|
api, store, targetKey: identity.targetKey, id: identity.id, assertActive: options.assertEditable,
|
|
71
59
|
});
|
|
72
60
|
if (recovered?.status === "committed")
|
|
73
|
-
return { value: resultValue(recovered), operation: recovered };
|
|
61
|
+
return { value: resultValue(recovered), operation: recovered, reused: true };
|
|
74
62
|
throw new Error("Scene edit outcome is uncertain. Inspect the scene before making a new edit; this operation was not replayed.");
|
|
75
63
|
}
|
|
76
64
|
let operation = {
|
|
@@ -82,33 +70,26 @@ export async function executeSceneOperation(options) {
|
|
|
82
70
|
try {
|
|
83
71
|
const edit = async (draft) => {
|
|
84
72
|
await options.assertEditable();
|
|
85
|
-
|
|
86
|
-
throw new Error("Scene changed since inspection. Read the scene again before editing.");
|
|
87
|
-
const result = savedResult(await options.edit(draft));
|
|
88
|
-
await options.assertEditable();
|
|
89
|
-
const changed = await api.getSceneRevision() !== identity.expectedRevision;
|
|
90
|
-
if (changed) {
|
|
91
|
-
const spark = metadataObject(draft.getSceneExt().spark);
|
|
92
|
-
const ids = [...committedIds(api.getSceneExt()), identity.id].slice(-64);
|
|
93
|
-
draft.patchSceneExt([{ op: "add", path: "/spark", value: { ...spark, committedSceneEdits: ids } }]);
|
|
94
|
-
}
|
|
95
|
-
operation = await store.saveSceneOperation({
|
|
96
|
-
...operation, status: "prepared", result, changed,
|
|
97
|
-
...(changed ? { resultRevision: await api.getSceneRevision() } : {}),
|
|
98
|
-
...(savesScene && !changed ? { save: { status: "not_needed" } } : {}),
|
|
99
|
-
});
|
|
100
|
-
await options.assertEditable();
|
|
101
|
-
if (changed)
|
|
102
|
-
await options.prepareCommit?.(operation);
|
|
73
|
+
operation = { ...operation, result: savedResult(await options.edit(draft)) };
|
|
103
74
|
await options.assertEditable();
|
|
104
75
|
};
|
|
105
|
-
|
|
106
|
-
|
|
76
|
+
const committed = await api.accessScene(edit, {
|
|
77
|
+
save: savesScene,
|
|
78
|
+
beforeCommit: async (changed) => {
|
|
79
|
+
await options.assertEditable();
|
|
80
|
+
operation = await store.saveSceneOperation({
|
|
81
|
+
...operation, status: "prepared", changed,
|
|
82
|
+
...(savesScene && !changed ? { save: { status: "not_needed" } } : {}),
|
|
83
|
+
});
|
|
84
|
+
await options.assertEditable();
|
|
85
|
+
if (changed)
|
|
86
|
+
await options.prepareCommit?.(operation);
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
if (savesScene)
|
|
107
90
|
operation = { ...operation, save: committed.save };
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
await api.editScene(edit);
|
|
111
|
-
}
|
|
91
|
+
if (committed.changed)
|
|
92
|
+
options.onCommittedScene?.(committed.scene);
|
|
112
93
|
}
|
|
113
94
|
catch (error) {
|
|
114
95
|
const status = error instanceof KakapoSceneCommitError ? error.outcome
|
|
@@ -125,7 +106,7 @@ export async function executeSceneOperation(options) {
|
|
|
125
106
|
}
|
|
126
107
|
throw error;
|
|
127
108
|
}
|
|
128
|
-
//
|
|
109
|
+
// A prepared record alone never proves an uncertain commit succeeded.
|
|
129
110
|
operation = await store.saveSceneOperation(complete(operation, "committed"));
|
|
130
111
|
return { value: resultValue(operation), operation };
|
|
131
112
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { type SdkSceneEditor } from "./sdk-scene-host.js";
|
|
2
2
|
import type { GeometryHost } from "./capabilities.js";
|
|
3
|
-
import {
|
|
3
|
+
import { readSdkSceneHealth } from "./scene-health.js";
|
|
4
4
|
import { type RepairStateStore } from "./repair-state.js";
|
|
5
5
|
export type SdkGeometryHostOptions = {
|
|
6
6
|
editor: SdkSceneEditor;
|
|
7
7
|
repairs: RepairStateStore;
|
|
8
8
|
canGrow?: () => boolean;
|
|
9
|
-
onHealth?: (health: Awaited<ReturnType<typeof
|
|
9
|
+
onHealth?: (health: Awaited<ReturnType<typeof readSdkSceneHealth>>) => void;
|
|
10
10
|
onSelect?: (objectId: number) => void | Promise<void>;
|
|
11
11
|
};
|
|
12
12
|
export declare function createSdkGeometryHost(options: SdkGeometryHostOptions): GeometryHost;
|
|
@@ -2,7 +2,7 @@ import { compileGeometry, migrateGraphExt, readEditableGeometry, } from "./geome
|
|
|
2
2
|
import { graphExtSchema } from "./geometry/graph.js";
|
|
3
3
|
import { boundedDiagnostics } from "./geometry/diagnostics.js";
|
|
4
4
|
import { migrateParameterOverrides, readParameterOverrides, } from "./geometry/parameters.js";
|
|
5
|
-
import {
|
|
5
|
+
import { unverifiedSceneHealth } from "./scene-health.js";
|
|
6
6
|
import { createGeometryRepairTracker, } from "./repair-state.js";
|
|
7
7
|
function errorMessage(error) {
|
|
8
8
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -57,7 +57,6 @@ async function readTarget(api, objectId, target = resolveTarget(api, objectId))
|
|
|
57
57
|
instanceId: target.instanceId,
|
|
58
58
|
source: null,
|
|
59
59
|
instances: [],
|
|
60
|
-
revision: await api.getSceneRevision(),
|
|
61
60
|
diagnostics: [],
|
|
62
61
|
};
|
|
63
62
|
if (target.graphId === undefined || target.graph === undefined)
|
|
@@ -93,7 +92,7 @@ function writeGraphStorage(draft, graphId, graph, instances) {
|
|
|
93
92
|
path: "/scripts",
|
|
94
93
|
value: { graphs: {}, instances: {} },
|
|
95
94
|
},
|
|
96
|
-
]);
|
|
95
|
+
], { returnScene: false });
|
|
97
96
|
}
|
|
98
97
|
else {
|
|
99
98
|
const record = scripts;
|
|
@@ -111,7 +110,7 @@ function writeGraphStorage(draft, graphId, graph, instances) {
|
|
|
111
110
|
value: {},
|
|
112
111
|
});
|
|
113
112
|
if (additions.length)
|
|
114
|
-
draft.patchSceneStorage(additions);
|
|
113
|
+
draft.patchSceneStorage(additions, { returnScene: false });
|
|
115
114
|
}
|
|
116
115
|
draft.patchSceneStorage([
|
|
117
116
|
{
|
|
@@ -124,7 +123,7 @@ function writeGraphStorage(draft, graphId, graph, instances) {
|
|
|
124
123
|
path: `/scripts/instances/${id}`,
|
|
125
124
|
value: instance,
|
|
126
125
|
})),
|
|
127
|
-
]);
|
|
126
|
+
], { returnScene: false });
|
|
128
127
|
}
|
|
129
128
|
export function createSdkGeometryHost(options) {
|
|
130
129
|
const editor = options.editor;
|
|
@@ -134,17 +133,7 @@ export function createSdkGeometryHost(options) {
|
|
|
134
133
|
read: (objectId, invocation) => editor.read(invocation, (api) => readTarget(api, objectId)),
|
|
135
134
|
write: async (input, invocation) => {
|
|
136
135
|
invocation.signal.throwIfAborted();
|
|
137
|
-
const
|
|
138
|
-
const current = resolveTarget(api, input.objectId);
|
|
139
|
-
return {
|
|
140
|
-
target: await readTarget(api, input.objectId, current),
|
|
141
|
-
current,
|
|
142
|
-
graphExt: current.graphId === undefined
|
|
143
|
-
? undefined
|
|
144
|
-
: api.getSceneExt().graphsExt?.[current.graphId],
|
|
145
|
-
};
|
|
146
|
-
});
|
|
147
|
-
const { target } = inspected;
|
|
136
|
+
const target = await editor.read(invocation, api => readTarget(api, input.objectId));
|
|
148
137
|
const pendingKey = target.graphId === undefined
|
|
149
138
|
? `object:${input.objectId}`
|
|
150
139
|
: `graph:${target.graphId}`;
|
|
@@ -152,8 +141,6 @@ export function createSdkGeometryHost(options) {
|
|
|
152
141
|
throw new Error("This Geometry Nodes target is still being updated. Await its current update first.");
|
|
153
142
|
pendingTargets.add(pendingKey);
|
|
154
143
|
try {
|
|
155
|
-
if (target.revision !== input.expectedRevision)
|
|
156
|
-
throw new Error("The scene changed. Read it again before editing.");
|
|
157
144
|
const repairingGeometry = target.graphId !== undefined &&
|
|
158
145
|
(await repairs.isPending(String(target.graphId)));
|
|
159
146
|
if (target.graphId !== undefined)
|
|
@@ -164,39 +151,13 @@ export function createSdkGeometryHost(options) {
|
|
|
164
151
|
graphId: target.graphId,
|
|
165
152
|
instanceId: target.instanceId,
|
|
166
153
|
affectedObjectIds: [],
|
|
167
|
-
revision: target.revision,
|
|
168
154
|
diagnostics: boundedDiagnostics(diagnostics),
|
|
169
155
|
});
|
|
170
156
|
let compiled;
|
|
171
|
-
let nextExt;
|
|
172
|
-
const updates = new Map();
|
|
173
157
|
const diagnostics = [];
|
|
174
158
|
try {
|
|
175
159
|
compiled = compileGeometry(input.source);
|
|
176
160
|
readEditableGeometry(compiled.graph, compiled.graphExt);
|
|
177
|
-
if (target.graphId === undefined) {
|
|
178
|
-
nextExt = migrateGraphExt({ nodes: {} }, {}, compiled);
|
|
179
|
-
}
|
|
180
|
-
else {
|
|
181
|
-
const { current } = inspected;
|
|
182
|
-
if (current.graph === undefined)
|
|
183
|
-
throw new Error("The Geometry Nodes graph is missing.");
|
|
184
|
-
const ext = graphExtSchema.parse(inspected.graphExt ?? {});
|
|
185
|
-
const previous = readEditableGeometry(current.graph, ext);
|
|
186
|
-
nextExt = migrateGraphExt(ext, previous.nodeIds, compiled);
|
|
187
|
-
for (const [id, instance] of Object.entries(current.scripts.instances)) {
|
|
188
|
-
if (instance.graph_id !== target.graphId)
|
|
189
|
-
continue;
|
|
190
|
-
const old = readParameterOverrides(previous.graph, ext, instance.data ?? {});
|
|
191
|
-
const migrated = migrateParameterOverrides(previous.graph, ext, compiled.graph, nextExt, old.data);
|
|
192
|
-
updates.set(id, {
|
|
193
|
-
graph_id: target.graphId,
|
|
194
|
-
data: migrated.data,
|
|
195
|
-
});
|
|
196
|
-
if (migrated.removed.length)
|
|
197
|
-
diagnostics.push(`Instance ${id}: removed or type-changed overrides ${migrated.removed.join(", ")} now use graph defaults where present.`);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
161
|
}
|
|
201
162
|
catch (error) {
|
|
202
163
|
return rejected([errorMessage(error)]);
|
|
@@ -209,7 +170,7 @@ export function createSdkGeometryHost(options) {
|
|
|
209
170
|
let operationId;
|
|
210
171
|
let applied;
|
|
211
172
|
try {
|
|
212
|
-
applied = await editor.
|
|
173
|
+
applied = await editor.access({
|
|
213
174
|
capability: "geometry",
|
|
214
175
|
operation: "write",
|
|
215
176
|
target: () => receiptTarget,
|
|
@@ -220,6 +181,36 @@ export function createSdkGeometryHost(options) {
|
|
|
220
181
|
throw new Error("Scene memory limits prevent applying this graph.");
|
|
221
182
|
if (current.graphId !== target.graphId)
|
|
222
183
|
throw new Error("The Geometry Nodes target changed. Read it again.");
|
|
184
|
+
let nextExt;
|
|
185
|
+
const updates = new Map();
|
|
186
|
+
try {
|
|
187
|
+
if (target.graphId === undefined) {
|
|
188
|
+
nextExt = migrateGraphExt({ nodes: {} }, {}, compiled);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
if (current.graph === undefined)
|
|
192
|
+
throw new Error("The Geometry Nodes graph is missing.");
|
|
193
|
+
const graphExt = draft.getSceneExt(["graphsExt"]).graphsExt;
|
|
194
|
+
const ext = graphExtSchema.parse(graphExt?.[target.graphId] ?? {});
|
|
195
|
+
const previous = readEditableGeometry(current.graph, ext);
|
|
196
|
+
nextExt = migrateGraphExt(ext, previous.nodeIds, compiled);
|
|
197
|
+
for (const [id, instance] of Object.entries(current.scripts.instances)) {
|
|
198
|
+
if (instance.graph_id !== target.graphId)
|
|
199
|
+
continue;
|
|
200
|
+
const old = readParameterOverrides(previous.graph, ext, instance.data ?? {});
|
|
201
|
+
const migrated = migrateParameterOverrides(previous.graph, ext, compiled.graph, nextExt, old.data);
|
|
202
|
+
updates.set(id, {
|
|
203
|
+
graph_id: target.graphId,
|
|
204
|
+
data: migrated.data,
|
|
205
|
+
});
|
|
206
|
+
if (migrated.removed.length)
|
|
207
|
+
diagnostics.push(`Instance ${id}: removed or type-changed overrides ${migrated.removed.join(", ")} now use graph defaults where present.`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
return { rejected: [errorMessage(error)] };
|
|
213
|
+
}
|
|
223
214
|
if (current.graphId !== undefined)
|
|
224
215
|
await repairs.reserve(String(current.graphId), id);
|
|
225
216
|
let objectId = input.objectId;
|
|
@@ -247,34 +238,25 @@ export function createSdkGeometryHost(options) {
|
|
|
247
238
|
if (!ext.graphsExt)
|
|
248
239
|
draft.patchSceneExt([
|
|
249
240
|
{ op: "add", path: "/graphsExt", value: {} },
|
|
250
|
-
]);
|
|
241
|
+
], { returnScene: false });
|
|
251
242
|
draft.patchSceneExt([
|
|
252
243
|
{
|
|
253
244
|
op: "add",
|
|
254
245
|
path: `/graphsExt/${graphId}`,
|
|
255
246
|
value: nextExt,
|
|
256
247
|
},
|
|
257
|
-
]);
|
|
248
|
+
], { returnScene: false });
|
|
258
249
|
const affectedObjectIds = Object.entries(scriptStorage(draft).instances)
|
|
259
250
|
.filter(([, instance]) => instance.graph_id === graphId)
|
|
260
251
|
.flatMap(([id]) => objectIdsFor(draft, Number(id)));
|
|
261
252
|
Object.assign(receiptTarget, { objectId, graphId, instanceId });
|
|
262
253
|
return { objectId, graphId, instanceId, affectedObjectIds };
|
|
263
|
-
},
|
|
264
|
-
const
|
|
265
|
-
if (
|
|
254
|
+
}, invocation, async (operation) => {
|
|
255
|
+
const graphId = receiptTarget.graphId;
|
|
256
|
+
if (graphId === undefined)
|
|
266
257
|
return;
|
|
267
|
-
const
|
|
268
|
-
.filter(([, instance]) => instance.graph_id === committed.graphId)
|
|
269
|
-
.flatMap(([id]) => objectIdsFor(api, Number(id)));
|
|
270
|
-
const areaIds = [
|
|
271
|
-
...new Set(affectedObjectIds.map((id) => getSdkSceneAreaId(api, id))),
|
|
272
|
-
];
|
|
273
|
-
const sceneHealth = await readStableSdkSceneHealth(api, areaIds, invocation.signal);
|
|
274
|
-
options.onHealth?.(sceneHealth);
|
|
275
|
-
const recorded = await repairs.record(String(committed.graphId), _operation.id, sceneHealth);
|
|
258
|
+
const recorded = await repairs.record(String(graphId), operation.id, unverifiedSceneHealth([], "Health was not requested."));
|
|
276
259
|
return {
|
|
277
|
-
sceneHealth,
|
|
278
260
|
geometryRepairAttempts: recorded.repairAttempts,
|
|
279
261
|
geometryRepairLimit: recorded.repairLimit,
|
|
280
262
|
};
|
|
@@ -288,6 +270,8 @@ export function createSdkGeometryHost(options) {
|
|
|
288
270
|
}
|
|
289
271
|
throw error;
|
|
290
272
|
}
|
|
273
|
+
if (applied.value.rejected)
|
|
274
|
+
return rejected(applied.value.rejected);
|
|
291
275
|
invocation.signal.throwIfAborted();
|
|
292
276
|
try {
|
|
293
277
|
await options.onSelect?.(applied.value.objectId);
|
|
@@ -296,7 +280,6 @@ export function createSdkGeometryHost(options) {
|
|
|
296
280
|
return {
|
|
297
281
|
status: "applied",
|
|
298
282
|
...applied.value,
|
|
299
|
-
revision: applied.operation.resultRevision ?? input.expectedRevision,
|
|
300
283
|
diagnostics: boundedDiagnostics(diagnostics),
|
|
301
284
|
};
|
|
302
285
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { getOpenScadGroundedPosition, MAX_PRINTABILITY_REPAIR_ATTEMPTS,
|
|
3
|
-
import { waitForOpenScadRenderer } from "./sdk-openscad-renderer.js";
|
|
2
|
+
import { getOpenScadGroundedPosition, MAX_PRINTABILITY_REPAIR_ATTEMPTS, requestTurnOpenScadPrintability, toOpenScadCompileResponse, } from "./openscad.js";
|
|
4
3
|
import { createGeometryRepairTracker, createRepairTracker, } from "./repair-state.js";
|
|
5
|
-
import { getSdkSceneAreaId,
|
|
4
|
+
import { getSdkSceneAreaId, unverifiedSceneHealth, } from "./scene-health.js";
|
|
6
5
|
const compileMetadata = z.object({
|
|
7
6
|
error: z.string(),
|
|
8
7
|
params: z.array(z.object({ name: z.string() })),
|
|
@@ -19,7 +18,6 @@ export function createSdkOpenScadHost(options) {
|
|
|
19
18
|
const printabilityRepairs = createRepairTracker(options.repairs, "openscad.printability", MAX_PRINTABILITY_REPAIR_ATTEMPTS, (status) => status === "unavailable" ? undefined : status === "failed");
|
|
20
19
|
const pendingTargets = new Set();
|
|
21
20
|
const resolveTarget = async (api, objectId) => {
|
|
22
|
-
const revision = await api.getSceneRevision();
|
|
23
21
|
const node = objectId === 0 ? undefined : api.getNode(objectId);
|
|
24
22
|
if (objectId === 0 || node?.kind === "union" || node?.kind === "group") {
|
|
25
23
|
return {
|
|
@@ -30,18 +28,12 @@ export function createSdkOpenScadHost(options) {
|
|
|
30
28
|
title: node?.name ?? "",
|
|
31
29
|
source: null,
|
|
32
30
|
params: {},
|
|
33
|
-
revision,
|
|
34
31
|
},
|
|
35
32
|
};
|
|
36
33
|
}
|
|
37
34
|
if (node?.kind !== "openScad")
|
|
38
35
|
throw new Error("The target must be an OpenSCAD part or an Area/Group.");
|
|
39
36
|
const script = api.getOpenScadScript(node.scriptId);
|
|
40
|
-
let rendererBounds;
|
|
41
|
-
try {
|
|
42
|
-
rendererBounds = await api.getNodeBoundingBox(objectId);
|
|
43
|
-
}
|
|
44
|
-
catch { }
|
|
45
37
|
return {
|
|
46
38
|
target: {
|
|
47
39
|
kind: "part",
|
|
@@ -50,10 +42,8 @@ export function createSdkOpenScadHost(options) {
|
|
|
50
42
|
source: script.source,
|
|
51
43
|
title: script.name || node.name || "OpenSCAD part",
|
|
52
44
|
params: structuredClone(node.params ?? {}),
|
|
53
|
-
revision,
|
|
54
45
|
},
|
|
55
46
|
repairKey: `script:${node.scriptId}`,
|
|
56
|
-
rendererBounds,
|
|
57
47
|
};
|
|
58
48
|
};
|
|
59
49
|
const write = async (source, targetId, creating, invocation) => {
|
|
@@ -68,8 +58,6 @@ export function createSdkOpenScadHost(options) {
|
|
|
68
58
|
let operationId = "";
|
|
69
59
|
let operationRepairKey = resolved.repairKey;
|
|
70
60
|
try {
|
|
71
|
-
if (target.revision !== source.expectedRevision)
|
|
72
|
-
throw new Error("The scene changed. Read it again before editing.");
|
|
73
61
|
if (target.kind !== (creating ? "container" : "part"))
|
|
74
62
|
throw new Error(creating
|
|
75
63
|
? "Creation requires a parent container ID."
|
|
@@ -85,63 +73,60 @@ export function createSdkOpenScadHost(options) {
|
|
|
85
73
|
}
|
|
86
74
|
if (creating && options.canGrow?.() === false)
|
|
87
75
|
throw new Error("OpenSCAD creation is disabled because scene memory is low.");
|
|
88
|
-
const params = z
|
|
89
|
-
.record(z.string(), z.json())
|
|
90
|
-
.parse({ ...target.params, ...source.params });
|
|
91
|
-
invocation.reportStatus("running", "Compiling the parametric part...");
|
|
92
|
-
let rawValidation;
|
|
93
|
-
try {
|
|
94
|
-
rawValidation = await editor.read(invocation, (api) => api.validateOpenScad(source.source, params));
|
|
95
|
-
}
|
|
96
|
-
catch {
|
|
97
|
-
invocation.signal.throwIfAborted();
|
|
98
|
-
}
|
|
99
|
-
const response = toOpenScadCompileResponse(rawValidation);
|
|
100
|
-
const metadataAvailable = compileMetadata.safeParse(rawValidation).success;
|
|
101
|
-
const validation = response && (response.error || metadataAvailable)
|
|
102
|
-
? {
|
|
103
|
-
status: response.error ? "invalid" : "valid",
|
|
104
|
-
error: response.error || undefined,
|
|
105
|
-
warnings: response.warnings,
|
|
106
|
-
}
|
|
107
|
-
: {
|
|
108
|
-
status: "unavailable",
|
|
109
|
-
warnings: [
|
|
110
|
-
"OpenSCAD validation is unavailable; compilation and parameter validity could not be confirmed.",
|
|
111
|
-
],
|
|
112
|
-
};
|
|
113
|
-
if (validation.status === "invalid" ||
|
|
114
|
-
(creating &&
|
|
115
|
-
(validation.status !== "valid" ||
|
|
116
|
-
response?.boundingBox === undefined))) {
|
|
117
|
-
if (creating && response?.boundingBox === undefined)
|
|
118
|
-
validation.warnings.push("The part was not added because validated placement bounds are unavailable.");
|
|
119
|
-
return {
|
|
120
|
-
status: "rejected",
|
|
121
|
-
objectId,
|
|
122
|
-
revision: target.revision,
|
|
123
|
-
validation,
|
|
124
|
-
repairAttempts: 0,
|
|
125
|
-
repairLimit: MAX_PRINTABILITY_REPAIR_ATTEMPTS,
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
const validNames = new Set(response && metadataAvailable
|
|
129
|
-
? response.params.map((param) => param.name)
|
|
130
|
-
: Object.keys(params));
|
|
131
|
-
const nextParams = Object.fromEntries(Object.entries(params).filter(([name]) => validNames.has(name)));
|
|
132
76
|
const receiptTarget = {
|
|
133
77
|
parentId: target.parentId,
|
|
134
78
|
};
|
|
135
79
|
if (objectId !== undefined)
|
|
136
80
|
receiptTarget.objectId = objectId;
|
|
137
81
|
let affectedObjectIds = [];
|
|
138
|
-
let
|
|
139
|
-
const applied = await editor.
|
|
82
|
+
let affectedAreaIds = [];
|
|
83
|
+
const applied = await editor.access({
|
|
140
84
|
capability: "openscad",
|
|
141
85
|
operation: creating ? "create" : "update",
|
|
142
86
|
target: () => receiptTarget,
|
|
143
|
-
}, async (
|
|
87
|
+
}, async (api, draft, id) => {
|
|
144
88
|
operationId = id;
|
|
89
|
+
const current = draft.getNode(targetId);
|
|
90
|
+
if (creating === false && (current.kind !== "openScad" || `script:${current.scriptId}` !== resolved.repairKey))
|
|
91
|
+
throw new Error("The OpenSCAD target changed. Read it again.");
|
|
92
|
+
const currentParams = current.kind === "openScad" ? current.params : {};
|
|
93
|
+
const params = z
|
|
94
|
+
.record(z.string(), z.json())
|
|
95
|
+
.parse({ ...currentParams, ...source.params });
|
|
96
|
+
invocation.reportStatus("running", "Compiling the parametric part...");
|
|
97
|
+
let rawValidation;
|
|
98
|
+
try {
|
|
99
|
+
rawValidation = await api.validateOpenScad(source.source, params);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
invocation.signal.throwIfAborted();
|
|
103
|
+
}
|
|
104
|
+
const response = toOpenScadCompileResponse(rawValidation);
|
|
105
|
+
const metadataAvailable = compileMetadata.safeParse(rawValidation).success;
|
|
106
|
+
const validation = response && (response.error || metadataAvailable)
|
|
107
|
+
? {
|
|
108
|
+
status: response.error ? "invalid" : "valid",
|
|
109
|
+
...(response.error ? { error: response.error } : {}),
|
|
110
|
+
warnings: response.warnings,
|
|
111
|
+
}
|
|
112
|
+
: {
|
|
113
|
+
status: "unavailable",
|
|
114
|
+
warnings: [
|
|
115
|
+
"OpenSCAD validation is unavailable; compilation and parameter validity could not be confirmed.",
|
|
116
|
+
],
|
|
117
|
+
};
|
|
118
|
+
if (validation.status === "invalid" ||
|
|
119
|
+
(creating &&
|
|
120
|
+
(validation.status !== "valid" ||
|
|
121
|
+
response?.boundingBox === undefined))) {
|
|
122
|
+
if (creating && response?.boundingBox === undefined)
|
|
123
|
+
validation.warnings.push("The part was not added because validated placement bounds are unavailable.");
|
|
124
|
+
return { rejected: validation };
|
|
125
|
+
}
|
|
126
|
+
const validNames = new Set(response && metadataAvailable
|
|
127
|
+
? response.params.map((param) => param.name)
|
|
128
|
+
: Object.keys(params));
|
|
129
|
+
const nextParams = Object.fromEntries(Object.entries(params).filter(([name]) => validNames.has(name)));
|
|
145
130
|
if (creating === false) {
|
|
146
131
|
if (resolved.repairKey === undefined)
|
|
147
132
|
throw new Error("The OpenSCAD repair target is unavailable.");
|
|
@@ -158,7 +143,8 @@ export function createSdkOpenScadHost(options) {
|
|
|
158
143
|
});
|
|
159
144
|
draft.setNodeName(targetId, source.title);
|
|
160
145
|
draft.setOpenScadScript(targetId, node.scriptId, nextParams);
|
|
161
|
-
|
|
146
|
+
affectedAreaIds = [...new Set(affectedObjectIds.map(id => Number(getSdkSceneAreaId(draft, id))))];
|
|
147
|
+
return { objectId: targetId, repairKey: resolved.repairKey, validation };
|
|
162
148
|
}
|
|
163
149
|
if (options.canGrow?.() === false)
|
|
164
150
|
throw new Error("OpenSCAD creation is disabled because scene memory is low.");
|
|
@@ -186,69 +172,46 @@ export function createSdkOpenScadHost(options) {
|
|
|
186
172
|
objectId = node.id;
|
|
187
173
|
affectedObjectIds = [node.id];
|
|
188
174
|
Object.assign(receiptTarget, { objectId: node.id, parentId });
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
return;
|
|
193
|
-
const areaIds = [
|
|
194
|
-
...new Set(affectedObjectIds.map((id) => getSdkSceneAreaId(api, id))),
|
|
195
|
-
];
|
|
196
|
-
try {
|
|
197
|
-
const rendererReady = response?.boundingBox === undefined ||
|
|
198
|
-
(creating === false && resolved.rendererBounds === undefined)
|
|
199
|
-
? false
|
|
200
|
-
: await waitForOpenScadRenderer({
|
|
201
|
-
api,
|
|
202
|
-
objectId,
|
|
203
|
-
expectedBounds: response.boundingBox,
|
|
204
|
-
baselineBounds: resolved.rendererBounds,
|
|
205
|
-
signal: invocation.signal,
|
|
206
|
-
});
|
|
207
|
-
sceneHealth = rendererReady
|
|
208
|
-
? await readStableSdkSceneHealth(api, areaIds, invocation.signal)
|
|
209
|
-
: unverifiedSceneHealth(areaIds, "The renderer did not confirm the committed OpenSCAD geometry before health checks.");
|
|
210
|
-
}
|
|
211
|
-
catch (error) {
|
|
212
|
-
sceneHealth = unverifiedSceneHealth(areaIds, `OpenSCAD renderer checks stopped after the edit committed: ${String(error)}`);
|
|
213
|
-
}
|
|
214
|
-
options.onHealth?.(sceneHealth);
|
|
175
|
+
affectedAreaIds = [Number(getSdkSceneAreaId(draft, node.id))];
|
|
176
|
+
return { objectId: node.id, repairKey: operationRepairKey, validation };
|
|
177
|
+
}, invocation, async (operation) => {
|
|
215
178
|
if (operationRepairKey === undefined)
|
|
216
179
|
return;
|
|
217
|
-
const recorded = await geometryRepairs.record(operationRepairKey, operation.id,
|
|
180
|
+
const recorded = await geometryRepairs.record(operationRepairKey, operation.id, unverifiedSceneHealth([], "Health was not requested."));
|
|
218
181
|
return {
|
|
219
|
-
sceneHealth,
|
|
220
182
|
affectedObjectIds,
|
|
221
183
|
geometryRepairAttempts: recorded.repairAttempts,
|
|
222
184
|
geometryRepairLimit: recorded.repairLimit,
|
|
223
185
|
};
|
|
224
186
|
});
|
|
187
|
+
if (applied.value.rejected)
|
|
188
|
+
return {
|
|
189
|
+
status: "rejected", objectId, validation: applied.value.rejected,
|
|
190
|
+
repairAttempts: 0, repairLimit: MAX_PRINTABILITY_REPAIR_ATTEMPTS,
|
|
191
|
+
};
|
|
192
|
+
const { validation } = applied.value;
|
|
225
193
|
objectId = applied.value.objectId;
|
|
226
194
|
try {
|
|
227
195
|
await options.onSelect?.(objectId);
|
|
228
196
|
}
|
|
229
197
|
catch { }
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
await printabilityRepairs.refund(applied.value.repairKey, operationId);
|
|
198
|
+
// Geometry repairs do not consume the independent printability budget.
|
|
199
|
+
// Explicit health must resolve the known geometry failure first.
|
|
200
|
+
if (repairingGeometry) {
|
|
234
201
|
return {
|
|
235
202
|
status: "applied",
|
|
236
203
|
objectId,
|
|
237
|
-
revision,
|
|
238
204
|
validation,
|
|
239
205
|
repairAttempts: 0,
|
|
240
206
|
repairLimit: MAX_PRINTABILITY_REPAIR_ATTEMPTS,
|
|
241
207
|
};
|
|
242
208
|
}
|
|
243
209
|
invocation.reportStatus("running", "Checking wall thickness...");
|
|
244
|
-
const printability = await editor.
|
|
210
|
+
const printability = await editor.withConnection(invocation, (api, beginSettlement) => (options.requestPrintability ?? requestTurnOpenScadPrintability)({
|
|
245
211
|
api,
|
|
246
|
-
expectedRevision: revision,
|
|
247
212
|
signal: invocation.signal,
|
|
248
213
|
onSubmissionStarted: beginSettlement,
|
|
249
|
-
objectIds:
|
|
250
|
-
...new Set(affectedObjectIds.map((id) => Number(getSdkSceneAreaId(api, id)))),
|
|
251
|
-
],
|
|
214
|
+
objectIds: affectedAreaIds,
|
|
252
215
|
}));
|
|
253
216
|
invocation.signal.throwIfAborted();
|
|
254
217
|
if (printability.reason === "context_busy") {
|
|
@@ -256,7 +219,6 @@ export function createSdkOpenScadHost(options) {
|
|
|
256
219
|
return {
|
|
257
220
|
status: "applied",
|
|
258
221
|
objectId,
|
|
259
|
-
revision,
|
|
260
222
|
validation,
|
|
261
223
|
printability,
|
|
262
224
|
repairAttempts: refunded.repairAttempts,
|
|
@@ -267,7 +229,6 @@ export function createSdkOpenScadHost(options) {
|
|
|
267
229
|
return {
|
|
268
230
|
status: "applied",
|
|
269
231
|
objectId,
|
|
270
|
-
revision,
|
|
271
232
|
validation,
|
|
272
233
|
printability,
|
|
273
234
|
repairAttempts: recorded.repairAttempts,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SceneHealthHost } from "./capabilities.js";
|
|
2
2
|
import { type SdkSceneEditor } from "./sdk-scene-host.js";
|
|
3
|
+
import { type RepairStateStore } from "./repair-state.js";
|
|
3
4
|
import type { SceneHealth } from "./scene-health.js";
|
|
4
|
-
export declare function createSdkSceneHealthHost(editor: SdkSceneEditor, onHealth?: (health: SceneHealth) => void): SceneHealthHost;
|
|
5
|
+
export declare function createSdkSceneHealthHost(editor: SdkSceneEditor, onHealth?: (health: SceneHealth) => void, repairs?: RepairStateStore): SceneHealthHost;
|