@womp/kakapo-sdk 0.1.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 +82 -0
- package/dist/api.d.ts +151 -0
- package/dist/api.js +671 -0
- package/dist/errors.d.ts +35 -0
- package/dist/errors.js +56 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +3 -0
- package/dist/node-handle.d.ts +32 -0
- package/dist/node-handle.js +69 -0
- package/dist/scene.d.ts +59 -0
- package/dist/scene.js +397 -0
- package/dist/transport.d.ts +28 -0
- package/dist/transport.js +317 -0
- package/dist/types.d.ts +359 -0
- package/dist/types.js +1 -0
- package/dist/validation.d.ts +8 -0
- package/dist/validation.js +195 -0
- package/package.json +53 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { BinaryFrame, JsonValue, TokenMessage } from "./types.js";
|
|
2
|
+
export interface TransportOptions {
|
|
3
|
+
url: string;
|
|
4
|
+
requestTimeoutMs: number;
|
|
5
|
+
connectTimeoutMs: number;
|
|
6
|
+
}
|
|
7
|
+
export declare class KakapoTransport {
|
|
8
|
+
private readonly options;
|
|
9
|
+
private socket?;
|
|
10
|
+
private browserSocket;
|
|
11
|
+
private nextRequestId;
|
|
12
|
+
private nextTokenId;
|
|
13
|
+
private readonly requests;
|
|
14
|
+
private readonly tokenWaiters;
|
|
15
|
+
private readonly tokenInbox;
|
|
16
|
+
constructor(options: TransportOptions);
|
|
17
|
+
get connected(): boolean;
|
|
18
|
+
connect(): Promise<void>;
|
|
19
|
+
disconnect(): void;
|
|
20
|
+
rpc<T>(method: string, params?: JsonValue[], timeoutMs?: number): Promise<T>;
|
|
21
|
+
newToken(): string;
|
|
22
|
+
waitForToken(token: string, timeoutMs?: number): Promise<TokenMessage>;
|
|
23
|
+
private handleMessage;
|
|
24
|
+
private deliverToken;
|
|
25
|
+
private handleClose;
|
|
26
|
+
private rejectAll;
|
|
27
|
+
}
|
|
28
|
+
export declare function parseBinaryFrame(bytes: Uint8Array): BinaryFrame;
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { KakapoConnectionError, KakapoRpcError, KakapoTimeoutError, KakapoValidationError, } from "./errors.js";
|
|
2
|
+
export class KakapoTransport {
|
|
3
|
+
options;
|
|
4
|
+
socket;
|
|
5
|
+
browserSocket = false;
|
|
6
|
+
nextRequestId = 1;
|
|
7
|
+
nextTokenId = 1;
|
|
8
|
+
requests = new Map();
|
|
9
|
+
tokenWaiters = new Map();
|
|
10
|
+
tokenInbox = new Map();
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.options = options;
|
|
13
|
+
}
|
|
14
|
+
get connected() {
|
|
15
|
+
return this.socket?.readyState === 1;
|
|
16
|
+
}
|
|
17
|
+
async connect() {
|
|
18
|
+
if (this.connected)
|
|
19
|
+
return;
|
|
20
|
+
if (this.socket && this.socket.readyState === 0) {
|
|
21
|
+
throw new KakapoConnectionError("A Kakapo WebSocket connection is already in progress.", {
|
|
22
|
+
code: "CONNECTION_IN_PROGRESS",
|
|
23
|
+
operation: "connect",
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
const BrowserWebSocket = globalThis.WebSocket;
|
|
27
|
+
let socket;
|
|
28
|
+
if (BrowserWebSocket) {
|
|
29
|
+
socket = new BrowserWebSocket(this.options.url);
|
|
30
|
+
this.browserSocket = true;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
const moduleName = "ws";
|
|
34
|
+
const module = await import(moduleName);
|
|
35
|
+
socket = new module.default(this.options.url, { perMessageDeflate: false });
|
|
36
|
+
this.browserSocket = false;
|
|
37
|
+
}
|
|
38
|
+
this.socket = socket;
|
|
39
|
+
socket.binaryType = this.browserSocket ? "arraybuffer" : "nodebuffer";
|
|
40
|
+
await new Promise((resolve, reject) => {
|
|
41
|
+
const timer = setTimeout(() => {
|
|
42
|
+
socket.terminate?.();
|
|
43
|
+
if (!socket.terminate)
|
|
44
|
+
socket.close();
|
|
45
|
+
reject(new KakapoTimeoutError(`Timed out connecting to ${this.options.url}.`, {
|
|
46
|
+
code: "CONNECT_TIMEOUT",
|
|
47
|
+
operation: "connect",
|
|
48
|
+
expected: `WebSocket open within ${this.options.connectTimeoutMs}ms`,
|
|
49
|
+
hint: "Start Kakapo and confirm that its WebSocket server is listening on the configured URL.",
|
|
50
|
+
}));
|
|
51
|
+
}, this.options.connectTimeoutMs);
|
|
52
|
+
const onOpen = () => {
|
|
53
|
+
clearTimeout(timer);
|
|
54
|
+
socket.off?.("error", onNodeError);
|
|
55
|
+
socket.removeEventListener?.("error", onBrowserError);
|
|
56
|
+
resolve();
|
|
57
|
+
};
|
|
58
|
+
const rejectConnection = (cause) => {
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
socket.off?.("open", onOpen);
|
|
61
|
+
socket.removeEventListener?.("open", onBrowserOpen);
|
|
62
|
+
reject(new KakapoConnectionError(`Could not connect to ${this.options.url}: ${cause.message}`, {
|
|
63
|
+
code: "CONNECTION_FAILED",
|
|
64
|
+
operation: "connect",
|
|
65
|
+
cause,
|
|
66
|
+
hint: "Start Kakapo or correct the WebSocket URL.",
|
|
67
|
+
}));
|
|
68
|
+
};
|
|
69
|
+
const onNodeError = (cause) => rejectConnection(cause);
|
|
70
|
+
const onBrowserOpen = () => onOpen();
|
|
71
|
+
const onBrowserError = (event) => rejectConnection(new Error(event.message ?? "WebSocket connection failed"));
|
|
72
|
+
if (this.browserSocket) {
|
|
73
|
+
socket.addEventListener?.("open", onBrowserOpen, { once: true });
|
|
74
|
+
socket.addEventListener?.("error", onBrowserError, { once: true });
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
socket.once?.("open", onOpen);
|
|
78
|
+
socket.once?.("error", onNodeError);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
if (this.browserSocket) {
|
|
82
|
+
socket.addEventListener?.("message", (event) => {
|
|
83
|
+
const data = event.data;
|
|
84
|
+
this.handleMessage(data, typeof data !== "string");
|
|
85
|
+
});
|
|
86
|
+
socket.addEventListener?.("close", () => this.handleClose());
|
|
87
|
+
socket.addEventListener?.("error", () => undefined);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
socket.on?.("message", ((data, isBinary) => this.handleMessage(data, isBinary)));
|
|
91
|
+
socket.on?.("close", (() => this.handleClose()));
|
|
92
|
+
socket.on?.("error", (() => undefined));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
disconnect() {
|
|
96
|
+
const socket = this.socket;
|
|
97
|
+
this.socket = undefined;
|
|
98
|
+
if (socket) {
|
|
99
|
+
socket.removeAllListeners?.();
|
|
100
|
+
if (socket.readyState === 1 || socket.readyState === 0) {
|
|
101
|
+
socket.close();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
this.rejectAll("The Kakapo WebSocket was disconnected by the client.");
|
|
105
|
+
}
|
|
106
|
+
rpc(method, params = [], timeoutMs = this.options.requestTimeoutMs) {
|
|
107
|
+
if (!this.connected || !this.socket) {
|
|
108
|
+
return Promise.reject(new KakapoConnectionError(`Cannot call '${method}' while disconnected.`, {
|
|
109
|
+
code: "NOT_CONNECTED",
|
|
110
|
+
operation: method,
|
|
111
|
+
hint: "Call connect() before issuing Kakapo API operations.",
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
if (!method.trim()) {
|
|
115
|
+
return Promise.reject(new KakapoValidationError("RPC method must be a non-empty string.", {
|
|
116
|
+
code: "INVALID_RPC_METHOD",
|
|
117
|
+
operation: "rpc",
|
|
118
|
+
path: "method",
|
|
119
|
+
received: method,
|
|
120
|
+
expected: "non-empty method name",
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
const id = this.nextRequestId++;
|
|
124
|
+
const payload = JSON.stringify({ jsonrpc: "2.0", id, method, params });
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
const timer = setTimeout(() => {
|
|
127
|
+
this.requests.delete(id);
|
|
128
|
+
reject(new KakapoTimeoutError(`Kakapo RPC '${method}' timed out after ${timeoutMs}ms.`, {
|
|
129
|
+
code: "RPC_TIMEOUT",
|
|
130
|
+
operation: method,
|
|
131
|
+
expected: `reply to request ${id} within ${timeoutMs}ms`,
|
|
132
|
+
hint: "The engine may be busy; retry after ping() confirms it is responsive.",
|
|
133
|
+
details: { requestId: id },
|
|
134
|
+
}));
|
|
135
|
+
}, timeoutMs);
|
|
136
|
+
this.requests.set(id, { method, resolve: resolve, reject, timer });
|
|
137
|
+
if (this.browserSocket) {
|
|
138
|
+
try {
|
|
139
|
+
this.socket.send(payload);
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
clearTimeout(timer);
|
|
143
|
+
this.requests.delete(id);
|
|
144
|
+
reject(new KakapoConnectionError(`Failed to send Kakapo RPC '${method}'.`, {
|
|
145
|
+
code: "SEND_FAILED",
|
|
146
|
+
operation: method,
|
|
147
|
+
cause: error,
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
this.socket.send(payload, (error) => {
|
|
153
|
+
if (!error)
|
|
154
|
+
return;
|
|
155
|
+
const pending = this.requests.get(id);
|
|
156
|
+
if (!pending)
|
|
157
|
+
return;
|
|
158
|
+
clearTimeout(pending.timer);
|
|
159
|
+
this.requests.delete(id);
|
|
160
|
+
reject(new KakapoConnectionError(`Failed to send Kakapo RPC '${method}': ${error.message}`, {
|
|
161
|
+
code: "SEND_FAILED",
|
|
162
|
+
operation: method,
|
|
163
|
+
cause: error,
|
|
164
|
+
}));
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
newToken() {
|
|
169
|
+
// Kakapo's fixed wire field is char[16], including the null terminator.
|
|
170
|
+
return `k${(this.nextTokenId++).toString(36)}`.slice(0, 15);
|
|
171
|
+
}
|
|
172
|
+
waitForToken(token, timeoutMs = this.options.requestTimeoutMs) {
|
|
173
|
+
if (!token || new TextEncoder().encode(token).byteLength > 15) {
|
|
174
|
+
return Promise.reject(new KakapoValidationError("Kakapo tokens must contain 1-15 UTF-8 bytes.", {
|
|
175
|
+
code: "INVALID_TOKEN",
|
|
176
|
+
operation: "waitForToken",
|
|
177
|
+
path: "token",
|
|
178
|
+
received: token,
|
|
179
|
+
expected: "1-15 UTF-8 bytes",
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
const queued = this.tokenInbox.get(token);
|
|
183
|
+
if (queued) {
|
|
184
|
+
this.tokenInbox.delete(token);
|
|
185
|
+
return Promise.resolve(queued);
|
|
186
|
+
}
|
|
187
|
+
if (this.tokenWaiters.has(token)) {
|
|
188
|
+
return Promise.reject(new KakapoValidationError(`A waiter already exists for token '${token}'.`, {
|
|
189
|
+
code: "DUPLICATE_TOKEN_WAITER",
|
|
190
|
+
operation: "waitForToken",
|
|
191
|
+
path: "token",
|
|
192
|
+
received: token,
|
|
193
|
+
hint: "Create a distinct token with newToken() for each asynchronous operation.",
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
return new Promise((resolve, reject) => {
|
|
197
|
+
const timer = setTimeout(() => {
|
|
198
|
+
this.tokenWaiters.delete(token);
|
|
199
|
+
reject(new KakapoTimeoutError(`Timed out waiting for Kakapo token '${token}'.`, {
|
|
200
|
+
code: "TOKEN_TIMEOUT",
|
|
201
|
+
operation: "waitForToken",
|
|
202
|
+
details: { token },
|
|
203
|
+
}));
|
|
204
|
+
}, timeoutMs);
|
|
205
|
+
this.tokenWaiters.set(token, { resolve, reject, timer });
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
handleMessage(data, isBinary) {
|
|
209
|
+
if (isBinary) {
|
|
210
|
+
const bytes = data instanceof Uint8Array
|
|
211
|
+
? data
|
|
212
|
+
: data instanceof ArrayBuffer
|
|
213
|
+
? new Uint8Array(data)
|
|
214
|
+
: undefined;
|
|
215
|
+
if (!bytes)
|
|
216
|
+
return;
|
|
217
|
+
if (bytes.length < 48)
|
|
218
|
+
return;
|
|
219
|
+
const frame = parseBinaryFrame(bytes);
|
|
220
|
+
this.deliverToken(frame.token, frame);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
let message;
|
|
224
|
+
try {
|
|
225
|
+
const text = typeof data === "string"
|
|
226
|
+
? data
|
|
227
|
+
: data instanceof Uint8Array
|
|
228
|
+
? new TextDecoder().decode(data)
|
|
229
|
+
: data instanceof ArrayBuffer
|
|
230
|
+
? new TextDecoder().decode(new Uint8Array(data))
|
|
231
|
+
: "";
|
|
232
|
+
message = JSON.parse(text);
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const isReply = Object.hasOwn(message, "result") || Object.hasOwn(message, "error") ||
|
|
238
|
+
(typeof message.method === "string" && !Object.hasOwn(message, "params"));
|
|
239
|
+
if (typeof message.id === "number" && isReply) {
|
|
240
|
+
const pending = this.requests.get(message.id);
|
|
241
|
+
if (!pending)
|
|
242
|
+
return;
|
|
243
|
+
clearTimeout(pending.timer);
|
|
244
|
+
this.requests.delete(message.id);
|
|
245
|
+
if (message.error) {
|
|
246
|
+
const error = typeof message.error === "string" ? { message: message.error } : message.error;
|
|
247
|
+
pending.reject(new KakapoRpcError(pending.method, error.message ?? "Unknown RPC error", error.code, {
|
|
248
|
+
requestId: message.id,
|
|
249
|
+
}));
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
pending.resolve(message.result);
|
|
253
|
+
}
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
// Broadcast echoes include jsonrpc/method/id but no result/error and are intentionally ignored.
|
|
257
|
+
if (typeof message.token === "string") {
|
|
258
|
+
this.deliverToken(message.token, message);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
deliverToken(token, value) {
|
|
262
|
+
const waiter = this.tokenWaiters.get(token);
|
|
263
|
+
if (!waiter) {
|
|
264
|
+
this.tokenInbox.set(token, value);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
clearTimeout(waiter.timer);
|
|
268
|
+
this.tokenWaiters.delete(token);
|
|
269
|
+
waiter.resolve(value);
|
|
270
|
+
}
|
|
271
|
+
handleClose() {
|
|
272
|
+
this.socket = undefined;
|
|
273
|
+
this.rejectAll("The Kakapo WebSocket connection closed unexpectedly.");
|
|
274
|
+
}
|
|
275
|
+
rejectAll(message) {
|
|
276
|
+
for (const pending of this.requests.values()) {
|
|
277
|
+
clearTimeout(pending.timer);
|
|
278
|
+
pending.reject(new KakapoConnectionError(message, {
|
|
279
|
+
code: "CONNECTION_CLOSED",
|
|
280
|
+
operation: pending.method,
|
|
281
|
+
hint: "Reconnect and refresh the scene before retrying the operation.",
|
|
282
|
+
}));
|
|
283
|
+
}
|
|
284
|
+
this.requests.clear();
|
|
285
|
+
for (const [token, waiter] of this.tokenWaiters) {
|
|
286
|
+
clearTimeout(waiter.timer);
|
|
287
|
+
waiter.reject(new KakapoConnectionError(message, {
|
|
288
|
+
code: "CONNECTION_CLOSED",
|
|
289
|
+
operation: "waitForToken",
|
|
290
|
+
details: { token },
|
|
291
|
+
}));
|
|
292
|
+
}
|
|
293
|
+
this.tokenWaiters.clear();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
export function parseBinaryFrame(bytes) {
|
|
297
|
+
if (bytes.byteLength < 48) {
|
|
298
|
+
throw new KakapoValidationError(`Binary frame is ${bytes.byteLength} bytes; Kakapo requires a 48-byte header.`, {
|
|
299
|
+
code: "INVALID_BINARY_FRAME",
|
|
300
|
+
operation: "parseBinaryFrame",
|
|
301
|
+
received: bytes.byteLength,
|
|
302
|
+
expected: "at least 48 bytes",
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
const header = bytes.subarray(0, 16);
|
|
306
|
+
const tokenEnd = header.indexOf(0);
|
|
307
|
+
const token = new TextDecoder().decode(header.subarray(0, tokenEnd === -1 ? 16 : tokenEnd));
|
|
308
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
309
|
+
return {
|
|
310
|
+
token,
|
|
311
|
+
version: view.getUint32(16, true),
|
|
312
|
+
width: view.getUint32(20, true),
|
|
313
|
+
height: view.getUint32(24, true),
|
|
314
|
+
type: view.getUint32(28, true),
|
|
315
|
+
payload: bytes.slice(48),
|
|
316
|
+
};
|
|
317
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
export type JsonPrimitive = string | number | boolean | null;
|
|
2
|
+
export type JsonValue = JsonPrimitive | JsonValue[] | {
|
|
3
|
+
[key: string]: JsonValue;
|
|
4
|
+
};
|
|
5
|
+
export interface Vec2 {
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
}
|
|
9
|
+
export interface Vec3 {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
z: number;
|
|
13
|
+
}
|
|
14
|
+
export interface Transform {
|
|
15
|
+
position: Vec3;
|
|
16
|
+
rotation: Vec3;
|
|
17
|
+
scale: Vec3;
|
|
18
|
+
}
|
|
19
|
+
export type NodeKind = "primitive" | "union" | "light" | "curve" | "group" | "text" | "field" | "svg" | "mesh" | "decal" | "openScad" | "socket";
|
|
20
|
+
export type PrimitiveOperation = "subtract" | "union" | "brush" | "intersect";
|
|
21
|
+
export type PrimitiveType = "sphere" | "cylinder" | "box" | "cone" | "torus" | "link" | "hexagon" | "triangular" | "octahedron" | "pyramid" | "glyph" | "field" | "ellipsoid" | "capsule";
|
|
22
|
+
export type FieldColorSampling = "ignore" | "average" | "override" | "overrideMaterial" | "multiply";
|
|
23
|
+
export type TextWrap = "disabled" | "always" | "whiteSpace";
|
|
24
|
+
export type TextAlign = "left" | "center" | "right";
|
|
25
|
+
export type LightType = "rect" | "sphere" | "distant";
|
|
26
|
+
export interface NodeBase {
|
|
27
|
+
id: number;
|
|
28
|
+
kind: NodeKind;
|
|
29
|
+
name: string;
|
|
30
|
+
parentId: number | null;
|
|
31
|
+
transform: Transform;
|
|
32
|
+
pickable: boolean;
|
|
33
|
+
}
|
|
34
|
+
export interface TransformNode extends NodeBase {
|
|
35
|
+
hidden: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface MaterialNode {
|
|
38
|
+
materialId: number | null;
|
|
39
|
+
}
|
|
40
|
+
export interface OperationNode {
|
|
41
|
+
operation: PrimitiveOperation;
|
|
42
|
+
blend: number;
|
|
43
|
+
}
|
|
44
|
+
export type MirrorPlaneConstraint = 0 | 1 | 2 | 3;
|
|
45
|
+
export interface MirrorPlane {
|
|
46
|
+
position: Vec3;
|
|
47
|
+
rotation: Vec3;
|
|
48
|
+
value: number;
|
|
49
|
+
hide: number;
|
|
50
|
+
hideMirrorPlane: boolean;
|
|
51
|
+
constraint: MirrorPlaneConstraint;
|
|
52
|
+
customRotation?: Vec3;
|
|
53
|
+
}
|
|
54
|
+
export type MirrorPlanes = [MirrorPlane, MirrorPlane, MirrorPlane, MirrorPlane];
|
|
55
|
+
export interface MirrorNode {
|
|
56
|
+
mirrors: MirrorPlanes;
|
|
57
|
+
}
|
|
58
|
+
export interface PrimitiveNode extends TransformNode, MaterialNode, OperationNode, MirrorNode {
|
|
59
|
+
kind: "primitive";
|
|
60
|
+
color: Vec3;
|
|
61
|
+
primitive: PrimitiveType;
|
|
62
|
+
round: number;
|
|
63
|
+
thickness: number;
|
|
64
|
+
inflation: number;
|
|
65
|
+
materialNeutralCutout: boolean;
|
|
66
|
+
}
|
|
67
|
+
export interface UnionNode extends TransformNode, OperationNode {
|
|
68
|
+
kind: "union";
|
|
69
|
+
resolution: number;
|
|
70
|
+
thickness: number;
|
|
71
|
+
inflation: number;
|
|
72
|
+
materialNeutralCutout: boolean;
|
|
73
|
+
}
|
|
74
|
+
export interface GroupNode extends TransformNode {
|
|
75
|
+
kind: "group";
|
|
76
|
+
}
|
|
77
|
+
export interface LightNode extends NodeBase {
|
|
78
|
+
kind: "light";
|
|
79
|
+
color: Vec3;
|
|
80
|
+
power: number;
|
|
81
|
+
collimation: number;
|
|
82
|
+
lightType: LightType;
|
|
83
|
+
size: Vec2;
|
|
84
|
+
textureFile: string;
|
|
85
|
+
}
|
|
86
|
+
export interface CurvePoint {
|
|
87
|
+
position: Vec3;
|
|
88
|
+
rotation: Vec3;
|
|
89
|
+
scale: Vec3;
|
|
90
|
+
materialId: number | null;
|
|
91
|
+
round: number;
|
|
92
|
+
fixed: boolean;
|
|
93
|
+
}
|
|
94
|
+
export interface CurveNode extends NodeBase, MaterialNode, OperationNode, MirrorNode {
|
|
95
|
+
kind: "curve";
|
|
96
|
+
hidden: boolean;
|
|
97
|
+
primitive: PrimitiveType;
|
|
98
|
+
density: number;
|
|
99
|
+
roundness: number;
|
|
100
|
+
smoothing: number;
|
|
101
|
+
materialNeutralCutout: boolean;
|
|
102
|
+
points: CurvePoint[];
|
|
103
|
+
}
|
|
104
|
+
export interface TextNode extends TransformNode, MaterialNode, OperationNode {
|
|
105
|
+
kind: "text";
|
|
106
|
+
text: string;
|
|
107
|
+
fontFamily: string;
|
|
108
|
+
weight: number;
|
|
109
|
+
italic: boolean;
|
|
110
|
+
round: number;
|
|
111
|
+
width: number;
|
|
112
|
+
wrap: TextWrap;
|
|
113
|
+
align: TextAlign;
|
|
114
|
+
spacing: number;
|
|
115
|
+
lineHeight: number;
|
|
116
|
+
materialNeutralCutout: boolean;
|
|
117
|
+
}
|
|
118
|
+
export interface SvgSegment {
|
|
119
|
+
point: Vec2;
|
|
120
|
+
handleIn: Vec2;
|
|
121
|
+
handleOut: Vec2;
|
|
122
|
+
}
|
|
123
|
+
export interface SvgPath {
|
|
124
|
+
segments: SvgSegment[];
|
|
125
|
+
closed: boolean;
|
|
126
|
+
}
|
|
127
|
+
export interface SvgNode extends TransformNode, MaterialNode, OperationNode {
|
|
128
|
+
kind: "svg";
|
|
129
|
+
paths: SvgPath[];
|
|
130
|
+
inflate: boolean;
|
|
131
|
+
outline: boolean;
|
|
132
|
+
outlineSize: number;
|
|
133
|
+
materialNeutralCutout: boolean;
|
|
134
|
+
}
|
|
135
|
+
export interface MeshNode extends TransformNode, MaterialNode {
|
|
136
|
+
kind: "mesh";
|
|
137
|
+
mesh: string;
|
|
138
|
+
meshIndex: number;
|
|
139
|
+
smoothNormals: boolean;
|
|
140
|
+
colorSampling: FieldColorSampling;
|
|
141
|
+
}
|
|
142
|
+
export interface FieldNode extends TransformNode, MaterialNode, OperationNode {
|
|
143
|
+
kind: "field";
|
|
144
|
+
field: string;
|
|
145
|
+
inflation: number;
|
|
146
|
+
colorSampling: FieldColorSampling;
|
|
147
|
+
materialNeutralCutout: boolean;
|
|
148
|
+
}
|
|
149
|
+
export interface DecalNode extends TransformNode, MaterialNode {
|
|
150
|
+
kind: "decal";
|
|
151
|
+
image: string;
|
|
152
|
+
global: boolean;
|
|
153
|
+
colorSampling: "texture" | "multiply" | "material";
|
|
154
|
+
}
|
|
155
|
+
export interface OpenScadNode extends TransformNode, MaterialNode, OperationNode {
|
|
156
|
+
kind: "openScad";
|
|
157
|
+
scriptId: number;
|
|
158
|
+
params: Record<string, JsonValue>;
|
|
159
|
+
enabled: boolean;
|
|
160
|
+
}
|
|
161
|
+
export interface SocketNode extends TransformNode {
|
|
162
|
+
kind: "socket";
|
|
163
|
+
tag: string;
|
|
164
|
+
}
|
|
165
|
+
export type KakapoNode = PrimitiveNode | UnionNode | LightNode | CurveNode | GroupNode | TextNode | FieldNode | SvgNode | MeshNode | DecalNode | OpenScadNode | SocketNode;
|
|
166
|
+
export interface NodeUpdate {
|
|
167
|
+
transform?: Partial<Transform>;
|
|
168
|
+
pickable?: boolean;
|
|
169
|
+
hidden?: boolean;
|
|
170
|
+
materialId?: number | null;
|
|
171
|
+
operation?: PrimitiveOperation;
|
|
172
|
+
blend?: number;
|
|
173
|
+
primitive?: PrimitiveType;
|
|
174
|
+
round?: number;
|
|
175
|
+
thickness?: number;
|
|
176
|
+
inflation?: number;
|
|
177
|
+
materialNeutralCutout?: boolean;
|
|
178
|
+
resolution?: number;
|
|
179
|
+
color?: Vec3;
|
|
180
|
+
power?: number;
|
|
181
|
+
collimation?: number;
|
|
182
|
+
lightType?: LightType;
|
|
183
|
+
size?: Vec2;
|
|
184
|
+
textureFile?: string;
|
|
185
|
+
density?: number;
|
|
186
|
+
roundness?: number;
|
|
187
|
+
smoothing?: number;
|
|
188
|
+
points?: CurvePoint[];
|
|
189
|
+
text?: string;
|
|
190
|
+
fontFamily?: string;
|
|
191
|
+
weight?: number;
|
|
192
|
+
italic?: boolean;
|
|
193
|
+
width?: number;
|
|
194
|
+
wrap?: TextWrap;
|
|
195
|
+
align?: TextAlign;
|
|
196
|
+
spacing?: number;
|
|
197
|
+
lineHeight?: number;
|
|
198
|
+
paths?: SvgPath[];
|
|
199
|
+
inflate?: boolean;
|
|
200
|
+
outline?: boolean;
|
|
201
|
+
outlineSize?: number;
|
|
202
|
+
mesh?: string;
|
|
203
|
+
meshIndex?: number;
|
|
204
|
+
smoothNormals?: boolean;
|
|
205
|
+
colorSampling?: FieldColorSampling | "texture" | "material";
|
|
206
|
+
field?: string;
|
|
207
|
+
image?: string;
|
|
208
|
+
global?: boolean;
|
|
209
|
+
scriptId?: number;
|
|
210
|
+
params?: Record<string, JsonValue>;
|
|
211
|
+
enabled?: boolean;
|
|
212
|
+
tag?: string;
|
|
213
|
+
mirrors?: MirrorPlanes;
|
|
214
|
+
}
|
|
215
|
+
export interface CreateNodeInput<K extends NodeKind = NodeKind> {
|
|
216
|
+
kind: K;
|
|
217
|
+
parentId: number;
|
|
218
|
+
name?: string;
|
|
219
|
+
properties?: NodeUpdate;
|
|
220
|
+
index?: number;
|
|
221
|
+
}
|
|
222
|
+
export interface MaterialData {
|
|
223
|
+
color: Vec3;
|
|
224
|
+
metalness: number;
|
|
225
|
+
roughness: number;
|
|
226
|
+
transmittance: number;
|
|
227
|
+
translucency: number;
|
|
228
|
+
translucencyWeight: number;
|
|
229
|
+
secondaryColor: Vec3;
|
|
230
|
+
subsurface: Vec3;
|
|
231
|
+
indexOfRefraction: number;
|
|
232
|
+
iridescence: number;
|
|
233
|
+
emission: number;
|
|
234
|
+
absorption: number;
|
|
235
|
+
sheen: number;
|
|
236
|
+
sheenRoughness: number;
|
|
237
|
+
specularTint: number;
|
|
238
|
+
dispersion: number;
|
|
239
|
+
surfaceOpacity: number;
|
|
240
|
+
volumetricEnabled: boolean;
|
|
241
|
+
}
|
|
242
|
+
export interface Material {
|
|
243
|
+
id: number;
|
|
244
|
+
data: MaterialData;
|
|
245
|
+
shaderIds: number[];
|
|
246
|
+
}
|
|
247
|
+
export interface OpenScadScript {
|
|
248
|
+
id: number;
|
|
249
|
+
name: string;
|
|
250
|
+
source: string;
|
|
251
|
+
}
|
|
252
|
+
export interface FontFace {
|
|
253
|
+
path: string;
|
|
254
|
+
weight: number;
|
|
255
|
+
italic: boolean;
|
|
256
|
+
shared: boolean;
|
|
257
|
+
}
|
|
258
|
+
export interface FontFamily {
|
|
259
|
+
name: string;
|
|
260
|
+
fonts: FontFace[];
|
|
261
|
+
}
|
|
262
|
+
export interface Scene {
|
|
263
|
+
version: number;
|
|
264
|
+
name: string;
|
|
265
|
+
nodes: Record<string, KakapoNode>;
|
|
266
|
+
tree: Record<string, number[]>;
|
|
267
|
+
materials: Record<string, Material>;
|
|
268
|
+
openScadScripts: Record<string, OpenScadScript>;
|
|
269
|
+
properties: Record<string, JsonValue>;
|
|
270
|
+
camera: Record<string, JsonValue>;
|
|
271
|
+
}
|
|
272
|
+
export interface BoundingBox {
|
|
273
|
+
min: Vec3;
|
|
274
|
+
max: Vec3;
|
|
275
|
+
transform: Transform;
|
|
276
|
+
}
|
|
277
|
+
export interface OpenScadParameter {
|
|
278
|
+
name: string;
|
|
279
|
+
default_value: JsonValue;
|
|
280
|
+
value: JsonValue;
|
|
281
|
+
min: JsonValue;
|
|
282
|
+
max: JsonValue;
|
|
283
|
+
step: JsonValue;
|
|
284
|
+
enum_options: Array<{
|
|
285
|
+
value: JsonValue;
|
|
286
|
+
raw_value: JsonValue;
|
|
287
|
+
label: string;
|
|
288
|
+
}>;
|
|
289
|
+
}
|
|
290
|
+
export interface OpenScadValidationResult {
|
|
291
|
+
ok: boolean;
|
|
292
|
+
version: number;
|
|
293
|
+
error: string;
|
|
294
|
+
warnings: string[];
|
|
295
|
+
params: OpenScadParameter[];
|
|
296
|
+
materials: Array<{
|
|
297
|
+
material_id: number;
|
|
298
|
+
color: Vec3;
|
|
299
|
+
}>;
|
|
300
|
+
bounding_box: {
|
|
301
|
+
min: Vec3;
|
|
302
|
+
max: Vec3;
|
|
303
|
+
} | null;
|
|
304
|
+
}
|
|
305
|
+
export interface BinaryFrame {
|
|
306
|
+
token: string;
|
|
307
|
+
version: number;
|
|
308
|
+
width: number;
|
|
309
|
+
height: number;
|
|
310
|
+
type: number;
|
|
311
|
+
payload: Uint8Array;
|
|
312
|
+
}
|
|
313
|
+
export type TokenMessage = BinaryFrame | Record<string, JsonValue>;
|
|
314
|
+
export type ScreenshotView = "front" | "back" | "left" | "right" | "top" | "bottom" | "isometric";
|
|
315
|
+
export interface CaptureScreenshotOptions {
|
|
316
|
+
/** Omit both selectors to frame all visible top-level scene nodes. */
|
|
317
|
+
targetId?: number;
|
|
318
|
+
targetIds?: number[];
|
|
319
|
+
view?: ScreenshotView;
|
|
320
|
+
timeoutMs?: number;
|
|
321
|
+
}
|
|
322
|
+
export interface ScreenshotImage {
|
|
323
|
+
data: Uint8Array;
|
|
324
|
+
mediaType: "image/jpeg";
|
|
325
|
+
width: number;
|
|
326
|
+
height: number;
|
|
327
|
+
view: ScreenshotView;
|
|
328
|
+
targetId: number;
|
|
329
|
+
}
|
|
330
|
+
export type JsonPatchOperation = {
|
|
331
|
+
op: "add" | "replace" | "test";
|
|
332
|
+
path: string;
|
|
333
|
+
value: JsonValue;
|
|
334
|
+
} | {
|
|
335
|
+
op: "remove";
|
|
336
|
+
path: string;
|
|
337
|
+
} | {
|
|
338
|
+
op: "move" | "copy";
|
|
339
|
+
from: string;
|
|
340
|
+
path: string;
|
|
341
|
+
};
|
|
342
|
+
export interface FindNodeOptions {
|
|
343
|
+
name?: string;
|
|
344
|
+
kind?: NodeKind;
|
|
345
|
+
ancestorId?: number;
|
|
346
|
+
}
|
|
347
|
+
export interface ListNodeOptions {
|
|
348
|
+
kind?: NodeKind;
|
|
349
|
+
parentId?: number;
|
|
350
|
+
}
|
|
351
|
+
export interface DeleteNodeOptions {
|
|
352
|
+
recursive?: boolean;
|
|
353
|
+
}
|
|
354
|
+
export interface KakapoAPIOptions {
|
|
355
|
+
url?: string;
|
|
356
|
+
requestTimeoutMs?: number;
|
|
357
|
+
connectTimeoutMs?: number;
|
|
358
|
+
}
|
|
359
|
+
export type PendingResources = JsonValue;
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|