vgpu-render 0.1.0 → 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 (42) hide show
  1. package/README.md +76 -26
  2. package/dist/curve.d.ts +1 -1
  3. package/dist/debug-draw-3d.d.ts +3 -3
  4. package/dist/debug-draw-3d.js +10 -10
  5. package/dist/{frame-recorder-2d.d.ts → frame-draw-list-2d.d.ts} +11 -11
  6. package/dist/{frame-recorder-2d.js → frame-draw-list-2d.js} +6 -6
  7. package/dist/{frame-recorder.d.ts → frame-draw-list.d.ts} +60 -8
  8. package/dist/{frame-recorder.js → frame-draw-list.js} +51 -10
  9. package/dist/geometry-generators.d.ts +2 -2
  10. package/dist/geometry-generators.js +1 -1
  11. package/dist/gpu-debug-draw-2d.d.ts +1 -1
  12. package/dist/gpu-debug-draw-3d.d.ts +1 -1
  13. package/dist/gpu-debug-draw-3d.js +4 -2
  14. package/dist/gpu-text-3d.d.ts +1 -1
  15. package/dist/gpu-text-3d.js +49 -2
  16. package/dist/index.d.ts +3 -3
  17. package/dist/index.js +2 -3
  18. package/dist/mesh-batch-3d.d.ts +1 -1
  19. package/dist/shape2d-shader.js +3 -2
  20. package/dist/text-atlas.d.ts +6 -4
  21. package/dist/text-atlas.js +9 -6
  22. package/dist/text-font.d.ts +48 -7
  23. package/dist/text-font.js +63 -11
  24. package/dist/text-layout.d.ts +63 -4
  25. package/dist/text-layout.js +58 -8
  26. package/dist/text-msdf-baked.d.ts +9 -4
  27. package/dist/text-msdf-baked.js +9 -4
  28. package/dist/text-msdf-font.d.ts +10 -4
  29. package/dist/text-msdf-font.js +9 -3
  30. package/dist/vector-math.d.ts +19 -0
  31. package/dist/vector-math.js +37 -0
  32. package/package.json +1 -1
  33. package/dist/layout-3d.d.ts +0 -227
  34. package/dist/layout-3d.js +0 -226
  35. package/dist/scene-examples.d.ts +0 -39
  36. package/dist/scene-examples.js +0 -200
  37. package/dist/scenes-2d.d.ts +0 -18
  38. package/dist/scenes-2d.js +0 -160
  39. package/dist/scenes-3d.d.ts +0 -46
  40. package/dist/scenes-3d.js +0 -1994
  41. package/dist/text-scenes.d.ts +0 -85
  42. package/dist/text-scenes.js +0 -338
package/README.md CHANGED
@@ -30,23 +30,23 @@ dependencies of this package.
30
30
 
31
31
  ## Usage
32
32
 
33
- The public API uses an explicit frame recorder:
33
+ The public API uses an explicit frame draw list:
34
34
 
35
35
  ```ts
36
- const recorder = debug.beginFrame();
37
- recorder.point([0, 0, 0], { color: [1, 0, 0, 1], size: 8 });
38
- recorder.line([0, 0, 0], [1, 0, 0], { color: [0, 1, 0, 1], width: 2 });
39
- recorder.arrow([0, 0, 0], [1, 1, 0], { color: [0, 0.5, 1, 1] });
40
- recorder.lineStrip([[0, 0, 0], [1, 0, 1], [2, 0, 0]], { width: 4, joint: "round" }, false);
36
+ const drawList = debug.beginFrame();
37
+ drawList.point([0, 0, 0], { color: [1, 0, 0, 1], size: 8 });
38
+ drawList.line([0, 0, 0], [1, 0, 0], { color: [0, 1, 0, 1], width: 2 });
39
+ drawList.arrow([0, 0, 0], [1, 1, 0], { color: [0, 0.5, 1, 1] });
40
+ drawList.lineStrip([[0, 0, 0], [1, 0, 1], [2, 0, 0]], { width: 4, joint: "round" }, false);
41
41
  // Per-vertex normals select the reference `*_n` shading: each segment uses its start
42
42
  // vertex's normal and the joints between them blend the two normals.
43
- recorder.lineStrip([[0, 0, 0], [1, 0, 0], [1, 1, 0]], { width: 4, joint: "round", normals: [[0, 1, 0], [0, 1, 0], [0, 1, 0]] }, false);
44
- recorder.grid([0, 0, 0], [4, 4], [0.5, 0.5], { color: [0.4, 0.4, 0.5, 1] });
45
- recorder.curve(cubicBezier, { color: [1, 1, 1, 1], tolerance: 0.08 });
46
- recorder.mesh(sphere(0.5), { solid: true, color: [0.2, 0.6, 1, 0.8] });
47
- recorder.text([0, 1, 0], "world label", { fontSize: 32, pixelSize: 0.01, color: [1, 0.8, 0.2, 1] });
48
- recorder.text([24, 24, 0], "screen HUD", { space: "screen", anchor: "top-left", fontSize: 16 });
49
- const commands = debug.finishFrame(recorder);
43
+ drawList.lineStrip([[0, 0, 0], [1, 0, 0], [1, 1, 0]], { width: 4, joint: "round", normals: [[0, 1, 0], [0, 1, 0], [0, 1, 0]] }, false);
44
+ drawList.grid([0, 0, 0], [4, 4], [0.5, 0.5], { color: [0.4, 0.4, 0.5, 1] });
45
+ drawList.curve(cubicBezier, { color: [1, 1, 1, 1], tolerance: 0.08 });
46
+ drawList.mesh(sphere(0.5), { solid: true, color: [0.2, 0.6, 1, 0.8] });
47
+ drawList.text([0, 1, 0], "world label", { fontSize: 32, pixelSize: 0.01, color: [1, 0.8, 0.2, 1] });
48
+ drawList.text([24, 24, 0], "screen HUD", { space: "screen", anchor: "top-left", fontSize: 16 });
49
+ const commands = debug.finishFrame(drawList);
50
50
 
51
51
  // `view` is the world-to-view matrix; normal-aware strokes read it like the reference view matrix.
52
52
  renderer.prepare(commands, { viewProjection, view });
@@ -108,7 +108,7 @@ alphas, which the addon does not expose.
108
108
  ## Pipeline
109
109
 
110
110
  ```text
