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
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 2D debug-draw commands and their drawList.
|
|
3
|
+
*
|
|
4
|
+
* The 2D layer is the same data flow as the 3D one — `FrameDrawList -> FrameCommand[] -> GPU` —
|
|
5
|
+
* reached through `frame.d2` instead of a second entry point, so a frame has one lifecycle, one
|
|
6
|
+
* layer mask and one set of defaults. The commands are separate types rather than 3D commands
|
|
7
|
+
* with a `space` flag because a 2D point is a `Vec2` and 2D strokes carry no surface normal;
|
|
8
|
+
* keeping them apart stops a 3D-only field from silently meaning something different here.
|
|
9
|
+
*
|
|
10
|
+
* See `camera-2d.ts` for what `world2d` and `screen` mean.
|
|
11
|
+
*/
|
|
12
|
+
import { gridLines2D } from "./geometry-generators.js";
|
|
13
|
+
import { sampleCurve2D } from "./curve.js";
|
|
14
|
+
const WIRE = { filled: false };
|
|
15
|
+
const SOLID = { filled: true };
|
|
16
|
+
const DEFAULT_2D_COLOR = [1, 1, 1, 1];
|
|
17
|
+
const DEFAULT_2D_LAYER = 1;
|
|
18
|
+
/** An unfilled shape still has to be visible, so a stroke defaults to one pixel. */
|
|
19
|
+
const DEFAULT_2D_STROKE_WIDTH = 1;
|
|
20
|
+
const TAU = Math.PI * 2;
|
|
21
|
+
function finite(value, name) {
|
|
22
|
+
if (!Number.isFinite(value)) {
|
|
23
|
+
throw new RangeError(`${name} must be finite`);
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
function positive(value, name) {
|
|
28
|
+
const resolved = finite(value, name);
|
|
29
|
+
if (resolved <= 0)
|
|
30
|
+
throw new RangeError(`${name} must be greater than zero`);
|
|
31
|
+
return resolved;
|
|
32
|
+
}
|
|
33
|
+
function nonNegative(value, name) {
|
|
34
|
+
const resolved = finite(value, name);
|
|
35
|
+
if (resolved < 0)
|
|
36
|
+
throw new RangeError(`${name} must not be negative`);
|
|
37
|
+
return resolved;
|
|
38
|
+
}
|
|
39
|
+
function positiveSize(value, name) {
|
|
40
|
+
const resolved = vec2(value, name);
|
|
41
|
+
if (resolved[0] <= 0 || resolved[1] <= 0)
|
|
42
|
+
throw new RangeError(`${name} values must be greater than zero`);
|
|
43
|
+
return resolved;
|
|
44
|
+
}
|
|
45
|
+
/** Start angle and sweep of the angle-parameterised shapes, with the same defaults and bounds. */
|
|
46
|
+
function angleRange(options) {
|
|
47
|
+
const startAngle = finite(options.startAngle ?? 0, "startAngle");
|
|
48
|
+
const sweep = finite(options.sweep ?? TAU, "sweep");
|
|
49
|
+
if (sweep === 0 || Math.abs(sweep) > TAU) {
|
|
50
|
+
throw new RangeError("sweep must be a non-zero angle of at most one full turn");
|
|
51
|
+
}
|
|
52
|
+
return { startAngle, sweep };
|
|
53
|
+
}
|
|
54
|
+
/** `[visible, gap, phase]` of a dashed outline; a dash needs a real gap to break the stroke. */
|
|
55
|
+
function dashValue(value) {
|
|
56
|
+
const visible = finite(value[0], "dash visible");
|
|
57
|
+
const gap = finite(value[1], "dash gap");
|
|
58
|
+
const phase = finite(value[2], "dash phase");
|
|
59
|
+
if (visible <= 0)
|
|
60
|
+
throw new RangeError("dash visible must be greater than zero");
|
|
61
|
+
if (gap <= 0)
|
|
62
|
+
throw new RangeError("dash gap must be greater than zero");
|
|
63
|
+
return [visible, gap, phase];
|
|
64
|
+
}
|
|
65
|
+
/** A dashed outline: the pattern is validated once, here. */
|
|
66
|
+
function dashed(dash) {
|
|
67
|
+
return { filled: false, dash: dashValue(dash) };
|
|
68
|
+
}
|
|
69
|
+
function vec2(value, name) {
|
|
70
|
+
if (value.length !== 2) {
|
|
71
|
+
throw new RangeError(`${name} must contain two values`);
|
|
72
|
+
}
|
|
73
|
+
return [finite(value[0], `${name}[0]`), finite(value[1], `${name}[1]`)];
|
|
74
|
+
}
|
|
75
|
+
function colorValue(value, name) {
|
|
76
|
+
if (value.length !== 4) {
|
|
77
|
+
throw new RangeError(`${name} must contain four values`);
|
|
78
|
+
}
|
|
79
|
+
return [
|
|
80
|
+
finite(value[0], `${name}[0]`),
|
|
81
|
+
finite(value[1], `${name}[1]`),
|
|
82
|
+
finite(value[2], `${name}[2]`),
|
|
83
|
+
finite(value[3], `${name}[3]`),
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
function color(value) {
|
|
87
|
+
return value === undefined ? DEFAULT_2D_COLOR : colorValue(value, "color");
|
|
88
|
+
}
|
|
89
|
+
function layer(value) {
|
|
90
|
+
const resolved = value ?? DEFAULT_2D_LAYER;
|
|
91
|
+
if (!Number.isInteger(resolved) || resolved < 0) {
|
|
92
|
+
throw new RangeError("layer must be a non-negative integer bitmask");
|
|
93
|
+
}
|
|
94
|
+
return resolved;
|
|
95
|
+
}
|
|
96
|
+
function sizeModeValue(value, name) {
|
|
97
|
+
if (value !== "screen" && value !== "world") {
|
|
98
|
+
throw new RangeError(`${name} must be "screen" or "world"`);
|
|
99
|
+
}
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
function jointValue(value, name) {
|
|
103
|
+
if (value !== "none" && value !== "round" && value !== "bevel" && value !== "miter") {
|
|
104
|
+
throw new RangeError(`${name} must be "none", "round", "bevel", or "miter"`);
|
|
105
|
+
}
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
/** Resolves the space, defaulting to `world2d` so a command only pays for a camera when it wants one. */
|
|
109
|
+
function spaceValue(value) {
|
|
110
|
+
if (value === undefined)
|
|
111
|
+
return "world2d";
|
|
112
|
+
if (value !== "world2d" && value !== "screen") {
|
|
113
|
+
throw new RangeError('space must be "world2d" or "screen"');
|
|
114
|
+
}
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Records 2D primitives for one frame. Obtained through `FrameDrawList.d2`; it seals with its
|
|
119
|
+
* parent, so a caller cannot keep drawing into a finished frame.
|
|
120
|
+
*/
|
|
121
|
+
export class FrameDrawList2D {
|
|
122
|
+
#points = [];
|
|
123
|
+
#lines = [];
|
|
124
|
+
#joints = [];
|
|
125
|
+
#shapes = [];
|
|
126
|
+
#defaults;
|
|
127
|
+
#assertOpen;
|
|
128
|
+
constructor(defaults, assertOpen) {
|
|
129
|
+
this.#defaults = defaults;
|
|
130
|
+
this.#assertOpen = assertOpen;
|
|
131
|
+
}
|
|
132
|
+
point(position, options = {}) {
|
|
133
|
+
this.#assertOpen();
|
|
134
|
+
const size = finite(options.size ?? this.#defaults.point.size ?? 1, "size");
|
|
135
|
+
if (size <= 0)
|
|
136
|
+
throw new RangeError("size must be greater than zero");
|
|
137
|
+
this.#points.push({
|
|
138
|
+
kind: "point2d",
|
|
139
|
+
position: vec2(position, "position"),
|
|
140
|
+
color: color(options.color ?? this.#defaults.point.color),
|
|
141
|
+
size,
|
|
142
|
+
sizeMode: options.sizeMode ?? this.#defaults.point.sizeMode ?? "screen",
|
|
143
|
+
space: spaceValue(options.space),
|
|
144
|
+
layer: layer(options.layer ?? this.#defaults.point.layer),
|
|
145
|
+
...(options.round === undefined ? {} : { round: options.round }),
|
|
146
|
+
});
|
|
147
|
+
return this;
|
|
148
|
+
}
|
|
149
|
+
/** Straight segment (the 3D `draw_line`). */
|
|
150
|
+
line(start, end, options = {}) {
|
|
151
|
+
return this.#line(start, end, options, undefined);
|
|
152
|
+
}
|
|
153
|
+
/** Straight segment dashed along its length (the 3D `draw_line_d`). */
|
|
154
|
+
line_d(start, end, dash, options = {}) {
|
|
155
|
+
return this.#line(start, end, options, dashValue(dash));
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Arrow with an open V head, the 2D twin of `FrameDrawList.arrow`: the shaft runs to `origin +
|
|
159
|
+
* direction`, and the head is two lines back from the tip.
|
|
160
|
+
*/
|
|
161
|
+
arrow(origin, direction, options = {}) {
|
|
162
|
+
const { end, left, right } = this.#arrowParts(origin, direction, options);
|
|
163
|
+
// The open head is an outline, so the shaft runs all the way to the tip and the two head lines
|
|
164
|
+
// sit on top of it.
|
|
165
|
+
this.line(origin, end, options);
|
|
166
|
+
this.line(end, left, options);
|
|
167
|
+
this.line(end, right, options);
|
|
168
|
+
return this;
|
|
169
|
+
}
|
|
170
|
+
/** Arrow with a filled triangular head (`arrow` keeps the open line head). */
|
|
171
|
+
arrow_s(origin, direction, options = {}) {
|
|
172
|
+
const { end, base, left, right } = this.#arrowParts(origin, direction, options);
|
|
173
|
+
this.line(origin, base, options);
|
|
174
|
+
this.triangle_s(end, left, right, {
|
|
175
|
+
...(options.color === undefined ? {} : { color: options.color }),
|
|
176
|
+
...(options.space === undefined ? {} : { space: options.space }),
|
|
177
|
+
...(options.layer === undefined ? {} : { layer: options.layer }),
|
|
178
|
+
});
|
|
179
|
+
return this;
|
|
180
|
+
}
|
|
181
|
+
/** Arrow whose shaft is dashed; the two head lines stay solid like the filled head. */
|
|
182
|
+
arrow_d(origin, direction, dash, options = {}) {
|
|
183
|
+
const { end, left, right } = this.#arrowParts(origin, direction, options);
|
|
184
|
+
this.line_d(origin, end, dashValue(dash), options);
|
|
185
|
+
// Only the shaft dashes: the head outline stays solid so the tip keeps its shape, matching the
|
|
186
|
+
// filled head of `arrow_s_d`.
|
|
187
|
+
this.line(end, left, options);
|
|
188
|
+
this.line(end, right, options);
|
|
189
|
+
return this;
|
|
190
|
+
}
|
|
191
|
+
/** Arrow with a filled head and a dashed shaft. */
|
|
192
|
+
arrow_s_d(origin, direction, dash, options = {}) {
|
|
193
|
+
const { end, base, left, right } = this.#arrowParts(origin, direction, options);
|
|
194
|
+
this.line_d(origin, base, dashValue(dash), options);
|
|
195
|
+
this.triangle_s(end, left, right, {
|
|
196
|
+
...(options.color === undefined ? {} : { color: options.color }),
|
|
197
|
+
...(options.space === undefined ? {} : { space: options.space }),
|
|
198
|
+
...(options.layer === undefined ? {} : { layer: options.layer }),
|
|
199
|
+
});
|
|
200
|
+
return this;
|
|
201
|
+
}
|
|
202
|
+
#arrowParts(origin, direction, options) {
|
|
203
|
+
const from = vec2(origin, "origin");
|
|
204
|
+
const delta = vec2(direction, "direction");
|
|
205
|
+
const length = Math.hypot(delta[0], delta[1]);
|
|
206
|
+
const end = [from[0] + delta[0], from[1] + delta[1]];
|
|
207
|
+
const unit = length > 0 ? [delta[0] / length, delta[1] / length] : [1, 0];
|
|
208
|
+
const headLength = finite(options.headLength ?? Math.min(14, Math.max(6, length * 0.25)), "headLength");
|
|
209
|
+
const headAngle = finite(options.headAngle ?? Math.PI / 6, "headAngle");
|
|
210
|
+
const back = Math.cos(headAngle) * headLength;
|
|
211
|
+
const spread = Math.sin(headAngle) * headLength;
|
|
212
|
+
const side = [-unit[1], unit[0]];
|
|
213
|
+
return {
|
|
214
|
+
end,
|
|
215
|
+
base: [end[0] - unit[0] * back, end[1] - unit[1] * back],
|
|
216
|
+
left: [end[0] - unit[0] * back + side[0] * spread, end[1] - unit[1] * back + side[1] * spread],
|
|
217
|
+
right: [end[0] - unit[0] * back - side[0] * spread, end[1] - unit[1] * back - side[1] * spread],
|
|
218
|
+
back,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
ray(origin, direction, options = {}) {
|
|
222
|
+
return this.#line(origin, [origin[0] + direction[0], origin[1] + direction[1]], options, undefined);
|
|
223
|
+
}
|
|
224
|
+
/** Ray dashed along its length. */
|
|
225
|
+
ray_d(origin, direction, dash, options = {}) {
|
|
226
|
+
return this.#line(origin, [origin[0] + direction[0], origin[1] + direction[1]], options, dashValue(dash));
|
|
227
|
+
}
|
|
228
|
+
#line(start, end, options, dashArgument) {
|
|
229
|
+
this.#assertOpen();
|
|
230
|
+
const resolvedStart = vec2(start, "start");
|
|
231
|
+
const resolvedEnd = vec2(end, "end");
|
|
232
|
+
if (resolvedStart[0] === resolvedEnd[0] && resolvedStart[1] === resolvedEnd[1]) {
|
|
233
|
+
return this;
|
|
234
|
+
}
|
|
235
|
+
const width = finite(options.width ?? this.#defaults.line.width ?? 1, "width");
|
|
236
|
+
if (width <= 0)
|
|
237
|
+
throw new RangeError("width must be greater than zero");
|
|
238
|
+
const dash = dashArgument ?? this.#defaults.line.dash;
|
|
239
|
+
const joint = options.joint ?? this.#defaults.line.joint;
|
|
240
|
+
this.#lines.push({
|
|
241
|
+
kind: "line2d",
|
|
242
|
+
start: resolvedStart,
|
|
243
|
+
end: resolvedEnd,
|
|
244
|
+
color: color(options.color ?? this.#defaults.line.color),
|
|
245
|
+
colorEnd: color(options.colorEnd ?? options.color ?? this.#defaults.line.colorEnd ?? this.#defaults.line.color),
|
|
246
|
+
width,
|
|
247
|
+
widthMode: options.widthMode ?? this.#defaults.line.widthMode ?? "screen",
|
|
248
|
+
space: spaceValue(options.space),
|
|
249
|
+
layer: layer(options.layer ?? this.#defaults.line.layer),
|
|
250
|
+
...(dash === undefined ? {} : { dash }),
|
|
251
|
+
...(joint === undefined ? {} : { joint }),
|
|
252
|
+
});
|
|
253
|
+
return this;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Path of segments, the 2D twin of `FrameDrawList.lineStrip`. Dash distance accumulates over
|
|
257
|
+
* the whole path and an open strip joins only its interior vertices, matching the 3D layer.
|
|
258
|
+
*/
|
|
259
|
+
lineStrip(points, options = {}, closed = false) {
|
|
260
|
+
return this.#strip(points, options, undefined, closed);
|
|
261
|
+
}
|
|
262
|
+
/** Open strip dashed along its length. */
|
|
263
|
+
lineStrip_d(points, dash, options = {}, closed = false) {
|
|
264
|
+
return this.#strip(points, options, dashValue(dash), closed);
|
|
265
|
+
}
|
|
266
|
+
/** Closed strip: the last point joins the first (the 3D `draw_line_loop`). */
|
|
267
|
+
lineLoop(points, options = {}) {
|
|
268
|
+
return this.#strip(points, options, undefined, true);
|
|
269
|
+
}
|
|
270
|
+
/** Closed strip dashed along its length. */
|
|
271
|
+
lineLoop_d(points, dash, options = {}) {
|
|
272
|
+
return this.#strip(points, options, dashValue(dash), true);
|
|
273
|
+
}
|
|
274
|
+
#strip(points, options, dash, closed) {
|
|
275
|
+
this.#assertOpen();
|
|
276
|
+
const vertexDistances = [0];
|
|
277
|
+
let traveled = 0;
|
|
278
|
+
const segment = (a, b) => {
|
|
279
|
+
this.#line(a, b, options, dash === undefined ? undefined : [dash[0], dash[1], dash[2] + traveled]);
|
|
280
|
+
traveled += Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
281
|
+
vertexDistances.push(traveled);
|
|
282
|
+
};
|
|
283
|
+
for (let i = 0; i + 1 < points.length; i++)
|
|
284
|
+
segment(points[i], points[i + 1]);
|
|
285
|
+
if (closed && points.length > 1)
|
|
286
|
+
segment(points[points.length - 1], points[0]);
|
|
287
|
+
const joint = options.joint ?? this.#defaults.line.joint;
|
|
288
|
+
if (joint === "round" || joint === "bevel" || joint === "miter") {
|
|
289
|
+
const count = points.length;
|
|
290
|
+
const first = closed ? 0 : 1;
|
|
291
|
+
const last = closed ? (count > 2 ? count : 0) : count - 1;
|
|
292
|
+
for (let i = first; i < last; i++) {
|
|
293
|
+
this.#jointCommand(points[(i - 1 + count) % count], points[i], points[(i + 1) % count], joint, options, dash, vertexDistances[i]);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return this;
|
|
297
|
+
}
|
|
298
|
+
polyline(points, options = {}, closed = false) {
|
|
299
|
+
return this.lineStrip(points, options, closed);
|
|
300
|
+
}
|
|
301
|
+
polyline_d(points, dash, options = {}, closed = false) {
|
|
302
|
+
return this.lineStrip_d(points, dash, options, closed);
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Cubic curve sampled into a polyline, the 2D twin of `FrameDrawList.curve`. The curve's own
|
|
306
|
+
* `closed` flag decides whether the last point joins the first.
|
|
307
|
+
*/
|
|
308
|
+
curve(curve, options = {}) {
|
|
309
|
+
const { tolerance, transform, ...lineOptions } = options;
|
|
310
|
+
return this.#strip(sampleCurve2D(curve, tolerance, transform), lineOptions, undefined, curve.closed ?? false);
|
|
311
|
+
}
|
|
312
|
+
/** Cubic curve sampled into a dashed polyline. */
|
|
313
|
+
curve_d(curve, dash, options = {}) {
|
|
314
|
+
const { tolerance, transform, ...lineOptions } = options;
|
|
315
|
+
return this.#strip(sampleCurve2D(curve, tolerance, transform), lineOptions, dashValue(dash), curve.closed ?? false);
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Axis-aligned grid of lines, the 2D twin of `FrameDrawList.grid` and reference `grid_2d`: `cells`
|
|
319
|
+
* are the cells per axis, so the drawn line count is one more on each side.
|
|
320
|
+
*/
|
|
321
|
+
grid(center, cells, spacing, options = {}) {
|
|
322
|
+
return this.#grid(center, cells, spacing, options, undefined);
|
|
323
|
+
}
|
|
324
|
+
/** Grid whose lines are dashed along their length. */
|
|
325
|
+
grid_d(center, cells, spacing, dash, options = {}) {
|
|
326
|
+
return this.#grid(center, cells, spacing, options, dashValue(dash));
|
|
327
|
+
}
|
|
328
|
+
#grid(center, cells, spacing, options, dash) {
|
|
329
|
+
this.#assertOpen();
|
|
330
|
+
const { rotation = 0, skew = [0, 0], outer = "none", ...lineOptions } = options;
|
|
331
|
+
const [columns, rows] = cells;
|
|
332
|
+
if (!Number.isInteger(columns) || !Number.isInteger(rows) || columns < 1 || rows < 1) {
|
|
333
|
+
throw new RangeError("grid cells must be positive integers");
|
|
334
|
+
}
|
|
335
|
+
const [stepX, stepY] = positiveSize(spacing, "spacing");
|
|
336
|
+
const [centerX, centerY] = vec2(center, "center");
|
|
337
|
+
const angle = finite(rotation, "rotation");
|
|
338
|
+
const [skewX, skewY] = skew;
|
|
339
|
+
finite(skewX, "skew x");
|
|
340
|
+
finite(skewY, "skew y");
|
|
341
|
+
if (outer !== "none" && outer !== "x" && outer !== "y" && outer !== "xy") {
|
|
342
|
+
throw new RangeError('grid outer must be "none", "x", "y" or "xy"');
|
|
343
|
+
}
|
|
344
|
+
for (const [a, b] of gridLines2D([columns, rows], [stepX, stepY], [centerX, centerY], angle, [skewX, skewY], outer)) {
|
|
345
|
+
this.#line(a, b, lineOptions, dash);
|
|
346
|
+
}
|
|
347
|
+
return this;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Rectangle with the given full width and height, stroked by default like the 3D `draw_rect`.
|
|
351
|
+
* `rect_s` fills it and `rect_d` dashes the outline.
|
|
352
|
+
*/
|
|
353
|
+
rect(center, size, options = {}) {
|
|
354
|
+
this.#rectVariant(center, size, options, WIRE);
|
|
355
|
+
return this;
|
|
356
|
+
}
|
|
357
|
+
/** Solid rectangle (reference `Rectangle`, the 3D `draw_rect_s`). */
|
|
358
|
+
rect_s(center, size, options = {}) {
|
|
359
|
+
this.#rectVariant(center, size, options, SOLID);
|
|
360
|
+
return this;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Rectangle outline dashed along its perimeter (the 3D `draw_rect_d`). `dash` is
|
|
364
|
+
* `[visible, gap, phase]` in shape-space units, the same triple the line layer takes.
|
|
365
|
+
*/
|
|
366
|
+
rect_d(center, size, dash, options = {}) {
|
|
367
|
+
this.#rectVariant(center, size, options, dashed(dash));
|
|
368
|
+
return this;
|
|
369
|
+
}
|
|
370
|
+
#rectVariant(center, size, options, variant) {
|
|
371
|
+
const full = positiveSize(size, "size");
|
|
372
|
+
this.#pushShape("rect", center, { size: [full[0] / 2, full[1] / 2] }, options, variant);
|
|
373
|
+
}
|
|
374
|
+
/** Rounded rectangle outline; the shader clamps the radius to half the shorter side. */
|
|
375
|
+
rect_r(center, size, cornerRadius, options = {}) {
|
|
376
|
+
this.#rectRVariant(center, size, cornerRadius, options, WIRE);
|
|
377
|
+
return this;
|
|
378
|
+
}
|
|
379
|
+
/** Solid rounded rectangle (reference `RoundedRectangle`, the 3D `_s` counterpart). */
|
|
380
|
+
rect_r_s(center, size, cornerRadius, options = {}) {
|
|
381
|
+
this.#rectRVariant(center, size, cornerRadius, options, SOLID);
|
|
382
|
+
return this;
|
|
383
|
+
}
|
|
384
|
+
/** Rounded rectangle outline dashed along its perimeter. */
|
|
385
|
+
rect_r_d(center, size, cornerRadius, dash, options = {}) {
|
|
386
|
+
this.#rectRVariant(center, size, cornerRadius, options, dashed(dash));
|
|
387
|
+
return this;
|
|
388
|
+
}
|
|
389
|
+
#rectRVariant(center, size, cornerRadius, options, variant) {
|
|
390
|
+
const full = positiveSize(size, "size");
|
|
391
|
+
this.#pushShape("rounded-rect", center, { size: [full[0] / 2, full[1] / 2], cornerRadius: nonNegative(cornerRadius, "cornerRadius") }, options, variant);
|
|
392
|
+
}
|
|
393
|
+
/** Disc outline with the given radius (reference `Circle`, the 3D `draw_circle`). */
|
|
394
|
+
circle(center, radius, options = {}) {
|
|
395
|
+
this.#circleVariant(center, radius, options, WIRE);
|
|
396
|
+
return this;
|
|
397
|
+
}
|
|
398
|
+
/** Solid disc (reference `Circle::new`, the 3D `draw_circle_s`). */
|
|
399
|
+
circle_s(center, radius, options = {}) {
|
|
400
|
+
this.#circleVariant(center, radius, options, SOLID);
|
|
401
|
+
return this;
|
|
402
|
+
}
|
|
403
|
+
/** Dashed circle outline. */
|
|
404
|
+
circle_d(center, radius, dash, options = {}) {
|
|
405
|
+
this.#circleVariant(center, radius, options, dashed(dash));
|
|
406
|
+
return this;
|
|
407
|
+
}
|
|
408
|
+
#circleVariant(center, radius, options, variant) {
|
|
409
|
+
const r = positive(radius, "radius");
|
|
410
|
+
this.#pushShape("circle", center, { size: [r, r] }, options, variant);
|
|
411
|
+
}
|
|
412
|
+
/** Ellipse outline with independent x and y radii (reference `Ellipse`). */
|
|
413
|
+
ellipse(center, radii, options = {}) {
|
|
414
|
+
this.#ellipseVariant(center, radii, options, WIRE);
|
|
415
|
+
return this;
|
|
416
|
+
}
|
|
417
|
+
/** Solid ellipse (reference `Ellipse::new`, the 3D `_s` counterpart). */
|
|
418
|
+
ellipse_s(center, radii, options = {}) {
|
|
419
|
+
this.#ellipseVariant(center, radii, options, SOLID);
|
|
420
|
+
return this;
|
|
421
|
+
}
|
|
422
|
+
/** Dashed ellipse outline. */
|
|
423
|
+
ellipse_d(center, radii, dash, options = {}) {
|
|
424
|
+
this.#ellipseVariant(center, radii, options, dashed(dash));
|
|
425
|
+
return this;
|
|
426
|
+
}
|
|
427
|
+
#ellipseVariant(center, radii, options, variant) {
|
|
428
|
+
const r = positiveSize(radii, "radii");
|
|
429
|
+
this.#pushShape("ellipse", center, { size: r }, options, variant);
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Circular arc from `startAngle` through `sweep`. It is a curve without an interior, so it is
|
|
433
|
+
* only ever stroked; `arc_s` would have nothing to fill.
|
|
434
|
+
*/
|
|
435
|
+
arc(center, radius, options = {}) {
|
|
436
|
+
this.#arcVariant(center, radius, options, WIRE);
|
|
437
|
+
return this;
|
|
438
|
+
}
|
|
439
|
+
/** Arc outline dashed along its sweep. */
|
|
440
|
+
arc_d(center, radius, dash, options = {}) {
|
|
441
|
+
this.#arcVariant(center, radius, options, dashed(dash));
|
|
442
|
+
return this;
|
|
443
|
+
}
|
|
444
|
+
#arcVariant(center, radius, options, variant) {
|
|
445
|
+
const r = positive(radius, "radius");
|
|
446
|
+
const angles = angleRange(options);
|
|
447
|
+
this.#pushShape("arc", center, { size: [r, r], ...angles }, options, variant);
|
|
448
|
+
}
|
|
449
|
+
/** Ring outline between two radii (reference `Annulus`). */
|
|
450
|
+
annulus(center, innerRadius, outerRadius, options = {}) {
|
|
451
|
+
this.#annulusVariant(center, innerRadius, outerRadius, options, WIRE);
|
|
452
|
+
return this;
|
|
453
|
+
}
|
|
454
|
+
/** Filled ring band (reference `Annulus::new`, the 3D `_s` counterpart). */
|
|
455
|
+
annulus_s(center, innerRadius, outerRadius, options = {}) {
|
|
456
|
+
this.#annulusVariant(center, innerRadius, outerRadius, options, SOLID);
|
|
457
|
+
return this;
|
|
458
|
+
}
|
|
459
|
+
/** Dashed annulus outline; both boundaries are stroked. */
|
|
460
|
+
annulus_d(center, innerRadius, outerRadius, dash, options = {}) {
|
|
461
|
+
this.#annulusVariant(center, innerRadius, outerRadius, options, dashed(dash));
|
|
462
|
+
return this;
|
|
463
|
+
}
|
|
464
|
+
#annulusVariant(center, innerRadius, outerRadius, options, variant) {
|
|
465
|
+
const inner = nonNegative(innerRadius, "innerRadius");
|
|
466
|
+
const outer = positive(outerRadius, "outerRadius");
|
|
467
|
+
if (inner >= outer)
|
|
468
|
+
throw new RangeError("innerRadius must be smaller than outerRadius");
|
|
469
|
+
this.#pushShape("annulus", center, { size: [outer, inner] }, options, variant);
|
|
470
|
+
}
|
|
471
|
+
/** Capsule outline whose straight section is `length` long along y (reference `Capsule2d`). */
|
|
472
|
+
capsule(center, radius, length, options = {}) {
|
|
473
|
+
this.#capsuleVariant(center, radius, length, options, WIRE);
|
|
474
|
+
return this;
|
|
475
|
+
}
|
|
476
|
+
/** Solid capsule (reference `Capsule2d::new`, the 3D `_s` counterpart). */
|
|
477
|
+
capsule_s(center, radius, length, options = {}) {
|
|
478
|
+
this.#capsuleVariant(center, radius, length, options, SOLID);
|
|
479
|
+
return this;
|
|
480
|
+
}
|
|
481
|
+
/** Dashed capsule outline. */
|
|
482
|
+
capsule_d(center, radius, length, dash, options = {}) {
|
|
483
|
+
this.#capsuleVariant(center, radius, length, options, dashed(dash));
|
|
484
|
+
return this;
|
|
485
|
+
}
|
|
486
|
+
#capsuleVariant(center, radius, length, options, variant) {
|
|
487
|
+
const r = positive(radius, "radius");
|
|
488
|
+
const straight = nonNegative(length, "length");
|
|
489
|
+
this.#pushShape("capsule", center, { size: [r, straight / 2] }, options, variant);
|
|
490
|
+
}
|
|
491
|
+
/** Regular polygon outline with a vertex on the +x axis (reference `RegularPolygon`). */
|
|
492
|
+
polygon(center, circumradius, sides, options = {}) {
|
|
493
|
+
this.#polygonVariant(center, circumradius, sides, options, WIRE);
|
|
494
|
+
return this;
|
|
495
|
+
}
|
|
496
|
+
/** Solid regular polygon (reference `RegularPolygon::new`, the 3D `_s` counterpart). */
|
|
497
|
+
polygon_s(center, circumradius, sides, options = {}) {
|
|
498
|
+
this.#polygonVariant(center, circumradius, sides, options, SOLID);
|
|
499
|
+
return this;
|
|
500
|
+
}
|
|
501
|
+
/** Dashed regular polygon outline. */
|
|
502
|
+
polygon_d(center, circumradius, sides, dash, options = {}) {
|
|
503
|
+
this.#polygonVariant(center, circumradius, sides, options, dashed(dash));
|
|
504
|
+
return this;
|
|
505
|
+
}
|
|
506
|
+
#polygonVariant(center, circumradius, sides, options, variant) {
|
|
507
|
+
const r = positive(circumradius, "circumradius");
|
|
508
|
+
if (!Number.isInteger(sides) || sides < 3)
|
|
509
|
+
throw new RangeError("sides must be an integer of at least 3");
|
|
510
|
+
this.#pushShape("polygon", center, { size: [r, r], sides }, options, variant);
|
|
511
|
+
}
|
|
512
|
+
/** Triangle outline (reference `Triangle2d`); `rotation` turns about its centroid. */
|
|
513
|
+
triangle(a, b, c, options = {}) {
|
|
514
|
+
this.#triangleVariant(a, b, c, options, WIRE);
|
|
515
|
+
return this;
|
|
516
|
+
}
|
|
517
|
+
/** Solid triangle (reference `Triangle2d::new`, the 3D `_s` counterpart). */
|
|
518
|
+
triangle_s(a, b, c, options = {}) {
|
|
519
|
+
this.#triangleVariant(a, b, c, options, SOLID);
|
|
520
|
+
return this;
|
|
521
|
+
}
|
|
522
|
+
/** Dashed triangle outline. */
|
|
523
|
+
triangle_d(a, b, c, dash, options = {}) {
|
|
524
|
+
this.#triangleVariant(a, b, c, options, dashed(dash));
|
|
525
|
+
return this;
|
|
526
|
+
}
|
|
527
|
+
#triangleVariant(a, b, c, options, variant) {
|
|
528
|
+
const p0 = vec2(a, "a");
|
|
529
|
+
const p1 = vec2(b, "b");
|
|
530
|
+
const p2 = vec2(c, "c");
|
|
531
|
+
const center = [(p0[0] + p1[0] + p2[0]) / 3, (p0[1] + p1[1] + p2[1]) / 3];
|
|
532
|
+
this.#pushShape("triangle", center, {
|
|
533
|
+
size: [p0[0] - center[0], p0[1] - center[1]],
|
|
534
|
+
vertexB: [p1[0] - center[0], p1[1] - center[1]],
|
|
535
|
+
vertexC: [p2[0] - center[0], p2[1] - center[1]],
|
|
536
|
+
}, options, variant);
|
|
537
|
+
}
|
|
538
|
+
/** Diamond outline with the given full diagonals (reference `Rhombus`). */
|
|
539
|
+
rhombus(center, diagonals, options = {}) {
|
|
540
|
+
this.#rhombusVariant(center, diagonals, options, WIRE);
|
|
541
|
+
return this;
|
|
542
|
+
}
|
|
543
|
+
/** Solid diamond (reference `Rhombus::new`, the 3D `_s` counterpart). */
|
|
544
|
+
rhombus_s(center, diagonals, options = {}) {
|
|
545
|
+
this.#rhombusVariant(center, diagonals, options, SOLID);
|
|
546
|
+
return this;
|
|
547
|
+
}
|
|
548
|
+
/** Dashed diamond outline. */
|
|
549
|
+
rhombus_d(center, diagonals, dash, options = {}) {
|
|
550
|
+
this.#rhombusVariant(center, diagonals, options, dashed(dash));
|
|
551
|
+
return this;
|
|
552
|
+
}
|
|
553
|
+
#rhombusVariant(center, diagonals, options, variant) {
|
|
554
|
+
const d = positiveSize(diagonals, "diagonals");
|
|
555
|
+
this.#pushShape("rhombus", center, { size: [d[0] / 2, d[1] / 2] }, options, variant);
|
|
556
|
+
}
|
|
557
|
+
/** Disc sector outline from `startAngle` through `sweep` (reference `CircularSector`). */
|
|
558
|
+
sector(center, radius, options = {}) {
|
|
559
|
+
this.#sectorVariant(center, radius, options, WIRE);
|
|
560
|
+
return this;
|
|
561
|
+
}
|
|
562
|
+
/** Solid disc sector (reference `CircularSector`, the 3D `_s` counterpart). */
|
|
563
|
+
sector_s(center, radius, options = {}) {
|
|
564
|
+
this.#sectorVariant(center, radius, options, SOLID);
|
|
565
|
+
return this;
|
|
566
|
+
}
|
|
567
|
+
/** Dashed sector outline: the arc plus both radii. */
|
|
568
|
+
sector_d(center, radius, dash, options = {}) {
|
|
569
|
+
this.#sectorVariant(center, radius, options, dashed(dash));
|
|
570
|
+
return this;
|
|
571
|
+
}
|
|
572
|
+
#sectorVariant(center, radius, options, variant) {
|
|
573
|
+
const r = positive(radius, "radius");
|
|
574
|
+
const angles = angleRange(options);
|
|
575
|
+
this.#pushShape("sector", center, { size: [r, r], ...angles }, options, variant);
|
|
576
|
+
}
|
|
577
|
+
/** Disc segment outline between a chord and its arc (reference `CircularSegment`). */
|
|
578
|
+
segment(center, radius, options = {}) {
|
|
579
|
+
this.#segmentVariant(center, radius, options, WIRE);
|
|
580
|
+
return this;
|
|
581
|
+
}
|
|
582
|
+
/** Solid disc segment (reference `CircularSegment`, the 3D `_s` counterpart). */
|
|
583
|
+
segment_s(center, radius, options = {}) {
|
|
584
|
+
this.#segmentVariant(center, radius, options, SOLID);
|
|
585
|
+
return this;
|
|
586
|
+
}
|
|
587
|
+
/** Dashed segment outline: the arc plus the chord. */
|
|
588
|
+
segment_d(center, radius, dash, options = {}) {
|
|
589
|
+
this.#segmentVariant(center, radius, options, dashed(dash));
|
|
590
|
+
return this;
|
|
591
|
+
}
|
|
592
|
+
#segmentVariant(center, radius, options, variant) {
|
|
593
|
+
const r = positive(radius, "radius");
|
|
594
|
+
const angles = angleRange(options);
|
|
595
|
+
this.#pushShape("segment", center, { size: [r, r], ...angles }, options, variant);
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* One joint instance for a path vertex. Degenerate wedges whose centre coincides with a
|
|
599
|
+
* neighbour are dropped, as in the 3D layer and the reference `emit_joint`.
|
|
600
|
+
*/
|
|
601
|
+
#jointCommand(previous, center, next, joint, options, dash, distance) {
|
|
602
|
+
const resolvedPrevious = vec2(previous, "previous");
|
|
603
|
+
const resolvedCenter = vec2(center, "center");
|
|
604
|
+
const resolvedNext = vec2(next, "next");
|
|
605
|
+
const same = (a, b) => a[0] === b[0] && a[1] === b[1];
|
|
606
|
+
if (same(resolvedPrevious, resolvedCenter) || same(resolvedCenter, resolvedNext))
|
|
607
|
+
return;
|
|
608
|
+
const width = finite(options.width ?? this.#defaults.line.width ?? 1, "width");
|
|
609
|
+
if (width <= 0)
|
|
610
|
+
throw new RangeError("width must be greater than zero");
|
|
611
|
+
jointValue(joint, "joint");
|
|
612
|
+
this.#joints.push({
|
|
613
|
+
kind: "joint2d",
|
|
614
|
+
previous: resolvedPrevious,
|
|
615
|
+
center: resolvedCenter,
|
|
616
|
+
next: resolvedNext,
|
|
617
|
+
color: color(options.color ?? this.#defaults.line.color),
|
|
618
|
+
width,
|
|
619
|
+
widthMode: options.widthMode ?? this.#defaults.line.widthMode ?? "screen",
|
|
620
|
+
joint,
|
|
621
|
+
space: spaceValue(options.space),
|
|
622
|
+
layer: layer(options.layer ?? this.#defaults.line.layer),
|
|
623
|
+
...(dash === undefined ? {} : { dash: [dash[0], dash[1], dash[2] + distance] }),
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
/** One instanced quad per shape is enough because the shader resolves the outline, and the
|
|
627
|
+
* dash variant only needs one extra per-instance pattern. */
|
|
628
|
+
#pushShape(shape, center, fields, options, variant) {
|
|
629
|
+
this.#assertOpen();
|
|
630
|
+
const thickness = positive(options.strokeWidth ?? DEFAULT_2D_STROKE_WIDTH, "strokeWidth");
|
|
631
|
+
const rotation = finite(options.rotation ?? 0, "rotation");
|
|
632
|
+
const cornerRadius = fields.cornerRadius ?? 0;
|
|
633
|
+
if (cornerRadius < 0)
|
|
634
|
+
throw new RangeError("cornerRadius must not be negative");
|
|
635
|
+
const dash = variant?.dash;
|
|
636
|
+
this.#shapes.push({
|
|
637
|
+
kind: "shape2d",
|
|
638
|
+
shape,
|
|
639
|
+
center: vec2(center, "center"),
|
|
640
|
+
size: fields.size ?? [0, 0],
|
|
641
|
+
vertexB: fields.vertexB ?? [0, 0],
|
|
642
|
+
vertexC: fields.vertexC ?? [0, 0],
|
|
643
|
+
cornerRadius,
|
|
644
|
+
thickness,
|
|
645
|
+
filled: variant.filled,
|
|
646
|
+
rotation,
|
|
647
|
+
startAngle: fields.startAngle ?? 0,
|
|
648
|
+
sweep: fields.sweep ?? 0,
|
|
649
|
+
sides: fields.sides ?? 0,
|
|
650
|
+
color: color(options.color),
|
|
651
|
+
space: spaceValue(options.space),
|
|
652
|
+
layer: layer(options.layer),
|
|
653
|
+
...(dash === undefined ? {} : { dash }),
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
get points() {
|
|
657
|
+
return this.#points;
|
|
658
|
+
}
|
|
659
|
+
get lines() {
|
|
660
|
+
return this.#lines;
|
|
661
|
+
}
|
|
662
|
+
get joints() {
|
|
663
|
+
return this.#joints;
|
|
664
|
+
}
|
|
665
|
+
get shapes() {
|
|
666
|
+
return this.#shapes;
|
|
667
|
+
}
|
|
668
|
+
}
|