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,378 @@
1
+ /**
2
+ * 2D debug-draw commands and their drawList.
3
+ *
4
+ * The 2D layer is the same data flow as the 3D one — `FrameDrawList -> FrameCommand[] -> GPU` —
5
+ * reached through `frame.d2` instead of a second entry point, so a frame has one lifecycle, one
6
+ * layer mask and one set of defaults. The commands are separate types rather than 3D commands
7
+ * with a `space` flag because a 2D point is a `Vec2` and 2D strokes carry no surface normal;
8
+ * keeping them apart stops a 3D-only field from silently meaning something different here.
9
+ *
10
+ * See `camera-2d.ts` for what `world2d` and `screen` mean.
11
+ */
12
+ const DEFAULT_2D_COLOR = [1, 1, 1, 1];
13
+ const DEFAULT_2D_LAYER = 1;
14
+ /** An unfilled shape still has to be visible, so a stroke defaults to one pixel. */
15
+ const DEFAULT_2D_STROKE_WIDTH = 1;
16
+ const TAU = Math.PI * 2;
17
+ function finite(value, name) {
18
+ if (!Number.isFinite(value)) {
19
+ throw new RangeError(`${name} must be finite`);
20
+ }
21
+ return value;
22
+ }
23
+ function positive(value, name) {
24
+ const resolved = finite(value, name);
25
+ if (resolved <= 0)
26
+ throw new RangeError(`${name} must be greater than zero`);
27
+ return resolved;
28
+ }
29
+ function nonNegative(value, name) {
30
+ const resolved = finite(value, name);
31
+ if (resolved < 0)
32
+ throw new RangeError(`${name} must not be negative`);
33
+ return resolved;
34
+ }
35
+ function positiveSize(value, name) {
36
+ const resolved = vec2(value, name);
37
+ if (resolved[0] <= 0 || resolved[1] <= 0)
38
+ throw new RangeError(`${name} values must be greater than zero`);
39
+ return resolved;
40
+ }
41
+ /** Start angle and sweep of the angle-parameterised shapes, with the same defaults and bounds. */
42
+ function angleRange(options) {
43
+ const startAngle = finite(options.startAngle ?? 0, "startAngle");
44
+ const sweep = finite(options.sweep ?? TAU, "sweep");
45
+ if (sweep === 0 || Math.abs(sweep) > TAU) {
46
+ throw new RangeError("sweep must be a non-zero angle of at most one full turn");
47
+ }
48
+ return { startAngle, sweep };
49
+ }
50
+ function vec2(value, name) {
51
+ if (value.length !== 2) {
52
+ throw new RangeError(`${name} must contain two values`);
53
+ }
54
+ return [finite(value[0], `${name}[0]`), finite(value[1], `${name}[1]`)];
55
+ }
56
+ function colorValue(value, name) {
57
+ if (value.length !== 4) {
58
+ throw new RangeError(`${name} must contain four values`);
59
+ }
60
+ return [
61
+ finite(value[0], `${name}[0]`),
62
+ finite(value[1], `${name}[1]`),
63
+ finite(value[2], `${name}[2]`),
64
+ finite(value[3], `${name}[3]`),
65
+ ];
66
+ }
67
+ function color(value) {
68
+ return value === undefined ? DEFAULT_2D_COLOR : colorValue(value, "color");
69
+ }
70
+ function layer(value) {
71
+ const resolved = value ?? DEFAULT_2D_LAYER;
72
+ if (!Number.isInteger(resolved) || resolved < 0) {
73
+ throw new RangeError("layer must be a non-negative integer bitmask");
74
+ }
75
+ return resolved;
76
+ }
77
+ function sizeModeValue(value, name) {
78
+ if (value !== "screen" && value !== "world") {
79
+ throw new RangeError(`${name} must be "screen" or "world"`);
80
+ }
81
+ return value;
82
+ }
83
+ function jointValue(value, name) {
84
+ if (value !== "none" && value !== "round" && value !== "bevel" && value !== "miter") {
85
+ throw new RangeError(`${name} must be "none", "round", "bevel", or "miter"`);
86
+ }
87
+ return value;
88
+ }
89
+ /** Resolves the space, defaulting to `world2d` so a command only pays for a camera when it wants one. */
90
+ function spaceValue(value) {
91
+ if (value === undefined)
92
+ return "world2d";
93
+ if (value !== "world2d" && value !== "screen") {
94
+ throw new RangeError('space must be "world2d" or "screen"');
95
+ }
96
+ return value;
97
+ }
98
+ /**
99
+ * Records 2D primitives for one frame. Obtained through `FrameDrawList.d2`; it seals with its
100
+ * parent, so a caller cannot keep drawing into a finished frame.
101
+ */
102
+ export class FrameDrawList2D {
103
+ #points = [];
104
+ #lines = [];
105
+ #joints = [];
106
+ #shapes = [];
107
+ #defaults;
108
+ #assertOpen;
109
+ constructor(defaults, assertOpen) {
110
+ this.#defaults = defaults;
111
+ this.#assertOpen = assertOpen;
112
+ }
113
+ point(position, options = {}) {
114
+ this.#assertOpen();
115
+ const size = finite(options.size ?? this.#defaults.point.size ?? 1, "size");
116
+ if (size <= 0)
117
+ throw new RangeError("size must be greater than zero");
118
+ this.#points.push({
119
+ kind: "point2d",
120
+ position: vec2(position, "position"),
121
+ color: color(options.color ?? this.#defaults.point.color),
122
+ size,
123
+ sizeMode: options.sizeMode ?? this.#defaults.point.sizeMode ?? "screen",
124
+ space: spaceValue(options.space),
125
+ layer: layer(options.layer ?? this.#defaults.point.layer),
126
+ ...(options.round === undefined ? {} : { round: options.round }),
127
+ });
128
+ return this;
129
+ }
130
+ line(start, end, options = {}) {
131
+ this.#assertOpen();
132
+ const resolvedStart = vec2(start, "start");
133
+ const resolvedEnd = vec2(end, "end");
134
+ if (resolvedStart[0] === resolvedEnd[0] && resolvedStart[1] === resolvedEnd[1]) {
135
+ return this;
136
+ }
137
+ const width = finite(options.width ?? this.#defaults.line.width ?? 1, "width");
138
+ if (width <= 0)
139
+ throw new RangeError("width must be greater than zero");
140
+ const dash = options.dash ?? this.#defaults.line.dash;
141
+ const joint = options.joint ?? this.#defaults.line.joint;
142
+ this.#lines.push({
143
+ kind: "line2d",
144
+ start: resolvedStart,
145
+ end: resolvedEnd,
146
+ color: color(options.color ?? this.#defaults.line.color),
147
+ colorEnd: color(options.colorEnd ?? options.color ?? this.#defaults.line.colorEnd ?? this.#defaults.line.color),
148
+ width,
149
+ widthMode: options.widthMode ?? this.#defaults.line.widthMode ?? "screen",
150
+ space: spaceValue(options.space),
151
+ layer: layer(options.layer ?? this.#defaults.line.layer),
152
+ ...(dash === undefined ? {} : { dash }),
153
+ ...(joint === undefined ? {} : { joint }),
154
+ });
155
+ return this;
156
+ }
157
+ ray(origin, direction, options = {}) {
158
+ return this.line(origin, [origin[0] + direction[0], origin[1] + direction[1]], options);
159
+ }
160
+ /**
161
+ * Path of segments, the 2D twin of `FrameDrawList.lineStrip`. Dash distance accumulates over
162
+ * the whole path and an open strip joins only its interior vertices, matching the 3D layer.
163
+ */
164
+ lineStrip(points, options = {}, closed = false) {
165
+ this.#assertOpen();
166
+ const dash = options.dash ?? this.#defaults.line.dash;
167
+ const vertexDistances = [0];
168
+ let traveled = 0;
169
+ const segment = (a, b) => {
170
+ this.line(a, b, {
171
+ ...options,
172
+ ...(dash === undefined ? {} : { dash: [dash[0], dash[1], dash[2] + traveled] }),
173
+ });
174
+ traveled += Math.hypot(b[0] - a[0], b[1] - a[1]);
175
+ vertexDistances.push(traveled);
176
+ };
177
+ for (let i = 0; i + 1 < points.length; i++)
178
+ segment(points[i], points[i + 1]);
179
+ if (closed && points.length > 1)
180
+ segment(points[points.length - 1], points[0]);
181
+ const joint = options.joint ?? this.#defaults.line.joint;
182
+ if (joint === "round" || joint === "bevel" || joint === "miter") {
183
+ const count = points.length;
184
+ const first = closed ? 0 : 1;
185
+ const last = closed ? (count > 2 ? count : 0) : count - 1;
186
+ for (let i = first; i < last; i++) {
187
+ this.#jointCommand(points[(i - 1 + count) % count], points[i], points[(i + 1) % count], joint, options, dash, vertexDistances[i]);
188
+ }
189
+ }
190
+ return this;
191
+ }
192
+ polyline(points, options = {}, closed = false) {
193
+ return this.lineStrip(points, options, closed);
194
+ }
195
+ /**
196
+ * Axis-aligned grid of lines, the 2D twin of `FrameDrawList.grid` and reference `grid_2d`: `cells`
197
+ * are the cells per axis, so the drawn line count is one more on each side.
198
+ */
199
+ grid(center, cells, spacing, options = {}) {
200
+ this.#assertOpen();
201
+ const [columns, rows] = cells;
202
+ if (!Number.isInteger(columns) || !Number.isInteger(rows) || columns < 1 || rows < 1) {
203
+ throw new RangeError("grid cells must be positive integers");
204
+ }
205
+ const [stepX, stepY] = positiveSize(spacing, "spacing");
206
+ const [centerX, centerY] = vec2(center, "center");
207
+ const halfWidth = (columns * stepX) / 2;
208
+ const halfHeight = (rows * stepY) / 2;
209
+ for (let column = 0; column <= columns; column++) {
210
+ const x = centerX - halfWidth + column * stepX;
211
+ this.line([x, centerY - halfHeight], [x, centerY + halfHeight], options);
212
+ }
213
+ for (let row = 0; row <= rows; row++) {
214
+ const y = centerY - halfHeight + row * stepY;
215
+ this.line([centerX - halfWidth, y], [centerX + halfWidth, y], options);
216
+ }
217
+ return this;
218
+ }
219
+ /** Rectangle with the given full width and height (reference `Rectangle::new`). */
220
+ rect(center, size, options = {}) {
221
+ const full = positiveSize(size, "size");
222
+ this.#pushShape("rect", center, { size: [full[0] / 2, full[1] / 2] }, options);
223
+ return this;
224
+ }
225
+ /** Rectangle with rounded corners; the shader clamps the radius to half the shorter side. */
226
+ roundedRect(center, size, cornerRadius, options = {}) {
227
+ const full = positiveSize(size, "size");
228
+ this.#pushShape("rounded-rect", center, { size: [full[0] / 2, full[1] / 2], cornerRadius: nonNegative(cornerRadius, "cornerRadius") }, options);
229
+ return this;
230
+ }
231
+ /** Filled disc with the given radius (reference `Circle::new`). */
232
+ circle(center, radius, options = {}) {
233
+ const r = positive(radius, "radius");
234
+ this.#pushShape("circle", center, { size: [r, r] }, options);
235
+ return this;
236
+ }
237
+ /** Ellipse with independent x and y radii (reference `Ellipse::new(x_radius, y_radius)`). */
238
+ ellipse(center, radii, options = {}) {
239
+ const r = positiveSize(radii, "radii");
240
+ this.#pushShape("ellipse", center, { size: r }, options);
241
+ return this;
242
+ }
243
+ /**
244
+ * Circular arc from `startAngle` through `sweep`. It is a curve without an interior, so an
245
+ * unfilled arc (the default) strokes it with `strokeWidth` while `fill: true` leaves a hairline.
246
+ */
247
+ arc(center, radius, options = {}) {
248
+ const r = positive(radius, "radius");
249
+ const angles = angleRange(options);
250
+ this.#pushShape("arc", center, { size: [r, r], ...angles }, options);
251
+ return this;
252
+ }
253
+ /** Ring band between two radii (reference `Annulus::new(inner_radius, outer_radius)`). */
254
+ annulus(center, innerRadius, outerRadius, options = {}) {
255
+ const inner = nonNegative(innerRadius, "innerRadius");
256
+ const outer = positive(outerRadius, "outerRadius");
257
+ if (inner >= outer)
258
+ throw new RangeError("innerRadius must be smaller than outerRadius");
259
+ this.#pushShape("annulus", center, { size: [outer, inner] }, options);
260
+ return this;
261
+ }
262
+ /** Capsule whose straight section is `length` long along y (reference `Capsule2d::new(radius, length)`). */
263
+ capsule(center, radius, length, options = {}) {
264
+ const r = positive(radius, "radius");
265
+ const straight = nonNegative(length, "length");
266
+ this.#pushShape("capsule", center, { size: [r, straight / 2] }, options);
267
+ return this;
268
+ }
269
+ /** Regular polygon with a vertex on the +x axis (reference `RegularPolygon::new(circumradius, sides)`). */
270
+ polygon(center, circumradius, sides, options = {}) {
271
+ const r = positive(circumradius, "circumradius");
272
+ if (!Number.isInteger(sides) || sides < 3)
273
+ throw new RangeError("sides must be an integer of at least 3");
274
+ this.#pushShape("polygon", center, { size: [r, r], sides }, options);
275
+ return this;
276
+ }
277
+ /** Arbitrary triangle (reference `Triangle2d::new(a, b, c)`); `rotation` turns about its centroid. */
278
+ triangle(a, b, c, options = {}) {
279
+ const p0 = vec2(a, "a");
280
+ const p1 = vec2(b, "b");
281
+ const p2 = vec2(c, "c");
282
+ const center = [(p0[0] + p1[0] + p2[0]) / 3, (p0[1] + p1[1] + p2[1]) / 3];
283
+ this.#pushShape("triangle", center, {
284
+ size: [p0[0] - center[0], p0[1] - center[1]],
285
+ vertexB: [p1[0] - center[0], p1[1] - center[1]],
286
+ vertexC: [p2[0] - center[0], p2[1] - center[1]],
287
+ }, options);
288
+ return this;
289
+ }
290
+ /** Diamond with the given full diagonals (reference `Rhombus::new(horizontal, vertical)`). */
291
+ rhombus(center, diagonals, options = {}) {
292
+ const d = positiveSize(diagonals, "diagonals");
293
+ this.#pushShape("rhombus", center, { size: [d[0] / 2, d[1] / 2] }, options);
294
+ return this;
295
+ }
296
+ /** Disc sector from `startAngle` through `sweep` (reference `CircularSector`). */
297
+ sector(center, radius, options = {}) {
298
+ const r = positive(radius, "radius");
299
+ const angles = angleRange(options);
300
+ this.#pushShape("sector", center, { size: [r, r], ...angles }, options);
301
+ return this;
302
+ }
303
+ /** Disc region between a chord and its arc (reference `CircularSegment`). */
304
+ segment(center, radius, options = {}) {
305
+ const r = positive(radius, "radius");
306
+ const angles = angleRange(options);
307
+ this.#pushShape("segment", center, { size: [r, r], ...angles }, options);
308
+ return this;
309
+ }
310
+ /**
311
+ * One joint instance for a path vertex. Degenerate wedges whose centre coincides with a
312
+ * neighbour are dropped, as in the 3D layer and the reference `emit_joint`.
313
+ */
314
+ #jointCommand(previous, center, next, joint, options, dash, distance) {
315
+ const resolvedPrevious = vec2(previous, "previous");
316
+ const resolvedCenter = vec2(center, "center");
317
+ const resolvedNext = vec2(next, "next");
318
+ const same = (a, b) => a[0] === b[0] && a[1] === b[1];
319
+ if (same(resolvedPrevious, resolvedCenter) || same(resolvedCenter, resolvedNext))
320
+ return;
321
+ const width = finite(options.width ?? this.#defaults.line.width ?? 1, "width");
322
+ if (width <= 0)
323
+ throw new RangeError("width must be greater than zero");
324
+ jointValue(joint, "joint");
325
+ this.#joints.push({
326
+ kind: "joint2d",
327
+ previous: resolvedPrevious,
328
+ center: resolvedCenter,
329
+ next: resolvedNext,
330
+ color: color(options.color ?? this.#defaults.line.color),
331
+ width,
332
+ widthMode: options.widthMode ?? this.#defaults.line.widthMode ?? "screen",
333
+ joint,
334
+ space: spaceValue(options.space),
335
+ layer: layer(options.layer ?? this.#defaults.line.layer),
336
+ ...(dash === undefined ? {} : { dash: [dash[0], dash[1], dash[2] + distance] }),
337
+ });
338
+ }
339
+ /** Records one shape once its per-shape arguments have been validated. */
340
+ #pushShape(shape, center, fields, options) {
341
+ this.#assertOpen();
342
+ const thickness = positive(options.strokeWidth ?? DEFAULT_2D_STROKE_WIDTH, "strokeWidth");
343
+ const rotation = finite(options.rotation ?? 0, "rotation");
344
+ const cornerRadius = fields.cornerRadius ?? 0;
345
+ if (cornerRadius < 0)
346
+ throw new RangeError("cornerRadius must not be negative");
347
+ this.#shapes.push({
348
+ kind: "shape2d",
349
+ shape,
350
+ center: vec2(center, "center"),
351
+ size: fields.size ?? [0, 0],
352
+ vertexB: fields.vertexB ?? [0, 0],
353
+ vertexC: fields.vertexC ?? [0, 0],
354
+ cornerRadius,
355
+ thickness,
356
+ filled: options.fill ?? true,
357
+ rotation,
358
+ startAngle: fields.startAngle ?? 0,
359
+ sweep: fields.sweep ?? 0,
360
+ sides: fields.sides ?? 0,
361
+ color: color(options.color),
362
+ space: spaceValue(options.space),
363
+ layer: layer(options.layer),
364
+ });
365
+ }
366
+ get points() {
367
+ return this.#points;
368
+ }
369
+ get lines() {
370
+ return this.#lines;
371
+ }
372
+ get joints() {
373
+ return this.#joints;
374
+ }
375
+ get shapes() {
376
+ return this.#shapes;
377
+ }
378
+ }