@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
|
@@ -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
|
-
/** Convert
|
|
4
|
+
/** Convert explicit cameras at the native boundary; never fit or move the default view. */
|
|
5
5
|
export declare function captureSdkSceneScreenshot(api: KakapoAPI, input: SceneScreenshotOptions): Promise<ExecutionScreenshot>;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { INTERNAL_UNITS_PER_MM, sceneUnitMultiplier, toEnginePosition } from "./coordinates.js";
|
|
2
|
-
/** Convert
|
|
2
|
+
/** Convert explicit cameras at the native boundary; never fit or move the default view. */
|
|
3
3
|
export async function captureSdkSceneScreenshot(api, input) {
|
|
4
4
|
if (!input.camera)
|
|
5
5
|
return api.captureScreenshot({ timeoutMs: 30_000 });
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
// Read authoritative project units without returning another full-scene copy.
|
|
7
|
+
await api.refreshScene({ returnScene: false });
|
|
8
|
+
const units = sceneUnitMultiplier(api.getSceneProperties().unit_multiplier);
|
|
8
9
|
const camera = {
|
|
9
10
|
...input.camera,
|
|
10
11
|
position: toEnginePosition(input.camera.position, units),
|
|
@@ -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
|
+
}
|
|
@@ -3,7 +3,7 @@ import { createOpenAI } from "@ai-sdk/openai";
|
|
|
3
3
|
import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
|
4
4
|
import express from "express";
|
|
5
5
|
import { getSparkModel, SPARK_MODELS } from "../models.js";
|
|
6
|
-
import { KakapoAPI, KakapoRpcError } from "@womp/kakapo-sdk";
|
|
6
|
+
import { KakapoAPI, KakapoRpcError, KakapoSceneCommitError } from "@womp/kakapo-sdk";
|
|
7
7
|
import { captureSdkSceneScreenshot } from "../sdk-scene-screenshot.js";
|
|
8
8
|
import { composeCapabilities, createAccountCapability, createGeometryCapability, createOpenScadCapability, createSceneCapability, } from "../capabilities.js";
|
|
9
9
|
import { loadDemoSceneMaterials } from "./demo-scene-materials.js";
|
|
@@ -13,7 +13,7 @@ import { createSdkGeometryHost } from "../sdk-geometry-host.js";
|
|
|
13
13
|
import { createSdkOpenScadHost } from "../sdk-openscad-host.js";
|
|
14
14
|
import { createSdkSceneHealthHost } from "../sdk-scene-health-host.js";
|
|
15
15
|
import { geometryReference } from "../geometry.js";
|
|
16
|
-
import {
|
|
16
|
+
import { hasSceneResourcePressure, } from "../scene-health.js";
|
|
17
17
|
import { QuickJSJavaScriptExecutor } from "../quickjs-executor.js";
|
|
18
18
|
import { sparkModelOptions } from "../model-options.js";
|
|
19
19
|
import { createSparkRunExecutor } from "./executor.js";
|
|
@@ -21,7 +21,7 @@ import { createSparkAgentRouter } from "./express.js";
|
|
|
21
21
|
import { SparkAgentService } from "./service.js";
|
|
22
22
|
import { SqliteSparkStore } from "./sqlite-store.js";
|
|
23
23
|
import { clearSceneContents } from "./scene-actions.js";
|
|
24
|
-
import {
|
|
24
|
+
import { standaloneSceneAccess } from "./scene-access.js";
|
|
25
25
|
export async function checkStandaloneServices(api) {
|
|
26
26
|
const [fonts, openScad] = await Promise.allSettled([
|
|
27
27
|
api.listFonts(),
|
|
@@ -57,6 +57,7 @@ export async function createStandaloneSparkAgentApp(options) {
|
|
|
57
57
|
throw error;
|
|
58
58
|
}
|
|
59
59
|
const artifactStore = store;
|
|
60
|
+
const sceneAccess = standaloneSceneAccess(options.kakapoUrl);
|
|
60
61
|
const executeRun = createSparkRunExecutor({
|
|
61
62
|
createHost: async (context) => {
|
|
62
63
|
const api = new KakapoAPI({
|
|
@@ -65,9 +66,7 @@ export async function createStandaloneSparkAgentApp(options) {
|
|
|
65
66
|
});
|
|
66
67
|
try {
|
|
67
68
|
context.signal.throwIfAborted();
|
|
68
|
-
await api.connect();
|
|
69
|
-
context.signal.throwIfAborted();
|
|
70
|
-
await initializeStandaloneScene(api, context.signal);
|
|
69
|
+
await api.connect({ refreshScene: false });
|
|
71
70
|
context.signal.throwIfAborted();
|
|
72
71
|
const support = await checkStandaloneServices(api);
|
|
73
72
|
const services = {
|
|
@@ -81,12 +80,12 @@ export async function createStandaloneSparkAgentApp(options) {
|
|
|
81
80
|
targetKey,
|
|
82
81
|
operations: store,
|
|
83
82
|
saveToRenderer: true,
|
|
84
|
-
withConnection:
|
|
85
|
-
invocation.signal.throwIfAborted();
|
|
83
|
+
withConnection: (invocation, action) => sceneAccess.run(invocation.signal, async (signal, beginSettlement) => {
|
|
86
84
|
if (!api.isConnected)
|
|
87
85
|
throw new Error("The Kakapo connection was interrupted; reconnect before continuing");
|
|
88
|
-
return action(api,
|
|
89
|
-
},
|
|
86
|
+
return action(api, beginSettlement, { ...invocation, signal });
|
|
87
|
+
}),
|
|
88
|
+
acquireAccess: async () => sceneAccess.release,
|
|
90
89
|
assertEditable: () => {
|
|
91
90
|
if (!api.isConnected)
|
|
92
91
|
throw new Error("The Kakapo connection is unavailable");
|
|
@@ -103,16 +102,11 @@ export async function createStandaloneSparkAgentApp(options) {
|
|
|
103
102
|
const sceneHost = createSdkSceneHost({
|
|
104
103
|
...editorOptions,
|
|
105
104
|
editor,
|
|
106
|
-
health: createSdkSceneHealthHost(editor, onHealth),
|
|
107
|
-
onSceneCommitted: async (_operation, api) => {
|
|
108
|
-
const sceneHealth = await readStableSdkSceneHealth(api, getSdkSceneAreaIds(api), context.signal);
|
|
109
|
-
onHealth(sceneHealth);
|
|
110
|
-
return { sceneHealth };
|
|
111
|
-
},
|
|
105
|
+
health: createSdkSceneHealthHost(editor, onHealth, repairs),
|
|
112
106
|
services: async () => services,
|
|
113
107
|
screenshot: async (settings, invocation) => {
|
|
114
108
|
invocation.signal.throwIfAborted();
|
|
115
|
-
const image = await captureSdkSceneScreenshot(
|
|
109
|
+
const image = await editor.withConnection(invocation, activeApi => captureSdkSceneScreenshot(activeApi, settings));
|
|
116
110
|
options.onScreenshot?.(context.runId);
|
|
117
111
|
invocation.signal.throwIfAborted();
|
|
118
112
|
return artifactStore.write(context.ownerId, image);
|
|
@@ -254,8 +248,19 @@ export async function createStandaloneSparkAgentApp(options) {
|
|
|
254
248
|
clearingScene = true;
|
|
255
249
|
const api = new KakapoAPI({ url: options.kakapoUrl, requestTimeoutMs: 30_000 });
|
|
256
250
|
try {
|
|
257
|
-
await api.connect();
|
|
258
|
-
const { save } = await
|
|
251
|
+
await api.connect({ refreshScene: false });
|
|
252
|
+
const { save } = await sceneAccess.run(new AbortController().signal, async () => {
|
|
253
|
+
try {
|
|
254
|
+
const result = await api.accessScene(clearSceneContents, { save: true });
|
|
255
|
+
sceneAccess.release(result.save.status !== "unknown");
|
|
256
|
+
return result;
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
if (error instanceof KakapoSceneCommitError && error.outcome === "unknown")
|
|
260
|
+
sceneAccess.release(false);
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
});
|
|
259
264
|
const warning = save.status === "failed" || save.status === "unknown"
|
|
260
265
|
? `Scene cleared, but saving ${save.status === "failed" ? "failed" : "could not be confirmed"}: ${save.error} Do not repeat the clear operation.`
|
|
261
266
|
: undefined;
|
package/dist/womp-scene-api.d.ts
CHANGED
|
@@ -306,5 +306,20 @@ export declare function createWompSceneApi(context: WompSceneApiContext): {
|
|
|
306
306
|
g: number;
|
|
307
307
|
b: number;
|
|
308
308
|
}) => void;
|
|
309
|
+
unitMultiplier: number;
|
|
310
|
+
/** Current draft summaries. Filters precede pagination; defaults to 100 results, maximum 1000. */
|
|
311
|
+
nodes(options?: {
|
|
312
|
+
ids?: number[];
|
|
313
|
+
type?: number;
|
|
314
|
+
parentId?: number;
|
|
315
|
+
name?: string;
|
|
316
|
+
offset?: number;
|
|
317
|
+
limit?: number;
|
|
318
|
+
}): {
|
|
319
|
+
id: number;
|
|
320
|
+
name: string;
|
|
321
|
+
type: number;
|
|
322
|
+
parentId: number;
|
|
323
|
+
}[];
|
|
309
324
|
};
|
|
310
325
|
export {};
|
package/dist/womp-scene-api.js
CHANGED
|
@@ -436,6 +436,23 @@ export function createWompSceneApi(context) {
|
|
|
436
436
|
return converted;
|
|
437
437
|
};
|
|
438
438
|
const api = {
|
|
439
|
+
get unitMultiplier() {
|
|
440
|
+
return sceneUnitMultiplier(scene.getSceneProperties().unit_multiplier);
|
|
441
|
+
},
|
|
442
|
+
/** Current draft summaries. Filters precede pagination; defaults to 100 results, maximum 1000. */
|
|
443
|
+
nodes(options = {}) {
|
|
444
|
+
const { offset = 0, limit = 100 } = options;
|
|
445
|
+
if (!Number.isInteger(offset) || offset < 0 || !Number.isInteger(limit) || limit < 1 || limit > 1000)
|
|
446
|
+
throw new Error("nodes() requires a nonnegative integer offset and a limit from 1 to 1000");
|
|
447
|
+
const ids = options.ids === undefined ? undefined : new Set(options.ids);
|
|
448
|
+
const query = options.name?.toLowerCase();
|
|
449
|
+
return scene.listNodeSummaries().filter(({ id, name, kind, parentId }) => (ids === undefined || ids.has(id)) &&
|
|
450
|
+
(options.type === undefined || sceneNodeType(kind) === options.type) &&
|
|
451
|
+
(options.parentId === undefined || (parentId ?? 0) === options.parentId) &&
|
|
452
|
+
(query === undefined || name.toLowerCase().includes(query))).slice(offset, offset + limit).map(({ id, name, kind, parentId }) => ({
|
|
453
|
+
id, name, type: sceneNodeType(kind), parentId: parentId ?? 0,
|
|
454
|
+
}));
|
|
455
|
+
},
|
|
439
456
|
...createSceneEnvironment(scene, context.hdrs, context.print),
|
|
440
457
|
listSuperMaterials: () => {
|
|
441
458
|
if (!context.superMaterialNames)
|
|
@@ -611,7 +628,7 @@ export function createWompSceneApi(context) {
|
|
|
611
628
|
nodeSetLock: (id, locked) => {
|
|
612
629
|
requireNode(id);
|
|
613
630
|
const current = metadataObject(scene.getSceneExt([String(id)])[String(id)]);
|
|
614
|
-
scene.patchSceneExt([{ op: "add", path: `/${id}`, value: { ...current, locked: Boolean(locked) } }]);
|
|
631
|
+
scene.patchSceneExt([{ op: "add", path: `/${id}`, value: { ...current, locked: Boolean(locked) } }], { returnScene: false });
|
|
615
632
|
},
|
|
616
633
|
nodeSetPos: (id, position) => {
|
|
617
634
|
requireEditableTransform(id);
|