vgpu-render 0.2.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 +30 -13
- package/dist/curve.d.ts +25 -0
- package/dist/curve.js +53 -0
- package/dist/frame-draw-list-2d.d.ts +128 -19
- package/dist/frame-draw-list-2d.js +347 -57
- package/dist/geometry-generators.d.ts +9 -0
- package/dist/geometry-generators.js +36 -0
- package/dist/gpu-debug-draw-2d.js +10 -2
- package/dist/shape2d-shader.js +292 -5
- package/package.json +1 -1
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
*
|
|
10
10
|
* See `camera-2d.ts` for what `world2d` and `screen` mean.
|
|
11
11
|
*/
|
|
12
|
+
import { gridLines2D } from "./geometry-generators.js";
|
|
13
|
+
import { sampleCurve2D } from "./curve.js";
|
|
14
|
+
const WIRE = { filled: false };
|
|
15
|
+
const SOLID = { filled: true };
|
|
12
16
|
const DEFAULT_2D_COLOR = [1, 1, 1, 1];
|
|
13
17
|
const DEFAULT_2D_LAYER = 1;
|
|
14
18
|
/** An unfilled shape still has to be visible, so a stroke defaults to one pixel. */
|
|
@@ -47,6 +51,21 @@ function angleRange(options) {
|
|
|
47
51
|
}
|
|
48
52
|
return { startAngle, sweep };
|
|
49
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
|
+
}
|
|
50
69
|
function vec2(value, name) {
|
|
51
70
|
if (value.length !== 2) {
|
|
52
71
|
throw new RangeError(`${name} must contain two values`);
|
|
@@ -127,7 +146,86 @@ export class FrameDrawList2D {
|
|
|
127
146
|
});
|
|
128
147
|
return this;
|
|
129
148
|
}
|
|
149
|
+
/** Straight segment (the 3D `draw_line`). */
|
|
130
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) {
|
|
131
229
|
this.#assertOpen();
|
|
132
230
|
const resolvedStart = vec2(start, "start");
|
|
133
231
|
const resolvedEnd = vec2(end, "end");
|
|
@@ -137,7 +235,7 @@ export class FrameDrawList2D {
|
|
|
137
235
|
const width = finite(options.width ?? this.#defaults.line.width ?? 1, "width");
|
|
138
236
|
if (width <= 0)
|
|
139
237
|
throw new RangeError("width must be greater than zero");
|
|
140
|
-
const dash =
|
|
238
|
+
const dash = dashArgument ?? this.#defaults.line.dash;
|
|
141
239
|
const joint = options.joint ?? this.#defaults.line.joint;
|
|
142
240
|
this.#lines.push({
|
|
143
241
|
kind: "line2d",
|
|
@@ -154,23 +252,31 @@ export class FrameDrawList2D {
|
|
|
154
252
|
});
|
|
155
253
|
return this;
|
|
156
254
|
}
|
|
157
|
-
ray(origin, direction, options = {}) {
|
|
158
|
-
return this.line(origin, [origin[0] + direction[0], origin[1] + direction[1]], options);
|
|
159
|
-
}
|
|
160
255
|
/**
|
|
161
256
|
* Path of segments, the 2D twin of `FrameDrawList.lineStrip`. Dash distance accumulates over
|
|
162
257
|
* the whole path and an open strip joins only its interior vertices, matching the 3D layer.
|
|
163
258
|
*/
|
|
164
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) {
|
|
165
275
|
this.#assertOpen();
|
|
166
|
-
const dash = options.dash ?? this.#defaults.line.dash;
|
|
167
276
|
const vertexDistances = [0];
|
|
168
277
|
let traveled = 0;
|
|
169
278
|
const segment = (a, b) => {
|
|
170
|
-
this
|
|
171
|
-
...options,
|
|
172
|
-
...(dash === undefined ? {} : { dash: [dash[0], dash[1], dash[2] + traveled] }),
|
|
173
|
-
});
|
|
279
|
+
this.#line(a, b, options, dash === undefined ? undefined : [dash[0], dash[1], dash[2] + traveled]);
|
|
174
280
|
traveled += Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
175
281
|
vertexDistances.push(traveled);
|
|
176
282
|
};
|
|
@@ -192,90 +298,233 @@ export class FrameDrawList2D {
|
|
|
192
298
|
polyline(points, options = {}, closed = false) {
|
|
193
299
|
return this.lineStrip(points, options, closed);
|
|
194
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
|
+
}
|
|
195
317
|
/**
|
|
196
318
|
* Axis-aligned grid of lines, the 2D twin of `FrameDrawList.grid` and reference `grid_2d`: `cells`
|
|
197
319
|
* are the cells per axis, so the drawn line count is one more on each side.
|
|
198
320
|
*/
|
|
199
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) {
|
|
200
329
|
this.#assertOpen();
|
|
330
|
+
const { rotation = 0, skew = [0, 0], outer = "none", ...lineOptions } = options;
|
|
201
331
|
const [columns, rows] = cells;
|
|
202
332
|
if (!Number.isInteger(columns) || !Number.isInteger(rows) || columns < 1 || rows < 1) {
|
|
203
333
|
throw new RangeError("grid cells must be positive integers");
|
|
204
334
|
}
|
|
205
335
|
const [stepX, stepY] = positiveSize(spacing, "spacing");
|
|
206
336
|
const [centerX, centerY] = vec2(center, "center");
|
|
207
|
-
const
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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"');
|
|
212
343
|
}
|
|
213
|
-
for (
|
|
214
|
-
|
|
215
|
-
this.line([centerX - halfWidth, y], [centerX + halfWidth, y], options);
|
|
344
|
+
for (const [a, b] of gridLines2D([columns, rows], [stepX, stepY], [centerX, centerY], angle, [skewX, skewY], outer)) {
|
|
345
|
+
this.#line(a, b, lineOptions, dash);
|
|
216
346
|
}
|
|
217
347
|
return this;
|
|
218
348
|
}
|
|
219
|
-
/**
|
|
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
|
+
*/
|
|
220
353
|
rect(center, size, options = {}) {
|
|
221
|
-
|
|
222
|
-
this.#pushShape("rect", center, { size: [full[0] / 2, full[1] / 2] }, options);
|
|
354
|
+
this.#rectVariant(center, size, options, WIRE);
|
|
223
355
|
return this;
|
|
224
356
|
}
|
|
225
|
-
/**
|
|
226
|
-
|
|
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) {
|
|
227
371
|
const full = positiveSize(size, "size");
|
|
228
|
-
this.#pushShape("
|
|
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));
|
|
229
387
|
return this;
|
|
230
388
|
}
|
|
231
|
-
|
|
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`). */
|
|
232
394
|
circle(center, radius, options = {}) {
|
|
233
|
-
|
|
234
|
-
this
|
|
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);
|
|
235
401
|
return this;
|
|
236
402
|
}
|
|
237
|
-
/**
|
|
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`). */
|
|
238
413
|
ellipse(center, radii, options = {}) {
|
|
239
|
-
|
|
240
|
-
this
|
|
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);
|
|
241
420
|
return this;
|
|
242
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
|
+
}
|
|
243
431
|
/**
|
|
244
|
-
* Circular arc from `startAngle` through `sweep`. It is a curve without an interior, so
|
|
245
|
-
*
|
|
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.
|
|
246
434
|
*/
|
|
247
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) {
|
|
248
445
|
const r = positive(radius, "radius");
|
|
249
446
|
const angles = angleRange(options);
|
|
250
|
-
this.#pushShape("arc", center, { size: [r, r], ...angles }, options);
|
|
251
|
-
return this;
|
|
447
|
+
this.#pushShape("arc", center, { size: [r, r], ...angles }, options, variant);
|
|
252
448
|
}
|
|
253
|
-
/** Ring
|
|
449
|
+
/** Ring outline between two radii (reference `Annulus`). */
|
|
254
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) {
|
|
255
465
|
const inner = nonNegative(innerRadius, "innerRadius");
|
|
256
466
|
const outer = positive(outerRadius, "outerRadius");
|
|
257
467
|
if (inner >= outer)
|
|
258
468
|
throw new RangeError("innerRadius must be smaller than outerRadius");
|
|
259
|
-
this.#pushShape("annulus", center, { size: [outer, inner] }, options);
|
|
260
|
-
return this;
|
|
469
|
+
this.#pushShape("annulus", center, { size: [outer, inner] }, options, variant);
|
|
261
470
|
}
|
|
262
|
-
/** Capsule whose straight section is `length` long along y (reference `Capsule2d
|
|
471
|
+
/** Capsule outline whose straight section is `length` long along y (reference `Capsule2d`). */
|
|
263
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) {
|
|
264
487
|
const r = positive(radius, "radius");
|
|
265
488
|
const straight = nonNegative(length, "length");
|
|
266
|
-
this.#pushShape("capsule", center, { size: [r, straight / 2] }, options);
|
|
267
|
-
return this;
|
|
489
|
+
this.#pushShape("capsule", center, { size: [r, straight / 2] }, options, variant);
|
|
268
490
|
}
|
|
269
|
-
/** Regular polygon with a vertex on the +x axis (reference `RegularPolygon
|
|
491
|
+
/** Regular polygon outline with a vertex on the +x axis (reference `RegularPolygon`). */
|
|
270
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) {
|
|
271
507
|
const r = positive(circumradius, "circumradius");
|
|
272
508
|
if (!Number.isInteger(sides) || sides < 3)
|
|
273
509
|
throw new RangeError("sides must be an integer of at least 3");
|
|
274
|
-
this.#pushShape("polygon", center, { size: [r, r], sides }, options);
|
|
275
|
-
return this;
|
|
510
|
+
this.#pushShape("polygon", center, { size: [r, r], sides }, options, variant);
|
|
276
511
|
}
|
|
277
|
-
/**
|
|
512
|
+
/** Triangle outline (reference `Triangle2d`); `rotation` turns about its centroid. */
|
|
278
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) {
|
|
279
528
|
const p0 = vec2(a, "a");
|
|
280
529
|
const p1 = vec2(b, "b");
|
|
281
530
|
const p2 = vec2(c, "c");
|
|
@@ -284,28 +533,66 @@ export class FrameDrawList2D {
|
|
|
284
533
|
size: [p0[0] - center[0], p0[1] - center[1]],
|
|
285
534
|
vertexB: [p1[0] - center[0], p1[1] - center[1]],
|
|
286
535
|
vertexC: [p2[0] - center[0], p2[1] - center[1]],
|
|
287
|
-
}, options);
|
|
288
|
-
return this;
|
|
536
|
+
}, options, variant);
|
|
289
537
|
}
|
|
290
|
-
/** Diamond with the given full diagonals (reference `Rhombus
|
|
538
|
+
/** Diamond outline with the given full diagonals (reference `Rhombus`). */
|
|
291
539
|
rhombus(center, diagonals, options = {}) {
|
|
292
|
-
|
|
293
|
-
this.#pushShape("rhombus", center, { size: [d[0] / 2, d[1] / 2] }, options);
|
|
540
|
+
this.#rhombusVariant(center, diagonals, options, WIRE);
|
|
294
541
|
return this;
|
|
295
542
|
}
|
|
296
|
-
/**
|
|
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`). */
|
|
297
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) {
|
|
298
573
|
const r = positive(radius, "radius");
|
|
299
574
|
const angles = angleRange(options);
|
|
300
|
-
this.#pushShape("sector", center, { size: [r, r], ...angles }, options);
|
|
301
|
-
return this;
|
|
575
|
+
this.#pushShape("sector", center, { size: [r, r], ...angles }, options, variant);
|
|
302
576
|
}
|
|
303
|
-
/** Disc
|
|
577
|
+
/** Disc segment outline between a chord and its arc (reference `CircularSegment`). */
|
|
304
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) {
|
|
305
593
|
const r = positive(radius, "radius");
|
|
306
594
|
const angles = angleRange(options);
|
|
307
|
-
this.#pushShape("segment", center, { size: [r, r], ...angles }, options);
|
|
308
|
-
return this;
|
|
595
|
+
this.#pushShape("segment", center, { size: [r, r], ...angles }, options, variant);
|
|
309
596
|
}
|
|
310
597
|
/**
|
|
311
598
|
* One joint instance for a path vertex. Degenerate wedges whose centre coincides with a
|
|
@@ -336,14 +623,16 @@ export class FrameDrawList2D {
|
|
|
336
623
|
...(dash === undefined ? {} : { dash: [dash[0], dash[1], dash[2] + distance] }),
|
|
337
624
|
});
|
|
338
625
|
}
|
|
339
|
-
/**
|
|
340
|
-
|
|
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) {
|
|
341
629
|
this.#assertOpen();
|
|
342
630
|
const thickness = positive(options.strokeWidth ?? DEFAULT_2D_STROKE_WIDTH, "strokeWidth");
|
|
343
631
|
const rotation = finite(options.rotation ?? 0, "rotation");
|
|
344
632
|
const cornerRadius = fields.cornerRadius ?? 0;
|
|
345
633
|
if (cornerRadius < 0)
|
|
346
634
|
throw new RangeError("cornerRadius must not be negative");
|
|
635
|
+
const dash = variant?.dash;
|
|
347
636
|
this.#shapes.push({
|
|
348
637
|
kind: "shape2d",
|
|
349
638
|
shape,
|
|
@@ -353,7 +642,7 @@ export class FrameDrawList2D {
|
|
|
353
642
|
vertexC: fields.vertexC ?? [0, 0],
|
|
354
643
|
cornerRadius,
|
|
355
644
|
thickness,
|
|
356
|
-
filled:
|
|
645
|
+
filled: variant.filled,
|
|
357
646
|
rotation,
|
|
358
647
|
startAngle: fields.startAngle ?? 0,
|
|
359
648
|
sweep: fields.sweep ?? 0,
|
|
@@ -361,6 +650,7 @@ export class FrameDrawList2D {
|
|
|
361
650
|
color: color(options.color),
|
|
362
651
|
space: spaceValue(options.space),
|
|
363
652
|
layer: layer(options.layer),
|
|
653
|
+
...(dash === undefined ? {} : { dash }),
|
|
364
654
|
});
|
|
365
655
|
}
|
|
366
656
|
get points() {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import type { Vec2 } from "./camera-2d.js";
|
|
1
2
|
import type { Vec3 } from "./frame-draw-list.js";
|
|
2
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[];
|
|
@@ -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
|
+
}
|
|
@@ -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;
|