111
- FrameRecorder -> FrameCommand[] -> GpuDebugDraw3D
111
+ FrameDrawList -> FrameCommand[] -> GpuDebugDraw3D
112
112
  points -> instanced billboards
113
113
  lines -> screen-space or world-space quads, dash + normal aware
114
114
  triangles -> transient solid meshes
@@ -146,7 +146,9 @@ Two coordinate spaces share one pixel-based pipeline:
146
146
  | `screen` | top-left corner | down | one canvas pixel | no |
147
147
 
148
148
  `world2d` mirrors the reference 2D camera: the origin is the centre of the target, `+y` points up and
149
- `zoom` is canvas pixels per world unit. `2d_shapes`' `X_EXTENT = 1000` is a pixel-scale world
149
+ `zoom` is canvas pixels per world unit. Both spaces are in **target pixels**, like the text `screen`
150
+ space, so a HiDPI caller scales `screen` content itself (`gallery/screen-scale.ts` does that for the
151
+ gallery); `world2d` needs no such step because its projection already reads the target size. `2d_shapes`' `X_EXTENT = 1000` is a pixel-scale world
150
152
  coordinate in that sense, not a top-left one — the top-left, +y-down space is the reference UI space,
151
153
  and here it is `screen`, the same one `TextSpace "screen"` already uses.
152
154
 
@@ -156,7 +158,7 @@ dashes with the same phase accumulation as the 3D paths, and round/bevel/miter j
156
158
  `triangle`, `rhombus`, `sector`, `segment`. Each is one instanced quad evaluated as a signed
157
159
  distance field in the fragment stage, so nothing is triangulated on the CPU and an outline
158
160
  width stays a pixel count at any zoom. `fill: false` plus `strokeWidth` turns any of them into
159
- an outline.
161
+ an outline; an `arc`'s outline is clipped to its `startAngle`/`sweep` wedge.
160
162
  - The layer is an overlay: it draws after every 3D primitive, pins itself to the near plane and
161
163
  does not write depth. Screen-space text remains the last thing drawn.
162
164
  - `InfiniteGrid2D` renders the reference's 2D grid as a full-viewport shader quad: decimal LOD levels,
@@ -164,11 +166,15 @@ does not write depth. Screen-space text remains the last thing drawn.
164
166
  `cellSize`, `lineWidthPixels`, `minPixelsBetweenCells`, the LOD range and both line colors from
165
167
  the gallery's grid panel, and draws as the 2D layer's background.
166
168
  - The 2D camera is independent of the 3D one (`mode: "3d" | "2d"` in the gallery config); the
167
- `2d-*` pages fit their declared `view2d` extent, drag to pan and scroll to zoom.
169
+ `2d-*` pages fit their declared `view2d` extent, drag or hold WASD to pan, and scroll to zoom. The gallery
170
+ classifies the 2D layer like the 3D scenes: `2d-gizmos` and `2d-shapes` are overviews, while
171
+ `2d-lines`, `2d-curves`, `2d-markers`, `2d-outlines`, `2d-fill`, `2d-spaces` and `2d-grid` are
172
+ column-by-row matrices whose column titles and sample labels are `world2d` text, so they pan
173
+ and zoom with the geometry they annotate.
168
174
 
