vgpu-render 0.1.0 → 0.3.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 (46) hide show
  1. package/README.md +101 -34
  2. package/dist/curve.d.ts +26 -1
  3. package/dist/curve.js +53 -0
  4. package/dist/debug-draw-3d.d.ts +3 -3
  5. package/dist/debug-draw-3d.js +10 -10
  6. package/dist/frame-draw-list-2d.d.ts +286 -0
  7. package/dist/frame-draw-list-2d.js +668 -0
  8. package/dist/{frame-recorder.d.ts → frame-draw-list.d.ts} +60 -8
  9. package/dist/{frame-recorder.js → frame-draw-list.js} +51 -10
  10. package/dist/geometry-generators.d.ts +11 -2
  11. package/dist/geometry-generators.js +37 -1
  12. package/dist/gpu-debug-draw-2d.d.ts +1 -1
  13. package/dist/gpu-debug-draw-2d.js +10 -2
  14. package/dist/gpu-debug-draw-3d.d.ts +1 -1
  15. package/dist/gpu-debug-draw-3d.js +4 -2
  16. package/dist/gpu-text-3d.d.ts +1 -1
  17. package/dist/gpu-text-3d.js +49 -2
  18. package/dist/index.d.ts +3 -3
  19. package/dist/index.js +2 -3
  20. package/dist/mesh-batch-3d.d.ts +1 -1
  21. package/dist/shape2d-shader.js +295 -7
  22. package/dist/text-atlas.d.ts +6 -4
  23. package/dist/text-atlas.js +9 -6
  24. package/dist/text-font.d.ts +48 -7
  25. package/dist/text-font.js +63 -11
  26. package/dist/text-layout.d.ts +63 -4
  27. package/dist/text-layout.js +58 -8
  28. package/dist/text-msdf-baked.d.ts +9 -4
  29. package/dist/text-msdf-baked.js +9 -4
  30. package/dist/text-msdf-font.d.ts +10 -4
  31. package/dist/text-msdf-font.js +9 -3
  32. package/dist/vector-math.d.ts +19 -0
  33. package/dist/vector-math.js +37 -0
  34. package/package.json +1 -1
  35. package/dist/frame-recorder-2d.d.ts +0 -177
  36. package/dist/frame-recorder-2d.js +0 -378
  37. package/dist/layout-3d.d.ts +0 -227
  38. package/dist/layout-3d.js +0 -226
  39. package/dist/scene-examples.d.ts +0 -39
  40. package/dist/scene-examples.js +0 -200
  41. package/dist/scenes-2d.d.ts +0 -18
  42. package/dist/scenes-2d.js +0 -160
  43. package/dist/scenes-3d.d.ts +0 -46
  44. package/dist/scenes-3d.js +0 -1994
  45. package/dist/text-scenes.d.ts +0 -85
  46. package/dist/text-scenes.js +0 -338
@@ -40,12 +40,62 @@ export type GlyphMetrics = {
40
40
  /** Ink box top edge relative to the baseline, y down: positive is below the baseline. */
41
41
  readonly bearingTop: number;
42
42
  };
