@womp/spark-agent 4.4.0 → 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 +58 -97
- package/dist/bench-camera.d.ts +9 -0
- package/dist/bench-camera.js +32 -0
- package/dist/bounded-output.js +8 -19
- 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 +39 -52
- 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/run-metrics.js +21 -7
- 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 +13 -22
- 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 +29 -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 -15
- 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/simple-bench-prompts.d.ts +5 -28
- package/dist/simple-bench-prompts.js +2 -1
- package/dist/simple-bench.js +11 -23
- package/dist/womp-scene-api.d.ts +30 -0
- package/dist/womp-scene-api.js +106 -24
- package/package.json +139 -139
- package/dist/server/scene-initialization.d.ts +0 -3
- package/dist/server/scene-initialization.js +0 -44
|
@@ -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;
|
|
@@ -1,28 +1,5 @@
|
|
|
1
|
-
export declare const
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
readonly prompt: "Make a matte-black desk lamp, 45 cm tall, with a circular base 18 cm in diameter, two slender articulated arms, and a conical shade 15 cm wide. Give the shade a white interior and a warm-white bulb.";
|
|
7
|
-
}, {
|
|
8
|
-
readonly id: "coffee-mug";
|
|
9
|
-
readonly prompt: "Make a teal ceramic mug, 10 cm tall and 9 cm in diameter, with a gently rounded base, an open top, a thick rim, and a large C-shaped handle. Give it a cream-colored interior.";
|
|
10
|
-
}, {
|
|
11
|
-
readonly id: "bookshelf";
|
|
12
|
-
readonly prompt: "Make a light-oak bookshelf, 180 cm tall, 90 cm wide, and 30 cm deep. Give it five evenly spaced open compartments, thick rectangular side panels, a solid back panel, and a recessed base.";
|
|
13
|
-
}, {
|
|
14
|
-
readonly id: "backpack";
|
|
15
|
-
readonly prompt: "Make a navy-blue backpack, 45 cm tall, 30 cm wide, and 18 cm deep. Give it a rounded rectangular body, a large front zipper pocket, two padded shoulder straps, a top carry handle, and light-gray zipper pulls.";
|
|
16
|
-
}, {
|
|
17
|
-
readonly id: "toaster";
|
|
18
|
-
readonly prompt: "Make a cream-colored two-slot toaster, 28 cm wide, 18 cm deep, and 20 cm tall. Give it rounded corners, a brushed-metal top, two long bread slots, a silver side lever, and a circular browning dial.";
|
|
19
|
-
}, {
|
|
20
|
-
readonly id: "plant-pot";
|
|
21
|
-
readonly prompt: "Make a terracotta plant pot, 25 cm tall, tapering from a 30 cm opening to a 20 cm base. Give it a thick rolled rim and dark soil containing a small plant with six broad green leaves on short stems.";
|
|
22
|
-
}, {
|
|
23
|
-
readonly id: "suitcase";
|
|
24
|
-
readonly prompt: "Make a burgundy hard-shell suitcase, 65 cm tall, 42 cm wide, and 26 cm deep. Give it rounded corners, five vertical ribs on each broad face, four black spinner wheels, a silver telescoping handle, and a black side carry handle.";
|
|
25
|
-
}, {
|
|
26
|
-
readonly id: "dining-table";
|
|
27
|
-
readonly prompt: "Make a rectangular walnut dining table, 160 cm long, 90 cm wide, and 75 cm tall. Give it a 4 cm thick tabletop with softly rounded corners, four square tapered legs, and a recessed wooden apron beneath the top.";
|
|
28
|
-
}];
|
|
1
|
+
export declare const BENCH_ORIENTATION = "Orient the object upright (+Y), with its front facing +Z and its base centered at the origin.";
|
|
2
|
+
export declare const BENCH_PROMPTS: {
|
|
3
|
+
prompt: string;
|
|
4
|
+
id: string;
|
|
5
|
+
}[];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export const BENCH_ORIENTATION = "Orient the object upright (+Y), with its front facing +Z and its base centered at the origin.";
|
|
1
2
|
export const BENCH_PROMPTS = [
|
|
2
3
|
{ id: "armchair", prompt: "Make a mustard-yellow upholstered armchair, 85 cm wide, 90 cm deep, and 85 cm tall. Give it a thick square seat cushion, a slightly reclined backrest, rounded armrests, and four short tapered wooden legs." },
|
|
3
4
|
{ id: "desk-lamp", prompt: "Make a matte-black desk lamp, 45 cm tall, with a circular base 18 cm in diameter, two slender articulated arms, and a conical shade 15 cm wide. Give the shade a white interior and a warm-white bulb." },
|
|
@@ -8,4 +9,4 @@ export const BENCH_PROMPTS = [
|
|
|
8
9
|
{ id: "plant-pot", prompt: "Make a terracotta plant pot, 25 cm tall, tapering from a 30 cm opening to a 20 cm base. Give it a thick rolled rim and dark soil containing a small plant with six broad green leaves on short stems." },
|
|
9
10
|
{ id: "suitcase", prompt: "Make a burgundy hard-shell suitcase, 65 cm tall, 42 cm wide, and 26 cm deep. Give it rounded corners, five vertical ribs on each broad face, four black spinner wheels, a silver telescoping handle, and a black side carry handle." },
|
|
10
11
|
{ id: "dining-table", prompt: "Make a rectangular walnut dining table, 160 cm long, 90 cm wide, and 75 cm tall. Give it a 4 cm thick tabletop with softly rounded corners, four square tapered legs, and a recessed wooden apron beneath the top." },
|
|
11
|
-
];
|
|
12
|
+
].map(item => ({ ...item, prompt: `${item.prompt} ${BENCH_ORIENTATION}` }));
|
package/dist/simple-bench.js
CHANGED
|
@@ -2,7 +2,7 @@ import { once } from "node:events";
|
|
|
2
2
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { KakapoAPI } from "@womp/kakapo-sdk";
|
|
5
|
-
import {
|
|
5
|
+
import { benchCamera, benchGeometryIds } from "./bench-camera.js";
|
|
6
6
|
import { SparkChatTransport } from "./client.js";
|
|
7
7
|
import { createStandaloneSparkAgentApp } from "./server/standalone.js";
|
|
8
8
|
import { BENCH_PROMPTS } from "./simple-bench-prompts.js";
|
|
@@ -40,7 +40,7 @@ export async function runSimpleBench(options) {
|
|
|
40
40
|
const report = async () => {
|
|
41
41
|
await writeFile(path.join(options.output, "results.json"), JSON.stringify({
|
|
42
42
|
version: 1, startedAt, nodeVersion: process.version, settings: options.settings,
|
|
43
|
-
timeoutMs: options.timeoutMs, camera: "
|
|
43
|
+
timeoutMs: options.timeoutMs, camera: "fixed-three-quarter-v3", plannedPrompts: prompts.map((item) => item.id),
|
|
44
44
|
totalCost: totalBenchCost(results), results,
|
|
45
45
|
}, null, 2));
|
|
46
46
|
const columns = ["id", "status", "steps", "readSkill", "executeJavaScript", "screenshots", "durationMs", "costUsd", "costStatus", "image", "error"];
|
|
@@ -100,30 +100,18 @@ export async function runSimpleBench(options) {
|
|
|
100
100
|
const api = new KakapoAPI({ url: options.kakapoUrl, requestTimeoutMs: 30_000 });
|
|
101
101
|
try {
|
|
102
102
|
await api.connect();
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
103
|
+
const snapshot = await api.rpc("scene_state_get");
|
|
104
|
+
await writeFile(path.join(options.output, `${item.id}.scene.json`), typeof snapshot === "string" ? snapshot : JSON.stringify(snapshot));
|
|
105
|
+
const ids = benchGeometryIds(api);
|
|
106
106
|
const bounds = await api.getNodesBoundingBox(ids);
|
|
107
107
|
const viewport = await api.captureScreenshot({});
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
if (
|
|
112
|
-
throw new Error("
|
|
113
|
-
|
|
114
|
-
x: (bounds.min.x + bounds.max.x) / 2,
|
|
115
|
-
y: (bounds.min.y + bounds.max.y) / 2,
|
|
116
|
-
z: (bounds.min.z + bounds.max.z) / 2,
|
|
117
|
-
};
|
|
118
|
-
const radius = Math.hypot(bounds.max.x - bounds.min.x, bounds.max.y - bounds.min.y, bounds.max.z - bounds.min.z) / 2;
|
|
119
|
-
const halfAngle = Math.atan(Math.tan(Math.PI / 6) * Math.min(1, viewport.height / viewport.width));
|
|
120
|
-
const offset = Math.max(0.1, radius * 1.1 / Math.sin(halfAngle)) / directionLength;
|
|
121
|
-
const image = await api.captureScreenshot({ camera: {
|
|
122
|
-
position: { x: target.x - direction.x * offset, y: target.y - direction.y * offset, z: target.z - direction.z * offset },
|
|
123
|
-
target, projection: "perspective", horizontalFovDegrees: 60,
|
|
124
|
-
}, timeoutMs: 30_000 });
|
|
108
|
+
const camera = benchCamera(bounds, viewport.width / viewport.height);
|
|
109
|
+
await writeFile(path.join(options.output, `${item.id}.framing.json`), JSON.stringify({ ids, bounds, width: viewport.width, height: viewport.height, cameras: { "three-quarter": camera } }, null, 2));
|
|
110
|
+
const image = await api.captureScreenshot({ camera, timeoutMs: 30_000 });
|
|
111
|
+
if (image.width !== viewport.width || image.height !== viewport.height)
|
|
112
|
+
throw new Error("Viewport changed during benchmark capture.");
|
|
113
|
+
await writeFile(path.join(options.output, `${item.id}.jpg`), image.data);
|
|
125
114
|
result.image = `${item.id}.jpg`;
|
|
126
|
-
await writeFile(path.join(options.output, result.image), image.data);
|
|
127
115
|
}
|
|
128
116
|
finally {
|
|
129
117
|
api.disconnect();
|
package/dist/womp-scene-api.d.ts
CHANGED
|
@@ -164,8 +164,20 @@ export declare function createWompSceneApi(context: WompSceneApiContext): {
|
|
|
164
164
|
nodeGetLightType: (id: number) => number;
|
|
165
165
|
nodeSetLightType: (id: number, lightType: number) => void;
|
|
166
166
|
nodeGetPower: (id: number) => number;
|
|
167
|
+
/** Linear emitted intensity before collimation; not a percentage or physical unit. */
|
|
167
168
|
nodeSetPower: (id: number, power: number) => void;
|
|
168
169
|
nodeGetText: (id: number) => string;
|
|
170
|
+
/** Aim light emission or the readable text/decal face using target minus node position in parent-local coordinates. Use instead of generic lookAt for these faces. Y-up, Z-up when vertical. */
|
|
171
|
+
nodeSetFacing: (id: number, direction: Vec3) => void;
|
|
172
|
+
/** Font em size and extrusion depth in project units, not text bounding-box dimensions. */
|
|
173
|
+
nodeSetTextSize: (id: number, size: {
|
|
174
|
+
fontSize: number;
|
|
175
|
+
depth: number;
|
|
176
|
+
}) => void;
|
|
177
|
+
nodeGetTextSize: (id: number) => {
|
|
178
|
+
fontSize: number;
|
|
179
|
+
depth: number;
|
|
180
|
+
};
|
|
169
181
|
nodeSetText: (id: number, text: string) => void;
|
|
170
182
|
nodeGetFont: (id: number) => string;
|
|
171
183
|
nodeSetFont: (id: number, font: string) => void;
|
|
@@ -177,8 +189,10 @@ export declare function createWompSceneApi(context: WompSceneApiContext): {
|
|
|
177
189
|
nodeSetAlign: (id: number, align: number) => void;
|
|
178
190
|
nodeGetWrap: (id: number) => number;
|
|
179
191
|
nodeSetWrap: (id: number, wrap: number) => void;
|
|
192
|
+
/** Text layout width in project units, including owning Area scale. */
|
|
180
193
|
nodeGetWidth: (id: number) => number;
|
|
181
194
|
nodeSetWidth: (id: number, width: number) => void;
|
|
195
|
+
/** Additional letter spacing in project units, including owning Area scale. */
|
|
182
196
|
nodeGetSpacing: (id: number) => number;
|
|
183
197
|
nodeSetSpacing: (id: number, spacing: number) => void;
|
|
184
198
|
nodeGetLineHeight: (id: number) => number;
|
|
@@ -272,6 +286,7 @@ export declare function createWompSceneApi(context: WompSceneApiContext): {
|
|
|
272
286
|
nodeResize: (id: number, dimensions: Partial<Vec3>) => Promise<void>;
|
|
273
287
|
aabbCenter: (box: AABB) => Vec3;
|
|
274
288
|
aabbRadius: (box: AABB) => number;
|
|
289
|
+
/** Align local +Z from origin toward target, not a light's emission or a text/decal face; use nodeSetFacing for those. */
|
|
275
290
|
lookAt: typeof lookAt;
|
|
276
291
|
alignAxis: typeof alignAxis;
|
|
277
292
|
scenePropertiesRead: () => {
|
|
@@ -291,5 +306,20 @@ export declare function createWompSceneApi(context: WompSceneApiContext): {
|
|
|
291
306
|
g: number;
|
|
292
307
|
b: number;
|
|
293
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
|
+
}[];
|
|
294
324
|
};
|
|
295
325
|
export {};
|
package/dist/womp-scene-api.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { xyzRotationFromMatrix, MAX_BLEND_RADIUS_MM, MAX_MIRROR_BLEND_RADIUS_MM } from "@womp/kakapo-sdk";
|
|
1
|
+
import { xyzRotationFromMatrix, MAX_BLEND_RADIUS_MM, MAX_MIRROR_BLEND_RADIUS_MM, maxLightIntensity } from "@womp/kakapo-sdk";
|
|
2
2
|
import { metadataObject, readMaterialMetadata, removeMaterialMetadata, shiftCurvePointMetadata, writeMaterialMetadata, } from "./scene-metadata.js";
|
|
3
3
|
import { createSceneEnvironment } from "./scene-environment.js";
|
|
4
4
|
import { assertColor255 } from "./color.js";
|
|
5
|
-
import { convertRotationBasis, sceneUnitMultiplier, fromEngineDimensions, fromEngineMirrorRotation, fromEnginePosition, toEngineDimensions, toEngineMirrorRotation, toEnginePosition, } from "./coordinates.js";
|
|
5
|
+
import { INTERNAL_UNITS_PER_MM, convertRotationBasis, sceneUnitMultiplier, fromEngineDimensions, fromEngineMirrorRotation, fromEnginePosition, toEngineDimensions, toEngineMirrorRotation, toEnginePosition, } from "./coordinates.js";
|
|
6
6
|
export const NodeType = {
|
|
7
7
|
Primitive: 0,
|
|
8
8
|
Union: 1,
|
|
@@ -395,6 +395,8 @@ export function createWompSceneApi(context) {
|
|
|
395
395
|
const y = finite(sizeValue?.y, "size.y");
|
|
396
396
|
if (x <= 0 || y <= 0)
|
|
397
397
|
throw new Error("Light size.x and size.y must be positive");
|
|
398
|
+
if ("z" in sizeValue)
|
|
399
|
+
finite(sizeValue.z, "size.z");
|
|
398
400
|
const converted = toEngineDimensions({ x, y, z: 1 }, unitMultiplier);
|
|
399
401
|
update(id, { size: { x: converted.z, y: converted.y } });
|
|
400
402
|
return;
|
|
@@ -434,6 +436,23 @@ export function createWompSceneApi(context) {
|
|
|
434
436
|
return converted;
|
|
435
437
|
};
|
|
436
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
|
+
},
|
|
437
456
|
...createSceneEnvironment(scene, context.hdrs, context.print),
|
|
438
457
|
listSuperMaterials: () => {
|
|
439
458
|
if (!context.superMaterialNames)
|
|
@@ -609,7 +628,7 @@ export function createWompSceneApi(context) {
|
|
|
609
628
|
nodeSetLock: (id, locked) => {
|
|
610
629
|
requireNode(id);
|
|
611
630
|
const current = metadataObject(scene.getSceneExt([String(id)])[String(id)]);
|
|
612
|
-
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 });
|
|
613
632
|
},
|
|
614
633
|
nodeSetPos: (id, position) => {
|
|
615
634
|
requireEditableTransform(id);
|
|
@@ -643,7 +662,7 @@ export function createWompSceneApi(context) {
|
|
|
643
662
|
return "operation" in node ? OPERATIONS.indexOf(node.operation) : OperationType.Union;
|
|
644
663
|
},
|
|
645
664
|
nodeSetOp: (id, operation) => {
|
|
646
|
-
const value = OPERATIONS[
|
|
665
|
+
const value = OPERATIONS[finite(operation, "operation")];
|
|
647
666
|
if (!value)
|
|
648
667
|
throw new Error("Operation must be from 0 to 3");
|
|
649
668
|
const node = requireNode(id);
|
|
@@ -701,51 +720,113 @@ export function createWompSceneApi(context) {
|
|
|
701
720
|
},
|
|
702
721
|
nodeGetLightType: (id) => LIGHT_TYPES.indexOf(requireLight(id).lightType),
|
|
703
722
|
nodeSetLightType: (id, lightType) => {
|
|
704
|
-
|
|
723
|
+
requireLight(id);
|
|
724
|
+
const value = LIGHT_TYPES[finite(lightType, "lightType")];
|
|
705
725
|
if (!value)
|
|
706
|
-
throw new Error("LightType must be from 0 to 2");
|
|
726
|
+
throw new Error("LightType must be an integer from 0 to 2");
|
|
707
727
|
update(id, { lightType: value });
|
|
708
728
|
},
|
|
709
729
|
nodeGetPower: (id) => requireLight(id).power,
|
|
730
|
+
/** Linear emitted intensity before collimation; not a percentage or physical unit. */
|
|
710
731
|
nodeSetPower: (id, power) => {
|
|
711
|
-
requireLight(id);
|
|
712
|
-
|
|
713
|
-
|
|
732
|
+
const light = requireLight(id);
|
|
733
|
+
const max = maxLightIntensity(light.lightType);
|
|
734
|
+
if (!Number.isFinite(power) || power < 0 || power > max)
|
|
735
|
+
throw new Error(`Light ${id} (${light.lightType}): linear intensity must be a finite number from 0 through ${max}`);
|
|
714
736
|
update(id, { power });
|
|
715
737
|
},
|
|
716
738
|
nodeGetText: (id) => requireText(id).text,
|
|
717
|
-
|
|
739
|
+
/** Aim light emission or the readable text/decal face using target minus node position in parent-local coordinates. Use instead of generic lookAt for these faces. Y-up, Z-up when vertical. */
|
|
740
|
+
nodeSetFacing: (id, direction) => {
|
|
741
|
+
requireNode(id);
|
|
742
|
+
const value = vector(direction, "direction");
|
|
743
|
+
const nativeDirection = { x: -value.z, y: value.y, z: -value.x };
|
|
744
|
+
const up = Math.abs(value.y) / Math.hypot(value.x, value.y, value.z) > 1 - 1e-8
|
|
745
|
+
? { x: -1, y: 0, z: 0 } : { x: 0, y: 1, z: 0 };
|
|
746
|
+
context.call("setNodeFacing", [id, nativeDirection, up], () => scene.setNodeFacing(id, nativeDirection, up));
|
|
747
|
+
},
|
|
748
|
+
/** Font em size and extrusion depth in project units, not text bounding-box dimensions. */
|
|
749
|
+
nodeSetTextSize: (id, size) => {
|
|
750
|
+
requireText(id);
|
|
751
|
+
const fontSize = finite(size?.fontSize, "fontSize");
|
|
752
|
+
const depth = finite(size?.depth, "depth");
|
|
753
|
+
if (fontSize <= 0 || depth <= 0)
|
|
754
|
+
throw new Error("Text fontSize and depth must be positive");
|
|
755
|
+
const factor = INTERNAL_UNITS_PER_MM / ancestorScale(id);
|
|
756
|
+
const nativeSize = {
|
|
757
|
+
fontSize: clamp(fontSize / unitMultiplier, 0.01, 1000) * factor,
|
|
758
|
+
depth: clamp(depth / unitMultiplier, 0.01, 1000) * factor,
|
|
759
|
+
};
|
|
760
|
+
context.call("setTextSize", [id, nativeSize], () => scene.setTextSize(id, nativeSize));
|
|
761
|
+
},
|
|
762
|
+
nodeGetTextSize: (id) => {
|
|
763
|
+
const node = requireText(id);
|
|
764
|
+
const factor = ancestorScale(id) * unitMultiplier / INTERNAL_UNITS_PER_MM;
|
|
765
|
+
return { fontSize: node.transform.scale.x * factor, depth: node.transform.scale.z * factor };
|
|
766
|
+
},
|
|
767
|
+
nodeSetText: (id, text) => {
|
|
768
|
+
requireText(id);
|
|
769
|
+
if (typeof text !== "string")
|
|
770
|
+
throw new Error("text must be a string; use an empty string to clear it");
|
|
771
|
+
update(id, { text });
|
|
772
|
+
},
|
|
718
773
|
nodeGetFont: (id) => requireText(id).fontFamily,
|
|
719
774
|
nodeSetFont: (id, font) => {
|
|
775
|
+
requireText(id);
|
|
776
|
+
if (typeof font !== "string" || !font.trim())
|
|
777
|
+
throw new Error("font must be a non-empty font family name");
|
|
778
|
+
if (!context.fonts.length)
|
|
779
|
+
throw new Error("Font catalog is unavailable; this host cannot validate a new font selection");
|
|
720
780
|
const match = context.fonts.find((family) => family.name.toLowerCase() === font.toLowerCase());
|
|
721
|
-
if (!match
|
|
722
|
-
throw new Error(`Unknown font "${font}"`);
|
|
781
|
+
if (!match)
|
|
782
|
+
throw new Error(`Unknown font "${font}"; choose a family from the font catalog`);
|
|
723
783
|
update(id, { fontFamily: match?.name ?? font });
|
|
724
784
|
},
|
|
725
785
|
nodeGetWeight: (id) => requireText(id).weight,
|
|
726
|
-
nodeSetWeight: (id, weight) =>
|
|
786
|
+
nodeSetWeight: (id, weight) => {
|
|
787
|
+
requireText(id);
|
|
788
|
+
update(id, { weight: clamp(finite(weight, "weight"), 100, 900) });
|
|
789
|
+
},
|
|
727
790
|
nodeGetItalic: (id) => requireText(id).italic,
|
|
728
|
-
nodeSetItalic: (id, italic) =>
|
|
791
|
+
nodeSetItalic: (id, italic) => {
|
|
792
|
+
requireText(id);
|
|
793
|
+
if (typeof italic !== "boolean")
|
|
794
|
+
throw new Error("italic must be a boolean");
|
|
795
|
+
update(id, { italic });
|
|
796
|
+
},
|
|
729
797
|
nodeGetAlign: (id) => ALIGNS.indexOf(requireText(id).align),
|
|
730
798
|
nodeSetAlign: (id, align) => {
|
|
731
|
-
|
|
799
|
+
requireText(id);
|
|
800
|
+
const value = ALIGNS[finite(align, "align")];
|
|
732
801
|
if (!value)
|
|
733
|
-
throw new Error("Align must be from 0 to 2");
|
|
802
|
+
throw new Error("Align must be an integer from 0 to 2");
|
|
734
803
|
update(id, { align: value });
|
|
735
804
|
},
|
|
736
805
|
nodeGetWrap: (id) => WRAPS.indexOf(requireText(id).wrap),
|
|
737
806
|
nodeSetWrap: (id, wrap) => {
|
|
738
|
-
|
|
807
|
+
requireText(id);
|
|
808
|
+
const value = WRAPS[finite(wrap, "wrap")];
|
|
739
809
|
if (!value)
|
|
740
|
-
throw new Error("Wrap must be from 0 to 2");
|
|
810
|
+
throw new Error("Wrap must be an integer from 0 to 2");
|
|
741
811
|
update(id, { wrap: value });
|
|
742
812
|
},
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
813
|
+
/** Text layout width in project units, including owning Area scale. */
|
|
814
|
+
nodeGetWidth: (id) => requireText(id).width * ancestorScale(id) * unitMultiplier / INTERNAL_UNITS_PER_MM,
|
|
815
|
+
nodeSetWidth: (id, width) => {
|
|
816
|
+
requireText(id);
|
|
817
|
+
update(id, { width: finite(width, "width") * INTERNAL_UNITS_PER_MM / unitMultiplier / ancestorScale(id) });
|
|
818
|
+
},
|
|
819
|
+
/** Additional letter spacing in project units, including owning Area scale. */
|
|
820
|
+
nodeGetSpacing: (id) => requireText(id).spacing * ancestorScale(id) * unitMultiplier / INTERNAL_UNITS_PER_MM,
|
|
821
|
+
nodeSetSpacing: (id, spacing) => {
|
|
822
|
+
requireText(id);
|
|
823
|
+
update(id, { spacing: clamp(finite(spacing, "spacing") / unitMultiplier, -25, 25) * INTERNAL_UNITS_PER_MM / ancestorScale(id) });
|
|
824
|
+
},
|
|
747
825
|
nodeGetLineHeight: (id) => requireText(id).lineHeight,
|
|
748
|
-
nodeSetLineHeight: (id, lineHeight) =>
|
|
826
|
+
nodeSetLineHeight: (id, lineHeight) => {
|
|
827
|
+
requireText(id);
|
|
828
|
+
update(id, { lineHeight: clamp(finite(lineHeight, "lineHeight"), 0.5, 3) });
|
|
829
|
+
},
|
|
749
830
|
nodeGetDensity: (id) => requireCurve(id).density,
|
|
750
831
|
nodeSetDensity: (id, density) => update(id, { density: clamp(Math.round(finite(density, "density")), 0, 250) }),
|
|
751
832
|
nodeGetRoundness: (id) => requireCurve(id).roundness,
|
|
@@ -762,7 +843,7 @@ export function createWompSceneApi(context) {
|
|
|
762
843
|
return value;
|
|
763
844
|
},
|
|
764
845
|
nodeSetPrimType: (id, primitive) => {
|
|
765
|
-
const value = PUBLIC_PRIMITIVES.get(
|
|
846
|
+
const value = PUBLIC_PRIMITIVES.get(finite(primitive, "primitive"));
|
|
766
847
|
if (!value)
|
|
767
848
|
throw new Error("PrimitiveType must be Sphere, Cylinder, Cube, Cone, Torus, or Pyramid");
|
|
768
849
|
const node = requireNode(id);
|
|
@@ -991,6 +1072,7 @@ export function createWompSceneApi(context) {
|
|
|
991
1072
|
z: (box.min.z + box.max.z) / 2,
|
|
992
1073
|
}),
|
|
993
1074
|
aabbRadius: (box) => Math.hypot(box.max.x - box.min.x, box.max.y - box.min.y, box.max.z - box.min.z) / 2,
|
|
1075
|
+
/** Align local +Z from origin toward target, not a light's emission or a text/decal face; use nodeSetFacing for those. */
|
|
994
1076
|
lookAt,
|
|
995
1077
|
alignAxis,
|
|
996
1078
|
};
|