169
175
  ## Text
170
176
 
171
- `recorder.text(position, text, options)` draws one block per call. `text` is a string or a list
177
+ `drawList.text(position, text, options)` draws one block per call. `text` is a string or a list
172
178
  of spans, the equivalent of a text root entity plus its child span entities.
173
179
 
174
180
  Three placement spaces share one pipeline and are selected per block:
@@ -179,6 +185,13 @@ Three placement spaces share one pipeline and are selected per block:
179
185
  | `world-billboard` | world units, block rebuilt from the camera's axes | `fontSize × pixelSize` |
180
186
  | `screen` | **top-left origin pixels** (`position.z` ignored) | `fontSize` pixels |
181
187
 
188
+ The `screen` space is in **target pixels** — the same units as the pass's viewport — because the
189
+ library has no DOM access and must not guess a display density. A caller on a HiDPI display owns the
190
+ conversion: the gallery authors its pages in CSS pixels and multiplies every screen-space command by
191
+ `devicePixelRatio` before handing the frame over (`gallery/screen-scale.ts`). A block placed at
192
+ `[24, 24]` with `fontSize: 18` in CSS pixels then draws at `[48, 48]` and 36 target pixels on a 2×
193
+ display, which is what keeps a label the same physical size on every screen.
194
+
182
195
  `fontSize` is in **font pixels** and is the layout unit; `pixelSize` is world units per font pixel.
183
196
  That pair is the reference `font_size` / `pixel_size`, and it lets a block keep a fixed world
184
197
  size while its glyphs are rasterised at any resolution. Which resolution that is belongs to the glyph
@@ -187,6 +200,12 @@ source: one that can rasterise on demand is asked for the size the block covers
187
200
  `fontSize × rasterScale`. Layout options follow the reference layout model: `justify`, `lineBreak`,
188
201
  `bounds` (`TextBounds`, in font pixels), `anchor`, `lineHeight` and `letterSpacing`.
189
202
 
203
+ `rotation` turns a `world-plane` block away from the XY plane, with the reference `Label3D.rotation`
204
+ Euler order (Y, then X, then Z) in radians. `[-Math.PI / 2, 0, 0]` lays a block flat on the XZ plane
205
+ so it reads from above, which is what the reference example labels do; the two camera-relative
206
+ spaces ignore it. The basis travels in the instance world matrix, so the text pipeline stays inside
207
+ the device's vertex attribute budget.
208
+
190
209
  `lineHeight` is the absolute pixel form when you pass one, and the default is a `1.2` multiple of the
191
210
  font size otherwise — which means a line's box is that multiple of **the line's own
192
211
  largest span**, so a line holding a bigger span is taller. `lineHeightScale` names that multiple
@@ -198,6 +217,34 @@ shaping pass adds it to each cluster's advance.
198
217
  World text is depth-tested but never depth-written, so geometry in front of it
199
218
  occludes it; `screen` text pins itself to the near plane and always wins.
200
219
 
220
+ ### Font face
221
+
222
+ `weight` (1–1000, default `400`), `style` (`normal` or `italic`) and `width` (the nine
223
+ `font-stretch` keywords) select the face a block is rasterised in, mirroring bevy's `TextFont`.
224
+ They are block defaults, and a span overrides one axis of them at a time rather than replacing the
225
+ whole face. The glyph atlas keys on all three, so two weights of one character cannot share texels.
226
+
227
+ How much of that a browser can honour depends on the loaded face, and the limits are real:
228
+
229
+ | option | what Canvas2D does |
230
+ | --- | --- |
231
+ | `weight` | A weight the loaded family does not carry is **synthesised** in one step (the threshold sits between 500 and 600), so `700` and `900` then look identical and the advance never changes. A variable font's real `wght` axis is not reachable — Canvas2D has no `font-variation-settings` — but the browser **does** pick a face by the weight it was registered with, so registering the same family once per weight gives real weights. |
232
+ | `style` | `italic` is the only slant the font shorthand keeps; `oblique <angle>` reads back as plain `italic`, which is why bevy's `FontStyle::Oblique(a)` is not exposed. A face without an italic is slanted by the browser. |
233
+ | `width` | The keyword only reaches the font when the face has a `wdth` axis. The bundled static subset has none, so every stop draws at normal width there. A percentage is rejected by the shorthand, so only the nine named stops exist here. |
234
+
235
+ The bundled Fira Mono subset is a static Medium face, so out of the box `weight` only ever produces
236
+ one synthesised step. `loadFontFaces(family, faces)` is the way out: it registers each entry as a face
237
+ of `family` with its own `weight` descriptor, so `weight: 200` through `900` select genuinely different
238
+ faces. The gallery does exactly that for the `text-font-query` page, which draws with eight instances
239
+ of a variable font built by `bun scripts/build-font-faces.ts` (see
240
+ `gallery/assets/`). That is gallery-only payload: the library's own entry point does not
241
+ carry it.
242
+
243
+ A custom glyph source receives the resolved face — `{ family, weight, style, width }` — instead of a
244
+ family string, because those are the fields the atlas keys on. A source that holds one fixed face (a
245
+ prebaked page, one parsed font buffer) sets `faceKey` to the part it can honour, so a weight it cannot
246
+ rasterise does not multiply identical atlas entries.
247
+
201
248
  ### Outline
