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.
- package/README.md +101 -34
- package/dist/curve.d.ts +26 -1
- package/dist/curve.js +53 -0
- package/dist/debug-draw-3d.d.ts +3 -3
- package/dist/debug-draw-3d.js +10 -10
- package/dist/frame-draw-list-2d.d.ts +286 -0
- package/dist/frame-draw-list-2d.js +668 -0
- 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 +11 -2
- package/dist/geometry-generators.js +37 -1
- package/dist/gpu-debug-draw-2d.d.ts +1 -1
- package/dist/gpu-debug-draw-2d.js +10 -2
- 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 +295 -7
- 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/frame-recorder-2d.d.ts +0 -177
- package/dist/frame-recorder-2d.js +0 -378
- 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,6 +1,6 @@
|
|
|
1
1
|
import { type Curve3D, type CurveTransform } from "./curve.js";
|
|
2
|
-
import {
|
|
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
|
|
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
|
|
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
|
|
322
|
-
|
|
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?:
|
|
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():
|
|
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;
|
|
@@ -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,7 +1,9 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import {
|
|
1
|
+
import type { Vec2 } from "./camera-2d.js";
|
|
2
|
+
import type { Vec3 } from "./frame-draw-list.js";
|
|
3
|
+
import { type CircleBasis } from "./vector-math.js";
|
|
3
4
|
export type Triangle = readonly [Vec3, Vec3, Vec3];
|
|
4
5
|
export type Segment = readonly [Vec3, Vec3];
|
|
6
|
+
export type Segment2D = readonly [Vec2, Vec2];
|
|
5
7
|
export type MeshData = {
|
|
6
8
|
readonly triangles: readonly Triangle[];
|
|
7
9
|
readonly segments: readonly Segment[];
|
|
@@ -99,3 +101,10 @@ export declare function coneWire(radius: number, height: number, segments: numbe
|
|
|
99
101
|
*/
|
|
100
102
|
export declare function cylinderWire(radius: number, halfHeight: number, segments: number, sideSegmentCount: number, center: Vec3, basis: CircleBasis): ShapeWire;
|
|
101
103
|
export declare function gridLines(cells: readonly [number, number], spacing: readonly [number, number], center?: Vec3, rotation?: readonly [number, number, number], skew?: readonly [number, number], outer?: "none" | "x" | "z" | "xz"): Segment[];
|
|
104
|
+
/**
|
|
105
|
+
* 2D counterpart of `gridLines`: the same cell/skew/outer contract in the XY plane.
|
|
106
|
+
*
|
|
107
|
+
* `rotation` is one anticlockwise angle around the grid centre. `outer` keeps the x-axis
|
|
108
|
+
* boundary lines (`"x"`), the y-axis ones (`"y"`), or both (`"xy"`).
|
|
109
|
+
*/
|
|
110
|
+
export declare function gridLines2D(cells: readonly [number, number], spacing: readonly [number, number], center?: Vec2, rotation?: number, skew?: readonly [number, number], outer?: "none" | "x" | "y" | "xy"): Segment2D[];
|
|
@@ -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) => {
|
|
@@ -479,3 +479,39 @@ export function gridLines(cells, spacing, center = [0, 0, 0], rotation = [0, 0,
|
|
|
479
479
|
}
|
|
480
480
|
return segments;
|
|
481
481
|
}
|
|
482
|
+
/**
|
|
483
|
+
* 2D counterpart of `gridLines`: the same cell/skew/outer contract in the XY plane.
|
|
484
|
+
*
|
|
485
|
+
* `rotation` is one anticlockwise angle around the grid centre. `outer` keeps the x-axis
|
|
486
|
+
* boundary lines (`"x"`), the y-axis ones (`"y"`), or both (`"xy"`).
|
|
487
|
+
*/
|
|
488
|
+
export function gridLines2D(cells, spacing, center = [0, 0], rotation = 0, skew = [0, 0], outer = "none") {
|
|
489
|
+
const [cellX, cellY] = cells;
|
|
490
|
+
const [spacingX, spacingY] = spacing;
|
|
491
|
+
if (cellX <= 0 || cellY <= 0 || spacingX <= 0 || spacingY <= 0)
|
|
492
|
+
return [];
|
|
493
|
+
const skewTan = [Math.tan(skew[0]), Math.tan(skew[1])];
|
|
494
|
+
const dx = [spacingX, spacingX * skewTan[1]];
|
|
495
|
+
const dy = [spacingY * skewTan[0], spacingY];
|
|
496
|
+
const start = [-dx[0] * cellX * 0.5 - dy[0] * cellY * 0.5, -dx[1] * cellX * 0.5 - dy[1] * cellY * 0.5];
|
|
497
|
+
const cos = Math.cos(rotation);
|
|
498
|
+
const sin = Math.sin(rotation);
|
|
499
|
+
const place = (p) => [
|
|
500
|
+
center[0] + p[0] * cos - p[1] * sin,
|
|
501
|
+
center[1] + p[0] * sin + p[1] * cos,
|
|
502
|
+
];
|
|
503
|
+
const plus = (a, b) => [a[0] + b[0], a[1] + b[1]];
|
|
504
|
+
const scale = (v, n) => [v[0] * n, v[1] * n];
|
|
505
|
+
const keepX = outer === "x" || outer === "xy";
|
|
506
|
+
const keepY = outer === "y" || outer === "xy";
|
|
507
|
+
const segments = [];
|
|
508
|
+
for (let yIndex = keepX ? 0 : 1; yIndex < (keepX ? cellY + 1 : cellY); yIndex++) {
|
|
509
|
+
const from = plus(start, scale(dy, yIndex));
|
|
510
|
+
segments.push([place(from), place(plus(from, scale(dx, cellX)))]);
|
|
511
|
+
}
|
|
512
|
+
for (let xIndex = keepY ? 0 : 1; xIndex < (keepY ? cellX + 1 : cellX); xIndex++) {
|
|
513
|
+
const from = plus(start, scale(dx, xIndex));
|
|
514
|
+
segments.push([place(from), place(plus(from, scale(dy, cellY)))]);
|
|
515
|
+
}
|
|
516
|
+
return segments;
|
|
517
|
+
}
|
|
@@ -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
|
|
@@ -43,6 +43,12 @@ function sameVec2(a, b) {
|
|
|
43
43
|
function sameColor(a, b) {
|
|
44
44
|
return a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3];
|
|
45
45
|
}
|
|
46
|
+
/** Dash patterns are optional, so both sides being absent is also a match. */
|
|
47
|
+
function sameDash(a, b) {
|
|
48
|
+
if (a === undefined || b === undefined)
|
|
49
|
+
return a === b;
|
|
50
|
+
return a[0] === b[0] && a[1] === b[1] && a[2] === b[2];
|
|
51
|
+
}
|
|
46
52
|
function samePoint2D(a, b) {
|
|
47
53
|
return sameVec2(a.position, b.position) && sameColor(a.color, b.color) && a.size === b.size && a.sizeMode === b.sizeMode && a.round === b.round;
|
|
48
54
|
}
|
|
@@ -83,6 +89,7 @@ function sameShape2D(a, b) {
|
|
|
83
89
|
a.startAngle === b.startAngle &&
|
|
84
90
|
a.sweep === b.sweep &&
|
|
85
91
|
a.sides === b.sides &&
|
|
92
|
+
sameDash(a.dash, b.dash) &&
|
|
86
93
|
sameColor(a.color, b.color));
|
|
87
94
|
}
|
|
88
95
|
/** Two-dimensional point lifted onto the near plane the projection matrix maps to. */
|
|
@@ -163,7 +170,7 @@ class SpaceBucket2D {
|
|
|
163
170
|
this.#points = instances({ capacity: capacities.points, attributes: { point: "float32x3", color: "float32x4", size: "float32", flags: "uint32" } });
|
|
164
171
|
this.#lines = instances({ capacity: capacities.lines, attributes: { start: "float32x3", end: "float32x3", color: "float32x4", colorEnd: "float32x4", width: "float32", flags: "uint32", dash: "float32x3", normal: "float32x3", joint: "uint32" } });
|
|
165
172
|
this.#joints = instances({ capacity: capacities.joints, attributes: { previous: "float32x3", center: "float32x3", next: "float32x3", color: "float32x4", width: "float32", flags: "uint32", dash: "float32x3", joint: "uint32", previousNormal: "float32x3", nextNormal: "float32x3" } });
|
|
166
|
-
this.#shapes = instances({ capacity: capacities.shapes, attributes: { shape: "uint32", center: "float32x3", size: "float32x2", vertexB: "float32x2", vertexC: "float32x2", params: "float32x4", rotationRadius: "float32x2", color: "float32x4", flags: "uint32" } });
|
|
173
|
+
this.#shapes = instances({ capacity: capacities.shapes, attributes: { shape: "uint32", center: "float32x3", size: "float32x2", vertexB: "float32x2", vertexC: "float32x2", params: "float32x4", rotationRadius: "float32x2", color: "float32x4", flags: "uint32", dash: "float32x3" } });
|
|
167
174
|
this.#pointBridge = instanceGeometry(gpu, this.#points, { mesh: zeroGeometry(gpu) });
|
|
168
175
|
this.#lineBridge = instanceGeometry(gpu, this.#lines, { mesh: zeroGeometry(gpu) });
|
|
169
176
|
this.#jointBridge = instanceGeometry(gpu, this.#joints, { mesh: jointGeometry(gpu, options.jointVertexCount) });
|
|
@@ -380,10 +387,11 @@ class SpaceBucket2D {
|
|
|
380
387
|
rotationRadius: [command.rotation, boundingRadius(command)],
|
|
381
388
|
color: command.color,
|
|
382
389
|
flags: (command.filled ? 1 : 0) | (command.sides << 8),
|
|
390
|
+
dash: command.dash ?? [0, 0, 0],
|
|
383
391
|
});
|
|
384
392
|
}
|
|
385
393
|
#addShapeId(index) {
|
|
386
|
-
const id = this.#shapes.add({ shape: 0, center: [0, 0, 0], size: [0, 0], vertexB: [0, 0], vertexC: [0, 0], params: [0, 0, 0, 0], rotationRadius: [0, 0], color: [0, 0, 0, 0], flags: 0 });
|
|
394
|
+
const id = this.#shapes.add({ shape: 0, center: [0, 0, 0], size: [0, 0], vertexB: [0, 0], vertexC: [0, 0], params: [0, 0, 0, 0], rotationRadius: [0, 0], color: [0, 0, 0, 0], flags: 0, dash: [0, 0, 0] });
|
|
387
395
|
world(this.#shapes, id);
|
|
388
396
|
this.#shapeIds[index] = id;
|
|
389
397
|
return id;
|
|
@@ -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);
|