vgpu-render 0.0.0-stage → 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 (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +605 -2
  3. package/dist/assets/fira-mono-medium-subset.d.ts +4 -0
  4. package/dist/assets/fira-mono-medium-subset.js +889 -0
  5. package/dist/assets/fira-mono-msdf-atlas.d.ts +13 -0
  6. package/dist/assets/fira-mono-msdf-atlas.js +6222 -0
  7. package/dist/camera-2d.d.ts +43 -0
  8. package/dist/camera-2d.js +75 -0
  9. package/dist/color-space.d.ts +32 -0
  10. package/dist/color-space.js +45 -0
  11. package/dist/curve.d.ts +16 -0
  12. package/dist/curve.js +46 -0
  13. package/dist/debug-draw-3d.d.ts +14 -0
  14. package/dist/debug-draw-3d.js +36 -0
  15. package/dist/frame-draw-list-2d.d.ts +177 -0
  16. package/dist/frame-draw-list-2d.js +378 -0
  17. package/dist/frame-draw-list.d.ts +413 -0
  18. package/dist/frame-draw-list.js +556 -0
  19. package/dist/geometry-generators.d.ts +101 -0
  20. package/dist/geometry-generators.js +481 -0
  21. package/dist/gpu-debug-draw-2d.d.ts +48 -0
  22. package/dist/gpu-debug-draw-2d.js +450 -0
  23. package/dist/gpu-debug-draw-3d.d.ts +52 -0
  24. package/dist/gpu-debug-draw-3d.js +892 -0
  25. package/dist/gpu-instancing.d.ts +34 -0
  26. package/dist/gpu-instancing.js +93 -0
  27. package/dist/gpu-text-3d.d.ts +98 -0
  28. package/dist/gpu-text-3d.js +514 -0
  29. package/dist/index.d.ts +21 -0
  30. package/dist/index.js +20 -0
  31. package/dist/infinite-grid-2d.d.ts +73 -0
  32. package/dist/infinite-grid-2d.js +253 -0
  33. package/dist/infinite-grid-3d.d.ts +134 -0
  34. package/dist/infinite-grid-3d.js +407 -0
  35. package/dist/mesh-batch-3d.d.ts +10 -0
  36. package/dist/mesh-batch-3d.js +67 -0
  37. package/dist/shape2d-shader.d.ts +14 -0
  38. package/dist/shape2d-shader.js +288 -0
  39. package/dist/text-atlas.d.ts +176 -0
  40. package/dist/text-atlas.js +498 -0
  41. package/dist/text-font.d.ts +190 -0
  42. package/dist/text-font.js +261 -0
  43. package/dist/text-layout.d.ts +175 -0
  44. package/dist/text-layout.js +390 -0
  45. package/dist/text-msdf-baked.d.ts +41 -0
  46. package/dist/text-msdf-baked.js +117 -0
  47. package/dist/text-msdf-font.d.ts +85 -0
  48. package/dist/text-msdf-font.js +159 -0
  49. package/dist/text-msdf.d.ts +70 -0
  50. package/dist/text-msdf.js +242 -0
  51. package/dist/text-raster.d.ts +40 -0
  52. package/dist/text-raster.js +60 -0
  53. package/dist/text-sdf.d.ts +28 -0
  54. package/dist/text-sdf.js +119 -0
  55. package/dist/vector-math.d.ts +19 -0
  56. package/dist/vector-math.js +37 -0
  57. package/package.json +80 -4
@@ -0,0 +1,450 @@
1
+ /**
2
+ * GPU renderer for the 2D debug-draw layer.
3
+ *
4
+ * The 2D primitives are the 3D ones under a different projection. `camera-2d.ts` produces a clip
5
+ * matrix that lands `world2d`/`screen` pixels on the near plane, and the line, point and joint
6
+ * shaders already do everything after the projection in screen space — pixel widths, dash phase,
7
+ * joint wedges, the near-plane clip. So this renderer reuses that WGSL verbatim (injected as
8
+ * `shaders`, because the 3D renderer owns the source and must not be imported back) and only
9
+ * supplies its own instance buffers, indirect draws and camera uniforms.
10
+ *
11
+ * `world2d` and `screen` commands need different matrices, and one draw binds one uniform, so each
12
+ * space gets its own bucket of primitives. The layer draws after every 3D primitive with depth
13
+ * write off, which is what makes the 2D overlay unconditionally cover the 3D world.
14
+ */
15
+ import { compute, draw, geometry, storage } from "vgpu";
16
+ import { prepareShader } from "@vgpu/wgsl/prepare";
17
+ import { instanceGeometry } from "vgpu/scene/gpu";
18
+ import { instances } from "vgpu/scene";
19
+ import { DEFAULT_CAMERA_2D, projection2D } from "./camera-2d.js";
20
+ import { identity, indirectArgs, INDIRECT_SHADER, trim, world, zeroGeometry } from "./gpu-instancing.js";
21
+ import { shape2dShader } from "./shape2d-shader.js";
22
+ /** Must match the `SHAPE_*` constants in `shape2d-shader.ts`. */
23
+ const SHAPE2D_CODES = {
24
+ "rect": 0,
25
+ "rounded-rect": 1,
26
+ "circle": 2,
27
+ "ellipse": 3,
28
+ "arc": 4,
29
+ "annulus": 5,
30
+ "capsule": 6,
31
+ "polygon": 7,
32
+ "triangle": 8,
33
+ "rhombus": 9,
34
+ "sector": 10,
35
+ "segment": 11,
36
+ };
37
+ /** One view matrix, shared by every 2D draw: the layer never applies the back-face normal fade. */
38
+ const IDENTITY_VIEW = identity();
39
+ const ZERO_NORMAL = [0, 0, 0];
40
+ function sameVec2(a, b) {
41
+ return a[0] === b[0] && a[1] === b[1];
42
+ }
43
+ function sameColor(a, b) {
44
+ return a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3];
45
+ }
46
+ function samePoint2D(a, b) {
47
+ return sameVec2(a.position, b.position) && sameColor(a.color, b.color) && a.size === b.size && a.sizeMode === b.sizeMode && a.round === b.round;
48
+ }
49
+ function sameLine2D(a, b) {
50
+ return (sameVec2(a.start, b.start) &&
51
+ sameVec2(a.end, b.end) &&
52
+ sameColor(a.color, b.color) &&
53
+ sameColor(a.colorEnd, b.colorEnd) &&
54
+ a.width === b.width &&
55
+ a.widthMode === b.widthMode &&
56
+ a.dash?.[0] === b.dash?.[0] &&
57
+ a.dash?.[1] === b.dash?.[1] &&
58
+ a.dash?.[2] === b.dash?.[2] &&
59
+ a.joint === b.joint);
60
+ }
61
+ function sameJoint2D(a, b) {
62
+ return (sameVec2(a.previous, b.previous) &&
63
+ sameVec2(a.center, b.center) &&
64
+ sameVec2(a.next, b.next) &&
65
+ sameColor(a.color, b.color) &&
66
+ a.width === b.width &&
67
+ a.widthMode === b.widthMode &&
68
+ a.joint === b.joint &&
69
+ a.dash?.[0] === b.dash?.[0] &&
70
+ a.dash?.[1] === b.dash?.[1] &&
71
+ a.dash?.[2] === b.dash?.[2]);
72
+ }
73
+ function sameShape2D(a, b) {
74
+ return (a.shape === b.shape &&
75
+ sameVec2(a.center, b.center) &&
76
+ sameVec2(a.size, b.size) &&
77
+ sameVec2(a.vertexB, b.vertexB) &&
78
+ sameVec2(a.vertexC, b.vertexC) &&
79
+ a.cornerRadius === b.cornerRadius &&
80
+ a.thickness === b.thickness &&
81
+ a.filled === b.filled &&
82
+ a.rotation === b.rotation &&
83
+ a.startAngle === b.startAngle &&
84
+ a.sweep === b.sweep &&
85
+ a.sides === b.sides &&
86
+ sameColor(a.color, b.color));
87
+ }
88
+ /** Two-dimensional point lifted onto the near plane the projection matrix maps to. */
89
+ function lift2D(point) {
90
+ return [point[0], point[1], 0];
91
+ }
92
+ /** Circle that bounds the shape at any rotation: the instanced quad's extent in shape space. */
93
+ function boundingRadius(command) {
94
+ const [x, y] = command.size;
95
+ switch (command.shape) {
96
+ case "rect":
97
+ case "rounded-rect":
98
+ case "rhombus":
99
+ return Math.hypot(x, y);
100
+ case "ellipse":
101
+ return Math.max(x, y);
102
+ case "capsule":
103
+ return y + x;
104
+ case "triangle": {
105
+ const corners = [command.size, command.vertexB, command.vertexC];
106
+ return Math.max(...corners.map((corner) => Math.hypot(corner[0], corner[1])));
107
+ }
108
+ default:
109
+ // circle, arc, annulus (x is the outer radius), polygon, sector and segment.
110
+ return x;
111
+ }
112
+ }
113
+ function jointGeometry(gpu, vertexCount) {
114
+ return geometry(gpu, {
115
+ buffers: [{ attributes: { position: "float32x3" }, data: new Float32Array(vertexCount * 3) }],
116
+ vertexCount,
117
+ topology: "triangle-list",
118
+ });
119
+ }
120
+ /**
121
+ * The primitives of one 2D space. Separate instance collections from the 3D layer, because one
122
+ * draw binds one camera uniform and both layers can be visible in the same frame.
123
+ */
124
+ class SpaceBucket2D {
125
+ space;
126
+ #points;
127
+ #lines;
128
+ #joints;
129
+ #shapes;
130
+ #pointIds = [];
131
+ #lineIds = [];
132
+ #jointIds = [];
133
+ #shapeIds = [];
134
+ #pointCache = [];
135
+ #lineCache = [];
136
+ #jointCache = [];
137
+ #shapeCache = [];
138
+ #pointBridge;
139
+ #lineBridge;
140
+ #jointBridge;
141
+ #shapeBridge;
142
+ #pointDraw;
143
+ #lineDraw;
144
+ #jointDraw;
145
+ #shapeDraw;
146
+ #pointIndirect;
147
+ #lineIndirect;
148
+ #jointIndirect;
149
+ #shapeIndirect;
150
+ #indirectCompute;
151
+ #jointVertexCount;
152
+ #pointCount = 0;
153
+ #lineCount = 0;
154
+ #jointCount = 0;
155
+ #shapeCount = 0;
156
+ #pointDirty = true;
157
+ #lineDirty = true;
158
+ #jointDirty = true;
159
+ #shapeDirty = true;
160
+ constructor(gpu, space, options, capacities) {
161
+ this.space = space;
162
+ this.#jointVertexCount = options.jointVertexCount;
163
+ this.#points = instances({ capacity: capacities.points, attributes: { point: "float32x3", color: "float32x4", size: "float32", flags: "uint32" } });
164
+ this.#lines = instances({ capacity: capacities.lines, attributes: { start: "float32x3", end: "float32x3", color: "float32x4", colorEnd: "float32x4", width: "float32", flags: "uint32", dash: "float32x3", normal: "float32x3", joint: "uint32" } });
165
+ this.#joints = instances({ capacity: capacities.joints, attributes: { previous: "float32x3", center: "float32x3", next: "float32x3", color: "float32x4", width: "float32", flags: "uint32", dash: "float32x3", joint: "uint32", previousNormal: "float32x3", nextNormal: "float32x3" } });
166
+ this.#shapes = instances({ capacity: capacities.shapes, attributes: { shape: "uint32", center: "float32x3", size: "float32x2", vertexB: "float32x2", vertexC: "float32x2", params: "float32x4", rotationRadius: "float32x2", color: "float32x4", flags: "uint32" } });
167
+ this.#pointBridge = instanceGeometry(gpu, this.#points, { mesh: zeroGeometry(gpu) });
168
+ this.#lineBridge = instanceGeometry(gpu, this.#lines, { mesh: zeroGeometry(gpu) });
169
+ this.#jointBridge = instanceGeometry(gpu, this.#joints, { mesh: jointGeometry(gpu, options.jointVertexCount) });
170
+ this.#shapeBridge = instanceGeometry(gpu, this.#shapes, { mesh: zeroGeometry(gpu) });
171
+ // No depth write: the 2D layer is an overlay, and the projection puts it on the near plane
172
+ // where a "less-equal" test against anything the 3D layer drew succeeds.
173
+ const depth = { write: false, compare: "less-equal" };
174
+ this.#pointDraw = draw(gpu, { shader: prepareShader(options.shaders.point), geometry: this.#pointBridge.geometry, instances: 0, blend: "alpha", cull: "none", depth });
175
+ this.#lineDraw = draw(gpu, { shader: prepareShader(options.shaders.line), geometry: this.#lineBridge.geometry, instances: 0, blend: "alpha", cull: "none", depth });
176
+ this.#jointDraw = draw(gpu, { shader: prepareShader(options.shaders.joint), geometry: this.#jointBridge.geometry, instances: 0, blend: "alpha", cull: "none", depth });
177
+ this.#shapeDraw = draw(gpu, { shader: prepareShader(shape2dShader()), geometry: this.#shapeBridge.geometry, instances: 0, blend: "alpha", cull: "none", depth });
178
+ this.#pointIndirect = storage(gpu, 16, { access: "read-write", indirect: true });
179
+ this.#lineIndirect = storage(gpu, 16, { access: "read-write", indirect: true });
180
+ this.#jointIndirect = storage(gpu, 16, { access: "read-write", indirect: true });
181
+ this.#shapeIndirect = storage(gpu, 16, { access: "read-write", indirect: true });
182
+ this.#indirectCompute = compute(gpu, INDIRECT_SHADER);
183
+ indirectArgs(this.#pointIndirect, 6, 0);
184
+ indirectArgs(this.#lineIndirect, 6, 0);
185
+ indirectArgs(this.#jointIndirect, options.jointVertexCount, 0);
186
+ indirectArgs(this.#shapeIndirect, 6, 0);
187
+ }
188
+ prepare(points, lines, joints, shapes) {
189
+ this.#pointDirty = false;
190
+ this.#lineDirty = false;
191
+ this.#jointDirty = false;
192
+ this.#shapeDirty = false;
193
+ for (let index = 0; index < points.length; index++)
194
+ this.#addPoint(points[index], index);
195
+ for (let index = 0; index < lines.length; index++)
196
+ this.#addLine(lines[index], index);
197
+ for (let index = 0; index < joints.length; index++)
198
+ this.#addJoint(joints[index], index);
199
+ for (let index = 0; index < shapes.length; index++)
200
+ this.#addShape(shapes[index], index);
201
+ if (trim(this.#points, this.#pointIds, points.length))
202
+ this.#pointDirty = true;
203
+ if (trim(this.#lines, this.#lineIds, lines.length))
204
+ this.#lineDirty = true;
205
+ if (trim(this.#joints, this.#jointIds, joints.length))
206
+ this.#jointDirty = true;
207
+ if (trim(this.#shapes, this.#shapeIds, shapes.length))
208
+ this.#shapeDirty = true;
209
+ if (this.#pointCache.length !== points.length)
210
+ this.#pointDirty = true;
211
+ if (this.#lineCache.length !== lines.length)
212
+ this.#lineDirty = true;
213
+ if (this.#jointCache.length !== joints.length)
214
+ this.#jointDirty = true;
215
+ if (this.#shapeCache.length !== shapes.length)
216
+ this.#shapeDirty = true;
217
+ this.#pointCache.length = points.length;
218
+ this.#lineCache.length = lines.length;
219
+ this.#jointCache.length = joints.length;
220
+ this.#shapeCache.length = shapes.length;
221
+ if (this.#pointDirty) {
222
+ this.#pointCount = this.#pointBridge.publish();
223
+ indirectArgs(this.#pointIndirect, 6, this.#pointCount);
224
+ }
225
+ if (this.#lineDirty) {
226
+ this.#lineCount = this.#lineBridge.publish();
227
+ indirectArgs(this.#lineIndirect, 6, this.#lineCount);
228
+ }
229
+ if (this.#jointDirty) {
230
+ this.#jointCount = this.#jointBridge.publish();
231
+ indirectArgs(this.#jointIndirect, this.#jointVertexCount, this.#jointCount);
232
+ }
233
+ if (this.#shapeDirty) {
234
+ this.#shapeCount = this.#shapeBridge.publish();
235
+ indirectArgs(this.#shapeIndirect, 6, this.#shapeCount);
236
+ }
237
+ }
238
+ dispatchIndirect(pass) {
239
+ this.#indirectCompute.set({ params: { vertexCount: 6, instanceCount: this.#pointCount }, args: this.#pointIndirect });
240
+ pass.dispatch(this.#indirectCompute, 1);
241
+ this.#indirectCompute.set({ params: { vertexCount: 6, instanceCount: this.#lineCount }, args: this.#lineIndirect });
242
+ pass.dispatch(this.#indirectCompute, 1);
243
+ this.#indirectCompute.set({ params: { vertexCount: this.#jointVertexCount, instanceCount: this.#jointCount }, args: this.#jointIndirect });
244
+ pass.dispatch(this.#indirectCompute, 1);
245
+ this.#indirectCompute.set({ params: { vertexCount: 6, instanceCount: this.#shapeCount }, args: this.#shapeIndirect });
246
+ pass.dispatch(this.#indirectCompute, 1);
247
+ }
248
+ draw(pass, camera) {
249
+ if (this.#pointCount === 0 && this.#lineCount === 0 && this.#jointCount === 0 && this.#shapeCount === 0)
250
+ return;
251
+ const viewportSize = pass.target.size;
252
+ const viewProjection = projection2D(this.space, camera, viewportSize);
253
+ // Filled shapes first, then strokes, then points and joints: the layer reads as a filled
254
+ // backdrop with its outlines and markers drawn over it.
255
+ if (this.#shapeCount > 0) {
256
+ this.#shapeDraw.set({
257
+ camera: {
258
+ viewProjection,
259
+ viewportSize,
260
+ yFlip: this.space === "screen" ? 1 : -1,
261
+ pixelsPerUnit: this.space === "screen" ? 1 : camera.zoom,
262
+ },
263
+ });
264
+ pass.draw(this.#shapeDraw, { indirect: this.#shapeIndirect });
265
+ }
266
+ if (this.#pointCount > 0) {
267
+ this.#pointDraw.set({ camera: { viewProjection, viewportSize } });
268
+ pass.draw(this.#pointDraw, { indirect: this.#pointIndirect });
269
+ }
270
+ if (this.#lineCount > 0) {
271
+ this.#lineDraw.set({ camera: { viewProjection, view: IDENTITY_VIEW, viewportSize } });
272
+ pass.draw(this.#lineDraw, { indirect: this.#lineIndirect });
273
+ }
274
+ if (this.#jointCount > 0) {
275
+ this.#jointDraw.set({ camera: { viewProjection, view: IDENTITY_VIEW, viewportSize } });
276
+ pass.draw(this.#jointDraw, { indirect: this.#jointIndirect });
277
+ }
278
+ }
279
+ destroy() {
280
+ this.#pointDraw.dispose();
281
+ this.#lineDraw.dispose();
282
+ this.#jointDraw.dispose();
283
+ this.#shapeDraw.dispose();
284
+ this.#indirectCompute.dispose();
285
+ this.#pointBridge.destroy();
286
+ this.#lineBridge.destroy();
287
+ this.#jointBridge.destroy();
288
+ this.#shapeBridge.destroy();
289
+ }
290
+ get count() {
291
+ return this.#pointCount + this.#lineCount + this.#jointCount + this.#shapeCount;
292
+ }
293
+ #addPoint(command, index) {
294
+ const previous = this.#pointCache[index];
295
+ this.#pointCache[index] = command;
296
+ if (previous !== undefined && samePoint2D(previous, command))
297
+ return;
298
+ this.#pointDirty = true;
299
+ const id = this.#pointIds[index] ?? this.#addPointId(index);
300
+ this.#points.set(id, {
301
+ point: lift2D(command.position),
302
+ color: command.color,
303
+ size: command.size,
304
+ flags: (command.sizeMode === "world" ? 1 : 0) | (command.round ? 2 : 0),
305
+ });
306
+ }
307
+ #addPointId(index) {
308
+ const id = this.#points.add({ point: [0, 0, 0], color: [0, 0, 0, 0], size: 0, flags: 0 });
309
+ world(this.#points, id);
310
+ this.#pointIds[index] = id;
311
+ return id;
312
+ }
313
+ #addLine(command, index) {
314
+ const previous = this.#lineCache[index];
315
+ this.#lineCache[index] = command;
316
+ if (previous !== undefined && sameLine2D(previous, command))
317
+ return;
318
+ this.#lineDirty = true;
319
+ const id = this.#lineIds[index] ?? this.#addLineId(index);
320
+ const dash = command.dash ?? [0, 0, 0];
321
+ const joint = command.joint === "round" ? 1 : command.joint === "bevel" ? 2 : command.joint === "miter" ? 3 : 0;
322
+ this.#lines.set(id, {
323
+ start: lift2D(command.start),
324
+ end: lift2D(command.end),
325
+ color: command.color,
326
+ colorEnd: command.colorEnd,
327
+ width: command.width,
328
+ flags: command.widthMode === "world" ? 1 : 0,
329
+ dash,
330
+ normal: ZERO_NORMAL,
331
+ joint,
332
+ });
333
+ }
334
+ #addLineId(index) {
335
+ const id = this.#lines.add({ start: [0, 0, 0], end: [0, 0, 0], color: [0, 0, 0, 0], colorEnd: [0, 0, 0, 0], width: 0, flags: 0, dash: [0, 0, 0], normal: [0, 0, 0], joint: 0 });
336
+ world(this.#lines, id);
337
+ this.#lineIds[index] = id;
338
+ return id;
339
+ }
340
+ #addJoint(command, index) {
341
+ const previous = this.#jointCache[index];
342
+ this.#jointCache[index] = command;
343
+ if (previous !== undefined && sameJoint2D(previous, command))
344
+ return;
345
+ this.#jointDirty = true;
346
+ const id = this.#jointIds[index] ?? this.#addJointId(index);
347
+ this.#joints.set(id, {
348
+ previous: lift2D(command.previous),
349
+ center: lift2D(command.center),
350
+ next: lift2D(command.next),
351
+ color: command.color,
352
+ width: command.width,
353
+ flags: command.widthMode === "world" ? 1 : 0,
354
+ dash: command.dash ?? [0, 0, 0],
355
+ joint: command.joint === "round" ? 1 : command.joint === "bevel" ? 2 : 3,
356
+ previousNormal: ZERO_NORMAL,
357
+ nextNormal: ZERO_NORMAL,
358
+ });
359
+ }
360
+ #addJointId(index) {
361
+ const id = this.#joints.add({ previous: [0, 0, 0], center: [0, 0, 0], next: [0, 0, 0], color: [0, 0, 0, 0], width: 0, flags: 0, dash: [0, 0, 0], joint: 0, previousNormal: [0, 0, 0], nextNormal: [0, 0, 0] });
362
+ world(this.#joints, id);
363
+ this.#jointIds[index] = id;
364
+ return id;
365
+ }
366
+ #addShape(command, index) {
367
+ const previous = this.#shapeCache[index];
368
+ this.#shapeCache[index] = command;
369
+ if (previous !== undefined && sameShape2D(previous, command))
370
+ return;
371
+ this.#shapeDirty = true;
372
+ const id = this.#shapeIds[index] ?? this.#addShapeId(index);
373
+ this.#shapes.set(id, {
374
+ shape: SHAPE2D_CODES[command.shape],
375
+ center: lift2D(command.center),
376
+ size: command.size,
377
+ vertexB: command.vertexB,
378
+ vertexC: command.vertexC,
379
+ params: [command.cornerRadius, command.thickness, command.startAngle, command.sweep],
380
+ rotationRadius: [command.rotation, boundingRadius(command)],
381
+ color: command.color,
382
+ flags: (command.filled ? 1 : 0) | (command.sides << 8),
383
+ });
384
+ }
385
+ #addShapeId(index) {
386
+ const id = this.#shapes.add({ shape: 0, center: [0, 0, 0], size: [0, 0], vertexB: [0, 0], vertexC: [0, 0], params: [0, 0, 0, 0], rotationRadius: [0, 0], color: [0, 0, 0, 0], flags: 0 });
387
+ world(this.#shapes, id);
388
+ this.#shapeIds[index] = id;
389
+ return id;
390
+ }
391
+ }
392
+ export class GpuDebugDraw2D {
393
+ #buckets;
394
+ #camera = DEFAULT_CAMERA_2D;
395
+ constructor(gpu, options) {
396
+ const capacities = {
397
+ points: options.points ?? 4096,
398
+ lines: options.lines ?? 16384,
399
+ joints: options.joints ?? 8192,
400
+ shapes: options.shapes ?? 8192,
401
+ };
402
+ this.#buckets = {
403
+ world2d: new SpaceBucket2D(gpu, "world2d", options, capacities),
404
+ screen: new SpaceBucket2D(gpu, "screen", options, capacities),
405
+ };
406
+ }
407
+ /** Camera the `world2d` bucket projects through. `screen` commands ignore it. */
408
+ setCamera(camera) {
409
+ this.#camera = { pan: [camera.pan[0], camera.pan[1]], zoom: camera.zoom };
410
+ }
411
+ get camera() {
412
+ return this.#camera;
413
+ }
414
+ prepare(commands) {
415
+ const points = { world2d: [], screen: [] };
416
+ const lines = { world2d: [], screen: [] };
417
+ const joints = { world2d: [], screen: [] };
418
+ const shapes = { world2d: [], screen: [] };
419
+ for (const command of commands) {
420
+ if (command.kind === "point2d")
421
+ points[command.space].push(command);
422
+ else if (command.kind === "line2d")
423
+ lines[command.space].push(command);
424
+ else if (command.kind === "shape2d")
425
+ shapes[command.space].push(command);
426
+ else
427
+ joints[command.space].push(command);
428
+ }
429
+ // Both buckets are refreshed every frame, empty or not: that is what drops the previous
430
+ // frame's primitives instead of leaving them on screen.
431
+ this.#buckets.world2d.prepare(points.world2d, lines.world2d, joints.world2d, shapes.world2d);
432
+ this.#buckets.screen.prepare(points.screen, lines.screen, joints.screen, shapes.screen);
433
+ }
434
+ dispatchIndirect(pass) {
435
+ this.#buckets.world2d.dispatchIndirect(pass);
436
+ this.#buckets.screen.dispatchIndirect(pass);
437
+ }
438
+ draw(pass) {
439
+ this.#buckets.world2d.draw(pass, this.#camera);
440
+ this.#buckets.screen.draw(pass, this.#camera);
441
+ }
442
+ destroy() {
443
+ this.#buckets.world2d.destroy();
444
+ this.#buckets.screen.destroy();
445
+ }
446
+ /** Instances the last `prepare` left in both buckets, for tests and status readouts. */
447
+ get primitiveCount() {
448
+ return this.#buckets.world2d.count + this.#buckets.screen.count;
449
+ }
450
+ }
@@ -0,0 +1,52 @@
1
+ import { type Frame, type FramePass, type Gpu } from "vgpu";
2
+ import type { Camera2D } from "./camera-2d.js";
3
+ import type { FrameCommand } from "./frame-draw-list.js";
4
+ import { type GpuText3DOptions } from "./gpu-text-3d.js";
5
+ /**
6
+ * Named glyph sources. A text command picks one by name through `TextOptions.source`; commands that
7
+ * name nothing use the first one registered.
8
+ *
9
+ * More than one source is what lets a single frame mix quality levels — a sharp pass for the block
10
+ * the user is reading and a cheap one for the rest — or put two encodings side by side.
11
+ */
12
+ export type TextSources = Readonly<Record<string, GpuText3DOptions["font"]>>;
13
+ export type GpuTextOptions = {
14
+ readonly sources: TextSources;
15
+ readonly capacity?: number;
16
+ readonly atlasSize?: number;
17
+ readonly mipLevels?: number;
18
+ /** Atlas pages per source. See `GpuText3DOptions.pages`. */
19
+ readonly pages?: number;
20
+ /** What a glyph that no page can hold costs. See `GpuText3DOptions.onFull`. Defaults to `"skip"`. */
21
+ readonly onFull?: "throw" | "skip";
22
+ };
23
+ /**
24
+ * Reference scenes set `depth_bias = -0.001` on the reference before drawing. Keeping it as an
25
+ * option (and baking it into the WGSL) means the shader can match the reference exactly
26
+ * without adding a fourth uniform field that would break the shared camera layout.
27
+ */
28
+ export declare const DEFAULT_DEPTH_BIAS = -0.001;
29
+ type Camera = {
30
+ viewProjection: ArrayLike<number>;
31
+ view: ArrayLike<number>;
32
+ };
33
+ export declare class GpuDebugDraw3D {
34
+ #private;
35
+ constructor(gpu: Gpu, capacities?: {
36
+ points?: number;
37
+ lines?: number;
38
+ triangles?: number;
39
+ joints?: number;
40
+ depthBias?: number;
41
+ text?: GpuTextOptions;
42
+ });
43
+ prepare(commands: readonly FrameCommand[], camera: Camera, camera2d?: Camera2D): void;
44
+ updateIndirect(frame: Frame): void;
45
+ draw(pass: FramePass): void;
46
+ destroy(): void;
47
+ private addTriangle;
48
+ private addPoint;
49
+ private addLine;
50
+ private addJoint;
51
+ }
52
+ export {};