202
249
 
203
250
  `outlineWidth` (in font pixels) rings every glyph in the block in `outlineColor`, which defaults to
@@ -381,8 +428,8 @@ const renderer = new GpuDebugDraw3D(gpu, {
381
428
  },
382
429
  });
383
430
 
384
- recorder.text([0, 0, 0], "read me", { source: "sharp" });
385
- recorder.text([0, -1, 0], "and me", { source: "cheap" });
431
+ drawList.text([0, 0, 0], "read me", { source: "sharp" });
432
+ drawList.text([0, -1, 0], "and me", { source: "cheap" });
386
433
  ```
387
434
 
388
435
  Blocks that name nothing use the first source registered, and `defaultTextStyle` can set one for the
@@ -391,7 +438,10 @@ actually names it, so registering sources you never draw costs nothing.
391
438
 
392
439
  Seven scenes exercise text end to end, and none of them is a port — the reference addon has no text
393
440
  API; the layout they run follows the reference layout model, and `docs/text-reference.md` records how
394
- that was verified.
441
+ that was verified. The geometry examples also draw their own labels, because the reference scenes
442
+ own `Label3D` captions for every column and row; those go through the same pipeline, lying flat on
443
+ the ground with `rotation: [-Math.PI / 2, 0, 0]`. A `GpuDebugDraw3D` built without a `text` option
444
+ skips those blocks instead of failing, so geometry-only callers keep working.
395
445
 
396
446
  `text-compare` is the parity matrix, in the shape of the line/circle examples: rows are glyph sources,
397
447
  columns are font sizes, transposed relative to those because text grows sideways. The six
@@ -457,7 +507,7 @@ gallery to a single glyph source (`auto`, `bitmap`, `bitmap4x`, `sdf`, `sdf4x`,
457
507
  - `InfiniteGrid3D` shader-driven ground grid ported from the reference grid addon
458
508
  (decimal LOD layers, screen-space width, edge/grazing/center/far fades).
459
509
  - Orbit/pan/zoom camera controls in the gallery, tuned to the reference
460
- `entities/free_camera.gd` and layered on a velocity model (`src/demo-camera.ts`): a drag is
510
+ `entities/free_camera.gd` and layered on a velocity model (`gallery/demo-camera.ts`): a drag is
461
511
  an angular velocity (0.25°/px) or a linear velocity (`distance`/FOV in perspective,
462
512
  `orthographicHeight` in orthographic), a wheel notch is a scalar velocity in log₂ space
463
513
  (1.08× per notch), and a released drag keeps its velocity and glides — the exact integral of
@@ -488,7 +538,7 @@ const debug = new DebugDraw3D({
488
538
  });
489
539
  ```
490
540
 
491
- `src/demo-config.ts` contains independently validated camera/view configuration
541
+ `gallery/demo-config.ts` contains independently validated camera/view configuration
492
542
  and injected storage helpers. Gallery controls support grid, opacity, animation,
493
543
  width multiplier, projection, background, camera preset and reset; camera
494
544
  interaction is persisted. The sidebar groups them into three collapsible
@@ -507,11 +557,11 @@ deterministic preset without localStorage: `/?compare=1&scene=circle`. Orthograp
507
557
  changes the orthographic extent.
508
558
 
509
559
  The gallery UI is built with **Base UI + shadcn (base-lyra preset) + Tailwind v4**
510
- in `src/components/`, and the imperative WebGPU side lives in `src/demo-engine.ts`.
560
+ in `gallery/components/`, and the imperative WebGPU side lives in `gallery/demo-engine.ts`.
511
561
  React only renders chrome; the render loop, camera, and persistence stay outside
512
562
  React and are read through `useSyncExternalStore`
513
- (`src/demo-hooks.ts`). `src/demo.tsx` mounts the shell, `src/index.css` holds the base-lyra
514
- theme tokens, and `src/demo.css` keeps the three-column layout, compare mode
563
+ (`gallery/demo-hooks.ts`). `gallery/demo.tsx` mounts the shell, `gallery/index.css` holds the base-lyra
564
+ theme tokens, and `gallery/demo.css` keeps the three-column layout, compare mode
515
565
  and code highlighting.
516
566
 
517
567
  Runtime comparison viewers (see `docs/visual-comparison.md` and `docs/text-reference.md`):
