@womp/kakapo-sdk 0.2.2 → 0.3.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/README.md +125 -84
- package/dist/api.d.ts +41 -1
- package/dist/api.js +308 -46
- 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,128 @@
|
|
|
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.
|
|
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
56
|
`captureScreenshot({ view: "current" })` requests the renderer's JPEG without changing the camera
|
|
57
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
|
-
|
|
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
|
+
|
|
71
83
|
Each `editScene()` refreshes before creating its private draft. Top-level edits are serialized per
|
|
72
|
-
client.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
+
`editSceneAndSave(callback)` submits the standard `scene_state_patch` command and, after
|
|
94
|
+
acknowledgement, calls `save_scene` once on the same connection. No new Kakapo command is required.
|
|
95
|
+
The result preserves the callback value separately from save status: `acknowledged` includes the
|
|
96
|
+
returned filename, `unknown` includes an error, and `not_needed` means the draft did not change.
|
|
97
|
+
A save acknowledgement does not prove a new durable snapshot: some endpoints return an existing
|
|
98
|
+
filename. Verify persistence through the owning service. Never repeat the edit to recover saving.
|
|
99
|
+
The backend wrapper may apply geometry before extension processing fails; patch errors and lost
|
|
100
|
+
replies therefore have an `unknown` outcome unless rejection before execution is proven. SDK state
|
|
101
|
+
is refreshed after submission, and no mutation is automatically replayed.
|
|
102
|
+
|
|
103
|
+
`getSceneRevision()` fingerprints the cached native scene or active draft, including renderer fields
|
|
104
|
+
not exposed by `getScene()`. Camera movement and object-key ordering do not affect it. Refresh before
|
|
105
|
+
inspection; this fingerprint is not a server-issued revision or an atomic commit guarantee through a wrapper.
|
|
106
|
+
|
|
107
|
+
Mesh, field, and decal names are syntax-checked, but their existence cannot be enumerated by the
|
|
108
|
+
current engine RPC surface. Font family/weight/style combinations are checked against `listFonts()`.
|
|
109
|
+
|
|
110
|
+
## Verification
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
yarn test # fake WebSocket protocol and API behavior
|
|
114
|
+
yarn test:live # launches the sibling Kakapo build and runs named live tests per API method
|
|
115
|
+
yarn test:all # runs both suites
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The live suite starts Kakapo once, runs methods sequentially, and reports failures as names such as
|
|
119
|
+
`live:setNodeParent` or `live:createNode:mesh`. Mutations refresh the authoritative engine scene
|
|
120
|
+
before asserting their result. See [TEST_MATRIX.md](./TEST_MATRIX.md) for the exact unit/live mapping.
|
|
121
|
+
|
|
122
|
+
Build the sibling engine before running live tests. On Windows the default executable is
|
|
123
|
+
`../kakapo/build/code/RelWithDebInfo/kakapo_app.exe`; on other platforms it is
|
|
124
|
+
`../kakapo/build/code/kakapo_app`. Set `KAKAPO_BINARY` to test another build explicitly.
|
|
125
|
+
|
|
126
|
+
### Importing catalog materials
|
|
127
|
+
|
|
128
|
+
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[];
|
|
@@ -76,16 +94,33 @@ export declare class KakapoAPI {
|
|
|
76
94
|
waitForToken(token: string, timeoutMs?: number): Promise<TokenMessage>;
|
|
77
95
|
refreshScene(): Promise<Scene>;
|
|
78
96
|
getScene(): Scene;
|
|
97
|
+
/** Fingerprint of the cached scene or active draft, including unknown native fields.
|
|
98
|
+
* Camera movement is excluded. This is not a server-issued revision or commit fence.
|
|
99
|
+
*/
|
|
100
|
+
getSceneRevision(): Promise<string>;
|
|
101
|
+
/** Wait until the renderer has produced a frame after this API's latest edit. */
|
|
102
|
+
waitForRenderer(): Promise<void>;
|
|
79
103
|
editScene<T>(callback: (scene: SceneEdit) => T | Promise<T>): Promise<T>;
|
|
104
|
+
editSceneAndSave<T>(callback: (scene: SceneEdit) => T | Promise<T>): Promise<{
|
|
105
|
+
value: T;
|
|
106
|
+
save: SceneSaveResult;
|
|
107
|
+
}>;
|
|
108
|
+
private prepareDraft;
|
|
109
|
+
private runSceneEdit;
|
|
80
110
|
private applyScenePatch;
|
|
81
111
|
/** The scene's consumer-owned extension bag. Kakapo does not interpret it. */
|
|
82
112
|
getSceneExt(): Record<string, JsonValue>;
|
|
83
113
|
/** Apply JSON Patch operations rooted at the scene's `ext` bag. */
|
|
84
114
|
patchSceneExt(operations: JsonPatchOperation[]): Scene;
|
|
115
|
+
/** Consumer-owned resources stored in the scene file. */
|
|
116
|
+
getSceneStorage(): Record<string, JsonValue>;
|
|
117
|
+
/** Apply JSON Patch operations rooted at the scene's `storage` bag. */
|
|
118
|
+
patchSceneStorage(operations: JsonPatchOperation[]): Scene;
|
|
85
119
|
/** The scene's renderer/environment properties (HDR, background, and similar). */
|
|
86
120
|
getSceneProperties(): Record<string, JsonValue>;
|
|
87
121
|
/** Apply JSON Patch operations rooted at the scene's `properties` bag. */
|
|
88
122
|
patchSceneProperties(operations: JsonPatchOperation[]): Scene;
|
|
123
|
+
private patchSceneBag;
|
|
89
124
|
listNodes(options?: ListNodeOptions): KakapoNode[];
|
|
90
125
|
findNodes(options: FindNodeOptions): KakapoNode[];
|
|
91
126
|
getNode(id: number): KakapoNode;
|
|
@@ -105,6 +140,10 @@ export declare class KakapoAPI {
|
|
|
105
140
|
private createNode;
|
|
106
141
|
private cloneNode;
|
|
107
142
|
private updateNode;
|
|
143
|
+
private curvePointDraft;
|
|
144
|
+
private updateCurvePoint;
|
|
145
|
+
private cloneCurvePoint;
|
|
146
|
+
private deleteCurvePoint;
|
|
108
147
|
private deleteNode;
|
|
109
148
|
private setNodeParent;
|
|
110
149
|
private setNodeName;
|
|
@@ -127,6 +166,7 @@ export declare class KakapoAPI {
|
|
|
127
166
|
listMaterials(): Material[];
|
|
128
167
|
getMaterial(id: number): Material;
|
|
129
168
|
private createMaterial;
|
|
169
|
+
private importMaterial;
|
|
130
170
|
private updateMaterial;
|
|
131
171
|
private deleteMaterial;
|
|
132
172
|
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
|
]);
|
|
@@ -33,12 +33,14 @@ export class KakapoAPI {
|
|
|
33
33
|
}
|
|
34
34
|
get isConnected() { return this.transport.connected; }
|
|
35
35
|
async connect() {
|
|
36
|
-
await this.transport.connect();
|
|
37
36
|
try {
|
|
37
|
+
await this.transport.connect();
|
|
38
38
|
await this.ping();
|
|
39
39
|
await this.refreshScene();
|
|
40
40
|
}
|
|
41
41
|
catch (error) {
|
|
42
|
+
if (error instanceof KakapoConnectionError && error.code === "CONNECTION_IN_PROGRESS")
|
|
43
|
+
throw error;
|
|
42
44
|
this.disconnect();
|
|
43
45
|
throw error;
|
|
44
46
|
}
|
|
@@ -82,24 +84,107 @@ export class KakapoAPI {
|
|
|
82
84
|
getScene() {
|
|
83
85
|
return deepClone(toPublicScene(this.requireScene("getScene")));
|
|
84
86
|
}
|
|
87
|
+
/** Fingerprint of the cached scene or active draft, including unknown native fields.
|
|
88
|
+
* Camera movement is excluded. This is not a server-issued revision or commit fence.
|
|
89
|
+
*/
|
|
90
|
+
async getSceneRevision() {
|
|
91
|
+
return getSceneSnapshotRevision(this.requireScene("getSceneRevision"));
|
|
92
|
+
}
|
|
93
|
+
/** Wait until the renderer has produced a frame after this API's latest edit. */
|
|
94
|
+
async waitForRenderer() {
|
|
95
|
+
await this.ensureRendererReady();
|
|
96
|
+
}
|
|
85
97
|
editScene(callback) {
|
|
98
|
+
return this.runSceneEdit(callback, false).then(({ value }) => value);
|
|
99
|
+
}
|
|
100
|
+
editSceneAndSave(callback) {
|
|
101
|
+
return this.runSceneEdit(callback, true);
|
|
102
|
+
}
|
|
103
|
+
async prepareDraft(baseline, callback) {
|
|
104
|
+
this.draftScene = deepClone(baseline);
|
|
105
|
+
this.draftDirty = false;
|
|
106
|
+
let active = true;
|
|
107
|
+
const edit = this.createSceneEdit(() => {
|
|
108
|
+
if (active)
|
|
109
|
+
return;
|
|
110
|
+
throw new KakapoValidationError("The scene draft has expired.", {
|
|
111
|
+
code: "EXPIRED_SCENE_DRAFT",
|
|
112
|
+
operation: "editScene",
|
|
113
|
+
expected: "a draft used only inside its own callback",
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
let value;
|
|
117
|
+
try {
|
|
118
|
+
value = await callback(edit);
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
active = false;
|
|
122
|
+
}
|
|
123
|
+
const scene = this.requireDraft("editScene");
|
|
124
|
+
assertScene(scene, "editScene");
|
|
125
|
+
const operations = jsonpatch.compare(baseline, scene);
|
|
126
|
+
return { value, operations, scene };
|
|
127
|
+
}
|
|
128
|
+
runSceneEdit(callback, saveAfterPatch) {
|
|
86
129
|
const run = this.transactionTail.then(async () => {
|
|
87
130
|
await this.refreshScene();
|
|
88
131
|
const baseline = deepClone(this.requireScene("editScene"));
|
|
89
|
-
this.draftScene = deepClone(baseline);
|
|
90
|
-
this.draftDirty = false;
|
|
91
132
|
try {
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
assertScene(draft, "editScene");
|
|
95
|
-
const operations = jsonpatch.compare(baseline, draft);
|
|
133
|
+
const { value, operations, scene } = await this.prepareDraft(baseline, callback);
|
|
134
|
+
let save = { status: "not_needed" };
|
|
96
135
|
if (operations.length) {
|
|
97
|
-
|
|
136
|
+
const addsRootField = operations.some((operation) => operation.op === "add" && operation.path.split("/").length === 2);
|
|
137
|
+
const addsExtRoot = operations.some((operation) => operation.op === "add" && operation.path === "/ext");
|
|
138
|
+
if (addsExtRoot) {
|
|
139
|
+
throw new KakapoValidationError("Cannot safely add a missing scene extension root through a partitioned endpoint.", {
|
|
140
|
+
code: "UNSUPPORTED_PATCH_GUARD",
|
|
141
|
+
operation: "editScene",
|
|
142
|
+
path: "/ext",
|
|
143
|
+
expected: "an existing ext root",
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
const changesCamera = operations.some((operation) => operation.path === "/camera" || operation.path.startsWith("/camera/"));
|
|
147
|
+
const changesExt = operations.some((operation) => operation.path === "/ext" || operation.path.startsWith("/ext/"));
|
|
148
|
+
const changesNonExt = operations.some((operation) => operation.path !== "/ext" && !operation.path.startsWith("/ext/"));
|
|
149
|
+
const latest = parseJsonResult(await this.call("scene_state_get"), "scene_state_get");
|
|
150
|
+
assertScene(latest, "editScene");
|
|
151
|
+
const stale = jsonpatch.compare(baseline, latest).some((operation) => changesCamera || (operation.path !== "/camera" && !operation.path.startsWith("/camera/")));
|
|
152
|
+
if (stale)
|
|
153
|
+
throw new KakapoSceneCommitError("rolled_back", new Error("Scene changed during preparation. Read it again before editing."));
|
|
154
|
+
// Raw renderers evaluate these together; wrappers may partition the patch.
|
|
155
|
+
// A failed response cannot establish that no earlier group was applied.
|
|
156
|
+
const guards = [];
|
|
157
|
+
if (changesNonExt) {
|
|
158
|
+
if (addsRootField)
|
|
159
|
+
guards.push({ op: "test", path: "", value: baseline });
|
|
160
|
+
else
|
|
161
|
+
guards.push(...Object.entries(baseline)
|
|
162
|
+
.filter(([key]) => key !== "ext" && (key !== "camera" || changesCamera))
|
|
163
|
+
.map(([key, value]) => ({
|
|
164
|
+
op: "test",
|
|
165
|
+
path: `/${escapePointer(key)}`,
|
|
166
|
+
value: value,
|
|
167
|
+
})));
|
|
168
|
+
}
|
|
169
|
+
if (changesExt)
|
|
170
|
+
guards.push({ op: "test", path: "/ext", value: baseline.ext });
|
|
171
|
+
save = await this.sendPatch([...guards, ...operations], saveAfterPatch);
|
|
98
172
|
this.rendererSyncRequired = true;
|
|
99
173
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
174
|
+
if (operations.length) {
|
|
175
|
+
this.cacheValid = false;
|
|
176
|
+
try {
|
|
177
|
+
await this.refreshScene();
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// Keep the acknowledged result; the next read must refresh the invalid cache.
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
this.rawScene = scene;
|
|
185
|
+
this.cacheValid = true;
|
|
186
|
+
}
|
|
187
|
+
return { value, save };
|
|
103
188
|
}
|
|
104
189
|
catch (error) {
|
|
105
190
|
this.cacheValid = false;
|
|
@@ -133,7 +218,15 @@ export class KakapoAPI {
|
|
|
133
218
|
}
|
|
134
219
|
/** Apply JSON Patch operations rooted at the scene's `ext` bag. */
|
|
135
220
|
patchSceneExt(operations) {
|
|
136
|
-
return this.
|
|
221
|
+
return this.patchSceneBag("ext", operations);
|
|
222
|
+
}
|
|
223
|
+
/** Consumer-owned resources stored in the scene file. */
|
|
224
|
+
getSceneStorage() {
|
|
225
|
+
return deepClone(this.requireScene("getSceneStorage").storage);
|
|
226
|
+
}
|
|
227
|
+
/** Apply JSON Patch operations rooted at the scene's `storage` bag. */
|
|
228
|
+
patchSceneStorage(operations) {
|
|
229
|
+
return this.patchSceneBag("storage", operations);
|
|
137
230
|
}
|
|
138
231
|
/** The scene's renderer/environment properties (HDR, background, and similar). */
|
|
139
232
|
getSceneProperties() {
|
|
@@ -142,7 +235,24 @@ export class KakapoAPI {
|
|
|
142
235
|
}
|
|
143
236
|
/** Apply JSON Patch operations rooted at the scene's `properties` bag. */
|
|
144
237
|
patchSceneProperties(operations) {
|
|
145
|
-
return this.
|
|
238
|
+
return this.patchSceneBag("properties", operations);
|
|
239
|
+
}
|
|
240
|
+
patchSceneBag(key, operations) {
|
|
241
|
+
const draft = this.requireDraft("patchSceneBag");
|
|
242
|
+
if (operations.length === 0)
|
|
243
|
+
return this.getScene();
|
|
244
|
+
const prefix = `/${key}`;
|
|
245
|
+
const patch = draft[key] === undefined
|
|
246
|
+
? [{ op: "add", path: prefix, value: {} }]
|
|
247
|
+
: [];
|
|
248
|
+
for (const operation of operations) {
|
|
249
|
+
patch.push({
|
|
250
|
+
...operation,
|
|
251
|
+
path: `${prefix}${operation.path}`,
|
|
252
|
+
...("from" in operation ? { from: `${prefix}${operation.from}` } : {}),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
return this.applyScenePatch(patch);
|
|
146
256
|
}
|
|
147
257
|
listNodes(options = {}) {
|
|
148
258
|
const scene = this.requireScene("listNodes");
|
|
@@ -257,23 +367,35 @@ export class KakapoAPI {
|
|
|
257
367
|
z: (bounds.max.z - bounds.min.z) * framePadding,
|
|
258
368
|
};
|
|
259
369
|
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
|
-
|
|
370
|
+
const originalCamera = parseJsonResult(await this.call("get_cam"), "get_cam");
|
|
371
|
+
let framedCamera;
|
|
372
|
+
let message;
|
|
373
|
+
try {
|
|
374
|
+
await this.call("set_camera", [{
|
|
375
|
+
...originalCamera,
|
|
376
|
+
cent: center,
|
|
377
|
+
dir: { x: direction.x, y: direction.y, z: direction.z },
|
|
378
|
+
up: view === "top" || view === "bottom" ? { x: 0, y: 0, z: 1 } : { x: 0, y: 1, z: 0 },
|
|
379
|
+
pos: {
|
|
380
|
+
x: center.x - direction.x * distance,
|
|
381
|
+
y: center.y - direction.y * distance,
|
|
382
|
+
z: center.z - direction.z * distance,
|
|
383
|
+
},
|
|
384
|
+
}, true]);
|
|
385
|
+
const framed = await this.call("center_cam", [center, size]);
|
|
386
|
+
if (this.rawScene)
|
|
387
|
+
this.rawScene.camera = { ...this.rawScene.camera, ...framed };
|
|
388
|
+
framedCamera = parseJsonResult(await this.call("get_cam"), "get_cam");
|
|
389
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
390
|
+
message = await this.captureRendererFrame(timeoutMs);
|
|
391
|
+
}
|
|
392
|
+
finally {
|
|
393
|
+
if (framedCamera !== undefined) {
|
|
394
|
+
const latestCamera = parseJsonResult(await this.call("get_cam"), "get_cam");
|
|
395
|
+
if (jsonpatch.compare(framedCamera, latestCamera).length === 0)
|
|
396
|
+
await this.call("set_camera", [originalCamera, true]);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
277
399
|
return {
|
|
278
400
|
data: message.payload,
|
|
279
401
|
mediaType: "image/jpeg",
|
|
@@ -349,12 +471,77 @@ export class KakapoAPI {
|
|
|
349
471
|
const scene = this.requireDraft("updateNode");
|
|
350
472
|
if (id === 0 && ("hidden" in changes || "transform" in changes))
|
|
351
473
|
throw this.rootError("updateNode");
|
|
352
|
-
const
|
|
353
|
-
|
|
474
|
+
const current = wireToNode(scene, id);
|
|
475
|
+
const updated = this.mergeNode(current, changes);
|
|
476
|
+
assertNode(updated, scene, "updateNode");
|
|
477
|
+
const before = nodeToWire(current);
|
|
478
|
+
const after = nodeToWire(updated);
|
|
479
|
+
// Only replace fields changed through the typed API. Re-encoding the whole
|
|
480
|
+
// node discards renderer fields this SDK version does not know about.
|
|
481
|
+
const raw = deepClone(scene.nodes[String(id)]);
|
|
482
|
+
for (const key of new Set([...Object.keys(before), ...Object.keys(after)])) {
|
|
483
|
+
if (JSON.stringify(before[key]) === JSON.stringify(after[key]))
|
|
484
|
+
continue;
|
|
485
|
+
if (Object.hasOwn(after, key))
|
|
486
|
+
raw[key] = after[key];
|
|
487
|
+
else
|
|
488
|
+
delete raw[key];
|
|
489
|
+
}
|
|
490
|
+
scene.nodes[String(id)] = raw;
|
|
354
491
|
scene.ext = this.extWithNode(scene, updated);
|
|
355
492
|
this.draftDirty = true;
|
|
356
493
|
return deepClone(wireToNode(scene, id));
|
|
357
494
|
}
|
|
495
|
+
curvePointDraft(id, index, operation) {
|
|
496
|
+
const scene = this.requireDraft(operation);
|
|
497
|
+
const node = this.getNode(id);
|
|
498
|
+
if (node.kind !== "curve")
|
|
499
|
+
throw this.unsupported(operation, id, "curve points");
|
|
500
|
+
if (!Number.isInteger(index) || index < 0 || index >= node.points.length)
|
|
501
|
+
throw new KakapoValidationError("Curve point index is out of range.", {
|
|
502
|
+
code: "INVALID_CURVE_POINT_INDEX", operation, received: index,
|
|
503
|
+
expected: `integer from 0 through ${node.points.length - 1}`,
|
|
504
|
+
});
|
|
505
|
+
return { scene, node, points: scene.nodes[String(id)].points };
|
|
506
|
+
}
|
|
507
|
+
updateCurvePoint(id, index, changes) {
|
|
508
|
+
const { scene, node, points } = this.curvePointDraft(id, index, "updateCurvePoint");
|
|
509
|
+
const previous = node.points[index];
|
|
510
|
+
const next = { ...previous, ...deepClone(changes) };
|
|
511
|
+
node.points[index] = next;
|
|
512
|
+
assertNode(node, scene, "updateCurvePoint");
|
|
513
|
+
const before = encodeCurvePoint(previous);
|
|
514
|
+
const after = encodeCurvePoint(next);
|
|
515
|
+
// Edit only requested typed fields; retain opaque native data on this point.
|
|
516
|
+
for (const key of new Set([...Object.keys(before), ...Object.keys(after)])) {
|
|
517
|
+
if (JSON.stringify(before[key]) === JSON.stringify(after[key]))
|
|
518
|
+
continue;
|
|
519
|
+
if (Object.hasOwn(after, key))
|
|
520
|
+
points[index][key] = after[key];
|
|
521
|
+
else
|
|
522
|
+
delete points[index][key];
|
|
523
|
+
}
|
|
524
|
+
this.draftDirty = true;
|
|
525
|
+
}
|
|
526
|
+
cloneCurvePoint(id, sourceIndex, index) {
|
|
527
|
+
const { points } = this.curvePointDraft(id, sourceIndex, "cloneCurvePoint");
|
|
528
|
+
if (!Number.isInteger(index) || index < 0 || index > points.length)
|
|
529
|
+
throw new KakapoValidationError("Curve insertion index is out of range.", {
|
|
530
|
+
code: "INVALID_CURVE_POINT_INDEX", operation: "cloneCurvePoint", received: index,
|
|
531
|
+
expected: `integer from 0 through ${points.length}`,
|
|
532
|
+
});
|
|
533
|
+
points.splice(index, 0, deepClone(points[sourceIndex]));
|
|
534
|
+
this.draftDirty = true;
|
|
535
|
+
}
|
|
536
|
+
deleteCurvePoint(id, index) {
|
|
537
|
+
const { points } = this.curvePointDraft(id, index, "deleteCurvePoint");
|
|
538
|
+
if (points.length <= 1)
|
|
539
|
+
throw new KakapoValidationError("A curve must keep at least one point.", {
|
|
540
|
+
code: "INVALID_CURVE_POINT_COUNT", operation: "deleteCurvePoint", expected: "at least one point",
|
|
541
|
+
});
|
|
542
|
+
points.splice(index, 1);
|
|
543
|
+
this.draftDirty = true;
|
|
544
|
+
}
|
|
358
545
|
deleteNode(id, options = {}) {
|
|
359
546
|
const scene = this.requireDraft("deleteNode");
|
|
360
547
|
if (id === 0)
|
|
@@ -470,6 +657,25 @@ export class KakapoAPI {
|
|
|
470
657
|
this.draftDirty = true;
|
|
471
658
|
return deepClone(material);
|
|
472
659
|
}
|
|
660
|
+
importMaterial(resource) {
|
|
661
|
+
const scene = this.requireDraft("importMaterial");
|
|
662
|
+
const source = deepClone(resource);
|
|
663
|
+
const materials = scene.storage.materials ?? (scene.storage.materials = {});
|
|
664
|
+
const id = nextId(materials);
|
|
665
|
+
assertMaterial(wireToMaterial(id, { data: source.data }).data, "importMaterial");
|
|
666
|
+
const shaders = scene.storage.shaders ?? (scene.storage.shaders = {});
|
|
667
|
+
const shaderIds = source.shaders.map((shader) => {
|
|
668
|
+
if (shader === null)
|
|
669
|
+
return 0;
|
|
670
|
+
const shaderId = nextId(shaders);
|
|
671
|
+
shaders[String(shaderId)] = shader;
|
|
672
|
+
return shaderId;
|
|
673
|
+
});
|
|
674
|
+
const material = { data: source.data, ...(shaderIds.length ? { shaders: shaderIds } : {}) };
|
|
675
|
+
materials[String(id)] = material;
|
|
676
|
+
this.draftDirty = true;
|
|
677
|
+
return deepClone(wireToMaterial(id, material));
|
|
678
|
+
}
|
|
473
679
|
updateMaterial(id, data, shaderIds) {
|
|
474
680
|
const scene = this.requireDraft("updateMaterial");
|
|
475
681
|
const currentRaw = scene.storage.materials?.[String(id)];
|
|
@@ -478,10 +684,21 @@ export class KakapoAPI {
|
|
|
478
684
|
const current = wireToMaterial(id, currentRaw);
|
|
479
685
|
const updated = { id, data: this.mergeMaterialData(current.data, data), shaderIds: shaderIds ? [...shaderIds] : current.shaderIds };
|
|
480
686
|
assertMaterial(updated.data, "updateMaterial");
|
|
687
|
+
const before = materialToWire(current);
|
|
481
688
|
const next = materialToWire(updated);
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
689
|
+
const raw = { ...currentRaw, data: { ...currentRaw.data } };
|
|
690
|
+
for (const [key, value] of Object.entries(next.data)) {
|
|
691
|
+
if (value !== before.data[key])
|
|
692
|
+
raw.data[key] = value;
|
|
693
|
+
}
|
|
694
|
+
if (shaderIds !== undefined) {
|
|
695
|
+
delete raw.shader;
|
|
696
|
+
if (next.shaders)
|
|
697
|
+
raw.shaders = next.shaders;
|
|
698
|
+
else
|
|
699
|
+
delete raw.shaders;
|
|
700
|
+
}
|
|
701
|
+
scene.storage.materials[String(id)] = raw;
|
|
485
702
|
this.draftDirty = true;
|
|
486
703
|
return deepClone(updated);
|
|
487
704
|
}
|
|
@@ -542,10 +759,34 @@ export class KakapoAPI {
|
|
|
542
759
|
async reloadTextures() { return this.call("reload_textures"); }
|
|
543
760
|
async getPendingResources() { return parseJsonResult(await this.call("get_pending_resources"), "get_pending_resources"); }
|
|
544
761
|
call(method, params = []) { return this.transport.rpc(method, params); }
|
|
545
|
-
async sendPatch(operations) {
|
|
546
|
-
|
|
762
|
+
async sendPatch(operations, saveAfterPatch) {
|
|
763
|
+
let result;
|
|
764
|
+
try {
|
|
765
|
+
result = await this.call("scene_state_patch", [JSON.stringify(operations)]);
|
|
766
|
+
}
|
|
767
|
+
catch (cause) {
|
|
768
|
+
if (cause instanceof KakapoRpcError && cause.rpcCode === -32601)
|
|
769
|
+
throw new KakapoSceneCommitError("rolled_back", cause);
|
|
770
|
+
throw new KakapoSceneCommitError("unknown", cause);
|
|
771
|
+
}
|
|
547
772
|
if (result !== undefined && result !== null && result !== "") {
|
|
548
|
-
|
|
773
|
+
const message = typeof result === "object" && "message" in result && typeof result.message === "string"
|
|
774
|
+
? result.message
|
|
775
|
+
: JSON.stringify(result);
|
|
776
|
+
// The wrapper can apply geometry before a later extension patch fails.
|
|
777
|
+
throw new KakapoSceneCommitError("unknown", new KakapoRpcError("scene_state_patch", message));
|
|
778
|
+
}
|
|
779
|
+
if (!saveAfterPatch)
|
|
780
|
+
return { status: "not_needed" };
|
|
781
|
+
try {
|
|
782
|
+
const saved = await this.call("save_scene");
|
|
783
|
+
// A filename acknowledges the save handler, not a new durable snapshot.
|
|
784
|
+
if (typeof saved === "string" && saved.length > 0 && !saved.startsWith("{") && !saved.startsWith("["))
|
|
785
|
+
return { status: "acknowledged", filename: saved };
|
|
786
|
+
return { status: "unknown", error: `Save was not confirmed: ${JSON.stringify(saved)}` };
|
|
787
|
+
}
|
|
788
|
+
catch (error) {
|
|
789
|
+
return { status: "unknown", error: error instanceof Error ? error.message : String(error) };
|
|
549
790
|
}
|
|
550
791
|
}
|
|
551
792
|
saveNode(id, changes) {
|
|
@@ -586,19 +827,24 @@ export class KakapoAPI {
|
|
|
586
827
|
hint: "Finish the transaction, then perform the engine-backed read.",
|
|
587
828
|
});
|
|
588
829
|
}
|
|
589
|
-
createSceneEdit() {
|
|
590
|
-
|
|
830
|
+
createSceneEdit(assertActive) {
|
|
831
|
+
const edit = {
|
|
591
832
|
getScene: () => this.getScene(),
|
|
592
833
|
listNodes: (options) => this.listNodes(options),
|
|
593
834
|
findNodes: (options) => this.findNodes(options),
|
|
594
835
|
getNode: (id) => this.getNode(id),
|
|
595
|
-
node: ((id, kind) =>
|
|
836
|
+
node: ((id, kind) => createNodeHandle({
|
|
837
|
+
readNode: (nodeId) => { assertActive(); return this.getNode(nodeId); },
|
|
838
|
+
saveNode: (nodeId, changes) => { assertActive(); return this.saveNode(nodeId, changes); },
|
|
839
|
+
}, id, kind)),
|
|
596
840
|
getNodeName: (id) => this.getNodeName(id),
|
|
597
841
|
getNodeParent: (id) => this.getNodeParent(id),
|
|
598
842
|
getNodeChildren: (id) => this.getNodeChildren(id),
|
|
599
843
|
applyScenePatch: (operations) => this.applyScenePatch(operations),
|
|
600
844
|
getSceneExt: () => this.getSceneExt(),
|
|
601
845
|
patchSceneExt: (operations) => this.patchSceneExt(operations),
|
|
846
|
+
getSceneStorage: () => this.getSceneStorage(),
|
|
847
|
+
patchSceneStorage: (operations) => this.patchSceneStorage(operations),
|
|
602
848
|
getSceneProperties: () => this.getSceneProperties(),
|
|
603
849
|
patchSceneProperties: (operations) => this.patchSceneProperties(operations),
|
|
604
850
|
createNode: (input) => this.createNode(input),
|
|
@@ -617,6 +863,9 @@ export class KakapoAPI {
|
|
|
617
863
|
setNodeOperation: (id, operation) => this.setNodeOperation(id, operation),
|
|
618
864
|
setTextContent: (id, text) => this.setTextContent(id, text),
|
|
619
865
|
setTextFont: (id, family, weight, italic) => this.setTextFont(id, family, weight, italic),
|
|
866
|
+
updateCurvePoint: (id, index, changes) => this.updateCurvePoint(id, index, changes),
|
|
867
|
+
cloneCurvePoint: (id, sourceIndex, index) => this.cloneCurvePoint(id, sourceIndex, index),
|
|
868
|
+
deleteCurvePoint: (id, index) => this.deleteCurvePoint(id, index),
|
|
620
869
|
setSvgPaths: (id, paths) => this.setSvgPaths(id, paths),
|
|
621
870
|
setMeshSource: (id, mesh, index) => this.setMeshSource(id, mesh, index),
|
|
622
871
|
setFieldSource: (id, field) => this.setFieldSource(id, field),
|
|
@@ -626,6 +875,7 @@ export class KakapoAPI {
|
|
|
626
875
|
listMaterials: () => this.listMaterials(),
|
|
627
876
|
getMaterial: (id) => this.getMaterial(id),
|
|
628
877
|
createMaterial: (data, shaderIds) => this.createMaterial(data, shaderIds),
|
|
878
|
+
importMaterial: (resource) => this.importMaterial(resource),
|
|
629
879
|
updateMaterial: (id, data, shaderIds) => this.updateMaterial(id, data, shaderIds),
|
|
630
880
|
deleteMaterial: (id) => this.deleteMaterial(id),
|
|
631
881
|
listOpenScadScripts: () => this.listOpenScadScripts(),
|
|
@@ -634,6 +884,18 @@ export class KakapoAPI {
|
|
|
634
884
|
updateOpenScadScript: (id, changes) => this.updateOpenScadScript(id, changes),
|
|
635
885
|
deleteOpenScadScript: (id) => this.deleteOpenScadScript(id),
|
|
636
886
|
};
|
|
887
|
+
return new Proxy(edit, {
|
|
888
|
+
get(target, property, receiver) {
|
|
889
|
+
assertActive();
|
|
890
|
+
const method = Reflect.get(target, property, receiver);
|
|
891
|
+
if (typeof method !== "function")
|
|
892
|
+
return method;
|
|
893
|
+
return (...args) => {
|
|
894
|
+
assertActive();
|
|
895
|
+
return Reflect.apply(method, target, args);
|
|
896
|
+
};
|
|
897
|
+
},
|
|
898
|
+
});
|
|
637
899
|
}
|
|
638
900
|
treeArrays(scene) { return Object.fromEntries(Object.keys(scene.tree ?? {}).map((id) => [id, orderedChildren(scene, Number(id))])); }
|
|
639
901
|
extWithName(scene, id, name) {
|
|
@@ -669,10 +931,10 @@ export class KakapoAPI {
|
|
|
669
931
|
const common = ["transform", "pickable"];
|
|
670
932
|
const byKind = {
|
|
671
933
|
primitive: ["hidden", "materialId", "operation", "blend", "color", "primitive", "round", "thickness", "inflation", "materialNeutralCutout", "mirrors"],
|
|
672
|
-
union: ["hidden", "operation", "blend", "resolution", "thickness", "inflation", "materialNeutralCutout"],
|
|
934
|
+
union: ["hidden", "operation", "blend", "resolution", "thickness", "inflation", "materialNeutralCutout", "scriptInstanceId"],
|
|
673
935
|
group: ["hidden"],
|
|
674
936
|
light: ["color", "power", "collimation", "lightType", "size", "textureFile"],
|
|
675
|
-
curve: ["hidden", "materialId", "operation", "blend", "primitive", "density", "roundness", "smoothing", "points", "materialNeutralCutout", "mirrors"],
|
|
937
|
+
curve: ["hidden", "materialId", "operation", "blend", "primitive", "density", "roundness", "smoothing", "points", "materialNeutralCutout", "mirrors", "version", "curveType", "closed"],
|
|
676
938
|
text: ["hidden", "materialId", "operation", "blend", "text", "fontFamily", "weight", "italic", "round", "width", "wrap", "align", "spacing", "lineHeight", "materialNeutralCutout"],
|
|
677
939
|
svg: ["hidden", "materialId", "operation", "blend", "paths", "inflate", "outline", "outlineSize", "materialNeutralCutout"],
|
|
678
940
|
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.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
|
+
}
|