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.
- package/README.md +76 -26
- package/dist/curve.d.ts +1 -1
- package/dist/debug-draw-3d.d.ts +3 -3
- package/dist/debug-draw-3d.js +10 -10
- package/dist/{frame-recorder-2d.d.ts → frame-draw-list-2d.d.ts} +11 -11
- package/dist/{frame-recorder-2d.js → frame-draw-list-2d.js} +6 -6
- package/dist/{frame-recorder.d.ts → frame-draw-list.d.ts} +60 -8
- package/dist/{frame-recorder.js → frame-draw-list.js} +51 -10
- package/dist/geometry-generators.d.ts +2 -2
- package/dist/geometry-generators.js +1 -1
- package/dist/gpu-debug-draw-2d.d.ts +1 -1
- package/dist/gpu-debug-draw-3d.d.ts +1 -1
- package/dist/gpu-debug-draw-3d.js +4 -2
- package/dist/gpu-text-3d.d.ts +1 -1
- package/dist/gpu-text-3d.js +49 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -3
- package/dist/mesh-batch-3d.d.ts +1 -1
- package/dist/shape2d-shader.js +3 -2
- package/dist/text-atlas.d.ts +6 -4
- package/dist/text-atlas.js +9 -6
- package/dist/text-font.d.ts +48 -7
- package/dist/text-font.js +63 -11
- package/dist/text-layout.d.ts +63 -4
- package/dist/text-layout.js +58 -8
- package/dist/text-msdf-baked.d.ts +9 -4
- package/dist/text-msdf-baked.js +9 -4
- package/dist/text-msdf-font.d.ts +10 -4
- package/dist/text-msdf-font.js +9 -3
- package/dist/vector-math.d.ts +19 -0
- package/dist/vector-math.js +37 -0
- package/package.json +1 -1
- package/dist/layout-3d.d.ts +0 -227
- package/dist/layout-3d.js +0 -226
- package/dist/scene-examples.d.ts +0 -39
- package/dist/scene-examples.js +0 -200
- package/dist/scenes-2d.d.ts +0 -18
- package/dist/scenes-2d.js +0 -160
- package/dist/scenes-3d.d.ts +0 -46
- package/dist/scenes-3d.js +0 -1994
- package/dist/text-scenes.d.ts +0 -85
- package/dist/text-scenes.js +0 -338
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { sampleCurve } from "./curve.js";
|
|
2
|
-
import {
|
|
2
|
+
import { FrameDrawList2D } from "./frame-draw-list-2d.js";
|
|
3
3
|
import { gridLines } from "./geometry-generators.js";
|
|
4
4
|
import { DEFAULT_FONT_FAMILY } from "./text-font.js";
|
|
5
|
-
import { resolveTextAnchor, DEFAULT_LINE_HEIGHT_SCALE } from "./text-layout.js";
|
|
5
|
+
import { resolveTextAnchor, DEFAULT_LINE_HEIGHT_SCALE, DEFAULT_FONT_WEIGHT, MAX_FONT_WEIGHT, MIN_FONT_WEIGHT, TEXT_FONT_STYLES, TEXT_FONT_WIDTHS, } from "./text-layout.js";
|
|
6
6
|
export const DEFAULT_COLOR = [1, 1, 1, 1];
|
|
7
7
|
export const DEFAULT_LAYER = 1;
|
|
8
8
|
/** Default block font size in pixels; the world spaces scale it by `DEFAULT_TEXT_PIXEL_SIZE`. */
|
|
9
9
|
export const DEFAULT_TEXT_SIZE = 16;
|
|
10
10
|
/** Default world units per font pixel, i.e. a 0.32 world-unit glyph at the default size. */
|
|
11
11
|
export const DEFAULT_TEXT_PIXEL_SIZE = 0.02;
|
|
12
|
+
/** `world-plane` blocks face +Z until a rotation lays them down, like a reference `Label3D`. */
|
|
13
|
+
export const DEFAULT_TEXT_ROTATION = [0, 0, 0];
|
|
12
14
|
/** reference `LineHeight` default is `RelativeToFont(1.2)`. */
|
|
13
15
|
export const DEFAULT_LINE_HEIGHT_RATIO = DEFAULT_LINE_HEIGHT_SCALE;
|
|
14
16
|
function finite(value, name) {
|
|
@@ -79,6 +81,24 @@ function textSpaceValue(value, name) {
|
|
|
79
81
|
}
|
|
80
82
|
return value;
|
|
81
83
|
}
|
|
84
|
+
function fontWeightValue(value, name) {
|
|
85
|
+
if (!Number.isFinite(value) || value < MIN_FONT_WEIGHT || value > MAX_FONT_WEIGHT) {
|
|
86
|
+
throw new RangeError(`${name} must be between ${MIN_FONT_WEIGHT} and ${MAX_FONT_WEIGHT}`);
|
|
87
|
+
}
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
function textFontStyleValue(value, name) {
|
|
91
|
+
if (!TEXT_FONT_STYLES.includes(value)) {
|
|
92
|
+
throw new RangeError(`${name} must be one of ${TEXT_FONT_STYLES.join(", ")}`);
|
|
93
|
+
}
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
function textFontWidthValue(value, name) {
|
|
97
|
+
if (!TEXT_FONT_WIDTHS.includes(value)) {
|
|
98
|
+
throw new RangeError(`${name} must be one of ${TEXT_FONT_WIDTHS.join(", ")}`);
|
|
99
|
+
}
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
82
102
|
function textBounds(value, name) {
|
|
83
103
|
if (value.length !== 2)
|
|
84
104
|
throw new RangeError(`${name} must contain two values`);
|
|
@@ -90,7 +110,7 @@ function textBounds(value, name) {
|
|
|
90
110
|
* Folds the block defaults into every span so the renderer resolves a style once per span and
|
|
91
111
|
* never has to guess what an omitted field meant.
|
|
92
112
|
*/
|
|
93
|
-
function resolveTextSpans(text, options, defaults, baseFontSize, baseFontFamily) {
|
|
113
|
+
function resolveTextSpans(text, options, defaults, baseFontSize, baseFontFamily, baseWeight, baseStyle, baseWidth) {
|
|
94
114
|
const input = typeof text === "string" ? [{ text }] : text;
|
|
95
115
|
const baseColor = color(options.color ?? defaults.color);
|
|
96
116
|
return input.map((span) => {
|
|
@@ -102,11 +122,15 @@ function resolveTextSpans(text, options, defaults, baseFontSize, baseFontFamily)
|
|
|
102
122
|
color: span.color === undefined ? baseColor : color(span.color),
|
|
103
123
|
fontSize,
|
|
104
124
|
fontFamily: span.fontFamily ?? baseFontFamily,
|
|
125
|
+
// A span overrides the block's face one axis at a time, like a nested `TextFont` component.
|
|
126
|
+
weight: span.weight === undefined ? baseWeight : fontWeightValue(span.weight, "span weight"),
|
|
127
|
+
style: span.style === undefined ? baseStyle : textFontStyleValue(span.style, "span style"),
|
|
128
|
+
width: span.width === undefined ? baseWidth : textFontWidthValue(span.width, "span width"),
|
|
105
129
|
};
|
|
106
130
|
});
|
|
107
131
|
}
|
|
108
132
|
/** Validates and resolves style defaults so invalid values fail at construction time. */
|
|
109
|
-
export function
|
|
133
|
+
export function resolveFrameDrawListDefaults(defaults = {}) {
|
|
110
134
|
const point = defaults.point ?? {};
|
|
111
135
|
const line = defaults.line ?? {};
|
|
112
136
|
const text = defaults.text ?? {};
|
|
@@ -168,6 +192,9 @@ export function resolveFrameRecorderDefaults(defaults = {}) {
|
|
|
168
192
|
fontSize: textSize,
|
|
169
193
|
pixelSize: textPixelSize,
|
|
170
194
|
fontFamily: text.fontFamily,
|
|
195
|
+
weight: text.weight === undefined ? undefined : fontWeightValue(text.weight, "default text weight"),
|
|
196
|
+
style: text.style === undefined ? undefined : textFontStyleValue(text.style, "default text style"),
|
|
197
|
+
width: text.width === undefined ? undefined : textFontWidthValue(text.width, "default text width"),
|
|
171
198
|
lineHeight: textLineHeight,
|
|
172
199
|
lineHeightScale: textLineHeightScale,
|
|
173
200
|
letterSpacing: text.letterSpacing === undefined ? undefined : finite(text.letterSpacing, "default text letterSpacing"),
|
|
@@ -175,13 +202,19 @@ export function resolveFrameRecorderDefaults(defaults = {}) {
|
|
|
175
202
|
lineBreak: text.lineBreak === undefined ? undefined : textLineBreakValue(text.lineBreak, "default text lineBreak"),
|
|
176
203
|
anchor: textAnchor,
|
|
177
204
|
space: text.space === undefined ? undefined : textSpaceValue(text.space, "default text space"),
|
|
205
|
+
rotation: text.rotation === undefined ? undefined : vec3(text.rotation, "default text rotation"),
|
|
178
206
|
source: text.source,
|
|
179
207
|
depthTest: text.depthTest,
|
|
180
208
|
layer: text.layer === undefined ? undefined : layerValue(text.layer, "default text layer"),
|
|
181
209
|
},
|
|
182
210
|
};
|
|
183
211
|
}
|
|
184
|
-
|
|
212
|
+
/**
|
|
213
|
+
* One frame's draw list: records primitives, then hands them to `GpuDebugDraw3D` as
|
|
214
|
+
* `FrameCommand[]`. This is the `ImDrawList` / `GizmoBuffer` layer; GPU command encoding
|
|
215
|
+
* happens later in the renderer.
|
|
216
|
+
*/
|
|
217
|
+
export class FrameDrawList {
|
|
185
218
|
#points = [];
|
|
186
219
|
#lines = [];
|
|
187
220
|
#joints = [];
|
|
@@ -191,10 +224,10 @@ export class FrameRecorder {
|
|
|
191
224
|
#d2;
|
|
192
225
|
#sealed = false;
|
|
193
226
|
constructor(defaults = {}) {
|
|
194
|
-
this.#defaults =
|
|
195
|
-
// The 2D layer shares this
|
|
227
|
+
this.#defaults = resolveFrameDrawListDefaults(defaults);
|
|
228
|
+
// The 2D layer shares this drawList's resolved point/line styles, so a style set on
|
|
196
229
|
// `DebugDraw3D` cannot apply to one layer and not the other.
|
|
197
|
-
this.#d2 = new
|
|
230
|
+
this.#d2 = new FrameDrawList2D({ point: this.#defaults.point, line: this.#defaults.line }, () => this.assertOpen());
|
|
198
231
|
}
|
|
199
232
|
/**
|
|
200
233
|
* The 2D layer: `Vec2` primitives on the reference-style `Camera2d` spaces described in
|
|
@@ -431,10 +464,14 @@ export class FrameRecorder {
|
|
|
431
464
|
throw new RangeError("lineHeight must be greater than zero");
|
|
432
465
|
const letterSpacing = finite(options.letterSpacing ?? defaults.letterSpacing ?? 0, "letterSpacing");
|
|
433
466
|
const fontFamily = options.fontFamily ?? defaults.fontFamily ?? DEFAULT_FONT_FAMILY;
|
|
467
|
+
const weight = fontWeightValue(options.weight ?? defaults.weight ?? DEFAULT_FONT_WEIGHT, "weight");
|
|
468
|
+
const style = textFontStyleValue(options.style ?? defaults.style ?? "normal", "style");
|
|
469
|
+
const width = textFontWidthValue(options.width ?? defaults.width ?? "normal", "width");
|
|
434
470
|
const justify = options.justify ?? defaults.justify ?? "left";
|
|
435
471
|
const lineBreak = options.lineBreak ?? defaults.lineBreak ?? "word";
|
|
436
472
|
const anchor = options.anchor ?? defaults.anchor ?? "center";
|
|
437
473
|
const space = options.space ?? defaults.space ?? "world-plane";
|
|
474
|
+
const rotation = vec3(options.rotation ?? defaults.rotation ?? DEFAULT_TEXT_ROTATION, "rotation");
|
|
438
475
|
const source = options.source ?? defaults.source;
|
|
439
476
|
const bounds = options.bounds;
|
|
440
477
|
if (bounds !== undefined)
|
|
@@ -451,13 +488,16 @@ export class FrameRecorder {
|
|
|
451
488
|
this.#texts.push({
|
|
452
489
|
kind: "text",
|
|
453
490
|
position: vec3(position, "position"),
|
|
454
|
-
spans: resolveTextSpans(text, options, defaults, fontSize, fontFamily),
|
|
491
|
+
spans: resolveTextSpans(text, options, defaults, fontSize, fontFamily, weight, style, width),
|
|
455
492
|
color: color(options.color ?? defaults.color),
|
|
456
493
|
outlineWidth,
|
|
457
494
|
outlineColor,
|
|
458
495
|
fontSize,
|
|
459
496
|
pixelSize,
|
|
460
497
|
fontFamily,
|
|
498
|
+
weight,
|
|
499
|
+
style,
|
|
500
|
+
width,
|
|
461
501
|
lineHeight,
|
|
462
502
|
...(lineHeightScale === undefined ? {} : { lineHeightScale }),
|
|
463
503
|
letterSpacing,
|
|
@@ -465,6 +505,7 @@ export class FrameRecorder {
|
|
|
465
505
|
lineBreak,
|
|
466
506
|
anchor,
|
|
467
507
|
space,
|
|
508
|
+
rotation,
|
|
468
509
|
...(source === undefined ? {} : { source }),
|
|
469
510
|
depthTest: options.depthTest ?? defaults.depthTest ?? true,
|
|
470
511
|
layer: layer(options.layer ?? defaults.layer),
|
|
@@ -510,6 +551,6 @@ export class FrameRecorder {
|
|
|
510
551
|
}
|
|
511
552
|
assertOpen() {
|
|
512
553
|
if (this.#sealed)
|
|
513
|
-
throw new Error("frame
|
|
554
|
+
throw new Error("frame drawList is sealed");
|
|
514
555
|
}
|
|
515
556
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Vec3 } from "./frame-
|
|
2
|
-
import { type CircleBasis } from "./
|
|
1
|
+
import type { Vec3 } from "./frame-draw-list.js";
|
|
2
|
+
import { type CircleBasis } from "./vector-math.js";
|
|
3
3
|
export type Triangle = readonly [Vec3, Vec3, Vec3];
|
|
4
4
|
export type Segment = readonly [Vec3, Vec3];
|
|
5
5
|
export type MeshData = {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { crossVector, normalizeVector, rotateAroundAxis } from "./
|
|
1
|
+
import { crossVector, normalizeVector, rotateAroundAxis } from "./vector-math.js";
|
|
2
2
|
const add = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
|
|
3
3
|
const scale = (a, n) => [a[0] * n, a[1] * n, a[2] * n];
|
|
4
4
|
const rotate = (p, rotation) => {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { type FrameComputePass, type FramePass, type Gpu } from "vgpu";
|
|
16
16
|
import { type Camera2D } from "./camera-2d.js";
|
|
17
|
-
import type { FrameCommand2D } from "./frame-
|
|
17
|
+
import type { FrameCommand2D } from "./frame-draw-list-2d.js";
|
|
18
18
|
export type GpuDebugDraw2DOptions = {
|
|
19
19
|
/**
|
|
20
20
|
* WGSL for the shared primitives, generated by `GpuDebugDraw3D` so the two layers cannot drift
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Frame, type FramePass, type Gpu } from "vgpu";
|
|
2
2
|
import type { Camera2D } from "./camera-2d.js";
|
|
3
|
-
import type { FrameCommand } from "./frame-
|
|
3
|
+
import type { FrameCommand } from "./frame-draw-list.js";
|
|
4
4
|
import { type GpuText3DOptions } from "./gpu-text-3d.js";
|
|
5
5
|
/**
|
|
6
6
|
* Named glyph sources. A text command picks one by name through `TextOptions.source`; commands that
|
|
@@ -710,8 +710,10 @@ export class GpuDebugDraw3D {
|
|
|
710
710
|
this.#viewProjection = camera.viewProjection;
|
|
711
711
|
this.#view = camera.view;
|
|
712
712
|
// Every renderer that exists is prepared, even with an empty list: that is what drops the
|
|
713
|
-
// previous frame's glyphs instead of leaving them on screen.
|
|
714
|
-
|
|
713
|
+
// previous frame's glyphs instead of leaving them on screen. A caller that never configured a
|
|
714
|
+
// text source asked for geometry only, so the example scenes' own labels are drawn by nobody
|
|
715
|
+
// rather than failing the frame — a named source that is missing still throws.
|
|
716
|
+
if (this.#textOptions !== undefined && (textCommands.length > 0 || this.#texts.size > 0)) {
|
|
715
717
|
this.#prepareText(textCommands, camera);
|
|
716
718
|
}
|
|
717
719
|
// Always prepared, even with an empty list: that is what drops the previous frame's 2D
|
package/dist/gpu-text-3d.d.ts
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
* glyph's translucent ink box from clipping the glyphs drawn after it.
|
|
33
33
|
*/
|
|
34
34
|
import { type FrameComputePass, type FramePass, type Gpu } from "vgpu";
|
|
35
|
-
import type { TextCommand } from "./frame-
|
|
35
|
+
import type { TextCommand } from "./frame-draw-list.js";
|
|
36
36
|
import { type TextMeasurer } from "./text-layout.js";
|
|
37
37
|
import type { GlyphRasterizer } from "./text-font.js";
|
|
38
38
|
/** Both halves of a font: metrics for layout and pixels for the atlas. */
|
package/dist/gpu-text-3d.js
CHANGED
|
@@ -205,7 +205,12 @@ struct Out { @builtin(position) clip: vec4f, @location(0) uv: vec2f, @location(1
|
|
|
205
205
|
let up = vec3f(camera.view[0][1], camera.view[1][1], camera.view[2][1]);
|
|
206
206
|
worldPosition = input.origin + right * local.x - up * local.y;
|
|
207
207
|
} else {
|
|
208
|
-
|
|
208
|
+
// The instance world matrix carries the block's own plane basis: column 0 is the reading
|
|
209
|
+
// direction, column 1 the layout's down. Label3D.rotation lands on that basis, so the default
|
|
210
|
+
// identity keeps the plane facing +Z.
|
|
211
|
+
let axisX = input.world0.xyz;
|
|
212
|
+
let axisY = input.world1.xyz;
|
|
213
|
+
worldPosition = input.origin + axisX * local.x - axisY * local.y;
|
|
209
214
|
}
|
|
210
215
|
clip = camera.viewProjection * vec4f(worldPosition, 1.0);
|
|
211
216
|
}
|
|
@@ -233,6 +238,41 @@ fn atlasTexelsPerPixel(uv: vec2f) -> f32 {
|
|
|
233
238
|
|
|
234
239
|
${fragment}`;
|
|
235
240
|
}
|
|
241
|
+
/** `world-plane` blocks keep facing +Z until their own rotation lays them down. */
|
|
242
|
+
const IDENTITY_WORLD = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
|
|
243
|
+
/**
|
|
244
|
+
* The instance world matrix of a `world-plane` block, from the reference `Label3D.rotation` Euler
|
|
245
|
+
* order (Y, then X, then Z). Column 0 is the reading direction and column 1 the layout's down, so a
|
|
246
|
+
* `[-PI/2, 0, 0]` block lies flat on the XZ plane and reads from above.
|
|
247
|
+
*
|
|
248
|
+
* The basis travels in the instance world matrix rather than in two extra instance attributes: the
|
|
249
|
+
* text pipeline already uses the device's full vertex attribute budget.
|
|
250
|
+
*/
|
|
251
|
+
function textPlaneWorld(rotation) {
|
|
252
|
+
const [pitch, yaw, roll] = rotation;
|
|
253
|
+
if (pitch === 0 && yaw === 0 && roll === 0)
|
|
254
|
+
return IDENTITY_WORLD;
|
|
255
|
+
const cosRoll = Math.cos(roll);
|
|
256
|
+
const sinRoll = Math.sin(roll);
|
|
257
|
+
const cosPitch = Math.cos(pitch);
|
|
258
|
+
const sinPitch = Math.sin(pitch);
|
|
259
|
+
const cosYaw = Math.cos(yaw);
|
|
260
|
+
const sinYaw = Math.sin(yaw);
|
|
261
|
+
// Rz, then Rx, then Ry: the vector order that matches reference YXZ Euler convention.
|
|
262
|
+
let x = [cosRoll, sinRoll, 0];
|
|
263
|
+
let y = [-sinRoll, cosRoll, 0];
|
|
264
|
+
x = [x[0], x[1] * cosPitch - x[2] * sinPitch, x[1] * sinPitch + x[2] * cosPitch];
|
|
265
|
+
y = [y[0], y[1] * cosPitch - y[2] * sinPitch, y[1] * sinPitch + y[2] * cosPitch];
|
|
266
|
+
x = [x[0] * cosYaw + x[2] * sinYaw, x[1], -x[0] * sinYaw + x[2] * cosYaw];
|
|
267
|
+
y = [y[0] * cosYaw + y[2] * sinYaw, y[1], -y[0] * sinYaw + y[2] * cosYaw];
|
|
268
|
+
const z = [x[1] * y[2] - x[2] * y[1], x[2] * y[0] - x[0] * y[2], x[0] * y[1] - x[1] * y[0]];
|
|
269
|
+
return new Float32Array([
|
|
270
|
+
x[0], x[1], x[2], 0,
|
|
271
|
+
y[0], y[1], y[2], 0,
|
|
272
|
+
z[0], z[1], z[2], 0,
|
|
273
|
+
0, 0, 0, 1,
|
|
274
|
+
]);
|
|
275
|
+
}
|
|
236
276
|
export class GpuText3D {
|
|
237
277
|
#gpu;
|
|
238
278
|
#font;
|
|
@@ -349,6 +389,9 @@ export class GpuText3D {
|
|
|
349
389
|
color: command.color,
|
|
350
390
|
fontSize: command.fontSize,
|
|
351
391
|
fontFamily: command.fontFamily,
|
|
392
|
+
weight: command.weight,
|
|
393
|
+
style: command.style,
|
|
394
|
+
width: command.width,
|
|
352
395
|
// A relative line height stays relative all the way to layout, where each line resolves it
|
|
353
396
|
// against that line's own largest span; only an explicit one is absolute.
|
|
354
397
|
...(command.lineHeightScale === undefined
|
|
@@ -361,6 +404,7 @@ export class GpuText3D {
|
|
|
361
404
|
anchor: resolveTextAnchor(command.anchor),
|
|
362
405
|
}, this.#font);
|
|
363
406
|
const space = command.space === "world-billboard" ? SPACE_WORLD_BILLBOARD : command.space === "screen" ? SPACE_SCREEN : SPACE_WORLD_PLANE;
|
|
407
|
+
const planeWorld = space === SPACE_WORLD_PLANE ? textPlaneWorld(command.rotation) : IDENTITY_WORLD;
|
|
364
408
|
// Screen text never takes part in depth testing, whatever the command asked for.
|
|
365
409
|
const flags = space | (command.depthTest && space !== SPACE_SCREEN ? 0 : FLAG_NO_DEPTH);
|
|
366
410
|
// The quad's size comes from the layout, not from the atlas. A distance field can be drawn at
|
|
@@ -382,7 +426,7 @@ export class GpuText3D {
|
|
|
382
426
|
const rasterScale = dynamicRaster
|
|
383
427
|
? rasterScaleFor(quantizeRasterPx(Math.max(glyph.fontSize * screenPxPerFontPx, glyph.fontSize * rasterFloor)), glyph.fontSize)
|
|
384
428
|
: undefined;
|
|
385
|
-
const entry = this.#atlas.glyph(glyph.character, glyph.
|
|
429
|
+
const entry = this.#atlas.glyph(glyph.character, glyph.face, glyph.fontSize, rasterScale);
|
|
386
430
|
if (entry === undefined)
|
|
387
431
|
continue;
|
|
388
432
|
const id = this.#ids[index] ?? this.#addInstance(index);
|
|
@@ -398,6 +442,9 @@ export class GpuText3D {
|
|
|
398
442
|
outlineWidth: command.outlineWidth,
|
|
399
443
|
outlineColor: command.outlineColor,
|
|
400
444
|
});
|
|
445
|
+
// The matrix always reflects this block's own plane, because instances are reused across
|
|
446
|
+
// blocks and a stale basis would tilt a block that asked for none.
|
|
447
|
+
this.#glyphs.setWorld(id, planeWorld);
|
|
401
448
|
index++;
|
|
402
449
|
}
|
|
403
450
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export * from "./debug-draw-3d.js";
|
|
2
2
|
export * from "./camera-2d.js";
|
|
3
|
-
export * from "./frame-
|
|
4
|
-
export * from "./frame-
|
|
3
|
+
export * from "./frame-draw-list.js";
|
|
4
|
+
export * from "./frame-draw-list-2d.js";
|
|
5
5
|
export * from "./color-space.js";
|
|
6
6
|
export * from "./curve.js";
|
|
7
7
|
export * from "./geometry-generators.js";
|
|
@@ -11,7 +11,6 @@ export * from "./gpu-text-3d.js";
|
|
|
11
11
|
export * from "./infinite-grid-3d.js";
|
|
12
12
|
export * from "./infinite-grid-2d.js";
|
|
13
13
|
export * from "./mesh-batch-3d.js";
|
|
14
|
-
export * from "./scenes-3d.js";
|
|
15
14
|
export * from "./text-atlas.js";
|
|
16
15
|
export * from "./text-font.js";
|
|
17
16
|
export * from "./text-layout.js";
|
|
@@ -19,3 +18,4 @@ export * from "./text-sdf.js";
|
|
|
19
18
|
export * from "./text-msdf.js";
|
|
20
19
|
export * from "./text-msdf-font.js";
|
|
21
20
|
export * from "./text-msdf-baked.js";
|
|
21
|
+
export type { CircleBasis } from "./vector-math.js";
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export * from "./debug-draw-3d.js";
|
|
2
2
|
export * from "./camera-2d.js";
|
|
3
|
-
export * from "./frame-
|
|
4
|
-
export * from "./frame-
|
|
3
|
+
export * from "./frame-draw-list.js";
|
|
4
|
+
export * from "./frame-draw-list-2d.js";
|
|
5
5
|
export * from "./color-space.js";
|
|
6
6
|
export * from "./curve.js";
|
|
7
7
|
export * from "./geometry-generators.js";
|
|
@@ -11,7 +11,6 @@ export * from "./gpu-text-3d.js";
|
|
|
11
11
|
export * from "./infinite-grid-3d.js";
|
|
12
12
|
export * from "./infinite-grid-2d.js";
|
|
13
13
|
export * from "./mesh-batch-3d.js";
|
|
14
|
-
export * from "./scenes-3d.js";
|
|
15
14
|
export * from "./text-atlas.js";
|
|
16
15
|
export * from "./text-font.js";
|
|
17
16
|
export * from "./text-layout.js";
|
package/dist/mesh-batch-3d.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type FramePass, type Gpu } from "vgpu";
|
|
2
2
|
import type { MeshData } from "./geometry-generators.js";
|
|
3
|
-
import type { Color } from "./frame-
|
|
3
|
+
import type { Color } from "./frame-draw-list.js";
|
|
4
4
|
export declare class MeshBatch3D {
|
|
5
5
|
#private;
|
|
6
6
|
constructor(gpu: Gpu, mesh: MeshData, color: Color, solid: boolean);
|
package/dist/shape2d-shader.js
CHANGED
|
@@ -213,8 +213,9 @@ fn shape_distance(shape: u32, p: vec2f, size: vec2f, vertexB: vec2f, vertexC: ve
|
|
|
213
213
|
case SHAPE_ROUNDED_RECT: { return sd_rounded_box(p, size, cornerRadius); }
|
|
214
214
|
case SHAPE_CIRCLE: { return sd_ellipse(p, size); }
|
|
215
215
|
case SHAPE_ELLIPSE: { return sd_ellipse(p, size); }
|
|
216
|
-
// An arc has no interior:
|
|
217
|
-
|
|
216
|
+
// An arc has no interior: the ring alone ignores the angles, so it is clipped to the swept
|
|
217
|
+
// wedge. Filling one still yields a hairline, because the distance stays unsigned.
|
|
218
|
+
case SHAPE_ARC: { return max(abs(length(p) - size.x), sd_sector(p, size.x, startAngle, sweep)); }
|
|
218
219
|
case SHAPE_ANNULUS: { return sd_annulus(p, size.y, size.x); }
|
|
219
220
|
case SHAPE_CAPSULE: { return sd_capsule(p, size.y, size.x); }
|
|
220
221
|
case SHAPE_POLYGON: { return sd_regular_polygon(p, size.x, sides); }
|
package/dist/text-atlas.d.ts
CHANGED
|
@@ -9,14 +9,16 @@
|
|
|
9
9
|
* 2 px of padding; both choices are kept here, since the padding is what keeps linear
|
|
10
10
|
* sampling from bleeding between neighbours.
|
|
11
11
|
*
|
|
12
|
-
* Unlike reference the atlas is not keyed per font size: the cache key carries the
|
|
13
|
-
* instead, so one page set serves every size a frame happens to use.
|
|
14
|
-
* `
|
|
12
|
+
* Unlike reference the atlas is not keyed per font size: the cache key carries the face and size
|
|
13
|
+
* instead, so one page set serves every size a frame happens to use. The face part comes from
|
|
14
|
+
* `GlyphRasterizer.faceKey`, which is how a source states which of the axes it can actually vary.
|
|
15
|
+
* Glyphs are rasterised at `rasterScale` times their font size, which decouples the atlas' detail from the logical layout
|
|
15
16
|
* size — the same separation as resampling a font at a higher point size before scaling it down.
|
|
16
17
|
* Pages grow on demand until `maxPages` is reached — the GPU renderer pins that at one page so a
|
|
17
18
|
* single draw call covers the whole atlas — and a `revision` counter tells the renderer which of
|
|
18
19
|
* them changed since the last upload.
|
|
19
20
|
*/
|
|
21
|
+
import { type FontFace } from "./text-layout.js";
|
|
20
22
|
import type { GlyphEncoding, GlyphRasterizer } from "./text-font.js";
|
|
21
23
|
/** Page size and inter-glyph padding, matching reference minimum atlas and `GLYPH_ATLAS_PADDING`. */
|
|
22
24
|
export declare const DEFAULT_ATLAS_SIZE = 512;
|
|
@@ -111,7 +113,7 @@ export declare class GlyphAtlas {
|
|
|
111
113
|
* exactly as it did. The renderer passes the size the block covers on screen instead, so the entry
|
|
112
114
|
* that comes back is rasterised for the rung that face asked for.
|
|
113
115
|
*/
|
|
114
|
-
glyph(character: string,
|
|
116
|
+
glyph(character: string, face: FontFace, fontSize: number, rasterScale?: number): AtlasGlyph | undefined;
|
|
115
117
|
/** Drops every cached glyph. The next frame re-rasterises whatever it still draws. */
|
|
116
118
|
clear(): void;
|
|
117
119
|
}
|
package/dist/text-atlas.js
CHANGED
|
@@ -9,14 +9,16 @@
|
|
|
9
9
|
* 2 px of padding; both choices are kept here, since the padding is what keeps linear
|
|
10
10
|
* sampling from bleeding between neighbours.
|
|
11
11
|
*
|
|
12
|
-
* Unlike reference the atlas is not keyed per font size: the cache key carries the
|
|
13
|
-
* instead, so one page set serves every size a frame happens to use.
|
|
14
|
-
* `
|
|
12
|
+
* Unlike reference the atlas is not keyed per font size: the cache key carries the face and size
|
|
13
|
+
* instead, so one page set serves every size a frame happens to use. The face part comes from
|
|
14
|
+
* `GlyphRasterizer.faceKey`, which is how a source states which of the axes it can actually vary.
|
|
15
|
+
* Glyphs are rasterised at `rasterScale` times their font size, which decouples the atlas' detail from the logical layout
|
|
15
16
|
* size — the same separation as resampling a font at a higher point size before scaling it down.
|
|
16
17
|
* Pages grow on demand until `maxPages` is reached — the GPU renderer pins that at one page so a
|
|
17
18
|
* single draw call covers the whole atlas — and a `revision` counter tells the renderer which of
|
|
18
19
|
* them changed since the last upload.
|
|
19
20
|
*/
|
|
21
|
+
import { fontFaceKey } from "./text-layout.js";
|
|
20
22
|
/** Page size and inter-glyph padding, matching reference minimum atlas and `GLYPH_ATLAS_PADDING`. */
|
|
21
23
|
export const DEFAULT_ATLAS_SIZE = 512;
|
|
22
24
|
export const DEFAULT_ATLAS_PADDING = 2;
|
|
@@ -100,13 +102,14 @@ export class GlyphAtlas {
|
|
|
100
102
|
* exactly as it did. The renderer passes the size the block covers on screen instead, so the entry
|
|
101
103
|
* that comes back is rasterised for the rung that face asked for.
|
|
102
104
|
*/
|
|
103
|
-
glyph(character,
|
|
105
|
+
glyph(character, face, fontSize, rasterScale) {
|
|
104
106
|
const scale = rasterScale !== undefined && rasterScale > 0 ? rasterScale : this.rasterScale;
|
|
105
107
|
// The key carries the size the glyph was *rasterised* at rather than the layout font size: two
|
|
106
108
|
// font sizes that land on the same rung share one rasterisation, which is what keeps a block
|
|
107
109
|
// whose size changes every frame from rasterising the same character over and over.
|
|
108
110
|
const rasterPx = Math.max(1, Math.round(fontSize * scale));
|
|
109
|
-
const
|
|
111
|
+
const faceKey = this.#rasterizer.faceKey?.(face) ?? fontFaceKey(face);
|
|
112
|
+
const key = `${faceKey}|${rasterPx}|${character}`;
|
|
110
113
|
const cached = this.#glyphs.get(key);
|
|
111
114
|
if (cached !== undefined) {
|
|
112
115
|
cached.lastFrame = this.#frame;
|
|
@@ -114,7 +117,7 @@ export class GlyphAtlas {
|
|
|
114
117
|
}
|
|
115
118
|
if (this.#missing.has(key))
|
|
116
119
|
return undefined;
|
|
117
|
-
const rasterized = this.#rasterizer.rasterize(character,
|
|
120
|
+
const rasterized = this.#rasterizer.rasterize(character, face, rasterPx);
|
|
118
121
|
if (rasterized === undefined) {
|
|
119
122
|
this.#missing.add(key);
|
|
120
123
|
return undefined;
|
package/dist/text-font.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Everything here is browser-only. Layout depends on the `TextMeasurer` interface alone, so
|
|
14
14
|
* unit tests keep running in plain Node, and the GPU tests inject their own rasteriser.
|
|
15
15
|
*/
|
|
16
|
-
import type
|
|
16
|
+
import { type FontFace, type GlyphMetrics, type TextMeasurer, type VerticalMetrics } from "./text-layout.js";
|
|
17
17
|
/**
|
|
18
18
|
* How a glyph is stored in the atlas, and therefore how the shader has to decode it.
|
|
19
19
|
*
|
|
@@ -36,6 +36,23 @@ export type GlyphEncoding = "bitmap" | "sdf" | "msdf";
|
|
|
36
36
|
export declare const DEFAULT_FONT_FAMILY = "vgpu-render-fira-mono";
|
|
37
37
|
/** Decodes a base64 payload into bytes. Shared by the inlined font and the prebaked atlas. */
|
|
38
38
|
export declare function decodeBase64Bytes(base64: string): Uint8Array<ArrayBuffer>;
|
|
39
|
+
/** One inlined face of a multi-weight family: the weight it was built at, plus its WOFF2 bytes. */
|
|
40
|
+
export type EmbeddedFontFace = {
|
|
41
|
+
readonly weight: number;
|
|
42
|
+
/** Base64 WOFF2, so the payload stays plain text and every toolchain can read it. */
|
|
43
|
+
readonly woff2Base64: string;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Registers a family whose weights are separate faces, each carrying its own `weight` descriptor.
|
|
47
|
+
*
|
|
48
|
+
* This is the only way to get more than one drawn weight out of Canvas2D: the font shorthand carries a
|
|
49
|
+
* single synthesised bold step, and `font-variation-settings` does not exist there, but the browser
|
|
50
|
+
* does select a face by the weight it was registered with. Registering the same family once per
|
|
51
|
+
* weight therefore turns `weight: 700` into a real bold face instead of an emboldened regular one.
|
|
52
|
+
*
|
|
53
|
+
* Idempotent per family, like `loadDefaultFont`, and browser-only.
|
|
54
|
+
*/
|
|
55
|
+
export declare function loadFontFaces(family: string, faces: readonly EmbeddedFontFace[]): Promise<void>;
|
|
39
56
|
/** Decodes the inlined WOFF2 bytes of the bundled font, which is what `FontFace` takes. */
|
|
40
57
|
export declare function decodeEmbeddedFont(): Uint8Array<ArrayBuffer>;
|
|
41
58
|
/**
|
|
@@ -102,6 +119,15 @@ export interface GlyphRasterizer {
|
|
|
102
119
|
* were rasterised once at a fixed size, so the renderer leaves it on `rasterScale` instead.
|
|
103
120
|
*/
|
|
104
121
|
readonly dynamicRaster?: boolean;
|
|
122
|
+
/**
|
|
123
|
+
* Cache-key part for one face. The atlas keys glyphs by this plus the raster size, so a source that
|
|
124
|
+
* cannot change a face' axes reports just the parts it honours.
|
|
125
|
+
*
|
|
126
|
+
* The default keys on every axis (`fontFaceKey`). A source holding one fixed face — a prebaked page
|
|
127
|
+
* or a single parsed `opentype.js` buffer — overrides it so a weight the source cannot rasterise
|
|
128
|
+
* does not multiply identical atlas entries.
|
|
129
|
+
*/
|
|
130
|
+
readonly faceKey?: (face: FontFace) => string;
|
|
105
131
|
/**
|
|
106
132
|
* Returns `undefined` for characters with no ink, such as whitespace.
|
|
107
133
|
*
|
|
@@ -109,10 +135,25 @@ export interface GlyphRasterizer {
|
|
|
109
135
|
* size: the atlas asks for the size the glyph will cover on screen, so the same character stays
|
|
110
136
|
* sharp at any font size while still being rasterised once per size rung.
|
|
111
137
|
*/
|
|
112
|
-
rasterize(character: string,
|
|
113
|
-
/** Vertical metrics for a `fontSize`-sized
|
|
114
|
-
verticalMetrics(
|
|
138
|
+
rasterize(character: string, face: FontFace, rasterSize: number): RasterizedGlyph | undefined;
|
|
139
|
+
/** Vertical metrics for a `fontSize`-sized instance of `face`. */
|
|
140
|
+
verticalMetrics(face: FontFace, fontSize: number): VerticalMetrics;
|
|
115
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* The CSS font shorthand that selects `face` at `fontSize`.
|
|
144
|
+
*
|
|
145
|
+
* Order follows the shorthand's grammar — style, weight, stretch, size, family — because Canvas2D
|
|
146
|
+
* parses the value as one. Two of the axes are conditional:
|
|
147
|
+
*
|
|
148
|
+
* - `font-stretch` keywords are only kept by the parser when the face has a `wdth` axis. A static
|
|
149
|
+
* face drops the keyword and draws every stop identically, which is why the bundled subset cannot
|
|
150
|
+
* show a `FontWidth` difference at all.
|
|
151
|
+
* - `italic` is the only slant Canvas2D keeps; `oblique <angle>` reads back as plain `italic`.
|
|
152
|
+
*
|
|
153
|
+
* A weight the loaded face does not carry is synthesised by the browser (one emboldened step, which
|
|
154
|
+
* keeps the advance), so `weight` at least changes what is drawn for the bundled static face.
|
|
155
|
+
*/
|
|
156
|
+
export declare function canvasFontString(face: FontFace, fontSize: number): string;
|
|
116
157
|
/** Encodings Canvas2D coverage can produce directly. `msdf` needs outlines, not a mask. */
|
|
117
158
|
export type CoverageEncoding = Exclude<GlyphEncoding, "msdf">;
|
|
118
159
|
/**
|
|
@@ -141,9 +182,9 @@ export declare class CanvasGlyphRasterizer implements GlyphRasterizer, TextMeasu
|
|
|
141
182
|
rasterScale?: number;
|
|
142
183
|
dynamicRaster?: boolean;
|
|
143
184
|
});
|
|
144
|
-
verticalMetrics(
|
|
145
|
-
glyphMetrics(character: string,
|
|
146
|
-
rasterize(character: string,
|
|
185
|
+
verticalMetrics(face: FontFace, fontSize: number): VerticalMetrics;
|
|
186
|
+
glyphMetrics(character: string, face: FontFace, fontSize: number): GlyphMetrics;
|
|
187
|
+
rasterize(character: string, face: FontFace, rasterSize: number): RasterizedGlyph | undefined;
|
|
147
188
|
}
|
|
148
189
|
/** Packs Canvas2D coverage into the RGBA layout the chosen encoding expects. */
|
|
149
190
|
export declare function encodeCoverage(coverage: Uint8Array, width: number, height: number, encoding: CoverageEncoding, range: number): Uint8Array;
|