package/dist/curve.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Vec3 } from "./frame-recorder.js";
1
+ import type { Vec3 } from "./frame-draw-list.js";
2
2
  export type CurvePoint = {
3
3
  readonly position: Vec3;
4
4
  readonly in?: Vec3;
@@ -1,4 +1,4 @@
1
- import { FrameRecorder, type FrameCommand, type LayerMask, type LineDefaults, type PointDefaults } from "./frame-recorder.js";
1
+ import { FrameDrawList, type FrameCommand, type LayerMask, type LineDefaults, type PointDefaults } from "./frame-draw-list.js";
2
2
  export type DebugDrawOptions = {
3
3
  defaultVisibleLayers?: LayerMask;
4
4
  /** Style applied to every point that does not override it. */
@@ -9,6 +9,6 @@ export type DebugDrawOptions = {
9
9
  export declare class DebugDraw3D {
10
10
  #private;
11
11
  constructor(options?: DebugDrawOptions);
12
- beginFrame(): FrameRecorder;
13
- finishFrame(recorder: FrameRecorder): readonly FrameCommand[];
12
+ beginFrame(): FrameDrawList;
13
+ finishFrame(drawList: FrameDrawList): readonly FrameCommand[];
14
14
  }
@@ -1,4 +1,4 @@
1
- import { FrameRecorder, resolveFrameRecorderDefaults, } from "./frame-recorder.js";
1
+ import { FrameDrawList, resolveFrameDrawListDefaults, } from "./frame-draw-list.js";
2
2
  export class DebugDraw3D {
3
3
  #visibleLayers;
4
4
  #defaults;
@@ -14,22 +14,22 @@ export class DebugDraw3D {
14
14
  ...(options.defaultLineStyle === undefined ? {} : { line: { ...options.defaultLineStyle } }),
15
15
  };
16
16
  // Validate eagerly so invalid defaults fail at construction, not on the first frame.
17
- resolveFrameRecorderDefaults(this.#defaults);
17
+ resolveFrameDrawListDefaults(this.#defaults);
18
18
  }
19
19
  beginFrame() {
20
20
  if (this.#active !== undefined) {
21
21
  throw new Error("a debug draw frame is already active");
22
22
  }
23
- const recorder = new FrameRecorder(this.#defaults);
24
- this.#active = recorder;
25
- return recorder;
23
+ const drawList = new FrameDrawList(this.#defaults);
24
+ this.#active = drawList;
25
+ return drawList;
26
26
  }
27
- finishFrame(recorder) {
28
- if (this.#active !== recorder) {
29
- throw new Error("recorder does not belong to this debug draw instance");
27
+ finishFrame(drawList) {
28
+ if (this.#active !== drawList) {
29
+ throw new Error("drawList does not belong to this debug draw instance");
30
30
  }
31
- const commands = recorder.commands(this.#visibleLayers);
32
- recorder.seal();
31
+ const commands = drawList.commands(this.#visibleLayers);
32
+ drawList.seal();
33
33
  this.#active = undefined;
34
34
  return commands;
35
35
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
- * 2D debug-draw commands and their recorder.
2
+ * 2D debug-draw commands and their drawList.
3
3
  *
4
- * The 2D layer is the same data flow as the 3D one — `FrameRecorder -> FrameCommand[] -> GPU` —
4
+ * The 2D layer is the same data flow as the 3D one — `FrameDrawList -> FrameCommand[] -> GPU` —
5
5
  * reached through `frame.d2` instead of a second entry point, so a frame has one lifecycle, one
6
6
  * layer mask and one set of defaults. The commands are separate types rather than 3D commands
7
7
  * with a `space` flag because a 2D point is a `Vec2` and 2D strokes carry no surface normal;
@@ -10,7 +10,7 @@
10
10
  * See `camera-2d.ts` for what `world2d` and `screen` mean.
11
11
  */
12
12
  import type { Space2D, Vec2 } from "./camera-2d.js";
13
- import type { Color, LayerMask, LineJoint, RenderedJoint, ResolvedLineDefaults, ResolvedPointDefaults } from "./frame-recorder.js";
13
+ import type { Color, LayerMask, LineJoint, RenderedJoint, ResolvedLineDefaults, ResolvedPointDefaults } from "./frame-draw-list.js";
14
14
  export type Line2DOptions = {
15
15
  color?: Color;
16
16
  colorEnd?: Color;
@@ -115,31 +115,31 @@ export type Joint2DCommand = {
115
115
  readonly space: Space2D;
116
116
  readonly layer: LayerMask;
117
117
  };
118
- /** Everything a `FrameRecorder2D` can emit. */
118
+ /** Everything a `FrameDrawList2D` can emit. */
119
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 = {
120
+ /** The 3D drawList's resolved point/line styles, reused so 2D and 3D defaults cannot drift. */
121
+ export type FrameDrawList2DStyleDefaults = {
122
122
  readonly point: ResolvedPointDefaults;
123
123
  readonly line: ResolvedLineDefaults;
124
124
  };
125
125
  /**
126
- * Records 2D primitives for one frame. Obtained through `FrameRecorder.d2`; it seals with its
126
+ * Records 2D primitives for one frame. Obtained through `FrameDrawList.d2`; it seals with its
127
127
  * parent, so a caller cannot keep drawing into a finished frame.
128
128
  */
129
- export declare class FrameRecorder2D {
129
+ export declare class FrameDrawList2D {
130
130
  #private;
131
- constructor(defaults: FrameRecorder2DStyleDefaults, assertOpen: () => void);
131
+ constructor(defaults: FrameDrawList2DStyleDefaults, assertOpen: () => void);
132
132
  point(position: Vec2, options?: Point2DOptions): this;
133
133
  line(start: Vec2, end: Vec2, options?: Line2DOptions): this;
134
134
  ray(origin: Vec2, direction: Vec2, options?: Line2DOptions): this;
135
135
  /**
136
- * Path of segments, the 2D twin of `FrameRecorder.lineStrip`. Dash distance accumulates over
136
+ * Path of segments, the 2D twin of `FrameDrawList.lineStrip`. Dash distance accumulates over
137
137
  * the whole path and an open strip joins only its interior vertices, matching the 3D layer.
138
138
  */
139
139
  lineStrip(points: readonly Vec2[], options?: Line2DOptions, closed?: boolean): this;
140
140
  polyline(points: readonly Vec2[], options?: Line2DOptions, closed?: boolean): this;
141
141
  /**
142
- * Axis-aligned grid of lines, the 2D twin of `FrameRecorder.grid` and reference `grid_2d`: `cells`
142
+ * Axis-aligned grid of lines, the 2D twin of `FrameDrawList.grid` and reference `grid_2d`: `cells`
143
143
  * are the cells per axis, so the drawn line count is one more on each side.
144
144
  */
145
145
  grid(center: Vec2, cells: readonly [number, number], spacing: Vec2, options?: Line2DOptions): this;
@@ -1,7 +1,7 @@
1
1
  /**
2
- * 2D debug-draw commands and their recorder.
2
+ * 2D debug-draw commands and their drawList.
3
3
  *
4
- * The 2D layer is the same data flow as the 3D one — `FrameRecorder -> FrameCommand[] -> GPU` —
4
+ * The 2D layer is the same data flow as the 3D one — `FrameDrawList -> FrameCommand[] -> GPU` —
5
5
  * reached through `frame.d2` instead of a second entry point, so a frame has one lifecycle, one
6
6
  * layer mask and one set of defaults. The commands are separate types rather than 3D commands
7
7
  * with a `space` flag because a 2D point is a `Vec2` and 2D strokes carry no surface normal;
@@ -96,10 +96,10 @@ function spaceValue(value) {
96
96
  return value;
97
97
  }
98
98
  /**
99
- * Records 2D primitives for one frame. Obtained through `FrameRecorder.d2`; it seals with its
99
+ * Records 2D primitives for one frame. Obtained through `FrameDrawList.d2`; it seals with its
100
100
  * parent, so a caller cannot keep drawing into a finished frame.
101
101
  */
102
- export class FrameRecorder2D {
102
+ export class FrameDrawList2D {
103
103
  #points = [];
104
104
  #lines = [];
105
105
  #joints = [];
@@ -158,7 +158,7 @@ export class FrameRecorder2D {
158
158
  return this.line(origin, [origin[0] + direction[0], origin[1] + direction[1]], options);
159
159
  }
160
160
  /**
161
- * Path of segments, the 2D twin of `FrameRecorder.lineStrip`. Dash distance accumulates over
161
+ * Path of segments, the 2D twin of `FrameDrawList.lineStrip`. Dash distance accumulates over
162
162
  * the whole path and an open strip joins only its interior vertices, matching the 3D layer.
163
163
  */
164
164
  lineStrip(points, options = {}, closed = false) {
@@ -193,7 +193,7 @@ export class FrameRecorder2D {
193
193
  return this.lineStrip(points, options, closed);
194
194
  }
195
195
  /**
196
- * Axis-aligned grid of lines, the 2D twin of `FrameRecorder.grid` and reference `grid_2d`: `cells`
196
+ * Axis-aligned grid of lines, the 2D twin of `FrameDrawList.grid` and reference `grid_2d`: `cells`
197
197
  * are the cells per axis, so the drawn line count is one more on each side.
198
198
  */
199
199
  grid(center, cells, spacing, options = {}) {
@@ -1,6 +1,6 @@
1
1
  import { type Curve3D, type CurveTransform } from "./curve.js";
2
- import { FrameRecorder2D, type FrameCommand2D } from "./frame-recorder-2d.js";
3
- import { type TextAnchor, type TextJustify, type TextLineBreak } from "./text-layout.js";
2
+ import { FrameDrawList2D, type FrameCommand2D } from "./frame-draw-list-2d.js";
3
+ import { type TextAnchor, type TextFontStyle, type TextFontWidth, type TextJustify, type TextLineBreak } from "./text-layout.js";
4
4
  export type Vec3 = readonly [number, number, number];
5
5
  export type Color = readonly [number, number, number, number];
6
6
  export type LayerMask = number;
@@ -114,6 +114,12 @@ export type TextSpanOptions = {
114
114
  readonly color?: Color;
115
115
  readonly fontSize?: number;
116
116
  readonly fontFamily?: string;
117
+ /** reference `TextFont::weight`, 1 to 1000. Defaults to the block's, then to 400. */
118
+ readonly weight?: number;
119
+ /** reference `TextFont::style`, minus `Oblique`: Canvas2D drops the slant angle. */
120
+ readonly style?: TextFontStyle;
121
+ /** reference `TextFont::width`, as a `font-stretch` keyword. Inert unless the face has a `wdth` axis. */
122
+ readonly width?: TextFontWidth;
117
123
  };
118
124
  export type TextOptions = {
119
125
  color?: Color;
@@ -143,6 +149,21 @@ export type TextOptions = {
143
149
  */
144
150
  pixelSize?: number;
145
151
  fontFamily?: string;
152
+ /**
153
+ * reference `TextFont::weight`, 1 to 1000, default `400`. A face that does not carry the weight is
154
+ * emboldened by the browser in one synthesised step, so the advance does not change.
155
+ */
156
+ weight?: number;
157
+ /**
158
+ * reference `TextFont::style`. `italic` is the only slant Canvas2D honours: its shorthand drops the
159
+ * angle of `Oblique(a)`, so a synthesised slant is all an oblique could mean here.
160
+ */
161
+ style?: TextFontStyle;
162
+ /**
163
+ * reference `TextFont::width`, spelled as a `font-stretch` keyword. Canvas2D keeps the keyword only when
164
+ * the face has a `wdth` axis; the bundled static subset has none, so every stop draws identically.
165
+ */
166
+ width?: TextFontWidth;
146
167
  /**
147
168
  * Absolute line height in pixels, reference `LineHeight::Px`. Exclusive with `lineHeightScale`;
148
169
  * omitting both leaves the line height at reference default, `RelativeToFont(1.2)`.
@@ -164,6 +185,12 @@ export type TextOptions = {
164
185
  bounds?: readonly [number, number];
165
186
  anchor?: TextAnchor;
166
187
  space?: TextSpace;
188
+ /**
189
+ * Local rotation of a `world-plane` block, in radians, as reference `Label3D.rotation` Euler angles
190
+ * (applied Y, then X, then Z). The reference `Label3D` labels use `[-PI/2, 0, 0]` to lay the block
191
+ * flat on the XZ plane, readable from above; the two camera-relative spaces ignore it.
192
+ */
193
+ rotation?: readonly [number, number, number];
167
194
  /**
168
195
  * Which registered glyph source draws this block: its encoding and raster scale. Blocks that name
169
196
  * nothing use the first source, so a frame can mix a sharp pass and a cheap pass.
@@ -179,6 +206,9 @@ export type ResolvedTextSpan = {
179
206
  readonly color: Color;
180
207
  readonly fontSize: number;
181
208
  readonly fontFamily: string;
209
+ readonly weight: number;
210
+ readonly style: TextFontStyle;
211
+ readonly width: TextFontWidth;
182
212
  };
183
213
  export type TextCommand = {
184
214
  readonly kind: "text";
@@ -190,6 +220,9 @@ export type TextCommand = {
190
220
  readonly fontSize: number;
191
221
  readonly pixelSize: number;
192
222
  readonly fontFamily: string;
223
+ readonly weight: number;
224
+ readonly style: TextFontStyle;
225
+ readonly width: TextFontWidth;
193
226
  /**
194
227
  * Resolved line height in pixels: the value a `LineHeight::Px` block asked for, or
195
228
  * `1.2 × fontSize` for the default, which `lineHeightScale` then marks as resolved per line.
@@ -207,6 +240,7 @@ export type TextCommand = {
207
240
  readonly bounds?: readonly [number, number];
208
241
  readonly anchor: TextAnchor;
209
242
  readonly space: TextSpace;
243
+ readonly rotation: readonly [number, number, number];
210
244
  readonly source?: string;
211
245
  readonly depthTest: boolean;
212
246
  readonly layer: LayerMask;
@@ -248,6 +282,12 @@ export type TextDefaults = {
248
282
  fontSize?: number;
249
283
  pixelSize?: number;
250
284
  fontFamily?: string;
285
+ /** Default reference `TextFont::weight`, 1 to 1000. */
286
+ weight?: number;
287
+ /** Default reference `TextFont::style`. */
288
+ style?: TextFontStyle;
289
+ /** Default reference `TextFont::width`, as a `font-stretch` keyword. */
290
+ width?: TextFontWidth;
251
291
  /**
252
292
  * Absolute line height in pixels, reference `LineHeight::Px`. Omitted means reference default,
253
293
  * `RelativeToFont(1.2)`: every line is 1.2 times its own largest span, so a line holding a
@@ -261,11 +301,12 @@ export type TextDefaults = {
261
301
  lineBreak?: TextLineBreak;
262
302
  anchor?: TextAnchor;
263
303
  space?: TextSpace;
304
+ rotation?: readonly [number, number, number];
264
305
  source?: string;
265
306
  depthTest?: boolean;
266
307
  layer?: LayerMask;
267
308
  };
268
- export type FrameRecorderDefaults = {
309
+ export type FrameDrawListDefaults = {
269
310
  point?: PointDefaults;
270
311
  line?: LineDefaults;
271
312
  text?: TextDefaults;
@@ -276,6 +317,8 @@ export declare const DEFAULT_LAYER = 1;
276
317
  export declare const DEFAULT_TEXT_SIZE = 16;
277
318
  /** Default world units per font pixel, i.e. a 0.32 world-unit glyph at the default size. */
278
319
  export declare const DEFAULT_TEXT_PIXEL_SIZE = 0.02;
320
+ /** `world-plane` blocks face +Z until a rotation lays them down, like a reference `Label3D`. */
321
+ export declare const DEFAULT_TEXT_ROTATION: readonly [number, number, number];
279
322
  /** reference `LineHeight` default is `RelativeToFont(1.2)`. */
280
323
  export declare const DEFAULT_LINE_HEIGHT_RATIO = 1.2;
281
324
  export type ResolvedPointDefaults = {
@@ -301,6 +344,9 @@ export type ResolvedTextDefaults = {
301
344
  fontSize: number | undefined;
302
345
  pixelSize: number | undefined;
303
346
  fontFamily: string | undefined;
347
+ weight: number | undefined;
348
+ style: TextFontStyle | undefined;
349
+ width: TextFontWidth | undefined;
304
350
  lineHeight: number | undefined;
305
351
  lineHeightScale: number | undefined;
306
352
  letterSpacing: number | undefined;
@@ -308,25 +354,31 @@ export type ResolvedTextDefaults = {
308
354
  lineBreak: TextLineBreak | undefined;
309
355
  anchor: TextAnchor | undefined;
310
356
  space: TextSpace | undefined;
357
+ rotation: readonly [number, number, number] | undefined;
311
358
  source: string | undefined;
312
359
  depthTest: boolean | undefined;
313
360
  layer: LayerMask | undefined;
314
361
  };
315
- export type ResolvedFrameRecorderDefaults = {
362
+ export type ResolvedFrameDrawListDefaults = {
316
363
  point: ResolvedPointDefaults;
317
364
  line: ResolvedLineDefaults;
318
365
  text: ResolvedTextDefaults;
319
366
  };
320
367
  /** Validates and resolves style defaults so invalid values fail at construction time. */
321
- export declare function resolveFrameRecorderDefaults(defaults?: FrameRecorderDefaults): ResolvedFrameRecorderDefaults;
322
- export declare class FrameRecorder {
368
+ export declare function resolveFrameDrawListDefaults(defaults?: FrameDrawListDefaults): ResolvedFrameDrawListDefaults;
369
+ /**
370
+ * One frame's draw list: records primitives, then hands them to `GpuDebugDraw3D` as
371
+ * `FrameCommand[]`. This is the `ImDrawList` / `GizmoBuffer` layer; GPU command encoding
372
+ * happens later in the renderer.
373
+ */
374
+ export declare class FrameDrawList {
323
375
  #private;
324
- constructor(defaults?: FrameRecorderDefaults);
376
+ constructor(defaults?: FrameDrawListDefaults);
325
377
  /**
326
378
  * The 2D layer: `Vec2` primitives on the reference-style `Camera2d` spaces described in
327
379
  * `camera-2d.ts`. Drawing here records into the same frame and layer mask as the 3D methods.
328
380
  */
329
- get d2(): FrameRecorder2D;
381
+ get d2(): FrameDrawList2D;
330
382
  point(position: Vec3, options?: PointOptions): this;
331
383
  line(start: Vec3, end: Vec3, options?: LineOptions): this;
332
384
  ray(origin: Vec3, direction: Vec3, options?: LineOptions): this;