@womp/kakapo-sdk 0.1.0 → 0.2.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 CHANGED
@@ -51,6 +51,11 @@ kakapo.disconnect();
51
51
  The transport uses the browser's native `WebSocket` when bundled for the web and loads `ws`
52
52
  dynamically in Node.js.
53
53
 
54
+ Applications that already own a renderer connection can pass a `KakapoAPITransport` as
55
+ `new KakapoAPI({ transport })`. The adapter defines connection lifecycle, RPC, and token responses.
56
+ `captureScreenshot({ view: "current" })` requests the renderer's JPEG without changing the camera
57
+ or requiring a cached scene. Current-view captures cannot include `targetId` or `targetIds`.
58
+
54
59
  All public inputs are checked at runtime as well as by TypeScript. Invalid calls throw structured
55
60
  `KakapoValidationError` objects with a stable `code`, offending `path`, expected rule, and a
56
61
  correction `hint` intended for automated agents.
package/dist/api.d.ts CHANGED
@@ -13,6 +13,10 @@ export interface SceneEdit {
13
13
  getNodeParent(id: number): KakapoNode | null;
14
14
  getNodeChildren(id: number): KakapoNode[];
15
15
  applyScenePatch(operations: JsonPatchOperation[]): Scene;
16
+ getSceneExt(): Record<string, JsonValue>;
17
+ patchSceneExt(operations: JsonPatchOperation[]): Scene;
18
+ getSceneProperties(): Record<string, JsonValue>;
19
+ patchSceneProperties(operations: JsonPatchOperation[]): Scene;
16
20
  createNode(input: CreateNodeInput): KakapoNode;
17
21
  cloneNode(id: number, options?: {
18
22
  parentId?: number;
@@ -74,6 +78,14 @@ export declare class KakapoAPI {
74
78
  getScene(): Scene;
75
79
  editScene<T>(callback: (scene: SceneEdit) => T | Promise<T>): Promise<T>;
76
80
  private applyScenePatch;
81
+ /** The scene's consumer-owned extension bag. Kakapo does not interpret it. */
82
+ getSceneExt(): Record<string, JsonValue>;
83
+ /** Apply JSON Patch operations rooted at the scene's `ext` bag. */
84
+ patchSceneExt(operations: JsonPatchOperation[]): Scene;
85
+ /** The scene's renderer/environment properties (HDR, background, and similar). */
86
+ getSceneProperties(): Record<string, JsonValue>;
87
+ /** Apply JSON Patch operations rooted at the scene's `properties` bag. */
88
+ patchSceneProperties(operations: JsonPatchOperation[]): Scene;
77
89
  listNodes(options?: ListNodeOptions): KakapoNode[];
78
90
  findNodes(options: FindNodeOptions): KakapoNode[];
79
91
  getNode(id: number): KakapoNode;
package/dist/api.js CHANGED
@@ -25,7 +25,7 @@ export class KakapoAPI {
25
25
  draftDirty = false;
26
26
  rendererSyncRequired = false;
27
27
  constructor(options = {}) {
28
- this.transport = new KakapoTransport({
28
+ this.transport = options.transport ?? new KakapoTransport({
29
29
  url: options.url ?? "ws://127.0.0.1:5502",
30
30
  requestTimeoutMs: options.requestTimeoutMs ?? 10_000,
31
31
  connectTimeoutMs: options.connectTimeoutMs ?? 30_000,
@@ -126,6 +126,24 @@ export class KakapoAPI {
126
126
  this.draftDirty = true;
127
127
  return this.getScene();
128
128
  }
129
+ /** The scene's consumer-owned extension bag. Kakapo does not interpret it. */
130
+ getSceneExt() {
131
+ const scene = this.getScene();
132
+ return scene.ext ?? {};
133
+ }
134
+ /** Apply JSON Patch operations rooted at the scene's `ext` bag. */
135
+ patchSceneExt(operations) {
136
+ return this.applyScenePatch(operations.map((op) => ({ ...op, path: `/ext${op.path}` })));
137
+ }
138
+ /** The scene's renderer/environment properties (HDR, background, and similar). */
139
+ getSceneProperties() {
140
+ const scene = this.getScene();
141
+ return scene.properties ?? {};
142
+ }
143
+ /** Apply JSON Patch operations rooted at the scene's `properties` bag. */
144
+ patchSceneProperties(operations) {
145
+ return this.applyScenePatch(operations.map((op) => ({ ...op, path: `/properties${op.path}` })));
146
+ }
129
147
  listNodes(options = {}) {
130
148
  const scene = this.requireScene("listNodes");
131
149
  return Object.keys(scene.nodes).map(Number).sort((a, b) => a - b).map((id) => wireToNode(scene, id)).filter((node) => (options.kind === undefined || node.kind === options.kind) && (options.parentId === undefined || node.parentId === options.parentId)).map(deepClone);
@@ -193,6 +211,17 @@ export class KakapoAPI {
193
211
  async captureScreenshot(options) {
194
212
  this.assertEngineReadAllowed("captureScreenshot");
195
213
  const { targetId, timeoutMs } = options;
214
+ if (options.view === "current") {
215
+ if (targetId !== undefined || options.targetIds !== undefined) {
216
+ throw new KakapoValidationError("A current-view screenshot cannot specify framing targets.", {
217
+ code: "AMBIGUOUS_SCREENSHOT_TARGET",
218
+ operation: "captureScreenshot",
219
+ expected: "current view without target selectors",
220
+ });
221
+ }
222
+ const frame = await this.captureRendererFrame(timeoutMs);
223
+ return { data: frame.payload, mediaType: "image/jpeg", width: frame.width, height: frame.height, view: "current", targetId: 0 };
224
+ }
196
225
  if (targetId !== undefined && options.targetIds !== undefined) {
197
226
  throw new KakapoValidationError("captureScreenshot accepts targetId or targetIds, not both.", {
198
227
  code: "AMBIGUOUS_SCREENSHOT_TARGET",
@@ -264,8 +293,7 @@ export class KakapoAPI {
264
293
  async captureRendererFrame(timeoutMs) {
265
294
  const token = this.newToken();
266
295
  const waiting = this.waitForToken(token, timeoutMs);
267
- await this.call("get_color_buffer", [[token]]);
268
- const message = await waiting;
296
+ const [, message] = await Promise.all([this.call("get_color_buffer", [[token]]), waiting]);
269
297
  if (!isBinaryFrame(message)) {
270
298
  const error = "error" in message ? String(message.error) : "Kakapo returned a non-binary renderer frame";
271
299
  throw new KakapoRpcError("get_color_buffer", error);
@@ -569,6 +597,10 @@ export class KakapoAPI {
569
597
  getNodeParent: (id) => this.getNodeParent(id),
570
598
  getNodeChildren: (id) => this.getNodeChildren(id),
571
599
  applyScenePatch: (operations) => this.applyScenePatch(operations),
600
+ getSceneExt: () => this.getSceneExt(),
601
+ patchSceneExt: (operations) => this.patchSceneExt(operations),
602
+ getSceneProperties: () => this.getSceneProperties(),
603
+ patchSceneProperties: (operations) => this.patchSceneProperties(operations),
572
604
  createNode: (input) => this.createNode(input),
573
605
  cloneNode: (id, options) => this.cloneNode(id, options),
574
606
  updateNode: (id, changes) => this.updateNode(id, changes),
package/dist/scene.js CHANGED
@@ -294,7 +294,7 @@ export function toPublicScene(raw) {
294
294
  return id === undefined ? [] : [[key, wireToMaterial(id, value)]];
295
295
  }));
296
296
  const scripts = Object.fromEntries(Object.entries(raw.storage.openscad?.scripts ?? {}).map(([id, value]) => [id, { id: Number(id), name: str(value.name, "OpenSCAD Script"), source: str(value.source) }]));
297
- return { version: raw.version ?? 0, name: raw.name ?? "", nodes, tree, materials, openScadScripts: scripts, properties: (raw.properties ?? {}), camera: (raw.camera ?? {}) };
297
+ return { version: raw.version ?? 0, name: raw.name ?? "", nodes, tree, materials, openScadScripts: scripts, properties: (raw.properties ?? {}), camera: (raw.camera ?? {}), ext: (raw.ext ?? {}) };
298
298
  }
299
299
  export function missingNode(id, operation) {
300
300
  return new KakapoValidationError(`Node ${id} does not exist.`, { code: "NODE_NOT_FOUND", operation, path: "nodeId", received: id, expected: "an existing node ID", hint: "Call listNodes() or refreshScene() and use a current node ID." });
package/dist/types.d.ts CHANGED
@@ -268,6 +268,12 @@ export interface Scene {
268
268
  openScadScripts: Record<string, OpenScadScript>;
269
269
  properties: Record<string, JsonValue>;
270
270
  camera: Record<string, JsonValue>;
271
+ /**
272
+ * Consumer-owned extension bag carried in the scene file. Kakapo does not
273
+ * interpret its contents; Womp stores node and material metadata here
274
+ * (e.g. `ext[nodeId].locked`, `ext.materialsExt[materialId].tier`).
275
+ */
276
+ ext: Record<string, JsonValue>;
271
277
  }
272
278
  export interface BoundingBox {
273
279
  min: Vec3;
@@ -316,7 +322,8 @@ export interface CaptureScreenshotOptions {
316
322
  /** Omit both selectors to frame all visible top-level scene nodes. */
317
323
  targetId?: number;
318
324
  targetIds?: number[];
319
- view?: ScreenshotView;
325
+ /** "current" captures the renderer's existing camera without framing or reading scene state. */
326
+ view?: ScreenshotView | "current";
320
327
  timeoutMs?: number;
321
328
  }
322
329
  export interface ScreenshotImage {
@@ -324,7 +331,7 @@ export interface ScreenshotImage {
324
331
  mediaType: "image/jpeg";
325
332
  width: number;
326
333
  height: number;
327
- view: ScreenshotView;
334
+ view: ScreenshotView | "current";
328
335
  targetId: number;
329
336
  }
330
337
  export type JsonPatchOperation = {
@@ -351,7 +358,17 @@ export interface ListNodeOptions {
351
358
  export interface DeleteNodeOptions {
352
359
  recursive?: boolean;
353
360
  }
361
+ /** An application can supply its existing connection; its adapter owns connection lifecycle. */
362
+ export interface KakapoAPITransport {
363
+ readonly connected: boolean;
364
+ connect(): Promise<void>;
365
+ disconnect(): void;
366
+ rpc<T>(method: string, params?: JsonValue[]): Promise<T>;
367
+ newToken(): string;
368
+ waitForToken(token: string, timeoutMs?: number): Promise<TokenMessage>;
369
+ }
354
370
  export interface KakapoAPIOptions {
371
+ transport?: KakapoAPITransport;
355
372
  url?: string;
356
373
  requestTimeoutMs?: number;
357
374
  connectTimeoutMs?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@womp/kakapo-sdk",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Typed Node.js and browser SDK for controlling Kakapo over WebSocket",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -20,7 +20,7 @@
20
20
  ],
21
21
  "sideEffects": false,
22
22
  "engines": {
23
- "node": ">=20"
23
+ "node": ">=22.0.0"
24
24
  },
25
25
  "repository": {
26
26
  "type": "git",