@neta-art/cohub 2.15.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -0
- package/dist/board.d.ts +99 -0
- package/dist/board.js +2 -0
- package/dist/chunks/board.d.ts +2589 -0
- package/dist/chunks/board.js +392 -0
- package/dist/chunks/http.d.ts +238 -52
- package/dist/chunks/http.js +179 -66
- package/dist/chunks/websocket.d.ts +37 -116
- package/dist/chunks/websocket.js +0 -16
- package/dist/http.d.ts +4 -3
- package/dist/index.d.ts +5 -3
- package/dist/index.js +3 -2
- package/package.json +5 -1
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
//#region ../protocol/dist/board-constants.js
|
|
2
|
+
const DEFAULT_BOARD_RENDER_LIMITS = {
|
|
3
|
+
particles: 2e4,
|
|
4
|
+
vertices: 5e5,
|
|
5
|
+
dynamicVertices: 15e4,
|
|
6
|
+
drawCalls: 400,
|
|
7
|
+
filterPasses: 24,
|
|
8
|
+
renderTexturePixels: 16777216,
|
|
9
|
+
textureBytes: 512 * 1024 * 1024,
|
|
10
|
+
bufferBytes: 256 * 1024 * 1024,
|
|
11
|
+
simulationSteps: 1e5
|
|
12
|
+
};
|
|
13
|
+
const BOARD_BUILTIN_CLIP_KINDS = [
|
|
14
|
+
"motion.keyframes",
|
|
15
|
+
"motion.path",
|
|
16
|
+
"draw.reveal",
|
|
17
|
+
"draw.handwrite",
|
|
18
|
+
"text.reveal",
|
|
19
|
+
"effects.particles",
|
|
20
|
+
"effects.trail",
|
|
21
|
+
"effects.impact",
|
|
22
|
+
"effects.flash",
|
|
23
|
+
"effects.color",
|
|
24
|
+
"camera.pan",
|
|
25
|
+
"camera.zoom",
|
|
26
|
+
"camera.shake"
|
|
27
|
+
];
|
|
28
|
+
const BOARD_BUILTIN_EFFECT_KINDS = ["effects.pulse", "effects.float"];
|
|
29
|
+
const BOARD_BUILTIN_CAPABILITIES = [...BOARD_BUILTIN_CLIP_KINDS.map((id) => ({
|
|
30
|
+
kind: "clip",
|
|
31
|
+
id,
|
|
32
|
+
version: 1,
|
|
33
|
+
renderers: ["webgpu", "webgl"]
|
|
34
|
+
})), ...BOARD_BUILTIN_EFFECT_KINDS.map((id) => ({
|
|
35
|
+
kind: "effect",
|
|
36
|
+
id,
|
|
37
|
+
version: 1,
|
|
38
|
+
renderers: ["webgpu", "webgl"]
|
|
39
|
+
}))];
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/board.ts
|
|
42
|
+
const ZERO_COST = {
|
|
43
|
+
particles: 0,
|
|
44
|
+
vertices: 0,
|
|
45
|
+
dynamicVertices: 0,
|
|
46
|
+
drawCalls: 0,
|
|
47
|
+
filterPasses: 0,
|
|
48
|
+
renderTexturePixels: 0,
|
|
49
|
+
textureBytes: 0,
|
|
50
|
+
bufferBytes: 0,
|
|
51
|
+
simulationSteps: 0
|
|
52
|
+
};
|
|
53
|
+
const DEFAULT_BOARD_LIMITS = DEFAULT_BOARD_RENDER_LIMITS;
|
|
54
|
+
const BUILTIN_CAPABILITIES = BOARD_BUILTIN_CAPABILITIES;
|
|
55
|
+
function numericParam(params, key, fallback) {
|
|
56
|
+
const value = params[key];
|
|
57
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
58
|
+
}
|
|
59
|
+
function builtinDefinition(capability) {
|
|
60
|
+
return {
|
|
61
|
+
...capability,
|
|
62
|
+
validate(params) {
|
|
63
|
+
const diagnostics = [];
|
|
64
|
+
if (capability.id === "effects.particles") {
|
|
65
|
+
const count = params.count;
|
|
66
|
+
const bounds = params.bounds;
|
|
67
|
+
if (!Number.isSafeInteger(count) || count < 1 || count > DEFAULT_BOARD_LIMITS.particles) diagnostics.push({
|
|
68
|
+
severity: "error",
|
|
69
|
+
code: "INVALID_PARTICLE_COUNT",
|
|
70
|
+
message: `count must be an integer between 1 and ${DEFAULT_BOARD_LIMITS.particles}`,
|
|
71
|
+
path: "params.count"
|
|
72
|
+
});
|
|
73
|
+
if (!bounds || typeof bounds !== "object" || Array.isArray(bounds)) diagnostics.push({
|
|
74
|
+
severity: "error",
|
|
75
|
+
code: "PARTICLE_BOUNDS_REQUIRED",
|
|
76
|
+
message: "particles require finite bounds",
|
|
77
|
+
path: "params.bounds"
|
|
78
|
+
});
|
|
79
|
+
else {
|
|
80
|
+
const value = bounds;
|
|
81
|
+
if (![
|
|
82
|
+
value.x,
|
|
83
|
+
value.y,
|
|
84
|
+
value.width,
|
|
85
|
+
value.height
|
|
86
|
+
].every((item) => typeof item === "number" && Number.isFinite(item)) || value.width <= 0 || value.height <= 0) diagnostics.push({
|
|
87
|
+
severity: "error",
|
|
88
|
+
code: "INVALID_PARTICLE_BOUNDS",
|
|
89
|
+
message: "particle bounds must have a positive finite size",
|
|
90
|
+
path: "params.bounds"
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (capability.id === "motion.path") {
|
|
95
|
+
const points = params.points;
|
|
96
|
+
if (!Array.isArray(points) || points.length < 2 || points.length > 1e4) diagnostics.push({
|
|
97
|
+
severity: "error",
|
|
98
|
+
code: "INVALID_MOTION_PATH",
|
|
99
|
+
message: "motion path must contain 2 to 10000 points",
|
|
100
|
+
path: "params.points"
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return diagnostics;
|
|
104
|
+
},
|
|
105
|
+
getBounds(params) {
|
|
106
|
+
const bounds = params.bounds;
|
|
107
|
+
if (!bounds || typeof bounds !== "object" || Array.isArray(bounds)) return null;
|
|
108
|
+
const value = bounds;
|
|
109
|
+
return [
|
|
110
|
+
value.x,
|
|
111
|
+
value.y,
|
|
112
|
+
value.width,
|
|
113
|
+
value.height
|
|
114
|
+
].every((item) => typeof item === "number" && Number.isFinite(item)) ? value : null;
|
|
115
|
+
},
|
|
116
|
+
estimateCost(params) {
|
|
117
|
+
switch (capability.id) {
|
|
118
|
+
case "effects.particles": {
|
|
119
|
+
const particles = Math.max(0, Math.floor(numericParam(params, "count", 0)));
|
|
120
|
+
return {
|
|
121
|
+
particles,
|
|
122
|
+
vertices: particles * 4,
|
|
123
|
+
dynamicVertices: particles * 4,
|
|
124
|
+
drawCalls: 1,
|
|
125
|
+
bufferBytes: particles * 48,
|
|
126
|
+
simulationSteps: particles
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
case "effects.trail": return {
|
|
130
|
+
vertices: 32,
|
|
131
|
+
dynamicVertices: 32,
|
|
132
|
+
drawCalls: 1,
|
|
133
|
+
bufferBytes: 1024,
|
|
134
|
+
simulationSteps: 16
|
|
135
|
+
};
|
|
136
|
+
case "effects.impact":
|
|
137
|
+
case "effects.flash": return {
|
|
138
|
+
vertices: 64,
|
|
139
|
+
drawCalls: 1
|
|
140
|
+
};
|
|
141
|
+
case "effects.color": return {
|
|
142
|
+
drawCalls: 1,
|
|
143
|
+
filterPasses: 1
|
|
144
|
+
};
|
|
145
|
+
case "draw.reveal":
|
|
146
|
+
case "draw.handwrite": return {
|
|
147
|
+
drawCalls: 1,
|
|
148
|
+
dynamicVertices: 1
|
|
149
|
+
};
|
|
150
|
+
default: return {};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function stableHash(value) {
|
|
156
|
+
let hash = 2166136261;
|
|
157
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
158
|
+
hash ^= value.charCodeAt(index);
|
|
159
|
+
hash = Math.imul(hash, 16777619);
|
|
160
|
+
}
|
|
161
|
+
return (hash >>> 0).toString(36).padStart(7, "0");
|
|
162
|
+
}
|
|
163
|
+
function stableId(prefix, seed, path) {
|
|
164
|
+
return `${prefix}_${stableHash(`${seed}:${path}`)}`;
|
|
165
|
+
}
|
|
166
|
+
function assertDuration(value, field) {
|
|
167
|
+
if (!Number.isFinite(value) || value < 0) throw new TypeError(`${field} must be a finite non-negative number`);
|
|
168
|
+
}
|
|
169
|
+
const timeline = {
|
|
170
|
+
clip(clip) {
|
|
171
|
+
return {
|
|
172
|
+
type: "clip",
|
|
173
|
+
clip
|
|
174
|
+
};
|
|
175
|
+
},
|
|
176
|
+
parallel(...children) {
|
|
177
|
+
return {
|
|
178
|
+
type: "parallel",
|
|
179
|
+
children
|
|
180
|
+
};
|
|
181
|
+
},
|
|
182
|
+
sequence(...children) {
|
|
183
|
+
return {
|
|
184
|
+
type: "sequence",
|
|
185
|
+
children
|
|
186
|
+
};
|
|
187
|
+
},
|
|
188
|
+
stagger(each, ...children) {
|
|
189
|
+
assertDuration(each, "stagger each");
|
|
190
|
+
return {
|
|
191
|
+
type: "stagger",
|
|
192
|
+
each,
|
|
193
|
+
children
|
|
194
|
+
};
|
|
195
|
+
},
|
|
196
|
+
delay(duration, child) {
|
|
197
|
+
assertDuration(duration, "delay duration");
|
|
198
|
+
return {
|
|
199
|
+
type: "delay",
|
|
200
|
+
duration,
|
|
201
|
+
child
|
|
202
|
+
};
|
|
203
|
+
},
|
|
204
|
+
repeat(count, child) {
|
|
205
|
+
if (!Number.isSafeInteger(count) || count < 1 || count > 1e3) throw new TypeError("repeat count must be an integer between 1 and 1000");
|
|
206
|
+
return {
|
|
207
|
+
type: "repeat",
|
|
208
|
+
count,
|
|
209
|
+
child
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
function clip(input) {
|
|
214
|
+
assertDuration(input.duration, "clip duration");
|
|
215
|
+
if (input.duration === 0) throw new TypeError("clip duration must be greater than zero");
|
|
216
|
+
return timeline.clip({
|
|
217
|
+
id: input.id,
|
|
218
|
+
kind: input.kind,
|
|
219
|
+
kindVersion: input.kindVersion ?? 1,
|
|
220
|
+
target: input.target,
|
|
221
|
+
duration: input.duration,
|
|
222
|
+
layer: input.layer ?? "content",
|
|
223
|
+
fill: input.fill ?? "none",
|
|
224
|
+
easing: input.easing ?? "linear",
|
|
225
|
+
params: input.params ?? {},
|
|
226
|
+
keyframes: input.keyframes ?? [],
|
|
227
|
+
assetRefs: input.assetRefs ?? [],
|
|
228
|
+
seed: input.seed,
|
|
229
|
+
metadata: input.metadata ?? {}
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
function flatten(input, offset, sequenceSeed, path) {
|
|
233
|
+
if (input.type === "clip") {
|
|
234
|
+
const clipSeed = input.clip.seed ?? `${sequenceSeed}:${path}`;
|
|
235
|
+
return {
|
|
236
|
+
duration: input.clip.duration,
|
|
237
|
+
clips: [{
|
|
238
|
+
...input.clip,
|
|
239
|
+
id: input.clip.id ?? stableId("clip", sequenceSeed, path),
|
|
240
|
+
start: offset,
|
|
241
|
+
seed: clipSeed
|
|
242
|
+
}]
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
if (input.type === "delay") {
|
|
246
|
+
const nested = flatten(input.child, offset + input.duration, sequenceSeed, `${path}.child`);
|
|
247
|
+
return {
|
|
248
|
+
duration: input.duration + nested.duration,
|
|
249
|
+
clips: nested.clips
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
if (input.type === "repeat") {
|
|
253
|
+
const clips = [];
|
|
254
|
+
let cursor = offset;
|
|
255
|
+
let total = 0;
|
|
256
|
+
for (let index = 0; index < input.count; index += 1) {
|
|
257
|
+
const nested = flatten(input.child, cursor, sequenceSeed, `${path}.${index}`);
|
|
258
|
+
clips.push(...nested.clips);
|
|
259
|
+
cursor += nested.duration;
|
|
260
|
+
total += nested.duration;
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
duration: total,
|
|
264
|
+
clips
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
const clips = [];
|
|
268
|
+
let duration = 0;
|
|
269
|
+
for (let index = 0; index < input.children.length; index += 1) {
|
|
270
|
+
const childOffset = input.type === "sequence" ? duration : input.type === "stagger" ? input.each * index : 0;
|
|
271
|
+
const nested = flatten(input.children[index], offset + childOffset, sequenceSeed, `${path}.${index}`);
|
|
272
|
+
clips.push(...nested.clips);
|
|
273
|
+
duration = input.type === "sequence" ? duration + nested.duration : Math.max(duration, childOffset + nested.duration);
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
duration,
|
|
277
|
+
clips
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
function compileSequence(input) {
|
|
281
|
+
const flattened = flatten(input.timeline, 0, input.seed, "root");
|
|
282
|
+
flattened.clips.sort((left, right) => left.start - right.start || left.id.localeCompare(right.id));
|
|
283
|
+
const refs = /* @__PURE__ */ new Map();
|
|
284
|
+
for (const item of flattened.clips) for (const ref of item.assetRefs) refs.set(`${ref.type}:${ref.ref}:${ref.digest ?? ""}`, ref);
|
|
285
|
+
return {
|
|
286
|
+
sequence: {
|
|
287
|
+
id: input.id,
|
|
288
|
+
name: input.name,
|
|
289
|
+
duration: flattened.duration,
|
|
290
|
+
seed: input.seed,
|
|
291
|
+
restPose: input.restPose ?? {},
|
|
292
|
+
metadata: input.metadata ?? {}
|
|
293
|
+
},
|
|
294
|
+
clips: flattened.clips,
|
|
295
|
+
assetRefs: [...refs.values()]
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function addCost(target, source) {
|
|
299
|
+
for (const key of Object.keys(target)) target[key] += source[key] ?? 0;
|
|
300
|
+
}
|
|
301
|
+
var BoardExtensionRegistry = class {
|
|
302
|
+
#extensions = /* @__PURE__ */ new Map();
|
|
303
|
+
#presets = /* @__PURE__ */ new Map();
|
|
304
|
+
register(definition) {
|
|
305
|
+
const key = `${definition.kind}:${definition.id}@${definition.version}`;
|
|
306
|
+
const target = definition.kind === "preset" ? this.#presets : this.#extensions;
|
|
307
|
+
if (target.has(key)) throw new Error(`Board extension is already registered: ${key}`);
|
|
308
|
+
target.set(key, definition);
|
|
309
|
+
return this;
|
|
310
|
+
}
|
|
311
|
+
capabilities() {
|
|
312
|
+
const extensions = [...this.#extensions.values()].map(({ validate: _validate, getBounds: _bounds, getAssetRefs: _refs, estimateCost: _cost, ...definition }) => definition);
|
|
313
|
+
const presets = [...this.#presets.values()].map(({ compile: _compile, ...definition }) => definition);
|
|
314
|
+
return [...extensions, ...presets];
|
|
315
|
+
}
|
|
316
|
+
compilePreset(id, version, params) {
|
|
317
|
+
const preset = this.#presets.get(`preset:${id}@${version}`);
|
|
318
|
+
if (!preset) throw new Error(`Unknown Board preset: ${id}@${version}`);
|
|
319
|
+
return preset.compile(params);
|
|
320
|
+
}
|
|
321
|
+
validate(input) {
|
|
322
|
+
const diagnostics = [];
|
|
323
|
+
const events = [];
|
|
324
|
+
const profile = input.profile ?? "high";
|
|
325
|
+
const persistentCost = { ...ZERO_COST };
|
|
326
|
+
for (const [index, effect] of (input.effects ?? []).entries()) {
|
|
327
|
+
const definition = this.#extensions.get(`effect:${effect.kind}@${effect.kindVersion}`);
|
|
328
|
+
if (!definition) {
|
|
329
|
+
diagnostics.push({
|
|
330
|
+
severity: "warning",
|
|
331
|
+
code: "UNKNOWN_EFFECT",
|
|
332
|
+
message: `No renderer is registered for ${effect.kind}@${effect.kindVersion}`,
|
|
333
|
+
path: `effects.${index}`
|
|
334
|
+
});
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
diagnostics.push(...definition.validate?.(effect.params) ?? []);
|
|
338
|
+
addCost(persistentCost, definition.estimateCost(effect.params, profile));
|
|
339
|
+
}
|
|
340
|
+
for (const [index, clip] of input.clips.entries()) {
|
|
341
|
+
const definition = this.#extensions.get(`clip:${clip.kind}@${clip.kindVersion}`);
|
|
342
|
+
if (!definition) {
|
|
343
|
+
diagnostics.push({
|
|
344
|
+
severity: "warning",
|
|
345
|
+
code: "UNKNOWN_CLIP",
|
|
346
|
+
message: `No renderer is registered for ${clip.kind}@${clip.kindVersion}`,
|
|
347
|
+
path: `clips.${index}`
|
|
348
|
+
});
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
diagnostics.push(...definition.validate?.(clip.params) ?? []);
|
|
352
|
+
const cost = { ...ZERO_COST };
|
|
353
|
+
addCost(cost, definition.estimateCost(clip.params, profile));
|
|
354
|
+
events.push({
|
|
355
|
+
at: clip.start,
|
|
356
|
+
direction: 1,
|
|
357
|
+
cost
|
|
358
|
+
}, {
|
|
359
|
+
at: clip.start + clip.duration,
|
|
360
|
+
direction: -1,
|
|
361
|
+
cost
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
events.sort((left, right) => left.at - right.at || left.direction - right.direction);
|
|
365
|
+
const current = { ...persistentCost };
|
|
366
|
+
const peak = { ...persistentCost };
|
|
367
|
+
for (const event of events) for (const key of Object.keys(current)) {
|
|
368
|
+
current[key] += event.cost[key] * event.direction;
|
|
369
|
+
peak[key] = Math.max(peak[key], current[key]);
|
|
370
|
+
}
|
|
371
|
+
const limits = input.limits ?? DEFAULT_BOARD_LIMITS;
|
|
372
|
+
for (const key of Object.keys(limits)) if (peak[key] > limits[key]) diagnostics.push({
|
|
373
|
+
severity: "warning",
|
|
374
|
+
code: "RENDER_BUDGET_EXCEEDED",
|
|
375
|
+
message: `${key} peaks at ${peak[key]}, above ${limits[key]}`,
|
|
376
|
+
path: key,
|
|
377
|
+
adaptation: { quality: "lower" }
|
|
378
|
+
});
|
|
379
|
+
return {
|
|
380
|
+
valid: !diagnostics.some((item) => item.severity === "error"),
|
|
381
|
+
diagnostics,
|
|
382
|
+
peakCost: peak
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
function createBoardExtensionRegistry(input = {}) {
|
|
387
|
+
const registry = new BoardExtensionRegistry();
|
|
388
|
+
if (input.builtins !== false) for (const capability of BUILTIN_CAPABILITIES) registry.register(builtinDefinition(capability));
|
|
389
|
+
return registry;
|
|
390
|
+
}
|
|
391
|
+
//#endregion
|
|
392
|
+
export { createBoardExtensionRegistry as a, compileSequence as i, DEFAULT_BOARD_LIMITS as n, timeline as o, clip as r, BoardExtensionRegistry as t };
|