43
+ /**
44
+ * reference `FontStyle`, minus `Oblique(angle)`.
45
+ *
46
+ * Canvas2D accepts the `italic` keyword in its font shorthand and silently drops the slant angle —
47
+ * `oblique 14deg` reads back as `italic` — so an oblique angle could not be honoured and is left out
48
+ * rather than accepted and ignored. A face without an italic face is slanted by the browser.
49
+ */
50
+ export type TextFontStyle = "normal" | "italic";
51
+ /**
52
+ * reference `FontWidth` stops, spelled as the CSS `font-stretch` keywords Canvas2D understands.
53
+ *
54
+ * reference takes a percentage (`FontWidth::SEMI_CONDENSED` is 87.5), but Canvas2D rejects a percentage in
55
+ * the font shorthand and only accepts these nine keywords. The axis is inert unless the face actually
56
+ * carries a `wdth` axis: a static face like the bundled subset keeps the keyword out of its resolved
57
+ * font, so every stop draws the same width there.
58
+ */
59
+ export type TextFontWidth = "ultra-condensed" | "extra-condensed" | "condensed" | "semi-condensed" | "normal" | "semi-expanded" | "expanded" | "extra-expanded" | "ultra-expanded";
60
+ /** Every `FontWidth` stop, in reference order. */
61
+ export declare const TEXT_FONT_WIDTHS: readonly TextFontWidth[];
62
+ /** Every `FontStyle` variant this implementation can honour. */
63
+ export declare const TEXT_FONT_STYLES: readonly TextFontStyle[];
64
+ /** reference `FontWeight::NORMAL`, and the value the CSS font shorthand defaults to. */
65
+ export declare const DEFAULT_FONT_WEIGHT = 400;
66
+ /** reference `FontWeight` range, 1 to 1000. */
67
+ export declare const MIN_FONT_WEIGHT = 1;
68
+ export declare const MAX_FONT_WEIGHT = 1000;
69
+ /**
70
+ * One face request: everything that decides which glyphs a character rasterises to.
71
+ *
72
+ * This is the unit the glyph atlas keys on, so two blocks that differ only in weight cannot share a
73
+ * rasterised glyph. A source with a single fixed face reports the parts it cannot change through
74
+ * `GlyphRasterizer.faceKey`.
75
+ */
76
+ export type FontFace = {
77
+ readonly family: string;
78
+ readonly weight: number;
79
+ readonly style: TextFontStyle;
80
+ readonly width: TextFontWidth;
81
+ };
82
+ /** A face with the axes left optional, which is how callers and spans express one. */
83
+ export type FontFaceOptions = {
84
+ readonly family: string;
85
+ readonly weight?: number | undefined;
86
+ readonly style?: TextFontStyle | undefined;
87
+ readonly width?: TextFontWidth | undefined;
88
+ };
89
+ /** Folds a face's optional axes into the complete request the measurer and the atlas take. */
90
+ export declare function resolveFontFace(options: FontFaceOptions): FontFace;
91
+ /** Every field that can change which glyphs are rasterised, joined into one cache key. */
92
+ export declare function fontFaceKey(face: FontFace): string;
43
93
  /** Source of glyph metrics. The browser implementation measures through Canvas2D. */
44
94
  export interface TextMeasurer {
45
- /** Vertical metrics for a `fontSize`-sized face of `fontFamily`. */
46
- verticalMetrics(fontFamily: string, fontSize: number): VerticalMetrics;
95
+ /** Vertical metrics for a `fontSize`-sized instance of `face`. */
96
+ verticalMetrics(face: FontFace, fontSize: number): VerticalMetrics;
47
97
  /** Metrics for one character. Whitespace returns a zero-size ink box. */
48
- glyphMetrics(character: string, fontFamily: string, fontSize: number): GlyphMetrics;
98
+ glyphMetrics(character: string, face: FontFace, fontSize: number): GlyphMetrics;
49
99
  }
50
100
  /** Reference `Justify`. `start`/`end` resolve to left/right in this LTR-only layout. */
51
101
  export type TextJustify = "left" | "center" | "right" | "justified" | "start" | "end";
@@ -62,6 +112,9 @@ export type TextSpanInput = {
62
112
  readonly color?: Rgba;
63
113
  readonly fontSize?: number;
64
114
  readonly fontFamily?: string;
115
+ readonly weight?: number;
116
+ readonly style?: TextFontStyle;
117
+ readonly width?: TextFontWidth;
65
118
  };
66
119
  /** reference `LineHeight` default, `RelativeToFont(1.2)`. */
67
120
  export declare const DEFAULT_LINE_HEIGHT_SCALE = 1.2;
