@neta-art/cohub 2.14.1 → 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.
@@ -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 };