@womp/kakapo-sdk 0.2.2 → 0.3.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 +135 -87
- package/dist/api.d.ts +43 -1
- package/dist/api.js +324 -47
- package/dist/errors.d.ts +4 -0
- package/dist/errors.js +12 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/scene.d.ts +7 -2
- package/dist/scene.js +37 -6
- package/dist/types.d.ts +15 -0
- package/dist/validation.js +22 -2
- package/package.json +53 -53
package/README.md
CHANGED
|
@@ -1,87 +1,135 @@
|
|
|
1
|
-
# @womp/kakapo-sdk
|
|
2
|
-
|
|
3
|
-
Typed Node.js and browser SDK for manipulating a Kakapo scene through its JSON-RPC WebSocket.
|
|
4
|
-
|
|
5
|
-
```bash
|
|
6
|
-
yarn install
|
|
7
|
-
yarn build
|
|
8
|
-
```
|
|
9
|
-
|
|
10
|
-
```ts
|
|
11
|
-
import { KakapoAPI } from "@womp/kakapo-sdk";
|
|
12
|
-
|
|
13
|
-
const kakapo = new KakapoAPI();
|
|
14
|
-
await kakapo.connect();
|
|
15
|
-
|
|
16
|
-
const union = await kakapo.editScene((scene) => {
|
|
17
|
-
const root = scene.createNode({
|
|
18
|
-
kind: "union",
|
|
19
|
-
parentId: 0,
|
|
20
|
-
name: "Agent Model",
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
const box = scene.createNode({
|
|
24
|
-
kind: "primitive",
|
|
25
|
-
parentId: root.id,
|
|
26
|
-
name: "Body",
|
|
27
|
-
properties: {
|
|
28
|
-
primitive: "box",
|
|
29
|
-
transform: { scale: { x: 10, y: 4, z: 6 } },
|
|
30
|
-
},
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
const red = scene.createMaterial({
|
|
34
|
-
color: { x: 1, y: 0.05, z: 0.05 },
|
|
35
|
-
roughness: 0.4,
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
const body = scene.node(box.id, "primitive");
|
|
39
|
-
body.materialId = red.id;
|
|
40
|
-
body.position = { x: 0, y: 4, z: 0 };
|
|
41
|
-
body.round = 0.2;
|
|
42
|
-
body.save();
|
|
43
|
-
return root;
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
console.log(await kakapo.getNodeBoundingBox(union.id));
|
|
47
|
-
|
|
48
|
-
kakapo.disconnect();
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
The transport uses the browser's native `WebSocket` when bundled for the web and loads `ws`
|
|
52
|
-
dynamically in Node.js.
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
`
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
1
|
+
# @womp/kakapo-sdk
|
|
2
|
+
|
|
3
|
+
Typed Node.js and browser SDK for manipulating a Kakapo scene through its JSON-RPC WebSocket.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
yarn install
|
|
7
|
+
yarn build
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { KakapoAPI } from "@womp/kakapo-sdk";
|
|
12
|
+
|
|
13
|
+
const kakapo = new KakapoAPI();
|
|
14
|
+
await kakapo.connect();
|
|
15
|
+
|
|
16
|
+
const union = await kakapo.editScene((scene) => {
|
|
17
|
+
const root = scene.createNode({
|
|
18
|
+
kind: "union",
|
|
19
|
+
parentId: 0,
|
|
20
|
+
name: "Agent Model",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const box = scene.createNode({
|
|
24
|
+
kind: "primitive",
|
|
25
|
+
parentId: root.id,
|
|
26
|
+
name: "Body",
|
|
27
|
+
properties: {
|
|
28
|
+
primitive: "box",
|
|
29
|
+
transform: { scale: { x: 10, y: 4, z: 6 } },
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const red = scene.createMaterial({
|
|
34
|
+
color: { x: 1, y: 0.05, z: 0.05 },
|
|
35
|
+
roughness: 0.4,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const body = scene.node(box.id, "primitive");
|
|
39
|
+
body.materialId = red.id;
|
|
40
|
+
body.position = { x: 0, y: 4, z: 0 };
|
|
41
|
+
body.round = 0.2;
|
|
42
|
+
body.save();
|
|
43
|
+
return root;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
console.log(await kakapo.getNodeBoundingBox(union.id));
|
|
47
|
+
|
|
48
|
+
kakapo.disconnect();
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The transport uses the browser's native `WebSocket` when bundled for the web and loads `ws`
|
|
52
|
+
dynamically in Node.js.
|
|
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
|
+
Framed captures restore the camera they replaced after capture. If another client moves the camera
|
|
59
|
+
while capture is pending, the SDK detects that change and preserves the newer camera instead.
|
|
60
|
+
|
|
61
|
+
All public inputs are checked at runtime as well as by TypeScript. Invalid calls throw structured
|
|
62
|
+
`KakapoValidationError` objects with a stable `code`, offending `path`, expected rule, and a
|
|
63
|
+
correction `hint` intended for automated agents.
|
|
64
|
+
|
|
65
|
+
`editScene()` refreshes once, exposes synchronous JSON reads and mutations, then validates and sends
|
|
66
|
+
one RFC 6902 patch after the callback succeeds. Callback or commit failures discard the draft and
|
|
67
|
+
refresh the authoritative scene. `node(id, kind)` returns a typed editable handle; `save()` applies
|
|
68
|
+
its staged properties synchronously to the transaction draft.
|
|
69
|
+
|
|
70
|
+
Use the same SDK edit methods in browser and standalone hosts. Hosts coordinate connection ownership,
|
|
71
|
+
permissions, and local editor activity; the SDK reads the canonical scene and submits the change.
|
|
72
|
+
`getSceneSnapshotRevision(snapshot)` computes the same local fingerprint as `api.getSceneRevision()`;
|
|
73
|
+
neither fingerprint establishes a server-side atomic commit guarantee.
|
|
74
|
+
|
|
75
|
+
For incremental curve edits, use `SceneEdit.updateCurvePoint(id, index, changes)`,
|
|
76
|
+
`cloneCurvePoint(id, sourceIndex, insertionIndex)`, and `deleteCurvePoint(id, index)`.
|
|
77
|
+
These retain native per-point fields unknown to this SDK. Replacing a node's entire
|
|
78
|
+
typed `points` array replaces that data; use the granular operations when editing existing points.
|
|
79
|
+
|
|
80
|
+
Engine-backed reads such as bounding boxes and screenshots remain asynchronous. Run them before the
|
|
81
|
+
first draft mutation or after `editScene()` commits.
|
|
82
|
+
|
|
83
|
+
Each `editScene()` refreshes before creating its private draft. Top-level edits are serialized per
|
|
84
|
+
client. Changed drafts reread canonical state before submission and reject a changed baseline;
|
|
85
|
+
camera movement alone does not invalidate an ordinary scene edit. This read does not eliminate
|
|
86
|
+
a cross-client race between validation and submission through a backend wrapper. Commit patches add
|
|
87
|
+
RFC 6902 `test` guards only for the wrapper partitions the draft changes: `/ext` and the remaining
|
|
88
|
+
scene. This avoids turning a geometry-only edit into an unnecessary extension operation. Adding a
|
|
89
|
+
missing `/ext` root is rejected because the existing wrapper cannot guard that cross-partition change;
|
|
90
|
+
refresh or migrate the scene before retrying. Calling a scene-mutating method through raw `rpc()`
|
|
91
|
+
deliberately marks the cache stale.
|
|
92
|
+
|
|
93
|
+
Node IDs allocated by `createNode()` and `cloneNode()` increase for the lifetime of a `KakapoAPI`
|
|
94
|
+
instance. IDs observed in scene snapshots, draft patches, or numeric node-extension keys remain
|
|
95
|
+
reserved after deletion, rollback, and reconnect. This prevents delete-and-create edits from changing
|
|
96
|
+
an existing renderer object's type by reusing its ID. A new API instance starts above IDs in its current
|
|
97
|
+
snapshot; this is not a persistent project-wide allocator. Raw patches supply their own IDs and must
|
|
98
|
+
preserve object identity; concurrent clients remain subject to the scene revision guards.
|
|
99
|
+
|
|
100
|
+
`editSceneAndSave(callback)` submits the standard `scene_state_patch` command and, after
|
|
101
|
+
acknowledgement, calls `save_scene` once on the same connection. No new Kakapo command is required.
|
|
102
|
+
The result preserves the callback value separately from save status: `acknowledged` includes the
|
|
103
|
+
returned filename, `unknown` includes an error, and `not_needed` means the draft did not change.
|
|
104
|
+
A save acknowledgement does not prove a new durable snapshot: some endpoints return an existing
|
|
105
|
+
filename. Verify persistence through the owning service. Never repeat the edit to recover saving.
|
|
106
|
+
The backend wrapper may apply geometry before extension processing fails; patch errors and lost
|
|
107
|
+
replies therefore have an `unknown` outcome unless rejection before execution is proven. SDK state
|
|
108
|
+
is refreshed after submission, and no mutation is automatically replayed.
|
|
109
|
+
|
|
110
|
+
`getSceneRevision()` fingerprints the cached native scene or active draft, including renderer fields
|
|
111
|
+
not exposed by `getScene()`. Camera movement and object-key ordering do not affect it. Refresh before
|
|
112
|
+
inspection; this fingerprint is not a server-issued revision or an atomic commit guarantee through a wrapper.
|
|
113
|
+
|
|
114
|
+
Mesh, field, and decal names are syntax-checked, but their existence cannot be enumerated by the
|
|
115
|
+
current engine RPC surface. Font family/weight/style combinations are checked against `listFonts()`.
|
|
116
|
+
|
|
117
|
+
## Verification
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
yarn test # fake WebSocket protocol and API behavior
|
|
121
|
+
yarn test:live # launches the sibling Kakapo build and runs named live tests per API method
|
|
122
|
+
yarn test:all # runs both suites
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The live suite starts Kakapo once, runs methods sequentially, and reports failures as names such as
|
|
126
|
+
`live:setNodeParent` or `live:createNode:mesh`. Mutations refresh the authoritative engine scene
|
|
127
|
+
before asserting their result. See [TEST_MATRIX.md](./TEST_MATRIX.md) for the exact unit/live mapping.
|
|
128
|
+
|
|
129
|
+
Build the sibling engine before running live tests. On Windows the default executable is
|
|
130
|
+
`../kakapo/build/code/RelWithDebInfo/kakapo_app.exe`; on other platforms it is
|
|
131
|
+
`../kakapo/build/code/kakapo_app`. Set `KAKAPO_BINARY` to test another build explicitly.
|
|
132
|
+
|
|
133
|
+
### Importing catalog materials
|
|
134
|
+
|
|
135
|
+
Inside `editScene`, `scene.importMaterial({ data, shaders })` imports native renderer material data and shader slots. `data` retains opaque fields and constants. Each non-null shader gets a new scene-local ID; null slots remain zero. The returned material can be assigned with `setNodeMaterial`. Material and shader resources share the draft's commit and rollback boundary. The host supplies the catalog data; the SDK does not fetch account or asset services.
|
package/dist/api.d.ts
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { type NodeHandle } from "./node-handle.js";
|
|
2
|
-
import type { BoundingBox, CaptureScreenshotOptions, CreateNodeInput, DeleteNodeOptions, FindNodeOptions, FontFamily, JsonPatchOperation, JsonValue, KakapoAPIOptions, KakapoNode, NodeKind, ListNodeOptions, Material, MaterialData, NodeUpdate, OpenScadScript, OpenScadValidationResult, PendingResources, PrimitiveOperation, Scene, ScreenshotImage, SvgPath, TokenMessage, Transform, Vec3 } from "./types.js";
|
|
2
|
+
import type { BoundingBox, CaptureScreenshotOptions, CreateNodeInput, CurvePoint, DeleteNodeOptions, FindNodeOptions, FontFamily, JsonPatchOperation, JsonValue, KakapoAPIOptions, KakapoNode, NodeKind, ListNodeOptions, Material, MaterialData, MaterialResource, NodeUpdate, OpenScadScript, OpenScadValidationResult, PendingResources, PrimitiveOperation, Scene, ScreenshotImage, SvgPath, TokenMessage, Transform, Vec3 } from "./types.js";
|
|
3
|
+
export type SceneSaveResult = {
|
|
4
|
+
status: "not_needed";
|
|
5
|
+
} | {
|
|
6
|
+
status: "acknowledged";
|
|
7
|
+
filename: string;
|
|
8
|
+
} | {
|
|
9
|
+
status: "unknown";
|
|
10
|
+
error: string;
|
|
11
|
+
} | {
|
|
12
|
+
status: "failed";
|
|
13
|
+
error: string;
|
|
14
|
+
};
|
|
3
15
|
export interface SceneEdit {
|
|
4
16
|
getScene(): Scene;
|
|
5
17
|
listNodes(options?: ListNodeOptions): KakapoNode[];
|
|
@@ -15,6 +27,8 @@ export interface SceneEdit {
|
|
|
15
27
|
applyScenePatch(operations: JsonPatchOperation[]): Scene;
|
|
16
28
|
getSceneExt(): Record<string, JsonValue>;
|
|
17
29
|
patchSceneExt(operations: JsonPatchOperation[]): Scene;
|
|
30
|
+
getSceneStorage(): Record<string, JsonValue>;
|
|
31
|
+
patchSceneStorage(operations: JsonPatchOperation[]): Scene;
|
|
18
32
|
getSceneProperties(): Record<string, JsonValue>;
|
|
19
33
|
patchSceneProperties(operations: JsonPatchOperation[]): Scene;
|
|
20
34
|
createNode(input: CreateNodeInput): KakapoNode;
|
|
@@ -37,6 +51,9 @@ export interface SceneEdit {
|
|
|
37
51
|
setNodeOperation(id: number, operation: PrimitiveOperation): KakapoNode;
|
|
38
52
|
setTextContent(id: number, text: string): KakapoNode;
|
|
39
53
|
setTextFont(id: number, fontFamily: string, weight?: number, italic?: boolean): KakapoNode;
|
|
54
|
+
updateCurvePoint(id: number, index: number, changes: Partial<CurvePoint>): void;
|
|
55
|
+
cloneCurvePoint(id: number, sourceIndex: number, index: number): void;
|
|
56
|
+
deleteCurvePoint(id: number, index: number): void;
|
|
40
57
|
setSvgPaths(id: number, paths: SvgPath[]): KakapoNode;
|
|
41
58
|
setMeshSource(id: number, mesh: string, meshIndex?: number): KakapoNode;
|
|
42
59
|
setFieldSource(id: number, field: string): KakapoNode;
|
|
@@ -46,6 +63,7 @@ export interface SceneEdit {
|
|
|
46
63
|
listMaterials(): Material[];
|
|
47
64
|
getMaterial(id: number): Material;
|
|
48
65
|
createMaterial(data?: Partial<MaterialData>, shaderIds?: number[]): Material;
|
|
66
|
+
importMaterial(resource: MaterialResource): Material;
|
|
49
67
|
updateMaterial(id: number, data: Partial<MaterialData>, shaderIds?: number[]): Material;
|
|
50
68
|
deleteMaterial(id: number): void;
|
|
51
69
|
listOpenScadScripts(): OpenScadScript[];
|
|
@@ -66,6 +84,7 @@ export declare class KakapoAPI {
|
|
|
66
84
|
private draftScene?;
|
|
67
85
|
private draftDirty;
|
|
68
86
|
private rendererSyncRequired;
|
|
87
|
+
private nextNodeId;
|
|
69
88
|
constructor(options?: KakapoAPIOptions);
|
|
70
89
|
get isConnected(): boolean;
|
|
71
90
|
connect(): Promise<void>;
|
|
@@ -76,16 +95,33 @@ export declare class KakapoAPI {
|
|
|
76
95
|
waitForToken(token: string, timeoutMs?: number): Promise<TokenMessage>;
|
|
77
96
|
refreshScene(): Promise<Scene>;
|
|
78
97
|
getScene(): Scene;
|
|
98
|
+
/** Fingerprint of the cached scene or active draft, including unknown native fields.
|
|
99
|
+
* Camera movement is excluded. This is not a server-issued revision or commit fence.
|
|
100
|
+
*/
|
|
101
|
+
getSceneRevision(): Promise<string>;
|
|
102
|
+
/** Wait until the renderer has produced a frame after this API's latest edit. */
|
|
103
|
+
waitForRenderer(): Promise<void>;
|
|
79
104
|
editScene<T>(callback: (scene: SceneEdit) => T | Promise<T>): Promise<T>;
|
|
105
|
+
editSceneAndSave<T>(callback: (scene: SceneEdit) => T | Promise<T>): Promise<{
|
|
106
|
+
value: T;
|
|
107
|
+
save: SceneSaveResult;
|
|
108
|
+
}>;
|
|
109
|
+
private prepareDraft;
|
|
110
|
+
private runSceneEdit;
|
|
80
111
|
private applyScenePatch;
|
|
81
112
|
/** The scene's consumer-owned extension bag. Kakapo does not interpret it. */
|
|
82
113
|
getSceneExt(): Record<string, JsonValue>;
|
|
83
114
|
/** Apply JSON Patch operations rooted at the scene's `ext` bag. */
|
|
84
115
|
patchSceneExt(operations: JsonPatchOperation[]): Scene;
|
|
116
|
+
/** Consumer-owned resources stored in the scene file. */
|
|
117
|
+
getSceneStorage(): Record<string, JsonValue>;
|
|
118
|
+
/** Apply JSON Patch operations rooted at the scene's `storage` bag. */
|
|
119
|
+
patchSceneStorage(operations: JsonPatchOperation[]): Scene;
|
|
85
120
|
/** The scene's renderer/environment properties (HDR, background, and similar). */
|
|
86
121
|
getSceneProperties(): Record<string, JsonValue>;
|
|
87
122
|
/** Apply JSON Patch operations rooted at the scene's `properties` bag. */
|
|
88
123
|
patchSceneProperties(operations: JsonPatchOperation[]): Scene;
|
|
124
|
+
private patchSceneBag;
|
|
89
125
|
listNodes(options?: ListNodeOptions): KakapoNode[];
|
|
90
126
|
findNodes(options: FindNodeOptions): KakapoNode[];
|
|
91
127
|
getNode(id: number): KakapoNode;
|
|
@@ -102,9 +138,14 @@ export declare class KakapoAPI {
|
|
|
102
138
|
private ensureRendererReady;
|
|
103
139
|
private captureRendererFrame;
|
|
104
140
|
private screenshotDirection;
|
|
141
|
+
private reserveNodeIds;
|
|
105
142
|
private createNode;
|
|
106
143
|
private cloneNode;
|
|
107
144
|
private updateNode;
|
|
145
|
+
private curvePointDraft;
|
|
146
|
+
private updateCurvePoint;
|
|
147
|
+
private cloneCurvePoint;
|
|
148
|
+
private deleteCurvePoint;
|
|
108
149
|
private deleteNode;
|
|
109
150
|
private setNodeParent;
|
|
110
151
|
private setNodeName;
|
|
@@ -127,6 +168,7 @@ export declare class KakapoAPI {
|
|
|
127
168
|
listMaterials(): Material[];
|
|
128
169
|
getMaterial(id: number): Material;
|
|
129
170
|
private createMaterial;
|
|
171
|
+
private importMaterial;
|
|
130
172
|
private updateMaterial;
|
|
131
173
|
private deleteMaterial;
|
|
132
174
|
listOpenScadScripts(): OpenScadScript[];
|
package/dist/api.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { KakapoConnectionError, KakapoRpcError, KakapoValidationError, } from "./errors.js";
|
|
1
|
+
import { KakapoConnectionError, KakapoRpcError, KakapoSceneCommitError, KakapoValidationError, } from "./errors.js";
|
|
2
2
|
import jsonpatch from "fast-json-patch";
|
|
3
|
-
import { applyJsonPatch, deepClone, defaultMaterialData, defaultNode, encodeTree, materialToWire, missingNode, nextId, nodeToWire, orderedChildren, parseSafePositiveId, parseJsonResult, toPublicScene, wireToMaterial, wireToNode, } from "./scene.js";
|
|
3
|
+
import { applyJsonPatch, deepClone, defaultMaterialData, defaultNode, encodeTree, encodeCurvePoint, escapePointer, getSceneSnapshotRevision, materialToWire, missingNode, nextId, nodeToWire, orderedChildren, parseSafePositiveId, parseJsonResult, toPublicScene, wireToMaterial, wireToNode, } from "./scene.js";
|
|
4
4
|
import { KakapoTransport } from "./transport.js";
|
|
5
5
|
import { createNodeHandle } from "./node-handle.js";
|
|
6
|
-
import { assertFont, assertId, assertMaterial, assertScene } from "./validation.js";
|
|
6
|
+
import { assertFont, assertId, assertMaterial, assertNode, assertScene } from "./validation.js";
|
|
7
7
|
const SCENE_MUTATING_RPCS = new Set([
|
|
8
8
|
"scene_state_patch", "scene_state_set", "load_scene", "replace_scene", "undo", "redo", "convert_scene",
|
|
9
9
|
]);
|
|
@@ -24,6 +24,7 @@ export class KakapoAPI {
|
|
|
24
24
|
draftScene;
|
|
25
25
|
draftDirty = false;
|
|
26
26
|
rendererSyncRequired = false;
|
|
27
|
+
nextNodeId = 1;
|
|
27
28
|
constructor(options = {}) {
|
|
28
29
|
this.transport = options.transport ?? new KakapoTransport({
|
|
29
30
|
url: options.url ?? "ws://127.0.0.1:5502",
|
|
@@ -33,12 +34,14 @@ export class KakapoAPI {
|
|
|
33
34
|
}
|
|
34
35
|
get isConnected() { return this.transport.connected; }
|
|
35
36
|
async connect() {
|
|
36
|
-
await this.transport.connect();
|
|
37
37
|
try {
|
|
38
|
+
await this.transport.connect();
|
|
38
39
|
await this.ping();
|
|
39
40
|
await this.refreshScene();
|
|
40
41
|
}
|
|
41
42
|
catch (error) {
|
|
43
|
+
if (error instanceof KakapoConnectionError && error.code === "CONNECTION_IN_PROGRESS")
|
|
44
|
+
throw error;
|
|
42
45
|
this.disconnect();
|
|
43
46
|
throw error;
|
|
44
47
|
}
|
|
@@ -75,6 +78,7 @@ export class KakapoAPI {
|
|
|
75
78
|
const result = await this.call("scene_state_get");
|
|
76
79
|
const scene = parseJsonResult(result, "scene_state_get");
|
|
77
80
|
assertScene(scene, "refreshScene");
|
|
81
|
+
this.reserveNodeIds(scene);
|
|
78
82
|
this.rawScene = scene;
|
|
79
83
|
this.cacheValid = true;
|
|
80
84
|
return this.getScene();
|
|
@@ -82,24 +86,107 @@ export class KakapoAPI {
|
|
|
82
86
|
getScene() {
|
|
83
87
|
return deepClone(toPublicScene(this.requireScene("getScene")));
|
|
84
88
|
}
|
|
89
|
+
/** Fingerprint of the cached scene or active draft, including unknown native fields.
|
|
90
|
+
* Camera movement is excluded. This is not a server-issued revision or commit fence.
|
|
91
|
+
*/
|
|
92
|
+
async getSceneRevision() {
|
|
93
|
+
return getSceneSnapshotRevision(this.requireScene("getSceneRevision"));
|
|
94
|
+
}
|
|
95
|
+
/** Wait until the renderer has produced a frame after this API's latest edit. */
|
|
96
|
+
async waitForRenderer() {
|
|
97
|
+
await this.ensureRendererReady();
|
|
98
|
+
}
|
|
85
99
|
editScene(callback) {
|
|
100
|
+
return this.runSceneEdit(callback, false).then(({ value }) => value);
|
|
101
|
+
}
|
|
102
|
+
editSceneAndSave(callback) {
|
|
103
|
+
return this.runSceneEdit(callback, true);
|
|
104
|
+
}
|
|
105
|
+
async prepareDraft(baseline, callback) {
|
|
106
|
+
this.draftScene = deepClone(baseline);
|
|
107
|
+
this.draftDirty = false;
|
|
108
|
+
let active = true;
|
|
109
|
+
const edit = this.createSceneEdit(() => {
|
|
110
|
+
if (active)
|
|
111
|
+
return;
|
|
112
|
+
throw new KakapoValidationError("The scene draft has expired.", {
|
|
113
|
+
code: "EXPIRED_SCENE_DRAFT",
|
|
114
|
+
operation: "editScene",
|
|
115
|
+
expected: "a draft used only inside its own callback",
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
let value;
|
|
119
|
+
try {
|
|
120
|
+
value = await callback(edit);
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
active = false;
|
|
124
|
+
}
|
|
125
|
+
const scene = this.requireDraft("editScene");
|
|
126
|
+
assertScene(scene, "editScene");
|
|
127
|
+
const operations = jsonpatch.compare(baseline, scene);
|
|
128
|
+
return { value, operations, scene };
|
|
129
|
+
}
|
|
130
|
+
runSceneEdit(callback, saveAfterPatch) {
|
|
86
131
|
const run = this.transactionTail.then(async () => {
|
|
87
132
|
await this.refreshScene();
|
|
88
133
|
const baseline = deepClone(this.requireScene("editScene"));
|
|
89
|
-
this.draftScene = deepClone(baseline);
|
|
90
|
-
this.draftDirty = false;
|
|
91
134
|
try {
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
assertScene(draft, "editScene");
|
|
95
|
-
const operations = jsonpatch.compare(baseline, draft);
|
|
135
|
+
const { value, operations, scene } = await this.prepareDraft(baseline, callback);
|
|
136
|
+
let save = { status: "not_needed" };
|
|
96
137
|
if (operations.length) {
|
|
97
|
-
|
|
138
|
+
const addsRootField = operations.some((operation) => operation.op === "add" && operation.path.split("/").length === 2);
|
|
139
|
+
const addsExtRoot = operations.some((operation) => operation.op === "add" && operation.path === "/ext");
|
|
140
|
+
if (addsExtRoot) {
|
|
141
|
+
throw new KakapoValidationError("Cannot safely add a missing scene extension root through a partitioned endpoint.", {
|
|
142
|
+
code: "UNSUPPORTED_PATCH_GUARD",
|
|
143
|
+
operation: "editScene",
|
|
144
|
+
path: "/ext",
|
|
145
|
+
expected: "an existing ext root",
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
const changesCamera = operations.some((operation) => operation.path === "/camera" || operation.path.startsWith("/camera/"));
|
|
149
|
+
const changesExt = operations.some((operation) => operation.path === "/ext" || operation.path.startsWith("/ext/"));
|
|
150
|
+
const changesNonExt = operations.some((operation) => operation.path !== "/ext" && !operation.path.startsWith("/ext/"));
|
|
151
|
+
const latest = parseJsonResult(await this.call("scene_state_get"), "scene_state_get");
|
|
152
|
+
assertScene(latest, "editScene");
|
|
153
|
+
const stale = jsonpatch.compare(baseline, latest).some((operation) => changesCamera || (operation.path !== "/camera" && !operation.path.startsWith("/camera/")));
|
|
154
|
+
if (stale)
|
|
155
|
+
throw new KakapoSceneCommitError("rolled_back", new Error("Scene changed during preparation. Read it again before editing."));
|
|
156
|
+
// Raw renderers evaluate these together; wrappers may partition the patch.
|
|
157
|
+
// A failed response cannot establish that no earlier group was applied.
|
|
158
|
+
const guards = [];
|
|
159
|
+
if (changesNonExt) {
|
|
160
|
+
if (addsRootField)
|
|
161
|
+
guards.push({ op: "test", path: "", value: baseline });
|
|
162
|
+
else
|
|
163
|
+
guards.push(...Object.entries(baseline)
|
|
164
|
+
.filter(([key]) => key !== "ext" && (key !== "camera" || changesCamera))
|
|
165
|
+
.map(([key, value]) => ({
|
|
166
|
+
op: "test",
|
|
167
|
+
path: `/${escapePointer(key)}`,
|
|
168
|
+
value: value,
|
|
169
|
+
})));
|
|
170
|
+
}
|
|
171
|
+
if (changesExt)
|
|
172
|
+
guards.push({ op: "test", path: "/ext", value: baseline.ext });
|
|
173
|
+
save = await this.sendPatch([...guards, ...operations], saveAfterPatch);
|
|
98
174
|
this.rendererSyncRequired = true;
|
|
99
175
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
176
|
+
if (operations.length) {
|
|
177
|
+
this.cacheValid = false;
|
|
178
|
+
try {
|
|
179
|
+
await this.refreshScene();
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// Keep the acknowledged result; the next read must refresh the invalid cache.
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
this.rawScene = scene;
|
|
187
|
+
this.cacheValid = true;
|
|
188
|
+
}
|
|
189
|
+
return { value, save };
|
|
103
190
|
}
|
|
104
191
|
catch (error) {
|
|
105
192
|
this.cacheValid = false;
|
|
@@ -122,6 +209,7 @@ export class KakapoAPI {
|
|
|
122
209
|
applyScenePatch(operations) {
|
|
123
210
|
const candidate = applyJsonPatch(this.requireDraft("applyScenePatch"), operations);
|
|
124
211
|
assertScene(candidate, "applyScenePatch");
|
|
212
|
+
this.reserveNodeIds(candidate);
|
|
125
213
|
this.draftScene = candidate;
|
|
126
214
|
this.draftDirty = true;
|
|
127
215
|
return this.getScene();
|
|
@@ -133,7 +221,15 @@ export class KakapoAPI {
|
|
|
133
221
|
}
|
|
134
222
|
/** Apply JSON Patch operations rooted at the scene's `ext` bag. */
|
|
135
223
|
patchSceneExt(operations) {
|
|
136
|
-
return this.
|
|
224
|
+
return this.patchSceneBag("ext", operations);
|
|
225
|
+
}
|
|
226
|
+
/** Consumer-owned resources stored in the scene file. */
|
|
227
|
+
getSceneStorage() {
|
|
228
|
+
return deepClone(this.requireScene("getSceneStorage").storage);
|
|
229
|
+
}
|
|
230
|
+
/** Apply JSON Patch operations rooted at the scene's `storage` bag. */
|
|
231
|
+
patchSceneStorage(operations) {
|
|
232
|
+
return this.patchSceneBag("storage", operations);
|
|
137
233
|
}
|
|
138
234
|
/** The scene's renderer/environment properties (HDR, background, and similar). */
|
|
139
235
|
getSceneProperties() {
|
|
@@ -142,7 +238,24 @@ export class KakapoAPI {
|
|
|
142
238
|
}
|
|
143
239
|
/** Apply JSON Patch operations rooted at the scene's `properties` bag. */
|
|
144
240
|
patchSceneProperties(operations) {
|
|
145
|
-
return this.
|
|
241
|
+
return this.patchSceneBag("properties", operations);
|
|
242
|
+
}
|
|
243
|
+
patchSceneBag(key, operations) {
|
|
244
|
+
const draft = this.requireDraft("patchSceneBag");
|
|
245
|
+
if (operations.length === 0)
|
|
246
|
+
return this.getScene();
|
|
247
|
+
const prefix = `/${key}`;
|
|
248
|
+
const patch = draft[key] === undefined
|
|
249
|
+
? [{ op: "add", path: prefix, value: {} }]
|
|
250
|
+
: [];
|
|
251
|
+
for (const operation of operations) {
|
|
252
|
+
patch.push({
|
|
253
|
+
...operation,
|
|
254
|
+
path: `${prefix}${operation.path}`,
|
|
255
|
+
...("from" in operation ? { from: `${prefix}${operation.from}` } : {}),
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
return this.applyScenePatch(patch);
|
|
146
259
|
}
|
|
147
260
|
listNodes(options = {}) {
|
|
148
261
|
const scene = this.requireScene("listNodes");
|
|
@@ -257,23 +370,35 @@ export class KakapoAPI {
|
|
|
257
370
|
z: (bounds.max.z - bounds.min.z) * framePadding,
|
|
258
371
|
};
|
|
259
372
|
const distance = Math.max(Math.hypot(size.x, size.y, size.z), 1);
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
x:
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
373
|
+
const originalCamera = parseJsonResult(await this.call("get_cam"), "get_cam");
|
|
374
|
+
let framedCamera;
|
|
375
|
+
let message;
|
|
376
|
+
try {
|
|
377
|
+
await this.call("set_camera", [{
|
|
378
|
+
...originalCamera,
|
|
379
|
+
cent: center,
|
|
380
|
+
dir: { x: direction.x, y: direction.y, z: direction.z },
|
|
381
|
+
up: view === "top" || view === "bottom" ? { x: 0, y: 0, z: 1 } : { x: 0, y: 1, z: 0 },
|
|
382
|
+
pos: {
|
|
383
|
+
x: center.x - direction.x * distance,
|
|
384
|
+
y: center.y - direction.y * distance,
|
|
385
|
+
z: center.z - direction.z * distance,
|
|
386
|
+
},
|
|
387
|
+
}, true]);
|
|
388
|
+
const framed = await this.call("center_cam", [center, size]);
|
|
389
|
+
if (this.rawScene)
|
|
390
|
+
this.rawScene.camera = { ...this.rawScene.camera, ...framed };
|
|
391
|
+
framedCamera = parseJsonResult(await this.call("get_cam"), "get_cam");
|
|
392
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
393
|
+
message = await this.captureRendererFrame(timeoutMs);
|
|
394
|
+
}
|
|
395
|
+
finally {
|
|
396
|
+
if (framedCamera !== undefined) {
|
|
397
|
+
const latestCamera = parseJsonResult(await this.call("get_cam"), "get_cam");
|
|
398
|
+
if (jsonpatch.compare(framedCamera, latestCamera).length === 0)
|
|
399
|
+
await this.call("set_camera", [originalCamera, true]);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
277
402
|
return {
|
|
278
403
|
data: message.payload,
|
|
279
404
|
mediaType: "image/jpeg",
|
|
@@ -317,12 +442,24 @@ export class KakapoAPI {
|
|
|
317
442
|
};
|
|
318
443
|
return directions[view];
|
|
319
444
|
}
|
|
445
|
+
reserveNodeIds(scene) {
|
|
446
|
+
// Never rewind after deletion, rollback, or reconnect: old IDs may still be referenced.
|
|
447
|
+
for (const record of [scene.nodes, scene.ext ?? {}]) {
|
|
448
|
+
for (const key of Object.keys(record)) {
|
|
449
|
+
const id = Number(key);
|
|
450
|
+
if (Number.isSafeInteger(id) && id >= this.nextNodeId)
|
|
451
|
+
this.nextNodeId = id + 1;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
320
455
|
createNode(input) {
|
|
321
456
|
const scene = this.requireDraft("createNode");
|
|
322
457
|
assertId(input.parentId, "createNode", "parentId", true);
|
|
323
458
|
if (!scene.nodes[String(input.parentId)])
|
|
324
459
|
throw missingNode(input.parentId, "createNode");
|
|
325
|
-
const id =
|
|
460
|
+
const id = this.nextNodeId;
|
|
461
|
+
assertId(id, "createNode", "nodeId");
|
|
462
|
+
this.nextNodeId++;
|
|
326
463
|
const initial = defaultNode(input.kind, id, input.name ?? "");
|
|
327
464
|
const node = this.mergeNode(initial, input.properties ?? {});
|
|
328
465
|
const children = this.treeArrays(scene);
|
|
@@ -349,12 +486,77 @@ export class KakapoAPI {
|
|
|
349
486
|
const scene = this.requireDraft("updateNode");
|
|
350
487
|
if (id === 0 && ("hidden" in changes || "transform" in changes))
|
|
351
488
|
throw this.rootError("updateNode");
|
|
352
|
-
const
|
|
353
|
-
|
|
489
|
+
const current = wireToNode(scene, id);
|
|
490
|
+
const updated = this.mergeNode(current, changes);
|
|
491
|
+
assertNode(updated, scene, "updateNode");
|
|
492
|
+
const before = nodeToWire(current);
|
|
493
|
+
const after = nodeToWire(updated);
|
|
494
|
+
// Only replace fields changed through the typed API. Re-encoding the whole
|
|
495
|
+
// node discards renderer fields this SDK version does not know about.
|
|
496
|
+
const raw = deepClone(scene.nodes[String(id)]);
|
|
497
|
+
for (const key of new Set([...Object.keys(before), ...Object.keys(after)])) {
|
|
498
|
+
if (JSON.stringify(before[key]) === JSON.stringify(after[key]))
|
|
499
|
+
continue;
|
|
500
|
+
if (Object.hasOwn(after, key))
|
|
501
|
+
raw[key] = after[key];
|
|
502
|
+
else
|
|
503
|
+
delete raw[key];
|
|
504
|
+
}
|
|
505
|
+
scene.nodes[String(id)] = raw;
|
|
354
506
|
scene.ext = this.extWithNode(scene, updated);
|
|
355
507
|
this.draftDirty = true;
|
|
356
508
|
return deepClone(wireToNode(scene, id));
|
|
357
509
|
}
|
|
510
|
+
curvePointDraft(id, index, operation) {
|
|
511
|
+
const scene = this.requireDraft(operation);
|
|
512
|
+
const node = this.getNode(id);
|
|
513
|
+
if (node.kind !== "curve")
|
|
514
|
+
throw this.unsupported(operation, id, "curve points");
|
|
515
|
+
if (!Number.isInteger(index) || index < 0 || index >= node.points.length)
|
|
516
|
+
throw new KakapoValidationError("Curve point index is out of range.", {
|
|
517
|
+
code: "INVALID_CURVE_POINT_INDEX", operation, received: index,
|
|
518
|
+
expected: `integer from 0 through ${node.points.length - 1}`,
|
|
519
|
+
});
|
|
520
|
+
return { scene, node, points: scene.nodes[String(id)].points };
|
|
521
|
+
}
|
|
522
|
+
updateCurvePoint(id, index, changes) {
|
|
523
|
+
const { scene, node, points } = this.curvePointDraft(id, index, "updateCurvePoint");
|
|
524
|
+
const previous = node.points[index];
|
|
525
|
+
const next = { ...previous, ...deepClone(changes) };
|
|
526
|
+
node.points[index] = next;
|
|
527
|
+
assertNode(node, scene, "updateCurvePoint");
|
|
528
|
+
const before = encodeCurvePoint(previous);
|
|
529
|
+
const after = encodeCurvePoint(next);
|
|
530
|
+
// Edit only requested typed fields; retain opaque native data on this point.
|
|
531
|
+
for (const key of new Set([...Object.keys(before), ...Object.keys(after)])) {
|
|
532
|
+
if (JSON.stringify(before[key]) === JSON.stringify(after[key]))
|
|
533
|
+
continue;
|
|
534
|
+
if (Object.hasOwn(after, key))
|
|
535
|
+
points[index][key] = after[key];
|
|
536
|
+
else
|
|
537
|
+
delete points[index][key];
|
|
538
|
+
}
|
|
539
|
+
this.draftDirty = true;
|
|
540
|
+
}
|
|
541
|
+
cloneCurvePoint(id, sourceIndex, index) {
|
|
542
|
+
const { points } = this.curvePointDraft(id, sourceIndex, "cloneCurvePoint");
|
|
543
|
+
if (!Number.isInteger(index) || index < 0 || index > points.length)
|
|
544
|
+
throw new KakapoValidationError("Curve insertion index is out of range.", {
|
|
545
|
+
code: "INVALID_CURVE_POINT_INDEX", operation: "cloneCurvePoint", received: index,
|
|
546
|
+
expected: `integer from 0 through ${points.length}`,
|
|
547
|
+
});
|
|
548
|
+
points.splice(index, 0, deepClone(points[sourceIndex]));
|
|
549
|
+
this.draftDirty = true;
|
|
550
|
+
}
|
|
551
|
+
deleteCurvePoint(id, index) {
|
|
552
|
+
const { points } = this.curvePointDraft(id, index, "deleteCurvePoint");
|
|
553
|
+
if (points.length <= 1)
|
|
554
|
+
throw new KakapoValidationError("A curve must keep at least one point.", {
|
|
555
|
+
code: "INVALID_CURVE_POINT_COUNT", operation: "deleteCurvePoint", expected: "at least one point",
|
|
556
|
+
});
|
|
557
|
+
points.splice(index, 1);
|
|
558
|
+
this.draftDirty = true;
|
|
559
|
+
}
|
|
358
560
|
deleteNode(id, options = {}) {
|
|
359
561
|
const scene = this.requireDraft("deleteNode");
|
|
360
562
|
if (id === 0)
|
|
@@ -470,6 +672,25 @@ export class KakapoAPI {
|
|
|
470
672
|
this.draftDirty = true;
|
|
471
673
|
return deepClone(material);
|
|
472
674
|
}
|
|
675
|
+
importMaterial(resource) {
|
|
676
|
+
const scene = this.requireDraft("importMaterial");
|
|
677
|
+
const source = deepClone(resource);
|
|
678
|
+
const materials = scene.storage.materials ?? (scene.storage.materials = {});
|
|
679
|
+
const id = nextId(materials);
|
|
680
|
+
assertMaterial(wireToMaterial(id, { data: source.data }).data, "importMaterial");
|
|
681
|
+
const shaders = scene.storage.shaders ?? (scene.storage.shaders = {});
|
|
682
|
+
const shaderIds = source.shaders.map((shader) => {
|
|
683
|
+
if (shader === null)
|
|
684
|
+
return 0;
|
|
685
|
+
const shaderId = nextId(shaders);
|
|
686
|
+
shaders[String(shaderId)] = shader;
|
|
687
|
+
return shaderId;
|
|
688
|
+
});
|
|
689
|
+
const material = { data: source.data, ...(shaderIds.length ? { shaders: shaderIds } : {}) };
|
|
690
|
+
materials[String(id)] = material;
|
|
691
|
+
this.draftDirty = true;
|
|
692
|
+
return deepClone(wireToMaterial(id, material));
|
|
693
|
+
}
|
|
473
694
|
updateMaterial(id, data, shaderIds) {
|
|
474
695
|
const scene = this.requireDraft("updateMaterial");
|
|
475
696
|
const currentRaw = scene.storage.materials?.[String(id)];
|
|
@@ -478,10 +699,21 @@ export class KakapoAPI {
|
|
|
478
699
|
const current = wireToMaterial(id, currentRaw);
|
|
479
700
|
const updated = { id, data: this.mergeMaterialData(current.data, data), shaderIds: shaderIds ? [...shaderIds] : current.shaderIds };
|
|
480
701
|
assertMaterial(updated.data, "updateMaterial");
|
|
702
|
+
const before = materialToWire(current);
|
|
481
703
|
const next = materialToWire(updated);
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
704
|
+
const raw = { ...currentRaw, data: { ...currentRaw.data } };
|
|
705
|
+
for (const [key, value] of Object.entries(next.data)) {
|
|
706
|
+
if (value !== before.data[key])
|
|
707
|
+
raw.data[key] = value;
|
|
708
|
+
}
|
|
709
|
+
if (shaderIds !== undefined) {
|
|
710
|
+
delete raw.shader;
|
|
711
|
+
if (next.shaders)
|
|
712
|
+
raw.shaders = next.shaders;
|
|
713
|
+
else
|
|
714
|
+
delete raw.shaders;
|
|
715
|
+
}
|
|
716
|
+
scene.storage.materials[String(id)] = raw;
|
|
485
717
|
this.draftDirty = true;
|
|
486
718
|
return deepClone(updated);
|
|
487
719
|
}
|
|
@@ -542,10 +774,34 @@ export class KakapoAPI {
|
|
|
542
774
|
async reloadTextures() { return this.call("reload_textures"); }
|
|
543
775
|
async getPendingResources() { return parseJsonResult(await this.call("get_pending_resources"), "get_pending_resources"); }
|
|
544
776
|
call(method, params = []) { return this.transport.rpc(method, params); }
|
|
545
|
-
async sendPatch(operations) {
|
|
546
|
-
|
|
777
|
+
async sendPatch(operations, saveAfterPatch) {
|
|
778
|
+
let result;
|
|
779
|
+
try {
|
|
780
|
+
result = await this.call("scene_state_patch", [JSON.stringify(operations)]);
|
|
781
|
+
}
|
|
782
|
+
catch (cause) {
|
|
783
|
+
if (cause instanceof KakapoRpcError && cause.rpcCode === -32601)
|
|
784
|
+
throw new KakapoSceneCommitError("rolled_back", cause);
|
|
785
|
+
throw new KakapoSceneCommitError("unknown", cause);
|
|
786
|
+
}
|
|
547
787
|
if (result !== undefined && result !== null && result !== "") {
|
|
548
|
-
|
|
788
|
+
const message = typeof result === "object" && "message" in result && typeof result.message === "string"
|
|
789
|
+
? result.message
|
|
790
|
+
: JSON.stringify(result);
|
|
791
|
+
// The wrapper can apply geometry before a later extension patch fails.
|
|
792
|
+
throw new KakapoSceneCommitError("unknown", new KakapoRpcError("scene_state_patch", message));
|
|
793
|
+
}
|
|
794
|
+
if (!saveAfterPatch)
|
|
795
|
+
return { status: "not_needed" };
|
|
796
|
+
try {
|
|
797
|
+
const saved = await this.call("save_scene");
|
|
798
|
+
// A filename acknowledges the save handler, not a new durable snapshot.
|
|
799
|
+
if (typeof saved === "string" && saved.length > 0 && !saved.startsWith("{") && !saved.startsWith("["))
|
|
800
|
+
return { status: "acknowledged", filename: saved };
|
|
801
|
+
return { status: "unknown", error: `Save was not confirmed: ${JSON.stringify(saved)}` };
|
|
802
|
+
}
|
|
803
|
+
catch (error) {
|
|
804
|
+
return { status: "unknown", error: error instanceof Error ? error.message : String(error) };
|
|
549
805
|
}
|
|
550
806
|
}
|
|
551
807
|
saveNode(id, changes) {
|
|
@@ -586,19 +842,24 @@ export class KakapoAPI {
|
|
|
586
842
|
hint: "Finish the transaction, then perform the engine-backed read.",
|
|
587
843
|
});
|
|
588
844
|
}
|
|
589
|
-
createSceneEdit() {
|
|
590
|
-
|
|
845
|
+
createSceneEdit(assertActive) {
|
|
846
|
+
const edit = {
|
|
591
847
|
getScene: () => this.getScene(),
|
|
592
848
|
listNodes: (options) => this.listNodes(options),
|
|
593
849
|
findNodes: (options) => this.findNodes(options),
|
|
594
850
|
getNode: (id) => this.getNode(id),
|
|
595
|
-
node: ((id, kind) =>
|
|
851
|
+
node: ((id, kind) => createNodeHandle({
|
|
852
|
+
readNode: (nodeId) => { assertActive(); return this.getNode(nodeId); },
|
|
853
|
+
saveNode: (nodeId, changes) => { assertActive(); return this.saveNode(nodeId, changes); },
|
|
854
|
+
}, id, kind)),
|
|
596
855
|
getNodeName: (id) => this.getNodeName(id),
|
|
597
856
|
getNodeParent: (id) => this.getNodeParent(id),
|
|
598
857
|
getNodeChildren: (id) => this.getNodeChildren(id),
|
|
599
858
|
applyScenePatch: (operations) => this.applyScenePatch(operations),
|
|
600
859
|
getSceneExt: () => this.getSceneExt(),
|
|
601
860
|
patchSceneExt: (operations) => this.patchSceneExt(operations),
|
|
861
|
+
getSceneStorage: () => this.getSceneStorage(),
|
|
862
|
+
patchSceneStorage: (operations) => this.patchSceneStorage(operations),
|
|
602
863
|
getSceneProperties: () => this.getSceneProperties(),
|
|
603
864
|
patchSceneProperties: (operations) => this.patchSceneProperties(operations),
|
|
604
865
|
createNode: (input) => this.createNode(input),
|
|
@@ -617,6 +878,9 @@ export class KakapoAPI {
|
|
|
617
878
|
setNodeOperation: (id, operation) => this.setNodeOperation(id, operation),
|
|
618
879
|
setTextContent: (id, text) => this.setTextContent(id, text),
|
|
619
880
|
setTextFont: (id, family, weight, italic) => this.setTextFont(id, family, weight, italic),
|
|
881
|
+
updateCurvePoint: (id, index, changes) => this.updateCurvePoint(id, index, changes),
|
|
882
|
+
cloneCurvePoint: (id, sourceIndex, index) => this.cloneCurvePoint(id, sourceIndex, index),
|
|
883
|
+
deleteCurvePoint: (id, index) => this.deleteCurvePoint(id, index),
|
|
620
884
|
setSvgPaths: (id, paths) => this.setSvgPaths(id, paths),
|
|
621
885
|
setMeshSource: (id, mesh, index) => this.setMeshSource(id, mesh, index),
|
|
622
886
|
setFieldSource: (id, field) => this.setFieldSource(id, field),
|
|
@@ -626,6 +890,7 @@ export class KakapoAPI {
|
|
|
626
890
|
listMaterials: () => this.listMaterials(),
|
|
627
891
|
getMaterial: (id) => this.getMaterial(id),
|
|
628
892
|
createMaterial: (data, shaderIds) => this.createMaterial(data, shaderIds),
|
|
893
|
+
importMaterial: (resource) => this.importMaterial(resource),
|
|
629
894
|
updateMaterial: (id, data, shaderIds) => this.updateMaterial(id, data, shaderIds),
|
|
630
895
|
deleteMaterial: (id) => this.deleteMaterial(id),
|
|
631
896
|
listOpenScadScripts: () => this.listOpenScadScripts(),
|
|
@@ -634,6 +899,18 @@ export class KakapoAPI {
|
|
|
634
899
|
updateOpenScadScript: (id, changes) => this.updateOpenScadScript(id, changes),
|
|
635
900
|
deleteOpenScadScript: (id) => this.deleteOpenScadScript(id),
|
|
636
901
|
};
|
|
902
|
+
return new Proxy(edit, {
|
|
903
|
+
get(target, property, receiver) {
|
|
904
|
+
assertActive();
|
|
905
|
+
const method = Reflect.get(target, property, receiver);
|
|
906
|
+
if (typeof method !== "function")
|
|
907
|
+
return method;
|
|
908
|
+
return (...args) => {
|
|
909
|
+
assertActive();
|
|
910
|
+
return Reflect.apply(method, target, args);
|
|
911
|
+
};
|
|
912
|
+
},
|
|
913
|
+
});
|
|
637
914
|
}
|
|
638
915
|
treeArrays(scene) { return Object.fromEntries(Object.keys(scene.tree ?? {}).map((id) => [id, orderedChildren(scene, Number(id))])); }
|
|
639
916
|
extWithName(scene, id, name) {
|
|
@@ -669,10 +946,10 @@ export class KakapoAPI {
|
|
|
669
946
|
const common = ["transform", "pickable"];
|
|
670
947
|
const byKind = {
|
|
671
948
|
primitive: ["hidden", "materialId", "operation", "blend", "color", "primitive", "round", "thickness", "inflation", "materialNeutralCutout", "mirrors"],
|
|
672
|
-
union: ["hidden", "operation", "blend", "resolution", "thickness", "inflation", "materialNeutralCutout"],
|
|
949
|
+
union: ["hidden", "operation", "blend", "resolution", "thickness", "inflation", "materialNeutralCutout", "scriptInstanceId"],
|
|
673
950
|
group: ["hidden"],
|
|
674
951
|
light: ["color", "power", "collimation", "lightType", "size", "textureFile"],
|
|
675
|
-
curve: ["hidden", "materialId", "operation", "blend", "primitive", "density", "roundness", "smoothing", "points", "materialNeutralCutout", "mirrors"],
|
|
952
|
+
curve: ["hidden", "materialId", "operation", "blend", "primitive", "density", "roundness", "smoothing", "points", "materialNeutralCutout", "mirrors", "version", "curveType", "closed"],
|
|
676
953
|
text: ["hidden", "materialId", "operation", "blend", "text", "fontFamily", "weight", "italic", "round", "width", "wrap", "align", "spacing", "lineHeight", "materialNeutralCutout"],
|
|
677
954
|
svg: ["hidden", "materialId", "operation", "blend", "paths", "inflate", "outline", "outlineSize", "materialNeutralCutout"],
|
|
678
955
|
mesh: ["hidden", "materialId", "mesh", "meshIndex", "smoothNormals", "colorSampling"],
|
package/dist/errors.d.ts
CHANGED
|
@@ -23,6 +23,10 @@ export declare class KakapoConnectionError extends KakapoError {
|
|
|
23
23
|
}
|
|
24
24
|
export declare class KakapoTimeoutError extends KakapoError {
|
|
25
25
|
}
|
|
26
|
+
export declare class KakapoSceneCommitError extends KakapoError {
|
|
27
|
+
readonly outcome: "rolled_back" | "unknown";
|
|
28
|
+
constructor(outcome: "rolled_back" | "unknown", cause: unknown);
|
|
29
|
+
}
|
|
26
30
|
export declare class KakapoRpcError extends KakapoError {
|
|
27
31
|
readonly rpcCode?: number;
|
|
28
32
|
readonly method: string;
|
package/dist/errors.js
CHANGED
|
@@ -35,6 +35,18 @@ export class KakapoConnectionError extends KakapoError {
|
|
|
35
35
|
}
|
|
36
36
|
export class KakapoTimeoutError extends KakapoError {
|
|
37
37
|
}
|
|
38
|
+
export class KakapoSceneCommitError extends KakapoError {
|
|
39
|
+
outcome;
|
|
40
|
+
constructor(outcome, cause) {
|
|
41
|
+
super(cause instanceof Error ? cause.message : String(cause), {
|
|
42
|
+
code: "SCENE_COMMIT_FAILED",
|
|
43
|
+
operation: "editScene",
|
|
44
|
+
details: { outcome },
|
|
45
|
+
cause,
|
|
46
|
+
});
|
|
47
|
+
this.outcome = outcome;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
38
50
|
export class KakapoRpcError extends KakapoError {
|
|
39
51
|
rpcCode;
|
|
40
52
|
method;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { KakapoAPI } from "./api.js";
|
|
2
|
-
export type { SceneEdit } from "./api.js";
|
|
3
|
-
export {
|
|
2
|
+
export type { SceneEdit, SceneSaveResult } from "./api.js";
|
|
3
|
+
export type { WireScene as NativeSceneSnapshot } from "./scene.js";
|
|
4
|
+
export { getSceneSnapshotRevision } from "./scene.js";
|
|
5
|
+
export { KakapoError, KakapoConnectionError, KakapoTimeoutError, KakapoSceneCommitError, KakapoRpcError, KakapoValidationError, } from "./errors.js";
|
|
4
6
|
export { parseBinaryFrame } from "./transport.js";
|
|
5
7
|
export type { NodeHandle, NodeHandleActions } from "./node-handle.js";
|
|
6
8
|
export type * from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { KakapoAPI } from "./api.js";
|
|
2
|
-
export {
|
|
2
|
+
export { getSceneSnapshotRevision } from "./scene.js";
|
|
3
|
+
export { KakapoError, KakapoConnectionError, KakapoTimeoutError, KakapoSceneCommitError, KakapoRpcError, KakapoValidationError, } from "./errors.js";
|
|
3
4
|
export { parseBinaryFrame } from "./transport.js";
|
package/dist/scene.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { KakapoValidationError } from "./errors.js";
|
|
2
|
-
import type { JsonPatchOperation, KakapoNode, Material, MaterialData, NodeKind, Scene, Transform } from "./types.js";
|
|
2
|
+
import type { CurvePoint, JsonPatchOperation, KakapoNode, Material, MaterialData, NodeKind, Scene, Transform } from "./types.js";
|
|
3
3
|
export interface WireNode {
|
|
4
4
|
type: number;
|
|
5
5
|
[key: string]: unknown;
|
|
@@ -8,7 +8,7 @@ export interface WireScene {
|
|
|
8
8
|
version?: number;
|
|
9
9
|
name?: string;
|
|
10
10
|
nodes: Record<string, WireNode>;
|
|
11
|
-
tree: Record<string, Record<string, number
|
|
11
|
+
tree: Record<string, Record<string, number> | number[]>;
|
|
12
12
|
ext?: Record<string, unknown>;
|
|
13
13
|
storage: {
|
|
14
14
|
materials?: Record<string, {
|
|
@@ -30,6 +30,10 @@ export interface WireScene {
|
|
|
30
30
|
camera?: Record<string, unknown>;
|
|
31
31
|
[key: string]: unknown;
|
|
32
32
|
}
|
|
33
|
+
/** Local scene fingerprint; camera navigation does not invalidate an editing baseline.
|
|
34
|
+
* This is not a server-issued revision or an atomic commit fence.
|
|
35
|
+
*/
|
|
36
|
+
export declare function getSceneSnapshotRevision(snapshot: WireScene): Promise<string>;
|
|
33
37
|
export declare const identityTransform: () => Transform;
|
|
34
38
|
export declare function deepClone<T>(value: T): T;
|
|
35
39
|
export declare function parseJsonResult<T>(value: unknown, operation: string): T;
|
|
@@ -40,6 +44,7 @@ export declare function orderedChildren(scene: WireScene, parentId: number): num
|
|
|
40
44
|
export declare function encodeTree(tree: Record<string, number[]>): WireScene["tree"];
|
|
41
45
|
export declare function wireToNode(scene: WireScene, id: number): KakapoNode;
|
|
42
46
|
export declare function defaultNode(kind: NodeKind, id: number, name?: string): KakapoNode;
|
|
47
|
+
export declare function encodeCurvePoint(point: CurvePoint): Record<string, unknown>;
|
|
43
48
|
export declare function nodeToWire(node: KakapoNode): WireNode;
|
|
44
49
|
export declare const defaultMaterialData: () => MaterialData;
|
|
45
50
|
export declare function wireToMaterial(id: number, raw: {
|
package/dist/scene.js
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
import { KakapoValidationError } from "./errors.js";
|
|
2
|
+
/** Local scene fingerprint; camera navigation does not invalidate an editing baseline.
|
|
3
|
+
* This is not a server-issued revision or an atomic commit fence.
|
|
4
|
+
*/
|
|
5
|
+
export async function getSceneSnapshotRevision(snapshot) {
|
|
6
|
+
const { camera: _camera, ...state } = snapshot;
|
|
7
|
+
const json = JSON.stringify(state, (_key, value) => {
|
|
8
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
9
|
+
return value;
|
|
10
|
+
return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
|
|
11
|
+
});
|
|
12
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(json));
|
|
13
|
+
return Array.from(new Uint8Array(digest), (value) => value.toString(16).padStart(2, "0")).join("");
|
|
14
|
+
}
|
|
2
15
|
const KINDS = [
|
|
3
16
|
"primitive", "union", "light", "curve", "group", "text", "field", "svg", "mesh", "decal", "openScad", "socket",
|
|
4
17
|
];
|
|
@@ -134,6 +147,8 @@ function decodeCurvePoint(value) {
|
|
|
134
147
|
return {
|
|
135
148
|
position: vec3(p.pos), rotation: vec3(p.rot), scale: vec3(p.size, v3(10)),
|
|
136
149
|
materialId: materialId(p.material_id), round: num(p.round, 0), fixed: bool(p.fixed, false),
|
|
150
|
+
...(p.handle_in !== undefined ? { handleIn: vec3(p.handle_in) } : {}),
|
|
151
|
+
...(p.handle_out !== undefined ? { handleOut: vec3(p.handle_out) } : {}),
|
|
137
152
|
};
|
|
138
153
|
}
|
|
139
154
|
function decodeSvgPaths(value) {
|
|
@@ -160,10 +175,17 @@ export function wireToNode(scene, id) {
|
|
|
160
175
|
const material = materialId(raw.material_id);
|
|
161
176
|
switch (base.kind) {
|
|
162
177
|
case "primitive": return { ...base, kind: "primitive", hidden, operation, blend: num(raw.blend, 0), color: vec3(raw.color, v3(1)), primitive: PRIMITIVES[num(raw.prim, 2)] ?? "box", round: num(raw.round, 0), thickness: num(raw.thickness, -1), inflation: num(raw.inflation, 0), materialId: material, materialNeutralCutout: bool(raw.material_neutral_cutout, false), mirrors: mirrorPlanes(scene, id, raw) };
|
|
163
|
-
case "union":
|
|
178
|
+
case "union": {
|
|
179
|
+
const scriptInstanceId = num(raw.script_id, 0xffffffff);
|
|
180
|
+
return {
|
|
181
|
+
...base, kind: "union", hidden, operation, blend: num(raw.blend, 0), resolution: num(raw.resolution, 0.3),
|
|
182
|
+
thickness: num(raw.thickness, -1), inflation: num(raw.inflation, 0), materialNeutralCutout: bool(raw.material_neutral_cutout, false),
|
|
183
|
+
...(Number.isSafeInteger(scriptInstanceId) && scriptInstanceId >= 0 && scriptInstanceId < 0xffffffff ? { scriptInstanceId } : {}),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
164
186
|
case "group": return { ...base, kind: "group", hidden };
|
|
165
187
|
case "light": return { ...base, kind: "light", color: vec3(raw.color, { x: 1, y: 1, z: 0.9 }), power: num(raw.power, 80), collimation: num(raw.collimation, 0), lightType: LIGHT_TYPES[num(raw.lightType, 1)] ?? "sphere", size: vec2(raw.size, v2(1, 1)), textureFile: str(raw.texture_file) };
|
|
166
|
-
case "curve": return { ...base, kind: "curve", hidden, materialId: material, operation, blend: num(raw.blend, 20), primitive: PRIMITIVES[num(raw.prim, 0)] ?? "sphere", density: num(raw.density, 100), roundness: num(raw.roundness, 1), smoothing: num(raw.smoothing, 0.02), materialNeutralCutout: bool(raw.material_neutral_cutout, false), points: Array.isArray(raw.points) ? raw.points.map(decodeCurvePoint) : [], mirrors: mirrorPlanes(scene, id, raw) };
|
|
188
|
+
case "curve": return { ...base, kind: "curve", version: num(raw.version, 2), curveType: raw.curve_type === 1 ? "bezier" : "spline", closed: bool(raw.closed, false), hidden, materialId: material, operation, blend: num(raw.blend, 20), primitive: PRIMITIVES[num(raw.prim, 0)] ?? "sphere", density: num(raw.density, 100), roundness: num(raw.roundness, 1), smoothing: num(raw.smoothing, 0.02), materialNeutralCutout: bool(raw.material_neutral_cutout, false), points: Array.isArray(raw.points) ? raw.points.map(decodeCurvePoint) : [], mirrors: mirrorPlanes(scene, id, raw) };
|
|
167
189
|
case "text": return { ...base, kind: "text", hidden, materialId: material, operation, blend: num(raw.blend, 0), text: str(raw.text, "Text"), fontFamily: str(raw.font, "OpenSans"), weight: num(raw.weight, 400), italic: bool(raw.italic, false), round: num(raw.round, 0), width: num(raw.width, 20), wrap: TEXT_WRAP[num(raw.wrap, 0)] ?? "disabled", align: TEXT_ALIGN[num(raw.align, 0)] ?? "left", spacing: num(raw.spacing, 0), lineHeight: num(raw.line_height, 1), materialNeutralCutout: bool(raw.material_neutral_cutout, false) };
|
|
168
190
|
case "svg": return { ...base, kind: "svg", hidden, materialId: material, operation, blend: num(raw.blend, 0), paths: decodeSvgPaths(raw.paths), inflate: bool(raw.inflate, false), outline: bool(raw.outline, false), outlineSize: num(raw.outline_size, 0.1), materialNeutralCutout: bool(raw.material_neutral_cutout, false) };
|
|
169
191
|
case "mesh": return { ...base, kind: "mesh", hidden, materialId: material, mesh: str(raw.mesh), meshIndex: num(raw.mesh_index, 0), smoothNormals: bool(raw.smooth_normals, false), colorSampling: FIELD_SAMPLING[num(raw.color_option, 2)] ?? "override" };
|
|
@@ -198,8 +220,13 @@ export function defaultNode(kind, id, name = "") {
|
|
|
198
220
|
case "socket": return { ...base, kind, hidden: false, tag: "" };
|
|
199
221
|
}
|
|
200
222
|
}
|
|
201
|
-
function encodeCurvePoint(point) {
|
|
202
|
-
return {
|
|
223
|
+
export function encodeCurvePoint(point) {
|
|
224
|
+
return {
|
|
225
|
+
pos: point.position, rot: point.rotation, size: point.scale,
|
|
226
|
+
material_id: point.materialId ?? 0, round: point.round, fixed: point.fixed,
|
|
227
|
+
...(point.handleIn !== undefined ? { handle_in: point.handleIn } : {}),
|
|
228
|
+
...(point.handleOut !== undefined ? { handle_out: point.handleOut } : {}),
|
|
229
|
+
};
|
|
203
230
|
}
|
|
204
231
|
function encodeSvgPaths(paths) {
|
|
205
232
|
return paths.map((path) => ({ closed: path.closed, segments: path.segments.map((s) => ({ point: s.point, handle_in: s.handleIn, handle_out: s.handleOut })) }));
|
|
@@ -221,13 +248,17 @@ export function nodeToWire(node) {
|
|
|
221
248
|
Object.assign(raw, { color: node.color, prim: PRIMITIVES.indexOf(node.primitive), round: node.round, thickness: node.thickness, inflation: node.inflation, material_neutral_cutout: node.materialNeutralCutout, mirror_x: -1, mirror_y: -1, mirror_z: -1 }, encodeMirrorPlanes(node.mirrors));
|
|
222
249
|
break;
|
|
223
250
|
case "union":
|
|
224
|
-
Object.assign(raw, {
|
|
251
|
+
Object.assign(raw, {
|
|
252
|
+
resolution: node.resolution, thickness: node.thickness, inflation: node.inflation,
|
|
253
|
+
material_neutral_cutout: node.materialNeutralCutout,
|
|
254
|
+
...(node.scriptInstanceId !== undefined ? { script_id: node.scriptInstanceId } : {}),
|
|
255
|
+
});
|
|
225
256
|
break;
|
|
226
257
|
case "light":
|
|
227
258
|
Object.assign(raw, { color: node.color, power: node.power, collimation: node.collimation, lightType: LIGHT_TYPES.indexOf(node.lightType), size: node.size, texture_file: node.textureFile });
|
|
228
259
|
break;
|
|
229
260
|
case "curve":
|
|
230
|
-
Object.assign(raw, { prim: PRIMITIVES.indexOf(node.primitive), density: node.density, roundness: node.roundness, smoothing: node.smoothing, material_neutral_cutout: node.materialNeutralCutout, points: node.points.map(encodeCurvePoint), version: 2, mirror_x: -1, mirror_y: -1, mirror_z: -1 }, encodeMirrorPlanes(node.mirrors));
|
|
261
|
+
Object.assign(raw, { prim: PRIMITIVES.indexOf(node.primitive), density: node.density, roundness: node.roundness, smoothing: node.smoothing, material_neutral_cutout: node.materialNeutralCutout, points: node.points.map(encodeCurvePoint), version: node.version ?? 2, curve_type: node.curveType === "bezier" ? 1 : 0, closed: node.closed ?? false, mirror_x: -1, mirror_y: -1, mirror_z: -1 }, encodeMirrorPlanes(node.mirrors));
|
|
231
262
|
break;
|
|
232
263
|
case "text":
|
|
233
264
|
Object.assign(raw, { text: node.text, font: node.fontFamily, weight: node.weight, italic: node.italic, round: node.round, width: node.width, wrap: TEXT_WRAP.indexOf(node.wrap), align: TEXT_ALIGN.indexOf(node.align), spacing: node.spacing, line_height: node.lineHeight, material_neutral_cutout: node.materialNeutralCutout });
|
package/dist/types.d.ts
CHANGED
|
@@ -70,6 +70,7 @@ export interface UnionNode extends TransformNode, OperationNode {
|
|
|
70
70
|
thickness: number;
|
|
71
71
|
inflation: number;
|
|
72
72
|
materialNeutralCutout: boolean;
|
|
73
|
+
scriptInstanceId?: number;
|
|
73
74
|
}
|
|
74
75
|
export interface GroupNode extends TransformNode {
|
|
75
76
|
kind: "group";
|
|
@@ -90,9 +91,14 @@ export interface CurvePoint {
|
|
|
90
91
|
materialId: number | null;
|
|
91
92
|
round: number;
|
|
92
93
|
fixed: boolean;
|
|
94
|
+
handleIn?: Vec3;
|
|
95
|
+
handleOut?: Vec3;
|
|
93
96
|
}
|
|
94
97
|
export interface CurveNode extends NodeBase, MaterialNode, OperationNode, MirrorNode {
|
|
95
98
|
kind: "curve";
|
|
99
|
+
version?: number;
|
|
100
|
+
curveType?: "spline" | "bezier";
|
|
101
|
+
closed?: boolean;
|
|
96
102
|
hidden: boolean;
|
|
97
103
|
primitive: PrimitiveType;
|
|
98
104
|
density: number;
|
|
@@ -186,6 +192,9 @@ export interface NodeUpdate {
|
|
|
186
192
|
roundness?: number;
|
|
187
193
|
smoothing?: number;
|
|
188
194
|
points?: CurvePoint[];
|
|
195
|
+
version?: number;
|
|
196
|
+
curveType?: "spline" | "bezier";
|
|
197
|
+
closed?: boolean;
|
|
189
198
|
text?: string;
|
|
190
199
|
fontFamily?: string;
|
|
191
200
|
weight?: number;
|
|
@@ -211,6 +220,7 @@ export interface NodeUpdate {
|
|
|
211
220
|
enabled?: boolean;
|
|
212
221
|
tag?: string;
|
|
213
222
|
mirrors?: MirrorPlanes;
|
|
223
|
+
scriptInstanceId?: number;
|
|
214
224
|
}
|
|
215
225
|
export interface CreateNodeInput<K extends NodeKind = NodeKind> {
|
|
216
226
|
kind: K;
|
|
@@ -239,6 +249,11 @@ export interface MaterialData {
|
|
|
239
249
|
surfaceOpacity: number;
|
|
240
250
|
volumetricEnabled: boolean;
|
|
241
251
|
}
|
|
252
|
+
/** Native renderer material data and shader slots supplied by an asset catalog. */
|
|
253
|
+
export interface MaterialResource {
|
|
254
|
+
data: Record<string, JsonValue>;
|
|
255
|
+
shaders: readonly (Record<string, JsonValue> | null)[];
|
|
256
|
+
}
|
|
242
257
|
export interface Material {
|
|
243
258
|
id: number;
|
|
244
259
|
data: MaterialData;
|
package/dist/validation.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { KakapoValidationError } from "./errors.js";
|
|
2
2
|
import { kindFromWire, parentMap, wireToNode } from "./scene.js";
|
|
3
3
|
const CONTAINERS = new Set(["union", "group"]);
|
|
4
|
-
const GEOMETRY = new Set(["primitive", "curve", "text", "field", "svg", "mesh", "
|
|
4
|
+
const GEOMETRY = new Set(["primitive", "curve", "text", "field", "svg", "mesh", "openScad"]);
|
|
5
5
|
function fail(message, operation, path, received, expected, hint, code = "VALIDATION_ERROR") {
|
|
6
6
|
throw new KakapoValidationError(message, { code, operation, path, received, expected, hint });
|
|
7
7
|
}
|
|
@@ -79,6 +79,9 @@ export function assertNode(node, scene, operation) {
|
|
|
79
79
|
assertFinite(node.resolution, operation, "resolution", 0.01, 1);
|
|
80
80
|
assertFinite(node.thickness, operation, "thickness", -1, 10);
|
|
81
81
|
assertFinite(node.inflation, operation, "inflation", -10, 10);
|
|
82
|
+
if (node.scriptInstanceId !== undefined && (!Number.isSafeInteger(node.scriptInstanceId) || node.scriptInstanceId < 0 || node.scriptInstanceId >= 0xffffffff)) {
|
|
83
|
+
fail("scriptInstanceId must identify a stored script instance.", operation, "scriptInstanceId", node.scriptInstanceId, "safe integer from 0 through 4294967294");
|
|
84
|
+
}
|
|
82
85
|
break;
|
|
83
86
|
case "light":
|
|
84
87
|
assertColor(node.color, operation, "color");
|
|
@@ -92,9 +95,26 @@ export function assertNode(node, scene, operation) {
|
|
|
92
95
|
assertFinite(node.density, operation, "density", 1);
|
|
93
96
|
assertFinite(node.roundness, operation, "roundness", 0);
|
|
94
97
|
assertFinite(node.smoothing, operation, "smoothing", 0, 1);
|
|
98
|
+
if (node.version !== undefined)
|
|
99
|
+
assertId(node.version, operation, "version");
|
|
100
|
+
if (node.curveType !== undefined && node.curveType !== "spline" && node.curveType !== "bezier") {
|
|
101
|
+
fail("Unknown curve type.", operation, "curveType", node.curveType, "spline or bezier");
|
|
102
|
+
}
|
|
103
|
+
if (node.closed !== undefined && typeof node.closed !== "boolean") {
|
|
104
|
+
fail("closed must be a boolean.", operation, "closed", node.closed, "boolean");
|
|
105
|
+
}
|
|
95
106
|
if (!node.points.length)
|
|
96
107
|
fail("A curve requires at least one point.", operation, "points", node.points, "non-empty curve points");
|
|
97
|
-
node.points.forEach((
|
|
108
|
+
node.points.forEach((point, index) => {
|
|
109
|
+
assertVec3(point.position, operation, `points[${index}].position`);
|
|
110
|
+
assertVec3(point.rotation, operation, `points[${index}].rotation`);
|
|
111
|
+
assertVec3(point.scale, operation, `points[${index}].scale`);
|
|
112
|
+
assertFinite(point.round, operation, `points[${index}].round`, 0, 1);
|
|
113
|
+
if (point.handleIn !== undefined)
|
|
114
|
+
assertVec3(point.handleIn, operation, `points[${index}].handleIn`);
|
|
115
|
+
if (point.handleOut !== undefined)
|
|
116
|
+
assertVec3(point.handleOut, operation, `points[${index}].handleOut`);
|
|
117
|
+
});
|
|
98
118
|
break;
|
|
99
119
|
case "text":
|
|
100
120
|
if (!node.text.length)
|
package/package.json
CHANGED
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@womp/kakapo-sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Typed Node.js and browser SDK for controlling Kakapo over WebSocket",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "./dist/index.js",
|
|
7
|
-
"types": "./dist/index.d.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": {
|
|
10
|
-
"types": "./dist/index.d.ts",
|
|
11
|
-
"import": "./dist/index.js",
|
|
12
|
-
"default": "./dist/index.js"
|
|
13
|
-
},
|
|
14
|
-
"./package.json": "./package.json"
|
|
15
|
-
},
|
|
16
|
-
"files": [
|
|
17
|
-
"dist/**/*.js",
|
|
18
|
-
"dist/**/*.d.ts",
|
|
19
|
-
"README.md"
|
|
20
|
-
],
|
|
21
|
-
"sideEffects": false,
|
|
22
|
-
"engines": {
|
|
23
|
-
"node": ">=22.0.0"
|
|
24
|
-
},
|
|
25
|
-
"repository": {
|
|
26
|
-
"type": "git",
|
|
27
|
-
"url": "git+https://github.com/wompxyz/kakapo-sdk.git"
|
|
28
|
-
},
|
|
29
|
-
"publishConfig": {
|
|
30
|
-
"access": "public"
|
|
31
|
-
},
|
|
32
|
-
"scripts": {
|
|
33
|
-
"clean": "node -e \"const f=require('node:fs');for(const p of ['dist','dist-test'])f.rmSync(p,{recursive:true,force:true})\"",
|
|
34
|
-
"build": "yarn clean && tsc -p tsconfig.json",
|
|
35
|
-
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
36
|
-
"lint": "yarn typecheck",
|
|
37
|
-
"build:test": "tsc -p tsconfig.test.json",
|
|
38
|
-
"test": "yarn build && yarn build:test && node --test dist-test/test/unit.test.js",
|
|
39
|
-
"test:live": "yarn build && yarn build:test && node --test dist-test/test/live.test.js",
|
|
40
|
-
"test:all": "yarn test && yarn test:live",
|
|
41
|
-
"prepack": "yarn build"
|
|
42
|
-
},
|
|
43
|
-
"dependencies": {
|
|
44
|
-
"fast-json-patch": "^3.1.1",
|
|
45
|
-
"ws": "^8.18.3"
|
|
46
|
-
},
|
|
47
|
-
"devDependencies": {
|
|
48
|
-
"@types/node": "^24.0.0",
|
|
49
|
-
"@types/ws": "^8.18.1",
|
|
50
|
-
"typescript": "^5.9.3"
|
|
51
|
-
},
|
|
52
|
-
"license": "MIT"
|
|
53
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@womp/kakapo-sdk",
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "Typed Node.js and browser SDK for controlling Kakapo over WebSocket",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist/**/*.js",
|
|
18
|
+
"dist/**/*.d.ts",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=22.0.0"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/wompxyz/kakapo-sdk.git"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"clean": "node -e \"const f=require('node:fs');for(const p of ['dist','dist-test'])f.rmSync(p,{recursive:true,force:true})\"",
|
|
34
|
+
"build": "yarn clean && tsc -p tsconfig.json",
|
|
35
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
36
|
+
"lint": "yarn typecheck",
|
|
37
|
+
"build:test": "tsc -p tsconfig.test.json",
|
|
38
|
+
"test": "yarn build && yarn build:test && node --test dist-test/test/unit.test.js",
|
|
39
|
+
"test:live": "yarn build && yarn build:test && node --test dist-test/test/live.test.js",
|
|
40
|
+
"test:all": "yarn test && yarn test:live",
|
|
41
|
+
"prepack": "yarn build"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"fast-json-patch": "^3.1.1",
|
|
45
|
+
"ws": "^8.18.3"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^24.0.0",
|
|
49
|
+
"@types/ws": "^8.18.1",
|
|
50
|
+
"typescript": "^5.9.3"
|
|
51
|
+
},
|
|
52
|
+
"license": "MIT"
|
|
53
|
+
}
|