@@ -69,6 +122,12 @@ export type TextLayoutOptions = {
69
122
  readonly color: Rgba;
70
123
  readonly fontSize: number;
71
124
  readonly fontFamily: string;
125
+ /** Block default for `FontFace.weight`; spans override it. */
126
+ readonly weight?: number | undefined;
127
+ /** Block default for `FontFace.style`; spans override it. */
128
+ readonly style?: TextFontStyle | undefined;
129
+ /** Block default for `FontFace.width`; spans override it. */
130
+ readonly width?: TextFontWidth | undefined;
72
131
  /**
73
132
  * Absolute line height in layout units, reference `LineHeight::Px`. When omitted the line height
74
133
  * is `lineHeightScale` times the line's own largest font size, which is reference
@@ -95,7 +154,7 @@ export type TextLayoutOptions = {
95
154
  /** One laid-out glyph, ready to become an instance once its atlas rectangle is known. */
96
155
  export type PositionedGlyph = {
97
156
  readonly character: string;
98
- readonly fontFamily: string;
157
+ readonly face: FontFace;
99
158
  readonly fontSize: number;
100
159
  readonly color: Rgba;
101
160
  /** Ink box centre relative to the block's anchor point, y down. */
@@ -18,6 +18,38 @@
18
18
  * Layout happens in one local space: origin at the block's anchor point, x to the right and
19
19
  * **y down**, matching Canvas2D. The renderer flips y for spaces where up is +y.
20
20
  */
21
+ /** Every `FontWidth` stop, in reference order. */
22
+ export const TEXT_FONT_WIDTHS = [
23
+ "ultra-condensed",
24
+ "extra-condensed",
25
+ "condensed",
26
+ "semi-condensed",
27
+ "normal",
28
+ "semi-expanded",
29
+ "expanded",
30
+ "extra-expanded",
31
+ "ultra-expanded",
32
+ ];
33
+ /** Every `FontStyle` variant this implementation can honour. */
34
+ export const TEXT_FONT_STYLES = ["normal", "italic"];
35
+ /** reference `FontWeight::NORMAL`, and the value the CSS font shorthand defaults to. */
36
+ export const DEFAULT_FONT_WEIGHT = 400;
37
+ /** reference `FontWeight` range, 1 to 1000. */
38
+ export const MIN_FONT_WEIGHT = 1;
39
+ export const MAX_FONT_WEIGHT = 1000;
40
+ /** Folds a face's optional axes into the complete request the measurer and the atlas take. */
41
+ export function resolveFontFace(options) {
42
+ return {
43
+ family: options.family,
44
+ weight: options.weight ?? DEFAULT_FONT_WEIGHT,
45
+ style: options.style ?? "normal",
46
+ width: options.width ?? "normal",
47
+ };
48
+ }
49
+ /** Every field that can change which glyphs are rasterised, joined into one cache key. */
50
+ export function fontFaceKey(face) {
51
+ return `${face.family}|${face.weight}|${face.width}|${face.style}`;
52
+ }
21
53
  const TEXT_ANCHORS = {
22
54
  center: [0.5, 0.5],
23
55
  "top-left": [0, 0],
@@ -144,13 +176,20 @@ function measureLine(characters, letterSpacing) {
144
176
  while (end > 0 && content[end - 1].isSpace)
145
177
  end--;
146
178
  let fontSize = 0;
147
- for (const character of content)
148
- fontSize = Math.max(fontSize, character.fontSize);
179
+ let face;
180
+ // The line box follows the line's own largest run, so the face recorded here is that run's.
181
+ for (const character of content) {
182
+ if (character.fontSize > fontSize) {
183
+ fontSize = character.fontSize;
184
+ face = character.face;
185
+ }
186
+ }
149
187
  return {
150
188
  characters: content.slice(0, end),
151
189
  advance: runWidth(content.slice(0, end), letterSpacing),
152
190
  fullAdvance: runWidth(content, letterSpacing),
153
191
  fontSize,
192
+ face,
154
193
  isEmpty: content.length === 0,
155
194
  };
156
195
  }
@@ -241,20 +280,31 @@ function countSpaces(characters) {
241
280
  */
242
281
  export function layoutText(spans, options, measurer) {
243
282
  const items = [];
283
+ const blockFace = resolveFontFace({
284
+ family: options.fontFamily,
285
+ weight: options.weight,
286
+ style: options.style,
287
+ width: options.width,
288
+ });
244
289
  for (const span of spans) {
245
290
  const fontSize = span.fontSize ?? options.fontSize;
246
- const fontFamily = span.fontFamily ?? options.fontFamily;
291
+ const face = resolveFontFace({
292
+ family: span.fontFamily ?? options.fontFamily,
293
+ weight: span.weight ?? options.weight,
294
+ style: span.style ?? options.style,
295
+ width: span.width ?? options.width,
296
+ });
247
297
  const color = span.color ?? options.color;
248
298
  // Iterating the string yields code points, so surrogate pairs stay whole.
249
299
  for (const character of span.text) {
250
300
  if (character === "\n") {
251
- items.push({ character, fontFamily, fontSize, color, advance: 0, width: 0, height: 0, bearingLeft: 0, bearingTop: 0, isSpace: false });
301
+ items.push({ character, face, fontSize, color, advance: 0, width: 0, height: 0, bearingLeft: 0, bearingTop: 0, isSpace: false });
252
302
  continue;
253
303
  }
254
- const metrics = measurer.glyphMetrics(character, fontFamily, fontSize);
304
+ const metrics = measurer.glyphMetrics(character, face, fontSize);
255
305
  items.push({
256
306
  character,
257
- fontFamily,
307
+ face,
258
308
  fontSize,
259
309
  color,
260
310
  advance: metrics.advance,
@@ -273,7 +323,7 @@ export function layoutText(spans, options, measurer) {
273
323
  // instead of being held to the block's default size.
274
324
  const boxes = lines.map((line) => {
275
325
  const fontSize = line.fontSize > 0 ? line.fontSize : options.fontSize;
276
- const base = measurer.verticalMetrics(options.fontFamily, fontSize);
326
+ const base = measurer.verticalMetrics(line.face ?? blockFace, fontSize);
277
327
  const height = options.lineHeight ?? (options.lineHeightScale ?? DEFAULT_LINE_HEIGHT_SCALE) * fontSize;
278
328
  return { base, height, halfLeading: (height - (base.ascent + base.descent)) / 2 };
279
329
  });
@@ -310,7 +360,7 @@ export function layoutText(spans, options, measurer) {
310
360
  if (character.width > 0 && character.height > 0) {
311
361
  glyphs.push({
312
362
  character: character.character,
313
- fontFamily: character.fontFamily,
363
+ face: character.face,
314
364
  fontSize: character.fontSize,
315
365
  color: character.color,
316
366
  center: [offsetX + penX + character.bearingLeft + character.width / 2, offsetY + baseline + character.bearingTop + character.height / 2],
@@ -15,7 +15,7 @@
15
15
  * baking: one set of texels serves every size.
16
16
  */
17
17
  import { type GlyphRasterizer, type RasterizedGlyph } from "./text-font.js";
18
- import type { GlyphMetrics, TextMeasurer, VerticalMetrics } from "./text-layout.js";
18
+ import type { FontFace, GlyphMetrics, TextMeasurer, VerticalMetrics } from "./text-layout.js";
19
19
  export declare class BakedMsdfRasterizer implements GlyphRasterizer, TextMeasurer {
20
20
  #private;
21
21
  readonly encoding: "msdf";
@@ -30,7 +30,12 @@ export declare class BakedMsdfRasterizer implements GlyphRasterizer, TextMeasure
30
30
  get ready(): boolean;
31
31
  /** Decompresses the inlined page. Idempotent, so calling it per frame is harmless. */
32
32
  load(): Promise<void>;
33
- verticalMetrics(_fontFamily: string, fontSize: number): VerticalMetrics;
34
- glyphMetrics(character: string, _fontFamily: string, fontSize: number): GlyphMetrics;
35
- rasterize(character: string, fontFamily: string, fontSize: number): RasterizedGlyph | undefined;
33
+ /**
34
+ * The page stores one face, so the axes cannot change a sliced glyph. Keying on the family alone
35
+ * keeps the atlas from packing the same texels once per requested weight.
36
+ */
37
+ readonly faceKey: (face: FontFace) => string;
38
+ verticalMetrics(_face: FontFace, fontSize: number): VerticalMetrics;
39
+ glyphMetrics(character: string, _face: FontFace, fontSize: number): GlyphMetrics;
40
+ rasterize(character: string, face: FontFace, fontSize: number): RasterizedGlyph | undefined;
36
41
  }
@@ -48,11 +48,16 @@ export class BakedMsdfRasterizer {
48
48
  }
49
49
  this.#page = page;
50
50
  }
51
- verticalMetrics(_fontFamily, fontSize) {
51
+ /**
52
+ * The page stores one face, so the axes cannot change a sliced glyph. Keying on the family alone
53
+ * keeps the atlas from packing the same texels once per requested weight.
54
+ */
55
+ faceKey = (face) => face.family;
56
+ verticalMetrics(_face, fontSize) {
52
57
  const scale = fontSize / BAKED_MSDF_BAKE_SIZE;
53
58
  return { ascent: BAKED_MSDF_ASCENT * scale, descent: BAKED_MSDF_DESCENT * scale };
54
59
  }
55
- glyphMetrics(character, _fontFamily, fontSize) {
60
+ glyphMetrics(character, _face, fontSize) {
56
61
  const key = `${fontSize}|${character}`;
57
62
  const cached = this.#metrics.get(key);
58
63
  if (cached !== undefined)
@@ -71,7 +76,7 @@ export class BakedMsdfRasterizer {
71
76
  this.#metrics.set(key, value);
72
77
  return value;
73
78
  }
74
- rasterize(character, fontFamily, fontSize) {
79
+ rasterize(character, face, fontSize) {
75
80
  const page = this.#page;
76
81
  if (page === undefined)
77
82
  throw new Error("BakedMsdfRasterizer.load() must resolve before glyphs are requested");
@@ -96,7 +101,7 @@ export class BakedMsdfRasterizer {
96
101
  }
97
102
  // The texels stay at their baked size; only the placement scales with the requested font size.
98
103
  const scale = fontSize / BAKED_MSDF_BAKE_SIZE;
99
- const metrics = this.glyphMetrics(character, fontFamily, fontSize);
104
+ const metrics = this.glyphMetrics(character, face, fontSize);
100
105
  return {
101
106
  width,
102
107
  height,
@@ -13,7 +13,7 @@
13
13
  import type { Font } from "opentype.js";
14
14
  import { type GlyphRasterizer, type RasterizedGlyph } from "./text-font.js";
15
15
  import { type OutlineSegment } from "./text-msdf.js";
16
- import type { GlyphMetrics, TextMeasurer, VerticalMetrics } from "./text-layout.js";
16
+ import type { FontFace, GlyphMetrics, TextMeasurer, VerticalMetrics } from "./text-layout.js";
17
17
  /** The subset of `opentype.js`'s path commands this source understands. */
18
18
  type OutlineCommand = {
19
19
  readonly type: "M";
@@ -72,8 +72,14 @@ export declare class OutlineGlyphRasterizer implements GlyphRasterizer, TextMeas
72
72
  constructor(options?: OutlineGlyphRasterizerOptions);
73
73
  /** The face this source reads, for callers that want to name it. */
74
74
  get font(): Font;
75
- verticalMetrics(_fontFamily: string, fontSize: number): VerticalMetrics;
76
- glyphMetrics(character: string, _fontFamily: string, fontSize: number): GlyphMetrics;
77
- rasterize(character: string, _fontFamily: string, rasterSize: number): RasterizedGlyph | undefined;
75
+ /**
76
+ * This source holds one parsed buffer, so a weight, width or slant it was not built with cannot
77
+ * change the outlines it returns. Keying on the family alone keeps the atlas from caching the same
78
+ * glyph once per axis value.
79
+ */
80
+ readonly faceKey: (face: FontFace) => string;
81
+ verticalMetrics(_face: FontFace, fontSize: number): VerticalMetrics;
82
+ glyphMetrics(character: string, _face: FontFace, fontSize: number): GlyphMetrics;
83
+ rasterize(character: string, _face: FontFace, rasterSize: number): RasterizedGlyph | undefined;
78
84
  }
79
85
  export {};
@@ -105,7 +105,13 @@ export class OutlineGlyphRasterizer {
105
105
  const bottom = Number.isFinite(box.y2) ? box.y2 : top;
106
106
  return { left, top, width: Math.max(0, right - left), height: Math.max(0, bottom - top) };
107
107
  }
108
- verticalMetrics(_fontFamily, fontSize) {
108
+ /**
109
+ * This source holds one parsed buffer, so a weight, width or slant it was not built with cannot
110
+ * change the outlines it returns. Keying on the family alone keeps the atlas from caching the same
111
+ * glyph once per axis value.
112
+ */
113
+ faceKey = (face) => face.family;
114
+ verticalMetrics(_face, fontSize) {
109
115
  const cached = this.#vertical.get(String(fontSize));
110
116
  if (cached !== undefined)
111
117
  return cached;
@@ -114,7 +120,7 @@ export class OutlineGlyphRasterizer {
114
120
  this.#vertical.set(String(fontSize), value);
115
121
  return value;
116
122
  }
117
- glyphMetrics(character, _fontFamily, fontSize) {
123
+ glyphMetrics(character, _face, fontSize) {
118
124
  const key = `${fontSize}|${character}`;
119
125
  const cached = this.#metrics.get(key);
120
126
  if (cached !== undefined)
@@ -131,7 +137,7 @@ export class OutlineGlyphRasterizer {
131
137
  this.#metrics.set(key, value);
132
138
  return value;
133
139
  }
134
- rasterize(character, _fontFamily, rasterSize) {
140
+ rasterize(character, _face, rasterSize) {
135
141
  const ink = this.#inkBox(character, rasterSize);
136
142
  if (!(ink.width > 0) || !(ink.height > 0))
137
143
  return undefined;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Small vector helpers shared by the geometry generators and the example layouts.
3
+ *
4
+ * These live in the library rather than next to the gallery scenes because
5
+ * `geometry-generators.ts` needs them, and a library module must not reach into
6
+ * the example code.
7
+ */
8
+ import type { Vec3 } from "./frame-draw-list.js";
9
+ export declare function normalizeVector(p: Vec3): Vec3;
10
+ export declare function crossVector(a: Vec3, b: Vec3): Vec3;
11
+ /** Right-handed Rodrigues rotation, equivalent to reference `Vector3.rotated(axis, angle)`. */
12
+ export declare function rotateAroundAxis(p: Vec3, axis: Vec3, angle: number): Vec3;
13
+ export type CircleBasis = {
14
+ readonly x: Vec3;
15
+ readonly y: Vec3;
16
+ readonly z: Vec3;
17
+ };
18
+ /** Orthonormal basis for a circle in the plane whose normal is `normal`. */
19
+ export declare function circleBasis(normal: Vec3, angle: number): CircleBasis;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Small vector helpers shared by the geometry generators and the example layouts.
3
+ *
4
+ * These live in the library rather than next to the gallery scenes because
5
+ * `geometry-generators.ts` needs them, and a library module must not reach into
6
+ * the example code.
7
+ */
8
+ export function normalizeVector(p) {
9
+ const length = Math.hypot(p[0], p[1], p[2]) || 1;
10
+ return [p[0] / length, p[1] / length, p[2] / length];
11
+ }
12
+ export function crossVector(a, b) {
13
+ return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
14
+ }
15
+ /** Right-handed Rodrigues rotation, equivalent to reference `Vector3.rotated(axis, angle)`. */
16
+ export function rotateAroundAxis(p, axis, angle) {
17
+ const a = normalizeVector(axis);
18
+ const cos = Math.cos(angle);
19
+ const sin = Math.sin(angle);
20
+ const dot = p[0] * a[0] + p[1] * a[1] + p[2] * a[2];
21
+ const cross = crossVector(a, p);
22
+ return [
23
+ p[0] * cos + cross[0] * sin + a[0] * dot * (1 - cos),
24
+ p[1] * cos + cross[1] * sin + a[1] * dot * (1 - cos),
25
+ p[2] * cos + cross[2] * sin + a[2] * dot * (1 - cos),
26
+ ];
27
+ }
28
+ /** Orthonormal basis for a circle in the plane whose normal is `normal`. */
29
+ export function circleBasis(normal, angle) {
30
+ const n = normalizeVector(normal);
31
+ const reference = Math.abs(n[0]) > 0.95 ? [0, 0, 1] : [1, 0, 0];
32
+ const projection = reference[0] * n[0] + reference[1] * n[1] + reference[2] * n[2];
33
+ const planar = [reference[0] - n[0] * projection, reference[1] - n[1] * projection, reference[2] - n[2] * projection];
34
+ const widthAxis = normalizeVector(rotateAroundAxis(normalizeVector(planar), n, angle));
35
+ const heightAxis = normalizeVector(crossVector(n, widthAxis));
36
+ return { x: widthAxis, y: heightAxis, z: n };
37
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vgpu-render",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "WebGPU debug-draw, gizmo, infinite grid and text layer built on vgpu",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,177 +0,0 @@
1
- /**
2
- * 2D debug-draw commands and their recorder.
3
- *
4
- * The 2D layer is the same data flow as the 3D one — `FrameRecorder -> 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
- import type { Space2D, Vec2 } from "./camera-2d.js";
13
- import type { Color, LayerMask, LineJoint, RenderedJoint, ResolvedLineDefaults, ResolvedPointDefaults } from "./frame-recorder.js";
14
- export type Line2DOptions = {
15
- color?: Color;
16
- colorEnd?: Color;
17
- /** Stroke width in canvas pixels (`widthMode: "screen"`) or world2d units. */
18
- width?: number;
19
- widthMode?: "screen" | "world";
20
- dash?: readonly [number, number, number];
21
- joint?: LineJoint;
22
- /** Defaults to `world2d`; see `camera-2d.ts`. */
23
- space?: Space2D;
24
- layer?: LayerMask;
25
- };
26
- export type Point2DOptions = {
27
- color?: Color;
28
- size?: number;
29
- sizeMode?: "screen" | "world";
30
- round?: boolean;
31
- space?: Space2D;
32
- layer?: LayerMask;
33
- };
34
- /** Every filled/stroked 2D shape the layer can draw. */
35
- export type Shape2DKind = "rect" | "rounded-rect" | "circle" | "ellipse" | "arc" | "annulus" | "capsule" | "polygon" | "triangle" | "rhombus" | "sector" | "segment";
36
- export type Shape2DOptions = {
37
- color?: Color;
38
- /** Rotation in radians, anticlockwise in `world2d` and screen-clockwise in `screen`. */
39
- rotation?: number;
40
- /** Filled by default; `false` strokes the outline with `strokeWidth`. */
41
- fill?: boolean;
42
- /** Outline width in canvas pixels; only read when `fill` is false. */
43
- strokeWidth?: number;
44
- /** Start angle of `arc`/`sector`/`segment` in radians. Defaults to `0`, the +x axis. */
45
- startAngle?: number;
46
- /** Sweep of `arc`/`sector`/`segment` in radians. Defaults to a full turn; negative turns the other way. */
47
- sweep?: number;
48
- space?: Space2D;
49
- layer?: LayerMask;
50
- };
51
- export type Shape2DCommand = {
52
- readonly kind: "shape2d";
53
- readonly shape: Shape2DKind;
54
- readonly center: Vec2;
55
- /**
56
- * Primary half-size, meaning depends on `shape`: box half extents, ellipse radii, circumradius,
57
- * capsule `[radius, halfLength]`, annulus `[outer, inner]`, rhombus half diagonals, or a
58
- * triangle's first vertex offset from the centroid.
59
- */
60
- readonly size: Vec2;
61
- /** Second/third triangle vertices relative to the centroid; zero for every other shape. */
62
- readonly vertexB: Vec2;
63
- readonly vertexC: Vec2;
64
- readonly cornerRadius: number;
65
- /** Outline width in canvas pixels. */
66
- readonly thickness: number;
67
- readonly filled: boolean;
68
- readonly rotation: number;
69
- readonly startAngle: number;
70
- readonly sweep: number;
71
- /** Vertex count of `polygon`; zero for every other shape. */
72
- readonly sides: number;
73
- readonly color: Color;
74
- readonly space: Space2D;
75
- readonly layer: LayerMask;
76
- };
77
- export type Point2DCommand = {
78
- readonly kind: "point2d";
79
- readonly position: Vec2;
80
- readonly color: Color;
81
- readonly size: number;
82
- readonly sizeMode: "screen" | "world";
83
- readonly round?: boolean;
84
- readonly space: Space2D;
85
- readonly layer: LayerMask;
86
- };
87
- export type Line2DCommand = {
88
- readonly kind: "line2d";
89
- readonly start: Vec2;
90
- readonly end: Vec2;
91
- readonly color: Color;
92
- readonly colorEnd: Color;
93
- readonly width: number;
94
- readonly widthMode: "screen" | "world";
95
- readonly dash?: readonly [number, number, number];
96
- readonly joint?: LineJoint;
97
- readonly space: Space2D;
98
- readonly layer: LayerMask;
99
- };
100
- /**
101
- * One path vertex of a `lineStrip`/`polyline`, the 2D twin of `JointCommand`. The renderer
102
- * rebuilds the round/bevel/miter wedge from `previous`, `center` and `next`, exactly as the 3D
103
- * joint pipeline does.
104
- */
105
- export type Joint2DCommand = {
106
- readonly kind: "joint2d";
107
- readonly previous: Vec2;
108
- readonly center: Vec2;
109
- readonly next: Vec2;
110
- readonly color: Color;
111
- readonly width: number;
112
- readonly widthMode: "screen" | "world";
113
- readonly dash?: readonly [number, number, number];
114
- readonly joint: RenderedJoint;
115
- readonly space: Space2D;
116
- readonly layer: LayerMask;
117
- };
118
- /** Everything a `FrameRecorder2D` can emit. */
119
- export type FrameCommand2D = Point2DCommand | Line2DCommand | Joint2DCommand | Shape2DCommand;
120
- /** The 3D recorder's resolved point/line styles, reused so 2D and 3D defaults cannot drift. */
121
- export type FrameRecorder2DStyleDefaults = {
122
- readonly point: ResolvedPointDefaults;
123
- readonly line: ResolvedLineDefaults;
124
- };
125
- /**
126
- * Records 2D primitives for one frame. Obtained through `FrameRecorder.d2`; it seals with its
127
- * parent, so a caller cannot keep drawing into a finished frame.
128
- */
129
- export declare class FrameRecorder2D {
130
- #private;
131
- constructor(defaults: FrameRecorder2DStyleDefaults, assertOpen: () => void);
132
- point(position: Vec2, options?: Point2DOptions): this;
133
- line(start: Vec2, end: Vec2, options?: Line2DOptions): this;
134
- ray(origin: Vec2, direction: Vec2, options?: Line2DOptions): this;
135
- /**
136
- * Path of segments, the 2D twin of `FrameRecorder.lineStrip`. Dash distance accumulates over
137
- * the whole path and an open strip joins only its interior vertices, matching the 3D layer.
138
- */
139
- lineStrip(points: readonly Vec2[], options?: Line2DOptions, closed?: boolean): this;
140
- polyline(points: readonly Vec2[], options?: Line2DOptions, closed?: boolean): this;
141
- /**
142
- * Axis-aligned grid of lines, the 2D twin of `FrameRecorder.grid` and reference `grid_2d`: `cells`
143
- * are the cells per axis, so the drawn line count is one more on each side.
144
- */
145
- grid(center: Vec2, cells: readonly [number, number], spacing: Vec2, options?: Line2DOptions): this;
146
- /** Rectangle with the given full width and height (reference `Rectangle::new`). */
147
- rect(center: Vec2, size: Vec2, options?: Shape2DOptions): this;
148
- /** Rectangle with rounded corners; the shader clamps the radius to half the shorter side. */
149
- roundedRect(center: Vec2, size: Vec2, cornerRadius: number, options?: Shape2DOptions): this;
150
- /** Filled disc with the given radius (reference `Circle::new`). */
151
- circle(center: Vec2, radius: number, options?: Shape2DOptions): this;
152
- /** Ellipse with independent x and y radii (reference `Ellipse::new(x_radius, y_radius)`). */
153
- ellipse(center: Vec2, radii: Vec2, options?: Shape2DOptions): this;
154
- /**
155
- * Circular arc from `startAngle` through `sweep`. It is a curve without an interior, so an
156
- * unfilled arc (the default) strokes it with `strokeWidth` while `fill: true` leaves a hairline.
157
- */
158
- arc(center: Vec2, radius: number, options?: Shape2DOptions): this;
159
- /** Ring band between two radii (reference `Annulus::new(inner_radius, outer_radius)`). */
160
- annulus(center: Vec2, innerRadius: number, outerRadius: number, options?: Shape2DOptions): this;
161
- /** Capsule whose straight section is `length` long along y (reference `Capsule2d::new(radius, length)`). */
162
- capsule(center: Vec2, radius: number, length: number, options?: Shape2DOptions): this;
163
- /** Regular polygon with a vertex on the +x axis (reference `RegularPolygon::new(circumradius, sides)`). */
164
- polygon(center: Vec2, circumradius: number, sides: number, options?: Shape2DOptions): this;
165
- /** Arbitrary triangle (reference `Triangle2d::new(a, b, c)`); `rotation` turns about its centroid. */
166
- triangle(a: Vec2, b: Vec2, c: Vec2, options?: Shape2DOptions): this;
167
- /** Diamond with the given full diagonals (reference `Rhombus::new(horizontal, vertical)`). */
168
- rhombus(center: Vec2, diagonals: Vec2, options?: Shape2DOptions): this;
169
- /** Disc sector from `startAngle` through `sweep` (reference `CircularSector`). */
170
- sector(center: Vec2, radius: number, options?: Shape2DOptions): this;
171
- /** Disc region between a chord and its arc (reference `CircularSegment`). */
172
- segment(center: Vec2, radius: number, options?: Shape2DOptions): this;
173
- get points(): readonly Point2DCommand[];
174
- get lines(): readonly Line2DCommand[];
175
- get joints(): readonly Joint2DCommand[];
176
- get shapes(): readonly Shape2DCommand[];
177
- }