@womp/spark-agent 4.4.1 → 5.0.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/.env.example +6 -0
- package/README.md +21 -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 +4 -11
- package/dist/capabilities/scene.js +38 -55
- 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 +2 -2
- package/dist/sdk-scene-screenshot.js +3 -16
- 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
|
@@ -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;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { getSdkSceneAreaId, getSdkSceneAreaIds,
|
|
1
|
+
import { getSdkSceneAreaId, getSdkSceneAreaIds, readSdkSceneHealth, parseSceneHealth, } from "./scene-health.js";
|
|
2
|
+
import { createGeometryRepairTracker } from "./repair-state.js";
|
|
2
3
|
/**
|
|
3
4
|
* The Area that owns `objectId`, or undefined when the node is in no Area --
|
|
4
5
|
* the scene root 0, a root-level Group, a Light. The engine's thermometer has a
|
|
@@ -9,25 +10,74 @@ function owningAreaId(api, objectId) {
|
|
|
9
10
|
const areaId = getSdkSceneAreaId(api, objectId);
|
|
10
11
|
return api.getNode(Number(areaId)).kind === "union" ? areaId : undefined;
|
|
11
12
|
}
|
|
12
|
-
|
|
13
|
+
// Health is requested explicitly. Feed its checked Areas back to the same
|
|
14
|
+
// target budgets used by writes, without spending an attempt or settling an
|
|
15
|
+
// operation. A partial reading must not clear a linked target's other Areas.
|
|
16
|
+
async function observeRepairs(api, health, store) {
|
|
17
|
+
if (health.thermometer === undefined)
|
|
18
|
+
return;
|
|
19
|
+
const targets = new Map();
|
|
20
|
+
const add = (namespace, target, objectId) => {
|
|
21
|
+
const area = owningAreaId(api, objectId);
|
|
22
|
+
if (area === undefined)
|
|
23
|
+
return;
|
|
24
|
+
const key = `${namespace}:${target}`;
|
|
25
|
+
const entry = targets.get(key) ?? { namespace, target, areas: new Set() };
|
|
26
|
+
entry.areas.add(area);
|
|
27
|
+
targets.set(key, entry);
|
|
28
|
+
};
|
|
29
|
+
const unions = api.listNodes({ kind: "union" });
|
|
30
|
+
const scripts = api.getSceneStorage().scripts;
|
|
31
|
+
const instances = scripts && typeof scripts === "object" && !Array.isArray(scripts)
|
|
32
|
+
? scripts.instances : undefined;
|
|
33
|
+
for (const node of unions) {
|
|
34
|
+
if (node.kind !== "union" || node.scriptInstanceId === undefined)
|
|
35
|
+
continue;
|
|
36
|
+
const instance = instances && typeof instances === "object" && !Array.isArray(instances)
|
|
37
|
+
? instances[node.scriptInstanceId] : undefined;
|
|
38
|
+
if (instance && typeof instance === "object" && !Array.isArray(instance)
|
|
39
|
+
&& typeof instance.graph_id === "number") {
|
|
40
|
+
add("geometry", String(instance.graph_id), node.id);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
for (const node of api.listNodes({ kind: "openScad" })) {
|
|
44
|
+
if (node.kind === "openScad")
|
|
45
|
+
add("openscad.geometry", `script:${node.scriptId}`, node.id);
|
|
46
|
+
}
|
|
47
|
+
const checked = new Set(health.affectedAreaIds);
|
|
48
|
+
for (const { namespace, target, areas } of targets.values()) {
|
|
49
|
+
if ([...areas].some((id) => !checked.has(id)))
|
|
50
|
+
continue;
|
|
51
|
+
const scoped = parseSceneHealth(health.thermometer, [...areas]);
|
|
52
|
+
// A zero-count Area does not prove that its output is healthy, even if
|
|
53
|
+
// another Area in the full thermometer has an overflow flag.
|
|
54
|
+
if (scoped.emptyAreaIds?.some((id) => areas.has(id)))
|
|
55
|
+
continue;
|
|
56
|
+
await createGeometryRepairTracker(store, namespace).observe(target, scoped);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function createSdkSceneHealthHost(editor, onHealth, repairs) {
|
|
13
60
|
return {
|
|
14
61
|
read: (objectIds, context) => editor.read(context, async (api) => {
|
|
15
62
|
const owned = objectIds
|
|
16
63
|
.map((id) => ({ id, areaId: owningAreaId(api, id) }))
|
|
17
64
|
.filter((entry) => entry.areaId !== undefined);
|
|
18
|
-
// `scene.
|
|
65
|
+
// `scene.access()` publishes node 0, and ids outside any Area are legal
|
|
19
66
|
// arguments. They narrow nothing, so they are reported rather than
|
|
20
67
|
// allowed to make the whole reading unverified; with nothing left to
|
|
21
68
|
// narrow by, this is the whole-scene read `health()` already means.
|
|
22
69
|
const outside = objectIds.filter((id) => owned.some((entry) => entry.id === id) === false);
|
|
23
70
|
const areaIds = owned.length > 0 ? owned.map((entry) => entry.areaId) : getSdkSceneAreaIds(api);
|
|
24
|
-
const health = await
|
|
71
|
+
const health = await readSdkSceneHealth(api, [...new Set(areaIds)], context.signal);
|
|
25
72
|
const note = outside.length === 0
|
|
26
73
|
? undefined
|
|
27
74
|
: `${outside.join(", ")} ${outside.length === 1 ? "is" : "are"} in no Area, and the engine reports health per Area, so ${owned.length > 0 ? "health is reported for the remaining ids" : "this is the whole scene's health"}.`;
|
|
28
75
|
const reported = note === undefined
|
|
29
76
|
? health
|
|
30
77
|
: { ...health, reason: health.reason === undefined ? note : `${health.reason} ${note}` };
|
|
78
|
+
context.signal.throwIfAborted();
|
|
79
|
+
if (repairs)
|
|
80
|
+
await observeRepairs(api, reported, repairs);
|
|
31
81
|
onHealth?.(reported);
|
|
32
82
|
return reported;
|
|
33
83
|
}),
|
package/dist/sdk-scene-host.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FontFamily, KakapoAPI, SceneEdit, MaterialResource } from "@womp/kakapo-sdk";
|
|
1
|
+
import type { FontFamily, KakapoAPI, SceneEdit, MaterialResource, NativeSceneSnapshot } from "@womp/kakapo-sdk";
|
|
2
2
|
import type { ArtifactRef, SparkInvocationContext } from "./capabilities.js";
|
|
3
3
|
import type { SceneHost, SceneHealthHost, SceneScreenshotOptions } from "./capabilities/scene.js";
|
|
4
4
|
import { type SceneOperation, type SceneOperationStore } from "./scene-operations.js";
|
|
@@ -24,10 +24,10 @@ export type SdkSceneEditorOptions = {
|
|
|
24
24
|
/** Supply this when the host adds a stricter connection lifetime. */
|
|
25
25
|
context?: SparkInvocationContext) => Promise<T>): Promise<T>;
|
|
26
26
|
assertEditable(context: SparkInvocationContext): void | Promise<void>;
|
|
27
|
-
/**
|
|
28
|
-
|
|
27
|
+
/** Suspend for the complete access, including reads and failed preparation. */
|
|
28
|
+
acquireAccess?(context: SparkInvocationContext): Promise<(synchronized: boolean) => void | Promise<void>>;
|
|
29
29
|
/** Must adopt each operation ID at most once, including during recovery. */
|
|
30
|
-
onCommitted?(operation: SceneOperation, api: KakapoAPI, context: SparkInvocationContext, identity: SdkSceneEditIdentity): Promise<Record<string, unknown> | void>;
|
|
30
|
+
onCommitted?(operation: SceneOperation, api: KakapoAPI, context: SparkInvocationContext, identity: SdkSceneEditIdentity, scene: NativeSceneSnapshot): Promise<Record<string, unknown> | void>;
|
|
31
31
|
};
|
|
32
32
|
export type SdkSceneEditIdentity = {
|
|
33
33
|
capability: string;
|
|
@@ -36,8 +36,9 @@ export type SdkSceneEditIdentity = {
|
|
|
36
36
|
};
|
|
37
37
|
export type SdkSceneEditor = {
|
|
38
38
|
getOperation(operationId: string): Promise<SceneOperation | undefined>;
|
|
39
|
+
withConnection<T>(context: SparkInvocationContext, action: (api: KakapoAPI, beginSettlement: () => void) => T | Promise<T>): Promise<T>;
|
|
39
40
|
read<T>(context: SparkInvocationContext, action: (api: KakapoAPI, beginSettlement: () => void) => T | Promise<T>): Promise<T>;
|
|
40
|
-
|
|
41
|
+
access<T>(identity: SdkSceneEditIdentity, callback: (api: KakapoAPI, draft: SceneEdit, operationId: string) => T | Promise<T>, context: SparkInvocationContext, afterCommitted?: (operation: SceneOperation, api: KakapoAPI) => Promise<Record<string, unknown> | void>): Promise<{
|
|
41
42
|
value: T;
|
|
42
43
|
operation: SceneOperation;
|
|
43
44
|
}>;
|
|
@@ -308,6 +309,20 @@ export declare function createSdkSceneDraft(api: KakapoAPI, scene: SceneEdit, se
|
|
|
308
309
|
g: number;
|
|
309
310
|
b: number;
|
|
310
311
|
}) => void;
|
|
312
|
+
unitMultiplier: number;
|
|
313
|
+
nodes(options?: {
|
|
314
|
+
ids?: number[];
|
|
315
|
+
type?: number;
|
|
316
|
+
parentId?: number;
|
|
317
|
+
name?: string;
|
|
318
|
+
offset?: number;
|
|
319
|
+
limit?: number;
|
|
320
|
+
}): {
|
|
321
|
+
id: number;
|
|
322
|
+
name: string;
|
|
323
|
+
type: number;
|
|
324
|
+
parentId: number;
|
|
325
|
+
}[];
|
|
311
326
|
};
|
|
312
327
|
export declare function createSdkSceneEditor(options: SdkSceneEditorOptions): SdkSceneEditor;
|
|
313
328
|
export declare function createSdkSceneHost(options: SdkSceneEditorOptions & {
|
package/dist/sdk-scene-host.js
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import { createWompSceneApi
|
|
2
|
-
import { sceneUnitMultiplier } from "./coordinates.js";
|
|
1
|
+
import { createWompSceneApi } from "./womp-scene-api.js";
|
|
3
2
|
import { executeSceneOperation, } from "./scene-operations.js";
|
|
4
3
|
import { readMaterialMetadata, writeMaterialMetadata, } from "./scene-metadata.js";
|
|
5
4
|
import { boundedReceiptDetails } from "./scene-health.js";
|
|
5
|
+
function assertSceneSaved(operation) {
|
|
6
|
+
if (!operation.changed)
|
|
7
|
+
return;
|
|
8
|
+
if (operation.save?.status === "failed")
|
|
9
|
+
throw new Error(`The scene edit was applied, but the save failed: ${operation.save.error} Do not repeat the edit.`);
|
|
10
|
+
if (operation.save?.status === "unknown" || operation.save?.status === "requested")
|
|
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
|
+
}
|
|
6
13
|
export function createSdkSceneDraft(api, scene, services) {
|
|
7
14
|
let defaultMaterialId = scene
|
|
8
15
|
.listMaterials()
|
|
@@ -59,13 +66,17 @@ export function createSdkSceneEditor(options) {
|
|
|
59
66
|
const ordinals = new WeakMap();
|
|
60
67
|
return {
|
|
61
68
|
getOperation: (operationId) => options.operations.getSceneOperation(options.targetKey, operationId),
|
|
69
|
+
withConnection: (context, action) => options.withConnection(context, async (api, settle, active = context) => {
|
|
70
|
+
active.signal.throwIfAborted();
|
|
71
|
+
return action(api, settle);
|
|
72
|
+
}),
|
|
62
73
|
read: (context, action) => options.withConnection(context, async (api, beginSettlement, activeContext = context) => {
|
|
63
74
|
activeContext.signal.throwIfAborted();
|
|
64
|
-
await api.refreshScene();
|
|
75
|
+
await api.refreshScene({ returnScene: false });
|
|
65
76
|
activeContext.signal.throwIfAborted();
|
|
66
77
|
return action(api, beginSettlement);
|
|
67
78
|
}),
|
|
68
|
-
|
|
79
|
+
access: (identity, callback, context, afterCommitted) => {
|
|
69
80
|
const operationKey = `${identity.capability}.${identity.operation}`;
|
|
70
81
|
const contextOrdinals = ordinals.get(context) ?? new Map();
|
|
71
82
|
ordinals.set(context, contextOrdinals);
|
|
@@ -89,6 +100,8 @@ export function createSdkSceneEditor(options) {
|
|
|
89
100
|
});
|
|
90
101
|
let releaseCommit;
|
|
91
102
|
let synchronized = false;
|
|
103
|
+
let saveSettled = true;
|
|
104
|
+
let committedScene;
|
|
92
105
|
let hostDetails = {};
|
|
93
106
|
const record = async (operation, details) => {
|
|
94
107
|
hostDetails = { ...hostDetails, ...details };
|
|
@@ -113,6 +126,8 @@ export function createSdkSceneEditor(options) {
|
|
|
113
126
|
});
|
|
114
127
|
};
|
|
115
128
|
try {
|
|
129
|
+
releaseCommit = await options.acquireAccess?.(activeContext);
|
|
130
|
+
synchronized = true;
|
|
116
131
|
let result;
|
|
117
132
|
try {
|
|
118
133
|
result = await executeSceneOperation({
|
|
@@ -122,20 +137,20 @@ export function createSdkSceneEditor(options) {
|
|
|
122
137
|
operation: {
|
|
123
138
|
id,
|
|
124
139
|
targetKey: options.targetKey,
|
|
125
|
-
expectedRevision,
|
|
126
140
|
},
|
|
127
141
|
assertEditable: async () => {
|
|
128
142
|
activeContext.signal.throwIfAborted();
|
|
129
143
|
await options.assertEditable(activeContext);
|
|
130
144
|
},
|
|
131
145
|
edit: (draft) => callback(api, draft, id),
|
|
146
|
+
onCommittedScene: scene => { committedScene = scene; },
|
|
132
147
|
prepareCommit: async (operation) => {
|
|
133
148
|
await activeContext.recordReceipt({
|
|
134
149
|
...receipt(),
|
|
135
150
|
status: "unknown",
|
|
136
151
|
});
|
|
137
|
-
releaseCommit = await options.prepareCommit?.(operation, activeContext);
|
|
138
152
|
beginSettlement();
|
|
153
|
+
synchronized = false;
|
|
139
154
|
},
|
|
140
155
|
});
|
|
141
156
|
}
|
|
@@ -153,12 +168,19 @@ export function createSdkSceneEditor(options) {
|
|
|
153
168
|
}
|
|
154
169
|
throw error;
|
|
155
170
|
}
|
|
171
|
+
saveSettled = !result.operation.changed || !["unknown", "requested"].includes(result.operation.save?.status ?? "");
|
|
172
|
+
// A duplicate returns its journaled result; retain its original receipt
|
|
173
|
+
// and let explicit recovery handle any earlier synchronization failure.
|
|
174
|
+
if (result.reused) {
|
|
175
|
+
assertSceneSaved(result.operation);
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
156
178
|
synchronized = !result.operation.changed || !options.onCommitted;
|
|
157
179
|
await record(result.operation);
|
|
158
180
|
if (!synchronized) {
|
|
159
181
|
let details;
|
|
160
182
|
try {
|
|
161
|
-
details = await options.onCommitted(result.operation, api, activeContext, identity);
|
|
183
|
+
details = await options.onCommitted(result.operation, api, activeContext, identity, committedScene);
|
|
162
184
|
synchronized = true;
|
|
163
185
|
}
|
|
164
186
|
catch (error) {
|
|
@@ -175,11 +197,7 @@ export function createSdkSceneEditor(options) {
|
|
|
175
197
|
await record(result.operation, details || undefined);
|
|
176
198
|
}
|
|
177
199
|
if (result.operation.changed) {
|
|
178
|
-
|
|
179
|
-
throw new Error(`The scene edit was applied, but the save failed: ${result.operation.save.error} Do not repeat the edit.`);
|
|
180
|
-
if (result.operation.save?.status === "unknown" ||
|
|
181
|
-
result.operation.save?.status === "requested")
|
|
182
|
-
throw new Error("The scene edit was applied, but its save outcome is unknown. Inspect persistence before continuing; do not repeat the edit.");
|
|
200
|
+
assertSceneSaved(result.operation);
|
|
183
201
|
const details = await afterCommitted?.(result.operation, api);
|
|
184
202
|
if (details)
|
|
185
203
|
await record(result.operation, details);
|
|
@@ -187,7 +205,7 @@ export function createSdkSceneEditor(options) {
|
|
|
187
205
|
return result;
|
|
188
206
|
}
|
|
189
207
|
finally {
|
|
190
|
-
await releaseCommit?.(synchronized);
|
|
208
|
+
await releaseCommit?.(synchronized && saveSettled);
|
|
191
209
|
}
|
|
192
210
|
});
|
|
193
211
|
},
|
|
@@ -197,23 +215,8 @@ export function createSdkSceneHost(options) {
|
|
|
197
215
|
const editor = options.editor ?? createSdkSceneEditor(options);
|
|
198
216
|
return {
|
|
199
217
|
health: options.health?.read.bind(options.health),
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const revision = await api.getSceneRevision();
|
|
203
|
-
return {
|
|
204
|
-
revision,
|
|
205
|
-
projectId: options.projectId,
|
|
206
|
-
unitMultiplier: sceneUnitMultiplier(scene.properties.unit_multiplier),
|
|
207
|
-
nodes: Object.values(scene.nodes).map(({ id, name, kind, parentId }) => ({
|
|
208
|
-
id,
|
|
209
|
-
name,
|
|
210
|
-
type: sceneNodeType(kind),
|
|
211
|
-
parentId: parentId ?? 0,
|
|
212
|
-
})),
|
|
213
|
-
};
|
|
214
|
-
}),
|
|
215
|
-
edit: (callback, expectedRevision, context) => editor
|
|
216
|
-
.edit({ capability: "scene", operation: "edit" }, async (api, draft) => {
|
|
218
|
+
access: (callback, context) => editor
|
|
219
|
+
.access({ capability: "scene", operation: "access" }, async (api, draft) => {
|
|
217
220
|
const services = await options.services(api, context);
|
|
218
221
|
let active = true;
|
|
219
222
|
const scopedDraft = new Proxy(draft, {
|
|
@@ -234,7 +237,7 @@ export function createSdkSceneHost(options) {
|
|
|
234
237
|
finally {
|
|
235
238
|
active = false;
|
|
236
239
|
}
|
|
237
|
-
},
|
|
240
|
+
}, context, options.onSceneCommitted)
|
|
238
241
|
.then(({ value }) => value),
|
|
239
242
|
screenshot: options.screenshot,
|
|
240
243
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { KakapoAPI } from "@womp/kakapo-sdk";
|
|
2
2
|
import type { SceneScreenshotOptions } from "./capabilities/scene.js";
|
|
3
3
|
import type { ExecutionScreenshot } from "./types.js";
|
|
4
|
-
/**
|
|
5
|
-
export declare function captureSdkSceneScreenshot(api: KakapoAPI,
|
|
4
|
+
/** Capture the current native viewport without fetching the scene or changing its camera. */
|
|
5
|
+
export declare function captureSdkSceneScreenshot(api: KakapoAPI, _input: SceneScreenshotOptions): Promise<ExecutionScreenshot>;
|
|
@@ -1,17 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
if (!input.camera)
|
|
5
|
-
return api.captureScreenshot({ timeoutMs: 30_000 });
|
|
6
|
-
const scene = await api.refreshScene();
|
|
7
|
-
const units = sceneUnitMultiplier(scene.properties.unit_multiplier);
|
|
8
|
-
const camera = {
|
|
9
|
-
...input.camera,
|
|
10
|
-
position: toEnginePosition(input.camera.position, units),
|
|
11
|
-
target: toEnginePosition(input.camera.target, units),
|
|
12
|
-
};
|
|
13
|
-
if (camera.orthographicWidth !== undefined)
|
|
14
|
-
camera.orthographicWidth *= INTERNAL_UNITS_PER_MM / units;
|
|
15
|
-
await api.setCamera(camera);
|
|
16
|
-
return api.captureScreenshot({ camera, timeoutMs: 30_000 });
|
|
1
|
+
/** Capture the current native viewport without fetching the scene or changing its camera. */
|
|
2
|
+
export async function captureSdkSceneScreenshot(api, _input) {
|
|
3
|
+
return api.captureScreenshot({ timeoutMs: 30_000 });
|
|
17
4
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** One queue per native renderer, shared by every standalone run in this process. */
|
|
2
|
+
const renderers = new Map();
|
|
3
|
+
export function standaloneSceneAccess(url) {
|
|
4
|
+
const key = new URL(url).href;
|
|
5
|
+
let access = renderers.get(key);
|
|
6
|
+
if (!access) {
|
|
7
|
+
access = createSceneAccess();
|
|
8
|
+
renderers.set(key, access);
|
|
9
|
+
}
|
|
10
|
+
return access;
|
|
11
|
+
}
|
|
12
|
+
function createSceneAccess() {
|
|
13
|
+
let tail = Promise.resolve();
|
|
14
|
+
let uncertain = false;
|
|
15
|
+
return {
|
|
16
|
+
release(synchronized) {
|
|
17
|
+
if (!synchronized)
|
|
18
|
+
uncertain = true;
|
|
19
|
+
},
|
|
20
|
+
run(signal, action) {
|
|
21
|
+
const result = tail.then(async () => {
|
|
22
|
+
signal.throwIfAborted();
|
|
23
|
+
if (uncertain)
|
|
24
|
+
throw new Error("The renderer has an uncertain scene commit. Inspect it before restarting standalone scene access; do not repeat the edit.");
|
|
25
|
+
const active = new AbortController();
|
|
26
|
+
const abort = () => active.abort(signal.reason);
|
|
27
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
28
|
+
const beginSettlement = () => signal.removeEventListener("abort", abort);
|
|
29
|
+
try {
|
|
30
|
+
return await action(active.signal, beginSettlement);
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
beginSettlement();
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
tail = result.then(() => { }, () => { });
|
|
37
|
+
return result;
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|