@multiplekex/shallot 0.1.12 → 0.2.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.
Files changed (62) hide show
  1. package/package.json +3 -4
  2. package/src/core/builder.ts +71 -32
  3. package/src/core/component.ts +25 -11
  4. package/src/core/index.ts +14 -13
  5. package/src/core/math.ts +135 -0
  6. package/src/core/runtime.ts +0 -1
  7. package/src/core/state.ts +9 -68
  8. package/src/core/xml.ts +381 -265
  9. package/src/editor/format.ts +5 -0
  10. package/src/editor/index.ts +101 -0
  11. package/src/extras/arrows/index.ts +28 -69
  12. package/src/extras/gradient/index.ts +36 -52
  13. package/src/extras/lines/index.ts +51 -122
  14. package/src/extras/orbit/index.ts +40 -15
  15. package/src/extras/text/font.ts +546 -0
  16. package/src/extras/text/index.ts +158 -204
  17. package/src/extras/text/sdf.ts +429 -0
  18. package/src/standard/activity/index.ts +172 -0
  19. package/src/standard/compute/graph.ts +23 -23
  20. package/src/standard/compute/index.ts +76 -61
  21. package/src/standard/defaults.ts +8 -5
  22. package/src/standard/index.ts +1 -0
  23. package/src/standard/input/index.ts +30 -19
  24. package/src/standard/loading/index.ts +18 -13
  25. package/src/standard/render/bvh/blas.ts +752 -0
  26. package/src/standard/render/bvh/radix.ts +476 -0
  27. package/src/standard/render/bvh/structs.ts +167 -0
  28. package/src/standard/render/bvh/tlas.ts +886 -0
  29. package/src/standard/render/bvh/traverse.ts +467 -0
  30. package/src/standard/render/camera.ts +302 -27
  31. package/src/standard/render/data.ts +93 -0
  32. package/src/standard/render/depth.ts +117 -0
  33. package/src/standard/render/forward/index.ts +259 -0
  34. package/src/standard/render/forward/raster.ts +228 -0
  35. package/src/standard/render/index.ts +443 -70
  36. package/src/standard/render/indirect.ts +40 -0
  37. package/src/standard/render/instance.ts +214 -0
  38. package/src/standard/render/intersection.ts +72 -0
  39. package/src/standard/render/light.ts +16 -16
  40. package/src/standard/render/mesh/index.ts +67 -75
  41. package/src/standard/render/mesh/unified.ts +96 -0
  42. package/src/standard/render/{transparent.ts → overlay.ts} +14 -15
  43. package/src/standard/render/pass.ts +10 -4
  44. package/src/standard/render/postprocess.ts +142 -64
  45. package/src/standard/render/ray.ts +61 -0
  46. package/src/standard/render/scene.ts +38 -164
  47. package/src/standard/render/shaders.ts +484 -0
  48. package/src/standard/render/surface/compile.ts +3 -10
  49. package/src/standard/render/surface/index.ts +60 -30
  50. package/src/standard/render/surface/noise.ts +45 -0
  51. package/src/standard/render/surface/structs.ts +60 -19
  52. package/src/standard/render/surface/wgsl.ts +573 -0
  53. package/src/standard/render/triangle.ts +84 -0
  54. package/src/standard/transforms/index.ts +4 -6
  55. package/src/standard/tween/index.ts +10 -1
  56. package/src/standard/tween/sequence.ts +24 -16
  57. package/src/standard/tween/tween.ts +67 -16
  58. package/src/core/types.ts +0 -37
  59. package/src/standard/compute/inspect.ts +0 -201
  60. package/src/standard/compute/pass.ts +0 -23
  61. package/src/standard/compute/timing.ts +0 -139
  62. package/src/standard/render/forward.ts +0 -273
