@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.
- package/package.json +3 -4
- package/src/core/builder.ts +71 -32
- package/src/core/component.ts +25 -11
- package/src/core/index.ts +14 -13
- package/src/core/math.ts +135 -0
- package/src/core/runtime.ts +0 -1
- package/src/core/state.ts +9 -68
- package/src/core/xml.ts +381 -265
- package/src/editor/format.ts +5 -0
- package/src/editor/index.ts +101 -0
- package/src/extras/arrows/index.ts +28 -69
- package/src/extras/gradient/index.ts +36 -52
- package/src/extras/lines/index.ts +51 -122
- package/src/extras/orbit/index.ts +40 -15
- package/src/extras/text/font.ts +546 -0
- package/src/extras/text/index.ts +158 -204
- package/src/extras/text/sdf.ts +429 -0
- package/src/standard/activity/index.ts +172 -0
- package/src/standard/compute/graph.ts +23 -23
- package/src/standard/compute/index.ts +76 -61
- package/src/standard/defaults.ts +8 -5
- package/src/standard/index.ts +1 -0
- package/src/standard/input/index.ts +30 -19
- package/src/standard/loading/index.ts +18 -13
- package/src/standard/render/bvh/blas.ts +752 -0
- package/src/standard/render/bvh/radix.ts +476 -0
- package/src/standard/render/bvh/structs.ts +167 -0
- package/src/standard/render/bvh/tlas.ts +886 -0
- package/src/standard/render/bvh/traverse.ts +467 -0
- package/src/standard/render/camera.ts +302 -27
- package/src/standard/render/data.ts +93 -0
- package/src/standard/render/depth.ts +117 -0
- package/src/standard/render/forward/index.ts +259 -0
- package/src/standard/render/forward/raster.ts +228 -0
- package/src/standard/render/index.ts +443 -70
- package/src/standard/render/indirect.ts +40 -0
- package/src/standard/render/instance.ts +214 -0
- package/src/standard/render/intersection.ts +72 -0
- package/src/standard/render/light.ts +16 -16
- package/src/standard/render/mesh/index.ts +67 -75
- package/src/standard/render/mesh/unified.ts +96 -0
- package/src/standard/render/{transparent.ts → overlay.ts} +14 -15
- package/src/standard/render/pass.ts +10 -4
- package/src/standard/render/postprocess.ts +142 -64
- package/src/standard/render/ray.ts +61 -0
- package/src/standard/render/scene.ts +38 -164
- package/src/standard/render/shaders.ts +484 -0
- package/src/standard/render/surface/compile.ts +3 -10
- package/src/standard/render/surface/index.ts +60 -30
- package/src/standard/render/surface/noise.ts +45 -0
- package/src/standard/render/surface/structs.ts +60 -19
- package/src/standard/render/surface/wgsl.ts +573 -0
- package/src/standard/render/triangle.ts +84 -0
- package/src/standard/transforms/index.ts +4 -6
- package/src/standard/tween/index.ts +10 -1
- package/src/standard/tween/sequence.ts +24 -16
- package/src/standard/tween/tween.ts +67 -16
- package/src/core/types.ts +0 -37
- package/src/standard/compute/inspect.ts +0 -201
- package/src/standard/compute/pass.ts +0 -23
- package/src/standard/compute/timing.ts +0 -139
- 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
|
-
|
|
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
|
-
|
|
169
|
-
|
|
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
|
}
|