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
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
|
|
33
|
+
The public API uses an explicit frame draw list:
|
|
34
34
|
|
|
35
35
|
```ts
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const commands = debug.finishFrame(
|
|
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
|
-
|
|
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
|
|
@@ -129,10 +129,12 @@ const frame = debug.beginFrame();
|
|
|
129
129
|
frame.d2.line([0, 0], [120, 40], { width: 2, color: [1, 0, 0, 1] });
|
|
130
130
|
frame.d2.lineStrip([[0, 0], [40, 80], [90, 10]], { width: 4, joint: "round" });
|
|
131
131
|
frame.d2.grid([0, 0], [8, 5], [40, 40], { color: [0.4, 0.4, 0.5, 1] });
|
|
132
|
-
frame.d2.rect([0, 0], [80, 40], {
|
|
133
|
-
frame.d2.
|
|
134
|
-
frame.d2.
|
|
135
|
-
frame.d2.
|
|
132
|
+
frame.d2.rect([0, 0], [80, 40], { strokeWidth: 2 });
|
|
133
|
+
frame.d2.rect_s([160, 0], [80, 40], { color: [0.9, 0.9, 1, 1] });
|
|
134
|
+
frame.d2.rect_d([320, 0], [80, 40], [12, 6, 0], { strokeWidth: 2 });
|
|
135
|
+
frame.d2.rect_r_s([0, -60], [120, 40], 12, { color: [1, 0.7, 0.3, 1] });
|
|
136
|
+
frame.d2.circle_s([0, 120], 60, { color: [0, 0.5, 1, 1] });
|
|
137
|
+
frame.d2.arc([0, 120], 80, { strokeWidth: 3, startAngle: 0, sweep: Math.PI / 2 });
|
|
136
138
|
const commands = debug.finishFrame(frame);
|
|
137
139
|
// The third argument is the 2D camera; omit it and the last one stays in effect.
|
|
138
140
|
renderer.prepare(commands, { viewProjection, view }, { pan: [0, 0], zoom: 1 });
|
|
@@ -146,17 +148,32 @@ Two coordinate spaces share one pixel-based pipeline:
|
|
|
146
148
|
| `screen` | top-left corner | down | one canvas pixel | no |
|
|
147
149
|
|
|
148
150
|
`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.
|
|
151
|
+
`zoom` is canvas pixels per world unit. Both spaces are in **target pixels**, like the text `screen`
|
|
152
|
+
space, so a HiDPI caller scales `screen` content itself (`gallery/screen-scale.ts` does that for the
|
|
153
|
+
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
154
|
coordinate in that sense, not a top-left one — the top-left, +y-down space is the reference UI space,
|
|
151
155
|
and here it is `screen`, the same one `TextSpace "screen"` already uses.
|
|
152
156
|
|
|
153
|
-
- Strokes: `line`, `ray`, `lineStrip`/`polyline`, `grid`, `point`;
|
|
154
|
-
|
|
155
|
-
|
|
157
|
+
- Strokes: `line`, `ray`, `lineStrip`/`polyline`, `lineLoop`, `grid`, `point`; every stroke has a
|
|
158
|
+
`_d` twin (`line_d`, `ray_d`, `lineStrip_d`, `polyline_d`, `lineLoop_d`, `grid_d`) that takes the
|
|
159
|
+
same `[visible, gap, phase]` triple and phase accumulation as the 3D dashed paths. Screen-pixel or
|
|
160
|
+
world widths, and round/bevel/miter joints.
|
|
161
|
+
- Arrows: `arrow` / `arrow_s` / `arrow_d` / `arrow_s_d` — the open V head by default, a filled head
|
|
162
|
+
for `_s`, and the same two forms dashed for `_d`, with `headLength` / `headAngle` options.
|
|
163
|
+
- Curves: `curve(curve, options)` / `curve_d(curve, dash, options)` sample a cubic control polygon
|
|
164
|
+
(`Curve2D`, the 2D twin of `Curve3D`) into a strip, with `tolerance`, `transform.center/rotation/scale`
|
|
165
|
+
and `closed` the same way the 3D page uses them. The 2D Curve page mirrors the 3D one: a
|
|
166
|
+
construction band below the matrix draws every control point (pink anchors, amber handles) for the
|
|
167
|
+
seven reference curve shapes.
|
|
168
|
+
- Shapes: `rect`, `rect_r`, `circle`, `ellipse`, `arc`, `annulus`, `capsule`, `polygon`,
|
|
156
169
|
`triangle`, `rhombus`, `sector`, `segment`. Each is one instanced quad evaluated as a signed
|
|
157
170
|
distance field in the fragment stage, so nothing is triangulated on the CPU and an outline
|
|
158
|
-
width stays a pixel count at any zoom.
|
|
159
|
-
|
|
171
|
+
width stays a pixel count at any zoom. Every primitive follows the 3D naming: `<shape>` strokes
|
|
172
|
+
the outline, `<shape>_s` fills it and `<shape>_d` dashes the outline along its perimeter,
|
|
173
|
+
taking the same `[visible, gap, phase]` triple as the line layer. `arc` has no interior, so it
|
|
174
|
+
has no `_s` variant, and `Shape2DOptions` no longer carries `fill` — the method name decides.
|
|
175
|
+
Dashes are cut from the shape's perimeter parameter, a closed form for every primitive except
|
|
176
|
+
the ellipse (Simpson-integrated arc length), so they stay continuous across corners.
|
|
160
177
|
- The layer is an overlay: it draws after every 3D primitive, pins itself to the near plane and
|
|
161
178
|
does not write depth. Screen-space text remains the last thing drawn.
|
|
162
179
|
- `InfiniteGrid2D` renders the reference's 2D grid as a full-viewport shader quad: decimal LOD levels,
|
|
@@ -164,11 +181,17 @@ does not write depth. Screen-space text remains the last thing drawn.
|
|
|
164
181
|
`cellSize`, `lineWidthPixels`, `minPixelsBetweenCells`, the LOD range and both line colors from
|
|
165
182
|
the gallery's grid panel, and draws as the 2D layer's background.
|
|
166
183
|
- 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.
|
|
184
|
+
`2d-*` pages fit their declared `view2d` extent, drag or hold WASD to pan, hold shift to sprint, and scroll to zoom. The gallery
|
|
185
|
+
classifies the 2D layer like the 3D scenes: `2d-index` is the API index, the twelve shape pages
|
|
186
|
+
(`2d-rect`, `2d-rounded-rect`, `2d-circle`, `2d-ellipse`, `2d-arc`, `2d-annulus`, `2d-capsule`,
|
|
187
|
+
`2d-polygon`, `2d-triangle`, `2d-rhombus`, `2d-sector`, `2d-segment`) each own one primitive, and
|
|
188
|
+
`2d-lines`, `2d-curves`, `2d-markers`, `2d-arrows`, `2d-spaces`, `2d-grid` cover the stroke API
|
|
189
|
+
families. Every matrix page's column titles and sample labels are `world2d` text, so they pan
|
|
190
|
+
and zoom with the geometry they annotate.
|
|
168
191
|
|
|
169
192
|
## Text
|
|
170
193
|
|
|
171
|
-
`
|
|
194
|
+
`drawList.text(position, text, options)` draws one block per call. `text` is a string or a list
|
|
172
195
|
of spans, the equivalent of a text root entity plus its child span entities.
|
|
173
196
|
|
|
174
197
|
Three placement spaces share one pipeline and are selected per block:
|
|
@@ -179,6 +202,13 @@ Three placement spaces share one pipeline and are selected per block:
|
|
|
179
202
|
| `world-billboard` | world units, block rebuilt from the camera's axes | `fontSize × pixelSize` |
|
|
180
203
|
| `screen` | **top-left origin pixels** (`position.z` ignored) | `fontSize` pixels |
|
|
181
204
|
|
|
205
|
+
The `screen` space is in **target pixels** — the same units as the pass's viewport — because the
|
|
206
|
+
library has no DOM access and must not guess a display density. A caller on a HiDPI display owns the
|
|
207
|
+
conversion: the gallery authors its pages in CSS pixels and multiplies every screen-space command by
|
|
208
|
+
`devicePixelRatio` before handing the frame over (`gallery/screen-scale.ts`). A block placed at
|
|
209
|
+
`[24, 24]` with `fontSize: 18` in CSS pixels then draws at `[48, 48]` and 36 target pixels on a 2×
|
|
210
|
+
display, which is what keeps a label the same physical size on every screen.
|
|
211
|
+
|
|
182
212
|
`fontSize` is in **font pixels** and is the layout unit; `pixelSize` is world units per font pixel.
|
|
183
213
|
That pair is the reference `font_size` / `pixel_size`, and it lets a block keep a fixed world
|
|
184
214
|
size while its glyphs are rasterised at any resolution. Which resolution that is belongs to the glyph
|
|
@@ -187,6 +217,12 @@ source: one that can rasterise on demand is asked for the size the block covers
|
|
|
187
217
|
`fontSize × rasterScale`. Layout options follow the reference layout model: `justify`, `lineBreak`,
|
|
188
218
|
`bounds` (`TextBounds`, in font pixels), `anchor`, `lineHeight` and `letterSpacing`.
|
|
189
219
|
|
|
220
|
+
`rotation` turns a `world-plane` block away from the XY plane, with the reference `Label3D.rotation`
|
|
221
|
+
Euler order (Y, then X, then Z) in radians. `[-Math.PI / 2, 0, 0]` lays a block flat on the XZ plane
|
|
222
|
+
so it reads from above, which is what the reference example labels do; the two camera-relative
|
|
223
|
+
spaces ignore it. The basis travels in the instance world matrix, so the text pipeline stays inside
|
|
224
|
+
the device's vertex attribute budget.
|
|
225
|
+
|
|
190
226
|
`lineHeight` is the absolute pixel form when you pass one, and the default is a `1.2` multiple of the
|
|
191
227
|
font size otherwise — which means a line's box is that multiple of **the line's own
|
|
192
228
|
largest span**, so a line holding a bigger span is taller. `lineHeightScale` names that multiple
|
|
@@ -198,6 +234,34 @@ shaping pass adds it to each cluster's advance.
|
|
|
198
234
|
World text is depth-tested but never depth-written, so geometry in front of it
|
|
199
235
|
occludes it; `screen` text pins itself to the near plane and always wins.
|
|
200
236
|
|
|
237
|
+
### Font face
|
|
238
|
+
|
|
239
|
+
`weight` (1–1000, default `400`), `style` (`normal` or `italic`) and `width` (the nine
|
|
240
|
+
`font-stretch` keywords) select the face a block is rasterised in, mirroring bevy's `TextFont`.
|
|
241
|
+
They are block defaults, and a span overrides one axis of them at a time rather than replacing the
|
|
242
|
+
whole face. The glyph atlas keys on all three, so two weights of one character cannot share texels.
|
|
243
|
+
|
|
244
|
+
How much of that a browser can honour depends on the loaded face, and the limits are real:
|
|
245
|
+
|
|
246
|
+
| option | what Canvas2D does |
|
|
247
|
+
| --- | --- |
|
|
248
|
+
| `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. |
|
|
249
|
+
| `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. |
|
|
250
|
+
| `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. |
|
|
251
|
+
|
|
252
|
+
The bundled Fira Mono subset is a static Medium face, so out of the box `weight` only ever produces
|
|
253
|
+
one synthesised step. `loadFontFaces(family, faces)` is the way out: it registers each entry as a face
|
|
254
|
+
of `family` with its own `weight` descriptor, so `weight: 200` through `900` select genuinely different
|
|
255
|
+
faces. The gallery does exactly that for the `text-font-query` page, which draws with eight instances
|
|
256
|
+
of a variable font built by `bun scripts/build-font-faces.ts` (see
|
|
257
|
+
`gallery/assets/`). That is gallery-only payload: the library's own entry point does not
|
|
258
|
+
carry it.
|
|
259
|
+
|
|
260
|
+
A custom glyph source receives the resolved face — `{ family, weight, style, width }` — instead of a
|
|
261
|
+
family string, because those are the fields the atlas keys on. A source that holds one fixed face (a
|
|
262
|
+
prebaked page, one parsed font buffer) sets `faceKey` to the part it can honour, so a weight it cannot
|
|
263
|
+
rasterise does not multiply identical atlas entries.
|
|
264
|
+
|
|
201
265
|
### Outline
|
|
202
266
|
|
|
203
267
|
`outlineWidth` (in font pixels) rings every glyph in the block in `outlineColor`, which defaults to
|
|
@@ -381,8 +445,8 @@ const renderer = new GpuDebugDraw3D(gpu, {
|
|
|
381
445
|
},
|
|
382
446
|
});
|
|
383
447
|
|
|
384
|
-
|
|
385
|
-
|
|
448
|
+
drawList.text([0, 0, 0], "read me", { source: "sharp" });
|
|
449
|
+
drawList.text([0, -1, 0], "and me", { source: "cheap" });
|
|
386
450
|
```
|
|
387
451
|
|
|
388
452
|
Blocks that name nothing use the first source registered, and `defaultTextStyle` can set one for the
|
|
@@ -391,7 +455,10 @@ actually names it, so registering sources you never draw costs nothing.
|
|
|
391
455
|
|
|
392
456
|
Seven scenes exercise text end to end, and none of them is a port — the reference addon has no text
|
|
393
457
|
API; the layout they run follows the reference layout model, and `docs/text-reference.md` records how
|
|
394
|
-
that was verified.
|
|
458
|
+
that was verified. The geometry examples also draw their own labels, because the reference scenes
|
|
459
|
+
own `Label3D` captions for every column and row; those go through the same pipeline, lying flat on
|
|
460
|
+
the ground with `rotation: [-Math.PI / 2, 0, 0]`. A `GpuDebugDraw3D` built without a `text` option
|
|
461
|
+
skips those blocks instead of failing, so geometry-only callers keep working.
|
|
395
462
|
|
|
396
463
|
`text-compare` is the parity matrix, in the shape of the line/circle examples: rows are glyph sources,
|
|
397
464
|
columns are font sizes, transposed relative to those because text grows sideways. The six
|
|
@@ -457,7 +524,7 @@ gallery to a single glyph source (`auto`, `bitmap`, `bitmap4x`, `sdf`, `sdf4x`,
|
|
|
457
524
|
- `InfiniteGrid3D` shader-driven ground grid ported from the reference grid addon
|
|
458
525
|
(decimal LOD layers, screen-space width, edge/grazing/center/far fades).
|
|
459
526
|
- Orbit/pan/zoom camera controls in the gallery, tuned to the reference
|
|
460
|
-
`entities/free_camera.gd` and layered on a velocity model (`
|
|
527
|
+
`entities/free_camera.gd` and layered on a velocity model (`gallery/demo-camera.ts`): a drag is
|
|
461
528
|
an angular velocity (0.25°/px) or a linear velocity (`distance`/FOV in perspective,
|
|
462
529
|
`orthographicHeight` in orthographic), a wheel notch is a scalar velocity in log₂ space
|
|
463
530
|
(1.08× per notch), and a released drag keeps its velocity and glides — the exact integral of
|
|
@@ -488,7 +555,7 @@ const debug = new DebugDraw3D({
|
|
|
488
555
|
});
|
|
489
556
|
```
|
|
490
557
|
|
|
491
|
-
`
|
|
558
|
+
`gallery/demo-config.ts` contains independently validated camera/view configuration
|
|
492
559
|
and injected storage helpers. Gallery controls support grid, opacity, animation,
|
|
493
560
|
width multiplier, projection, background, camera preset and reset; camera
|
|
494
561
|
interaction is persisted. The sidebar groups them into three collapsible
|
|
@@ -507,11 +574,11 @@ deterministic preset without localStorage: `/?compare=1&scene=circle`. Orthograp
|
|
|
507
574
|
changes the orthographic extent.
|
|
508
575
|
|
|
509
576
|
The gallery UI is built with **Base UI + shadcn (base-lyra preset) + Tailwind v4**
|
|
510
|
-
in `
|
|
577
|
+
in `gallery/components/`, and the imperative WebGPU side lives in `gallery/demo-engine.ts`.
|
|
511
578
|
React only renders chrome; the render loop, camera, and persistence stay outside
|
|
512
579
|
React and are read through `useSyncExternalStore`
|
|
513
|
-
(`
|
|
514
|
-
theme tokens, and `
|
|
580
|
+
(`gallery/demo-hooks.ts`). `gallery/demo.tsx` mounts the shell, `gallery/index.css` holds the base-lyra
|
|
581
|
+
theme tokens, and `gallery/demo.css` keeps the three-column layout, compare mode
|
|
515
582
|
and code highlighting.
|
|
516
583
|
|
|
517
584
|
Runtime comparison viewers (see `docs/visual-comparison.md` and `docs/text-reference.md`):
|
package/dist/curve.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Vec2 } from "./camera-2d.js";
|
|
2
|
+
import type { Vec3 } from "./frame-draw-list.js";
|
|
2
3
|
export type CurvePoint = {
|
|
3
4
|
readonly position: Vec3;
|
|
4
5
|
readonly in?: Vec3;
|
|
@@ -14,3 +15,27 @@ export type CurveTransform = {
|
|
|
14
15
|
};
|
|
15
16
|
export declare function rotatePoint(p: Vec3, rotation: readonly [number, number, number]): Vec3;
|
|
16
17
|
export declare function sampleCurve(curve: Curve3D, tolerance?: number, transform?: CurveTransform): Vec3[];
|
|
18
|
+
/** One control point of a 2D cubic curve; `in`/`out` are handles relative to `position`. */
|
|
19
|
+
export type CurvePoint2D = {
|
|
20
|
+
readonly position: Vec2;
|
|
21
|
+
readonly in?: Vec2;
|
|
22
|
+
readonly out?: Vec2;
|
|
23
|
+
};
|
|
24
|
+
export type Curve2D = {
|
|
25
|
+
readonly points: readonly CurvePoint2D[];
|
|
26
|
+
readonly closed?: boolean;
|
|
27
|
+
};
|
|
28
|
+
export type CurveTransform2D = {
|
|
29
|
+
readonly center?: Vec2;
|
|
30
|
+
/** Anticlockwise rotation in radians. */
|
|
31
|
+
readonly rotation?: number;
|
|
32
|
+
readonly scale?: number;
|
|
33
|
+
};
|
|
34
|
+
/** Applies a curve transform to one point; callers drawing control points use this. */
|
|
35
|
+
export declare function placeCurvePoint2D(point: Vec2, transform?: CurveTransform2D): Vec2;
|
|
36
|
+
/**
|
|
37
|
+
* Samples a 2D cubic curve into a polyline, the 2D twin of `sampleCurve`. `tolerance` is the
|
|
38
|
+
* approximate segment length in `world2d` units: smaller values subdivide each cubic further, so
|
|
39
|
+
* a `lineStrip` can follow the curve as closely as the caller needs.
|
|
40
|
+
*/
|
|
41
|
+
export declare function sampleCurve2D(curve: Curve2D, tolerance?: number, transform?: CurveTransform2D): Vec2[];
|
package/dist/curve.js
CHANGED
|
@@ -44,3 +44,56 @@ export function sampleCurve(curve, tolerance = 0.08, transform = {}) {
|
|
|
44
44
|
out.push(place(curve.closed ? points[0].position : points[points.length - 1].position));
|
|
45
45
|
return out;
|
|
46
46
|
}
|
|
47
|
+
const add2 = (a, b) => [a[0] + b[0], a[1] + b[1]];
|
|
48
|
+
function cubic2(p0, p1, p2, p3, t) {
|
|
49
|
+
const u = 1 - t;
|
|
50
|
+
const a = u * u * u;
|
|
51
|
+
const b = 3 * u * u * t;
|
|
52
|
+
const c = 3 * u * t * t;
|
|
53
|
+
const d = t * t * t;
|
|
54
|
+
return [
|
|
55
|
+
a * p0[0] + b * p1[0] + c * p2[0] + d * p3[0],
|
|
56
|
+
a * p0[1] + b * p1[1] + c * p2[1] + d * p3[1],
|
|
57
|
+
];
|
|
58
|
+
}
|
|
59
|
+
/** Applies a curve transform to one point; callers drawing control points use this. */
|
|
60
|
+
export function placeCurvePoint2D(point, transform = {}) {
|
|
61
|
+
const rotation = transform.rotation ?? 0;
|
|
62
|
+
const scale = transform.scale ?? 1;
|
|
63
|
+
const center = transform.center ?? [0, 0];
|
|
64
|
+
const cos = Math.cos(rotation);
|
|
65
|
+
const sin = Math.sin(rotation);
|
|
66
|
+
const x = point[0] * scale;
|
|
67
|
+
const y = point[1] * scale;
|
|
68
|
+
return [center[0] + x * cos - y * sin, center[1] + x * sin + y * cos];
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Samples a 2D cubic curve into a polyline, the 2D twin of `sampleCurve`. `tolerance` is the
|
|
72
|
+
* approximate segment length in `world2d` units: smaller values subdivide each cubic further, so
|
|
73
|
+
* a `lineStrip` can follow the curve as closely as the caller needs.
|
|
74
|
+
*/
|
|
75
|
+
export function sampleCurve2D(curve, tolerance = 1, transform = {}) {
|
|
76
|
+
const points = curve.points;
|
|
77
|
+
const place = (p) => placeCurvePoint2D(p, transform);
|
|
78
|
+
if (points.length < 2)
|
|
79
|
+
return points.map((point) => place(point.position));
|
|
80
|
+
const out = [];
|
|
81
|
+
const count = curve.closed ? points.length : points.length - 1;
|
|
82
|
+
const step = Math.max(tolerance, 0.1);
|
|
83
|
+
for (let i = 0; i < count; i++) {
|
|
84
|
+
const a = points[i];
|
|
85
|
+
const b = points[(i + 1) % points.length];
|
|
86
|
+
const p0 = a.position;
|
|
87
|
+
const p1 = add2(a.position, a.out ?? [0, 0]);
|
|
88
|
+
const p2 = add2(b.position, b.in ?? [0, 0]);
|
|
89
|
+
const p3 = b.position;
|
|
90
|
+
const hull = Math.hypot(p1[0] - p0[0], p1[1] - p0[1]) +
|
|
91
|
+
Math.hypot(p2[0] - p1[0], p2[1] - p1[1]) +
|
|
92
|
+
Math.hypot(p3[0] - p2[0], p3[1] - p2[1]);
|
|
93
|
+
const steps = Math.max(2, Math.min(64, Math.ceil(hull / step)));
|
|
94
|
+
for (let s = 0; s < steps; s++)
|
|
95
|
+
out.push(place(cubic2(p0, p1, p2, p3, s / steps)));
|
|
96
|
+
}
|
|
97
|
+
out.push(place(curve.closed ? points[0].position : points[points.length - 1].position));
|
|
98
|
+
return out;
|
|
99
|
+
}
|
package/dist/debug-draw-3d.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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():
|
|
13
|
-
finishFrame(
|
|
12
|
+
beginFrame(): FrameDrawList;
|
|
13
|
+
finishFrame(drawList: FrameDrawList): readonly FrameCommand[];
|
|
14
14
|
}
|
package/dist/debug-draw-3d.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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
|
|
24
|
-
this.#active =
|
|
25
|
-
return
|
|
23
|
+
const drawList = new FrameDrawList(this.#defaults);
|
|
24
|
+
this.#active = drawList;
|
|
25
|
+
return drawList;
|
|
26
26
|
}
|
|
27
|
-
finishFrame(
|
|
28
|
-
if (this.#active !==
|
|
29
|
-
throw new Error("
|
|
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 =
|
|
32
|
-
|
|
31
|
+
const commands = drawList.commands(this.#visibleLayers);
|
|
32
|
+
drawList.seal();
|
|
33
33
|
this.#active = undefined;
|
|
34
34
|
return commands;
|
|
35
35
|
}
|