@@ -0,0 +1,429 @@
1
+ interface LineSegment {
2
+ x1: number;
3
+ y1: number;
4
+ x2: number;
5
+ y2: number;
6
+ }
7
+
8
+ function pointOnQuadraticBezier(
9
+ x0: number,
10
+ y0: number,
11
+ x1: number,
12
+ y1: number,
13
+ x2: number,
14
+ y2: number,
15
+ t: number
16
+ ): { x: number; y: number } {
17
+ const t2 = 1 - t;
18
+ return {
19
+ x: t2 * t2 * x0 + 2 * t2 * t * x1 + t * t * x2,
20
+ y: t2 * t2 * y0 + 2 * t2 * t * y1 + t * t * y2,
21
+ };
22
+ }
23
+
24
+ function pointOnCubicBezier(
25
+ x0: number,
26
+ y0: number,
27
+ x1: number,
28
+ y1: number,
29
+ x2: number,
30
+ y2: number,
31
+ x3: number,
32
+ y3: number,
33
+ t: number
34
+ ): { x: number; y: number } {
35
+ const t2 = 1 - t;
36
+ return {
37
+ x: t2 * t2 * t2 * x0 + 3 * t2 * t2 * t * x1 + 3 * t2 * t * t * x2 + t * t * t * x3,
38
+ y: t2 * t2 * t2 * y0 + 3 * t2 * t2 * t * y1 + 3 * t2 * t * t * y2 + t * t * t * y3,
39
+ };
40
+ }
41
+
42
+ export function segmentPath(pathString: string, curvePoints = 16): LineSegment[] {
43
+ const segments: LineSegment[] = [];
44
+ const segmentRE = /([MLQCZ])([^MLQCZ]*)/g;
45
+ let match: RegExpExecArray | null;
46
+ let firstX = 0,
47
+ firstY = 0,
48
+ prevX = 0,
49
+ prevY = 0;
50
+
51
+ while ((match = segmentRE.exec(pathString))) {
52
+ const args = match[2]
53
+ .trim()
54
+ .split(/[,\s]+/)
55
+ .filter((s) => s)
56
+ .map((v) => parseFloat(v));
57
+
58
+ switch (match[1]) {
59
+ case "M":
60
+ prevX = firstX = args[0];
61
+ prevY = firstY = args[1];
62
+ break;
63
+ case "L":
64
+ if (args[0] !== prevX || args[1] !== prevY) {
65
+ segments.push({ x1: prevX, y1: prevY, x2: args[0], y2: args[1] });
66
+ }
67
+ prevX = args[0];
68
+ prevY = args[1];
69
+ break;
70
+ case "Q": {
71
+ let curveX = prevX;
72
+ let curveY = prevY;
73
+ for (let i = 1; i < curvePoints; i++) {
74
+ const pt = pointOnQuadraticBezier(
75
+ prevX,
76
+ prevY,
77
+ args[0],
78
+ args[1],
79
+ args[2],
80
+ args[3],
81
+ i / (curvePoints - 1)
82
+ );
83
+ segments.push({ x1: curveX, y1: curveY, x2: pt.x, y2: pt.y });
84
+ curveX = pt.x;
85
+ curveY = pt.y;
86
+ }
87
+ prevX = args[2];
88
+ prevY = args[3];
89
+ break;
90
+ }
91
+ case "C": {
92
+ let curveX = prevX;
93
+ let curveY = prevY;
94
+ for (let i = 1; i < curvePoints; i++) {
95
+ const pt = pointOnCubicBezier(
96
+ prevX,
97
+ prevY,
98
+ args[0],
99
+ args[1],
100
+ args[2],
101
+ args[3],
102
+ args[4],
103
+ args[5],
104
+ i / (curvePoints - 1)
105
+ );
106
+ segments.push({ x1: curveX, y1: curveY, x2: pt.x, y2: pt.y });
107
+ curveX = pt.x;
108
+ curveY = pt.y;
109
+ }
110
+ prevX = args[4];
111
+ prevY = args[5];
112
+ break;
113
+ }
114
+ case "Z":
115
+ if (prevX !== firstX || prevY !== firstY) {
116
+ segments.push({ x1: prevX, y1: prevY, x2: firstX, y2: firstY });
117
+ }
118
+ prevX = firstX;
119
+ prevY = firstY;
120
+ break;
121
+ }
122
+ }
123
+
124
+ return segments;
125
+ }
126
+
127
+ const distanceShader = /* wgsl */ `
128
+ struct Uniforms {
129
+ glyphBounds: vec4<f32>,
130
+ maxDistance: f32,
131
+ exponent: f32,
132
+ _pad: vec2<f32>,
133
+ }
134
+
135
+ @group(0) @binding(0) var<uniform> uniforms: Uniforms;
136
+ @group(0) @binding(1) var<storage, read> segments: array<vec4<f32>>;
137
+
138
+ struct VertexOutput {
139
+ @builtin(position) position: vec4<f32>,
140
+ @location(0) glyphXY: vec2<f32>,
141
+ @location(1) @interpolate(flat) segmentIdx: u32,
142
+ }
143
+
144
+ @vertex
145
+ fn vs_distance(
146
+ @builtin(vertex_index) vid: u32,
147
+ @builtin(instance_index) segmentIdx: u32
148
+ ) -> VertexOutput {
149
+ let uv = vec2<f32>(
150
+ f32((vid << 1u) & 2u),
151
+ f32(vid & 2u)
152
+ );
153
+
154
+ var out: VertexOutput;
155
+ out.position = vec4<f32>(uv * 2.0 - 1.0, 0.0, 1.0);
156
+ out.glyphXY = mix(uniforms.glyphBounds.xy, uniforms.glyphBounds.zw, uv);
157
+ out.segmentIdx = segmentIdx;
158
+ return out;
159
+ }
160
+
161
+ @fragment
162
+ fn fs_distance(input: VertexOutput) -> @location(0) vec4<f32> {
163
+ let seg = segments[input.segmentIdx];
164
+ let p = input.glyphXY;
165
+
166
+ let lineDir = seg.zw - seg.xy;
167
+ let lenSq = dot(lineDir, lineDir);
168
+ let t = select(0.0, clamp(dot(p - seg.xy, lineDir) / lenSq, 0.0, 1.0), lenSq > 0.0);
169
+ let closest = seg.xy + t * lineDir;
170
+ let dist = distance(p, closest);
171
+
172
+ let val = pow(1.0 - clamp(dist / uniforms.maxDistance, 0.0, 1.0), uniforms.exponent) * 0.5;
173
+
174
+ let crosses = (seg.y > p.y) != (seg.w > p.y);
175
+ let crossX = (seg.z - seg.x) * (p.y - seg.y) / (seg.w - seg.y) + seg.x;
176
+ let crossingUp = crosses && (p.x < crossX) && (seg.y < seg.w);
177
+ let crossingDown = crosses && (p.x < crossX) && (seg.y > seg.w);
178
+
179
+ return vec4<f32>(
180
+ select(0.0, 1.0/255.0, crossingUp),
181
+ select(0.0, 1.0/255.0, crossingDown),
182
+ 0.0,
183
+ val
184
+ );
185
+ }
186
+ `;
187
+
188
+ const finalizeShader = /* wgsl */ `
189
+ @group(0) @binding(0) var intermediate: texture_2d<f32>;
190
+ @group(0) @binding(1) var samp: sampler;
191
+
192
+ struct VertexOutput {
193
+ @builtin(position) position: vec4<f32>,
194
+ @location(0) uv: vec2<f32>,
195
+ }
196
+
197
+ @vertex
198
+ fn vs_finalize(@builtin(vertex_index) vid: u32) -> VertexOutput {
199
+ let uv = vec2<f32>(
200
+ f32((vid << 1u) & 2u),
201
+ f32(vid & 2u)
202
+ );
203
+
204
+ var out: VertexOutput;
205
+ out.position = vec4<f32>(uv * 2.0 - 1.0, 0.0, 1.0);
206
+ out.uv = uv;
207
+ return out;
208
+ }
209
+
210
+ @fragment
211
+ fn fs_finalize(input: VertexOutput) -> @location(0) vec4<f32> {
212
+ let color = textureSample(intermediate, samp, input.uv);
213
+ let inside = color.r != color.g;
214
+ let val = select(color.a, 1.0 - color.a, inside);
215
+ return vec4<f32>(val, val, val, val);
216
+ }
217
+ `;
218
+
219
+ export interface SDFGeneratorConfig {
220
+ device: GPUDevice;
221
+ sdfSize?: number;
222
+ exponent?: number;
223
+ curveSubdivisions?: number;
224
+ }
225
+
226
+ export class SDFGenerator {
227
+ private device: GPUDevice;
228
+ private sdfSize: number;
229
+ private exponent: number;
230
+ private curveSubdivisions: number;
231
+
232
+ private distancePipeline: GPURenderPipeline | null = null;
233
+ private finalizePipeline: GPURenderPipeline | null = null;
234
+
235
+ private uniformBuffer: GPUBuffer;
236
+ private segmentBuffer: GPUBuffer;
237
+ private intermediateTexture: GPUTexture | null = null;
238
+ private sampler: GPUSampler;
239
+
240
+ private maxSegments = 4096;
241
+
242
+ constructor(config: SDFGeneratorConfig) {
243
+ this.device = config.device;
244
+ this.sdfSize = config.sdfSize ?? 64;
245
+ this.exponent = config.exponent ?? 9;
246
+ this.curveSubdivisions = config.curveSubdivisions ?? 16;
247
+
248
+ this.uniformBuffer = this.device.createBuffer({
249
+ size: 32,
250
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
251
+ });
252
+
253
+ this.segmentBuffer = this.device.createBuffer({
254
+ size: this.maxSegments * 16,
255
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
256
+ });
257
+
258
+ this.sampler = this.device.createSampler({
259
+ magFilter: "nearest",
260
+ minFilter: "nearest",
261
+ });
262
+ }
263
+
264
+ private ensurePipelines(): void {
265
+ if (this.distancePipeline) return;
266
+
267
+ const distanceModule = this.device.createShaderModule({ code: distanceShader });
268
+
269
+ this.distancePipeline = this.device.createRenderPipeline({
270
+ layout: "auto",
271
+ vertex: {
272
+ module: distanceModule,
273
+ entryPoint: "vs_distance",
274
+ },
275
+ fragment: {
276
+ module: distanceModule,
277
+ entryPoint: "fs_distance",
278
+ targets: [
279
+ {
280
+ format: "rgba8unorm",
281
+ blend: {
282
+ color: {
283
+ srcFactor: "one",
284
+ dstFactor: "one",
285
+ operation: "add",
286
+ },
287
+ alpha: {
288
+ srcFactor: "one",
289
+ dstFactor: "one",
290
+ operation: "max",
291
+ },
292
+ },
293
+ },
294
+ ],
295
+ },
296
+ primitive: {
297
+ topology: "triangle-list",
298
+ },
299
+ });
300
+
301
+ const finalizeModule = this.device.createShaderModule({ code: finalizeShader });
302
+
303
+ this.finalizePipeline = this.device.createRenderPipeline({
304
+ layout: "auto",
305
+ vertex: {
306
+ module: finalizeModule,
307
+ entryPoint: "vs_finalize",
308
+ },
309
+ fragment: {
310
+ module: finalizeModule,
311
+ entryPoint: "fs_finalize",
312
+ targets: [{ format: "r8unorm" }],
313
+ },
314
+ primitive: {
315
+ topology: "triangle-list",
316
+ },
317
+ });
318
+ }
319
+
320
+ private ensureIntermediateTexture(): void {
321
+ if (this.intermediateTexture) return;
322
+
323
+ this.intermediateTexture = this.device.createTexture({
324
+ size: { width: this.sdfSize, height: this.sdfSize },
325
+ format: "rgba8unorm",
326
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
327
+ });
328
+ }
329
+
330
+ generate(
331
+ path: string,
332
+ bounds: [number, number, number, number],
333
+ outputTexture: GPUTexture,
334
+ outputX: number,
335
+ outputY: number
336
+ ): void {
337
+ this.ensurePipelines();
338
+ this.ensureIntermediateTexture();
339
+
340
+ const segments = segmentPath(path, this.curveSubdivisions);
341
+ if (segments.length === 0) return;
342
+ if (segments.length > this.maxSegments) {
343
+ console.warn(`Too many segments (${segments.length}), truncating to ${this.maxSegments}`);
344
+ segments.length = this.maxSegments;
345
+ }
346
+
347
+ const [xMin, yMin, xMax, yMax] = bounds;
348
+ const maxDist = Math.max(xMax - xMin, yMax - yMin) / 2;
349
+
350
+ const uniformData = new Float32Array([xMin, yMin, xMax, yMax, maxDist, this.exponent, 0, 0]);
351
+ this.device.queue.writeBuffer(this.uniformBuffer, 0, uniformData);
352
+
353
+ const segmentData = new Float32Array(segments.length * 4);
354
+ for (let i = 0; i < segments.length; i++) {
355
+ const seg = segments[i];
356
+ segmentData[i * 4 + 0] = seg.x1;
357
+ segmentData[i * 4 + 1] = seg.y1;
358
+ segmentData[i * 4 + 2] = seg.x2;
359
+ segmentData[i * 4 + 3] = seg.y2;
360
+ }
361
+ this.device.queue.writeBuffer(this.segmentBuffer, 0, segmentData);
362
+
363
+ const encoder = this.device.createCommandEncoder();
364
+
365
+ const distanceBindGroup = this.device.createBindGroup({
366
+ layout: this.distancePipeline!.getBindGroupLayout(0),
367
+ entries: [
368
+ { binding: 0, resource: { buffer: this.uniformBuffer } },
369
+ { binding: 1, resource: { buffer: this.segmentBuffer } },
370
+ ],
371
+ });
372
+
373
+ const distancePass = encoder.beginRenderPass({
374
+ colorAttachments: [
375
+ {
376
+ view: this.intermediateTexture!.createView(),
377
+ clearValue: { r: 0, g: 0, b: 0, a: 0 },
378
+ loadOp: "clear",
379
+ storeOp: "store",
380
+ },
381
+ ],
382
+ });
383
+
384
+ distancePass.setPipeline(this.distancePipeline!);
385
+ distancePass.setBindGroup(0, distanceBindGroup);
386
+ distancePass.draw(3, segments.length);
387
+ distancePass.end();
388
+
389
+ const finalizeBindGroup = this.device.createBindGroup({
390
+ layout: this.finalizePipeline!.getBindGroupLayout(0),
391
+ entries: [
392
+ { binding: 0, resource: this.intermediateTexture!.createView() },
393
+ { binding: 1, resource: this.sampler },
394
+ ],
395
+ });
396
+
397
+ const outputView = outputTexture.createView({
398
+ baseMipLevel: 0,
399
+ mipLevelCount: 1,
400
+ baseArrayLayer: 0,
401
+ arrayLayerCount: 1,
402
+ });
403
+
404
+ const finalizePass = encoder.beginRenderPass({
405
+ colorAttachments: [
406
+ {
407
+ view: outputView,
408
+ loadOp: "load",
409
+ storeOp: "store",
410
+ },
411
+ ],
412
+ });
413
+
414
+ finalizePass.setViewport(outputX, outputY, this.sdfSize, this.sdfSize, 0, 1);
415
+ finalizePass.setScissorRect(outputX, outputY, this.sdfSize, this.sdfSize);
416
+ finalizePass.setPipeline(this.finalizePipeline!);
417
+ finalizePass.setBindGroup(0, finalizeBindGroup);
418
+ finalizePass.draw(3);
419
+ finalizePass.end();
420
+
421
+ this.device.queue.submit([encoder.finish()]);
422
+ }
423
+
424
+ destroy(): void {
425
+ this.uniformBuffer.destroy();
426
+ this.segmentBuffer.destroy();
427
+ this.intermediateTexture?.destroy();
428
+ }
429
+ }
@@ -0,0 +1,172 @@
1
+ import type { Plugin, State } from "../../core";
2
+ import { resource } from "../../core";
3
+ import { Canvas } from "../compute";
4
+
5
+ export interface Spinner {
6
+ show(message?: string): (() => void) | void;
7
+ }
8
+
9
+ export type SpinnerFactory = (canvas: HTMLCanvasElement) => Spinner;
10
+
11
+ export interface Activity {
12
+ active: boolean;
13
+ message: string | undefined;
14
+ acquire(message?: string): () => void;
15
+ }
16
+
17
+ export const Activity = resource<Activity>("activity");
18
+
19
+ export function createActivity(spinner: Spinner | null): Activity {
20
+ const messages: (string | undefined)[] = [];
21
+ let cleanup: (() => void) | void = undefined;
22
+ let displayedMessage: string | undefined = undefined;
23
+
24
+ const updateSpinner = () => {
25
+ const currentMessage = messages[messages.length - 1];
26
+
27
+ if (messages.length > 0 && spinner) {
28
+ if (cleanup && displayedMessage !== currentMessage) {
29
+ cleanup();
30
+ cleanup = undefined;
31
+ }
32
+ if (!cleanup) {
33
+ cleanup = spinner.show(currentMessage);
34
+ displayedMessage = currentMessage;
35
+ }
36
+ } else if (messages.length === 0 && cleanup) {
37
+ cleanup();
38
+ cleanup = undefined;
39
+ displayedMessage = undefined;
40
+ }
41
+ };
42
+
43
+ return {
44
+ get active() {
45
+ return messages.length > 0;
46
+ },
47
+
48
+ get message() {
49
+ return messages[messages.length - 1];
50
+ },
51
+
52
+ acquire(message?: string) {
53
+ messages.push(message);
54
+ updateSpinner();
55
+
56
+ let released = false;
57
+ return () => {
58
+ if (released) return;
59
+ released = true;
60
+ const idx = messages.lastIndexOf(message);
61
+ if (idx !== -1) messages.splice(idx, 1);
62
+ updateSpinner();
63
+ };
64
+ },
65
+ };
66
+ }
67
+
68
+ interface Theme {
69
+ track: string;
70
+ accent: string;
71
+ }
72
+
73
+ const dark: Theme = { track: "rgba(51, 51, 51, 0.5)", accent: "#c0b8b0" };
74
+ const light: Theme = { track: "rgba(200, 200, 200, 0.5)", accent: "#505050" };
75
+
76
+ function createSpinner(canvas: HTMLCanvasElement, theme: Theme): Spinner {
77
+ return {
78
+ show(message?: string) {
79
+ const container = document.createElement("div");
80
+ const spinnerEl = document.createElement("div");
81
+
82
+ const updatePosition = () => {
83
+ const rect = canvas.getBoundingClientRect();
84
+ const parentRect = canvas.parentElement?.getBoundingClientRect();
85
+ if (!parentRect) return;
86
+
87
+ const bottom = parentRect.bottom - rect.bottom + 12;
88
+ const right = parentRect.right - rect.right + 12;
89
+
90
+ container.style.cssText = `
91
+ position: absolute;
92
+ bottom: ${bottom}px;
93
+ right: ${right}px;
94
+ display: flex;
95
+ flex-direction: row;
96
+ align-items: center;
97
+ gap: 8px;
98
+ z-index: 1000;
99
+ pointer-events: none;
100
+ `;
101
+
102
+ spinnerEl.style.cssText = `
103
+ width: 20px;
104
+ height: 20px;
105
+ border: 2px solid ${theme.track};
106
+ border-top-color: ${theme.accent};
107
+ border-radius: 50%;
108
+ animation: shallot-activity-spin 0.8s linear infinite;
109
+ `;
110
+ };
111
+
112
+ updatePosition();
113
+
114
+ if (message) {
115
+ const label = document.createElement("div");
116
+ label.textContent = message;
117
+ label.style.cssText = `
118
+ font-family: system-ui, sans-serif;
119
+ font-size: 12px;
120
+ color: ${theme.accent};
121
+ white-space: nowrap;
122
+ text-align: right;
123
+ `;
124
+ container.appendChild(label);
125
+ }
126
+
127
+ container.appendChild(spinnerEl);
128
+
129
+ if (!document.getElementById("shallot-activity-style")) {
130
+ const style = document.createElement("style");
131
+ style.id = "shallot-activity-style";
132
+ style.textContent = `
133
+ @keyframes shallot-activity-spin {
134
+ to { transform: rotate(360deg); }
135
+ }
136
+ `;
137
+ document.head.appendChild(style);
138
+ }
139
+
140
+ const parent = canvas.parentElement;
141
+ if (parent) {
142
+ if (getComputedStyle(parent).position === "static") {
143
+ parent.style.position = "relative";
144
+ }
145
+ parent.appendChild(container);
146
+ }
147
+
148
+ const observer = new ResizeObserver(updatePosition);
149
+ observer.observe(canvas);
150
+
151
+ return () => {
152
+ observer.disconnect();
153
+ container.remove();
154
+ };
155
+ },
156
+ };
157
+ }
158
+
159
+ export const spinnerDark = (canvas: HTMLCanvasElement): Spinner => createSpinner(canvas, dark);
160
+
161
+ export const spinnerLight = (canvas: HTMLCanvasElement): Spinner => createSpinner(canvas, light);
162
+
163
+ export const ActivityPlugin: Plugin & { spinner: SpinnerFactory | null } = {
164
+ spinner: null,
165
+
166
+ initialize(state: State) {
167
+ const canvas = Canvas.from(state);
168
+ const spinner =
169
+ canvas && ActivityPlugin.spinner ? ActivityPlugin.spinner(canvas.element) : null;
170
+ state.setResource(Activity, createActivity(spinner));
171
+ },
172
+ };
@@ -1,6 +1,4 @@
1
1
  import { CycleError } from "../../core";
2
- import { inspect as inspectGraph, type GraphInspection } from "./inspect";
3
- import { Pass, PASS_ORDER } from "./pass";
4
2
 
5
3
  export type ResourceId = string;
6
4
  export type NodeId = string;
@@ -27,10 +25,11 @@ export interface ExecutionContext {
27
25
 
28
26
  export interface ComputeNode {
29
27
  readonly id: NodeId;
30
- readonly pass?: Pass;
31
28
  readonly inputs: readonly ResourceRef[];
32
29
  readonly outputs: readonly ResourceRef[];
30
+ readonly sync?: boolean;
33
31
  readonly execute: (ctx: ExecutionContext) => void;
32
+ readonly prepare?: (device: GPUDevice) => Promise<void>;
34
33
  }
35
34
 
36
35
  export interface ExecutionPlan {
@@ -50,7 +49,7 @@ function buildEdges(nodes: ComputeNode[]): [ComputeNode, ComputeNode][] {
50
49
  for (const node of nodes) {
51
50
  for (const input of node.inputs) {
52
51
  const producer = producers.get(input.id);
53
- if (producer) {
52
+ if (producer && producer !== node) {
54
53
  edges.push([producer, node]);
55
54
  }
56
55
  }
@@ -111,29 +110,17 @@ function compile(nodes: ComputeNode[]): ExecutionPlan {
111
110
  return { sorted: [] };
112
111
  }
113
112
 
114
- const byPass = new Map<Pass, ComputeNode[]>();
115
- for (const pass of PASS_ORDER) {
116
- byPass.set(pass, []);
117
- }
118
-
119
- for (const node of nodes) {
120
- const pass = node.pass ?? Pass.Opaque;
121
- byPass.get(pass)!.push(node);
122
- }
123
-
124
- const sorted: ComputeNode[] = [];
125
- for (const pass of PASS_ORDER) {
126
- const passNodes = byPass.get(pass)!;
127
- sorted.push(...topoSort(passNodes));
128
- }
129
-
130
- return { sorted };
113
+ return { sorted: topoSort(nodes) };
131
114
  }
132
115
 
133
116
  export class ComputeGraph {
134
117
  readonly nodes = new Map<NodeId, ComputeNode>();
135
118
  private _plan: ExecutionPlan | null = null;
136
119
 
120
+ get planCached(): boolean {
121
+ return this._plan !== null;
122
+ }
123
+
137
124
  add(node: ComputeNode): void {
138
125
  if (this.nodes.has(node.id)) {
139
126
  throw new Error(`Node '${node.id}' already exists`);
@@ -165,7 +152,20 @@ export class ComputeGraph {
165
152
  return this._plan;
166
153
  }
167
154
 
168
- inspect(): GraphInspection {
169
- return inspectGraph(Array.from(this.nodes.values()));
155
+ async prepare(
156
+ device: GPUDevice,
157
+ onProgress?: (done: number, total: number) => void
158
+ ): Promise<void> {
159
+ const preparable = Array.from(this.nodes.values()).filter((n) => n.prepare);
160
+ const total = preparable.length;
161
+ let done = 0;
162
+
163
+ const promises = preparable.map(async (node) => {
164
+ await node.prepare!(device);
165
+ done++;
166
+ onProgress?.(done, total);
167
+ });
168
+
169
+ await Promise.all(promises);
170
170
  }
171
171
  }