@es3d/es3d 1.0.0 → 1.0.1

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.
Files changed (79) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +86 -86
  3. package/docs/SPEC.md +417 -417
  4. package/package.json +1 -1
  5. package/scripts/es3d-cli.mjs +377 -377
  6. package/src/cache/geometry-store.js +392 -392
  7. package/src/cache/mesh-bundle.js +123 -123
  8. package/src/cache/sha256.js +73 -73
  9. package/src/format/ast.js +238 -238
  10. package/src/format/colors.js +93 -93
  11. package/src/format/compact-parser.js +171 -171
  12. package/src/format/compact-tokenizer.js +165 -165
  13. package/src/format/errors.js +247 -247
  14. package/src/format/json.js +55 -55
  15. package/src/format/limits.js +14 -14
  16. package/src/format/parse.js +92 -92
  17. package/src/format/schema.js +617 -617
  18. package/src/format/serializer.js +333 -333
  19. package/src/format/validator.js +330 -330
  20. package/src/format/value-error.js +12 -12
  21. package/src/format/value-format.js +314 -314
  22. package/src/format/values.js +459 -459
  23. package/src/format/xml-parser.js +136 -136
  24. package/src/format/xml-tokenizer.js +237 -237
  25. package/src/geometry/contour.js +86 -86
  26. package/src/geometry/extrude.js +175 -175
  27. package/src/geometry/gear.js +85 -85
  28. package/src/geometry/geometry-cache.js +159 -159
  29. package/src/geometry/lod-error.js +178 -178
  30. package/src/geometry/mesh.js +458 -458
  31. package/src/geometry/primitives.js +299 -299
  32. package/src/geometry/registry.js +645 -645
  33. package/src/geometry/revolve.js +136 -136
  34. package/src/geometry/sweep.js +441 -441
  35. package/src/index.js +47 -47
  36. package/src/io/compression.js +67 -67
  37. package/src/io/convert.js +236 -236
  38. package/src/io/download.js +13 -13
  39. package/src/io/external-scene.js +127 -127
  40. package/src/io/file-loader.js +28 -28
  41. package/src/io/gltf.js +630 -630
  42. package/src/io/mesh-import.js +104 -104
  43. package/src/io/obj.js +352 -352
  44. package/src/io/step-brep.js +439 -439
  45. package/src/io/step-exact.js +1149 -1149
  46. package/src/io/step.js +1418 -1418
  47. package/src/io/stl.js +193 -193
  48. package/src/io/svg-import.js +787 -787
  49. package/src/material/materials.js +154 -154
  50. package/src/math/mat4.js +475 -475
  51. package/src/math/random.js +30 -30
  52. package/src/math/trig.js +65 -65
  53. package/src/math/vec3.js +81 -81
  54. package/src/optimize/optimizer.js +579 -579
  55. package/src/path/path-flatten.js +322 -322
  56. package/src/path/path-parser.js +332 -332
  57. package/src/path/shapes.js +129 -129
  58. package/src/path/triangulate.js +517 -517
  59. package/src/pipeline.js +74 -74
  60. package/src/render/camera.js +168 -168
  61. package/src/render/frame-builder.js +362 -362
  62. package/src/render/geometry-pool.js +149 -149
  63. package/src/render/lighting.js +149 -149
  64. package/src/render/orbit-controls.js +237 -237
  65. package/src/render/renderer.js +258 -249
  66. package/src/render/scroll-timeline.js +104 -104
  67. package/src/render/shaders/glsl.js +285 -285
  68. package/src/render/shaders/wgsl.js +252 -252
  69. package/src/render/stage.js +364 -352
  70. package/src/render/texture-cache.js +40 -40
  71. package/src/render/textures.js +223 -223
  72. package/src/render/webgl2/webgl2-backend.js +586 -532
  73. package/src/render/webgpu/webgpu-backend.js +482 -480
  74. package/src/scene/flatten.js +754 -754
  75. package/src/scene/picking.js +161 -161
  76. package/src/scene/scene-graph.js +320 -320
  77. package/src/transform/transform.js +211 -211
  78. package/src/wasm/kernels-binary.js +3 -3
  79. package/src/wasm/kernels.js +385 -385
@@ -1,299 +1,299 @@
1
- // Tessellation of the analytic primitives.
2
- //
3
- // Generators work in a canonical space (unit sphere, unit-radius cylinder of
4
- // height 1, unit cube ...). Element parameters that are pure scale factors
5
- // (radius, size, height) are moved into the instance transform, so every
6
- // sphere in a document shares one tessellated mesh per level of detail. Only
7
- // shape-defining ratios (cone taper, torus tube ratio and so on) create new meshes.
8
- // All triangles are counter-clockwise when seen from outside.
9
-
10
- import { Mesh, MeshBuilder } from './mesh.js';
11
- import { primitivesWasm } from '../wasm/kernels.js';
12
-
13
- const TAU = Math.PI * 2;
14
-
15
- /** Segment count for a level of detail: lod 0 is the finest. */
16
- export function lodSegments(base, lod, quality = 1, min = 3) {
17
- return Math.max(min, Math.round((base * quality) / (1 << lod)));
18
- }
19
-
20
- export function unitBox() {
21
- const b = new MeshBuilder('triangles', 24, 36);
22
- const faces = [
23
- [[1, 0, 0], [0, 0, -1], [0, 1, 0]],
24
- [[-1, 0, 0], [0, 0, 1], [0, 1, 0]],
25
- [[0, 1, 0], [1, 0, 0], [0, 0, -1]],
26
- [[0, -1, 0], [1, 0, 0], [0, 0, 1]],
27
- [[0, 0, 1], [1, 0, 0], [0, 1, 0]],
28
- [[0, 0, -1], [-1, 0, 0], [0, 1, 0]],
29
- ];
30
- for (const [n, a, c] of faces) {
31
- const v = [];
32
- for (const [su, sv, u, w] of [[-1, -1, 0, 0], [1, -1, 1, 0], [1, 1, 1, 1], [-1, 1, 0, 1]]) {
33
- v.push(
34
- b.vertex(
35
- n[0] * 0.5 + a[0] * 0.5 * su + c[0] * 0.5 * sv,
36
- n[1] * 0.5 + a[1] * 0.5 * su + c[1] * 0.5 * sv,
37
- n[2] * 0.5 + a[2] * 0.5 * su + c[2] * 0.5 * sv,
38
- n[0], n[1], n[2], u, w,
39
- ),
40
- );
41
- }
42
- b.quad(v[0], v[1], v[2], v[3]);
43
- }
44
- return b.build();
45
- }
46
-
47
- /**
48
- * Box with rounded edges (Minkowski sum of an inner box and a sphere of radius r).
49
- * Each face is a grid whose rows follow the rounding angle uniformly.
50
- */
51
- export function roundedBox(w, h, d, r, seg) {
52
- const fast = primitivesWasm.roundedBox(w, h, d, r, seg);
53
- if (fast) return new Mesh({ ...fast, kind: 'triangles' });
54
- const H = [w / 2, h / 2, d / 2];
55
- const side = 2 * seg + 2;
56
- const b = new MeshBuilder('triangles', 6 * side * side, 6 * (side - 1) * (side - 1) * 6);
57
- const p = new Float64Array(3);
58
- const coords = (Hh) => {
59
- const inner = Hh - r;
60
- const out = [];
61
- for (let k = seg; k >= 1; k--) out.push(-inner - r * Math.tan(((k / seg) * Math.PI) / 4));
62
- out.push(-inner, inner);
63
- for (let k = 1; k <= seg; k++) out.push(inner + r * Math.tan(((k / seg) * Math.PI) / 4));
64
- return out;
65
- };
66
- const faces = [
67
- [0, 1, [0, 0, -1], [0, 1, 0]],
68
- [0, -1, [0, 0, 1], [0, 1, 0]],
69
- [1, 1, [1, 0, 0], [0, 0, -1]],
70
- [1, -1, [1, 0, 0], [0, 0, 1]],
71
- [2, 1, [1, 0, 0], [0, 1, 0]],
72
- [2, -1, [-1, 0, 0], [0, 1, 0]],
73
- ];
74
- for (const [axis, sign, au, av] of faces) {
75
- const ua = au.findIndex((x) => x !== 0);
76
- const va = av.findIndex((x) => x !== 0);
77
- const us = coords(H[ua]).map((x) => x * au[ua]);
78
- const vs = coords(H[va]).map((x) => x * av[va]);
79
- const n = us.length;
80
- const base = b.vertexCount;
81
- for (let j = 0; j < n; j++) {
82
- for (let i = 0; i < n; i++) {
83
- p[axis] = sign * H[axis];
84
- p[ua] = us[i];
85
- p[va] = vs[j];
86
- const c0 = Math.max(-(H[0] - r), Math.min(H[0] - r, p[0]));
87
- const c1 = Math.max(-(H[1] - r), Math.min(H[1] - r, p[1]));
88
- const c2 = Math.max(-(H[2] - r), Math.min(H[2] - r, p[2]));
89
- let nx = p[0] - c0, ny = p[1] - c1, nz = p[2] - c2;
90
- const l = Math.hypot(nx, ny, nz);
91
- if (l > 1e-12) {
92
- nx /= l;
93
- ny /= l;
94
- nz /= l;
95
- } else {
96
- // only reachable for r = 0: use the face normal
97
- nx = axis === 0 ? sign : 0;
98
- ny = axis === 1 ? sign : 0;
99
- nz = axis === 2 ? sign : 0;
100
- }
101
- b.vertex(c0 + nx * r, c1 + ny * r, c2 + nz * r, nx, ny, nz, (us[i] / H[ua] / au[ua] + 1) / 2, (vs[j] / H[va] / av[va] + 1) / 2);
102
- }
103
- }
104
- for (let j = 0; j < n - 1; j++) {
105
- for (let i = 0; i < n - 1; i++) {
106
- const a = base + j * n + i;
107
- b.quad(a, a + 1, a + n + 1, a + n);
108
- }
109
- }
110
- }
111
- return b.build();
112
- }
113
-
114
- /** Unit-radius UV sphere. */
115
- export function unitSphere(segments) {
116
- const fast = primitivesWasm.sphere(segments);
117
- if (fast) return new Mesh({ ...fast, kind: 'triangles' });
118
- const w = Math.max(3, segments);
119
- const hRings = Math.max(2, Math.round(w / 2));
120
- // at the poles one triangle of each quad is degenerate and skipped
121
- const b = new MeshBuilder('triangles', (w + 1) * (hRings + 1), w * (hRings - 1) * 6);
122
- for (let j = 0; j <= hRings; j++) {
123
- const th = (j / hRings) * Math.PI;
124
- const st = Math.sin(th), ct = Math.cos(th);
125
- for (let i = 0; i <= w; i++) {
126
- const ph = ((i % w) / w) * TAU; // the seam column repeats column 0 exactly
127
- const x = st * Math.cos(ph), y = ct, z = -st * Math.sin(ph);
128
- b.vertex(x, y, z, x, y, z, i / w, 1 - j / hRings);
129
- }
130
- }
131
- const row = w + 1;
132
- for (let j = 0; j < hRings; j++) {
133
- for (let i = 0; i < w; i++) {
134
- const a = j * row + i, bb = (j + 1) * row + i, c = bb + 1, d = a + 1;
135
- // at the poles one triangle of each quad is degenerate and skipped
136
- if (j !== hRings - 1) b.tri(a, bb, c);
137
- if (j !== 0) b.tri(a, c, d);
138
- }
139
- }
140
- return b.build();
141
- }
142
-
143
- /** Cylinder / truncated cone of bottom radius 1, top radius `top`, height 1, centred on the origin. */
144
- export function unitCylinder(top, segments, caps) {
145
- const fast = primitivesWasm.cylinder(top, segments, caps);
146
- if (fast) return new Mesh({ ...fast, kind: 'triangles' });
147
- const s = Math.max(3, segments);
148
- const b = new MeshBuilder(
149
- 'triangles',
150
- (s + 1) * 2 + (caps ? s + 2 + (top > 0 ? s + 2 : 0) : 0),
151
- s * (top > 0 ? 6 : 3) + (caps ? s * 3 * (top > 0 ? 2 : 1) : 0),
152
- );
153
- const slope = 1 - top;
154
- const ring = [];
155
- for (let i = 0; i <= s; i++) {
156
- const ph = ((i % s) / s) * TAU;
157
- ring.push([Math.cos(ph), -Math.sin(ph)]);
158
- }
159
- const nrm = (c, sN) => {
160
- const l = Math.hypot(c, slope, sN);
161
- return [c / l, slope / l, sN / l];
162
- };
163
- const bottom = [];
164
- const topV = [];
165
- for (let i = 0; i <= s; i++) {
166
- const [c, sN] = ring[i];
167
- const n = nrm(c, sN);
168
- bottom.push(b.vertex(c, -0.5, sN, n[0], n[1], n[2], i / s, 0));
169
- if (top > 0) {
170
- topV.push(b.vertex(c * top, 0.5, sN * top, n[0], n[1], n[2], i / s, 1));
171
- } else {
172
- // apex: use the normal halfway through the segment for smooth shading
173
- const ph = ((i + 0.5) / s) * TAU;
174
- const m = nrm(Math.cos(ph), -Math.sin(ph));
175
- topV.push(b.vertex(0, 0.5, 0, m[0], m[1], m[2], (i + 0.5) / s, 1));
176
- }
177
- }
178
- for (let i = 0; i < s; i++) {
179
- if (top > 0) b.quad(bottom[i], bottom[i + 1], topV[i + 1], topV[i]);
180
- else b.tri(bottom[i], bottom[i + 1], topV[i]);
181
- }
182
- if (caps) {
183
- const cb = b.vertex(0, -0.5, 0, 0, -1, 0, 0.5, 0.5);
184
- const rb = [];
185
- for (let i = 0; i <= s; i++) rb.push(b.vertex(ring[i][0], -0.5, ring[i][1], 0, -1, 0, ring[i][0] * 0.5 + 0.5, ring[i][1] * 0.5 + 0.5));
186
- for (let i = 0; i < s; i++) b.tri(cb, rb[i + 1], rb[i]);
187
- if (top > 0) {
188
- const ct = b.vertex(0, 0.5, 0, 0, 1, 0, 0.5, 0.5);
189
- const rt = [];
190
- for (let i = 0; i <= s; i++) rt.push(b.vertex(ring[i][0] * top, 0.5, ring[i][1] * top, 0, 1, 0, ring[i][0] * 0.5 + 0.5, ring[i][1] * 0.5 + 0.5));
191
- for (let i = 0; i < s; i++) b.tri(ct, rt[i], rt[i + 1]);
192
- }
193
- }
194
- return b.build();
195
- }
196
-
197
- /** Capsule of radius 1 whose straight section has length L (total height L + 2). */
198
- export function unitCapsule(L, segments) {
199
- const fast = primitivesWasm.capsule(L, segments);
200
- if (fast) return new Mesh({ ...fast, kind: 'triangles' });
201
- const w = Math.max(3, segments);
202
- const hr = Math.max(1, Math.round(w / 4));
203
- const rings = [];
204
- for (let k = 0; k <= hr; k++) rings.push([(k / hr) * (Math.PI / 2), L / 2]);
205
- for (let k = 0; k <= hr; k++) rings.push([Math.PI / 2 + (k / hr) * (Math.PI / 2), -L / 2]);
206
- const total = L + 2;
207
- const nRings = rings.length;
208
- const b = new MeshBuilder('triangles', nRings * (w + 1), w * 3 * (2 * (nRings - 1) - 2));
209
- for (let j = 0; j < rings.length; j++) {
210
- const [th, yo] = rings[j];
211
- const st = Math.sin(th), ct = Math.cos(th);
212
- const y = ct + yo;
213
- for (let i = 0; i <= w; i++) {
214
- const ph = ((i % w) / w) * TAU;
215
- const x = st * Math.cos(ph), z = -st * Math.sin(ph);
216
- b.vertex(x, y, z, x, ct, z, i / w, (y + total / 2) / total);
217
- }
218
- }
219
- const row = w + 1;
220
- const nr = rings.length;
221
- for (let j = 0; j < nr - 1; j++) {
222
- for (let i = 0; i < w; i++) {
223
- const a = j * row + i, bb = (j + 1) * row + i, c = bb + 1, d = a + 1;
224
- if (j !== nr - 2) b.tri(a, bb, c);
225
- if (j !== 0) b.tri(a, c, d);
226
- }
227
- }
228
- return b.build();
229
- }
230
-
231
- /** Unit plane in XZ facing +Y. */
232
- export function unitPlane() {
233
- const b = new MeshBuilder('triangles', 4, 6);
234
- const a = b.vertex(-0.5, 0, 0.5, 0, 1, 0, 0, 0);
235
- const c = b.vertex(0.5, 0, 0.5, 0, 1, 0, 1, 0);
236
- const d = b.vertex(0.5, 0, -0.5, 0, 1, 0, 1, 1);
237
- const e = b.vertex(-0.5, 0, -0.5, 0, 1, 0, 0, 1);
238
- b.quad(a, c, d, e);
239
- return b.build();
240
- }
241
-
242
- /** Torus with major radius 1 and tube radius t, around Y, swept over `arcDeg`. */
243
- export function unitTorus(t, arcDeg, segments, tubeSegments) {
244
- const fast = primitivesWasm.torus(t, arcDeg, segments, tubeSegments);
245
- if (fast) return new Mesh({ ...fast, kind: 'triangles' });
246
- const full = arcDeg >= 360 - 1e-9;
247
- const arc = (Math.min(arcDeg, 360) * Math.PI) / 180;
248
- const ts = Math.max(3, Math.round(segments * (full ? 1 : arcDeg / 360)) || 3);
249
- const rs = Math.max(3, tubeSegments);
250
- const b = new MeshBuilder('triangles', (ts + 1) * (rs + 1) + (full ? 0 : 2 * (rs + 2)), ts * rs * 6 + (full ? 0 : 2 * rs * 3));
251
- for (let i = 0; i <= ts; i++) {
252
- const ph = full ? ((i % ts) / ts) * TAU : (i / ts) * arc;
253
- const cp = Math.cos(ph), sp = Math.sin(ph);
254
- for (let j = 0; j <= rs; j++) {
255
- const th = ((j % rs) / rs) * TAU;
256
- const ct = Math.cos(th), st = Math.sin(th);
257
- const R = 1 + t * ct;
258
- b.vertex(R * cp, t * st, -R * sp, ct * cp, st, -ct * sp, i / ts, j / rs);
259
- }
260
- }
261
- const row = rs + 1;
262
- for (let i = 0; i < ts; i++) {
263
- for (let j = 0; j < rs; j++) {
264
- const a = i * row + j;
265
- b.quad(a, a + row, a + row + 1, a + 1);
266
- }
267
- }
268
- if (!full) {
269
- // end caps (discs) so a partial torus is closed
270
- for (const [ph, dir] of [[0, 1], [arc, -1]]) {
271
- const cp = Math.cos(ph), sp = Math.sin(ph);
272
- // outward normal: opposite to the sweep direction at the start, along it at the end
273
- const tx = -sp, tz = -cp;
274
- const nx = -tx * dir, nz = -tz * dir;
275
- const center = b.vertex(cp, 0, -sp, nx, 0, nz, 0.5, 0.5);
276
- const rim = [];
277
- for (let j = 0; j <= rs; j++) {
278
- const th = ((j % rs) / rs) * TAU;
279
- const R = 1 + t * Math.cos(th);
280
- rim.push(b.vertex(R * cp, t * Math.sin(th), -R * sp, nx, 0, nz, 0.5 + Math.cos(th) * 0.5, 0.5 + Math.sin(th) * 0.5));
281
- }
282
- for (let j = 0; j < rs; j++) {
283
- if (dir > 0) b.tri(center, rim[j], rim[j + 1]);
284
- else b.tri(center, rim[j + 1], rim[j]);
285
- }
286
- }
287
- }
288
- return b.build();
289
- }
290
-
291
- /** Hairline polyline through 3D points (rendered with the line pipeline). */
292
- export function lineMesh(points, closed) {
293
- const n = points.length / 3;
294
- const b = new MeshBuilder('lines', n, 2 * (n - 1) + (closed && n > 2 ? 2 : 0));
295
- for (let i = 0; i < n; i++) b.vertex(points[i * 3], points[i * 3 + 1], points[i * 3 + 2], 0, 0, 0, i / Math.max(1, n - 1), 0);
296
- for (let i = 0; i < n - 1; i++) b.line(i, i + 1);
297
- if (closed && n > 2) b.line(n - 1, 0);
298
- return b.build();
299
- }
1
+ // Tessellation of the analytic primitives.
2
+ //
3
+ // Generators work in a canonical space (unit sphere, unit-radius cylinder of
4
+ // height 1, unit cube ...). Element parameters that are pure scale factors
5
+ // (radius, size, height) are moved into the instance transform, so every
6
+ // sphere in a document shares one tessellated mesh per level of detail. Only
7
+ // shape-defining ratios (cone taper, torus tube ratio and so on) create new meshes.
8
+ // All triangles are counter-clockwise when seen from outside.
9
+
10
+ import { Mesh, MeshBuilder } from './mesh.js';
11
+ import { primitivesWasm } from '../wasm/kernels.js';
12
+
13
+ const TAU = Math.PI * 2;
14
+
15
+ /** Segment count for a level of detail: lod 0 is the finest. */
16
+ export function lodSegments(base, lod, quality = 1, min = 3) {
17
+ return Math.max(min, Math.round((base * quality) / (1 << lod)));
18
+ }
19
+
20
+ export function unitBox() {
21
+ const b = new MeshBuilder('triangles', 24, 36);
22
+ const faces = [
23
+ [[1, 0, 0], [0, 0, -1], [0, 1, 0]],
24
+ [[-1, 0, 0], [0, 0, 1], [0, 1, 0]],
25
+ [[0, 1, 0], [1, 0, 0], [0, 0, -1]],
26
+ [[0, -1, 0], [1, 0, 0], [0, 0, 1]],
27
+ [[0, 0, 1], [1, 0, 0], [0, 1, 0]],
28
+ [[0, 0, -1], [-1, 0, 0], [0, 1, 0]],
29
+ ];
30
+ for (const [n, a, c] of faces) {
31
+ const v = [];
32
+ for (const [su, sv, u, w] of [[-1, -1, 0, 0], [1, -1, 1, 0], [1, 1, 1, 1], [-1, 1, 0, 1]]) {
33
+ v.push(
34
+ b.vertex(
35
+ n[0] * 0.5 + a[0] * 0.5 * su + c[0] * 0.5 * sv,
36
+ n[1] * 0.5 + a[1] * 0.5 * su + c[1] * 0.5 * sv,
37
+ n[2] * 0.5 + a[2] * 0.5 * su + c[2] * 0.5 * sv,
38
+ n[0], n[1], n[2], u, w,
39
+ ),
40
+ );
41
+ }
42
+ b.quad(v[0], v[1], v[2], v[3]);
43
+ }
44
+ return b.build();
45
+ }
46
+
47
+ /**
48
+ * Box with rounded edges (Minkowski sum of an inner box and a sphere of radius r).
49
+ * Each face is a grid whose rows follow the rounding angle uniformly.
50
+ */
51
+ export function roundedBox(w, h, d, r, seg) {
52
+ const fast = primitivesWasm.roundedBox(w, h, d, r, seg);
53
+ if (fast) return new Mesh({ ...fast, kind: 'triangles' });
54
+ const H = [w / 2, h / 2, d / 2];
55
+ const side = 2 * seg + 2;
56
+ const b = new MeshBuilder('triangles', 6 * side * side, 6 * (side - 1) * (side - 1) * 6);
57
+ const p = new Float64Array(3);
58
+ const coords = (Hh) => {
59
+ const inner = Hh - r;
60
+ const out = [];
61
+ for (let k = seg; k >= 1; k--) out.push(-inner - r * Math.tan(((k / seg) * Math.PI) / 4));
62
+ out.push(-inner, inner);
63
+ for (let k = 1; k <= seg; k++) out.push(inner + r * Math.tan(((k / seg) * Math.PI) / 4));
64
+ return out;
65
+ };
66
+ const faces = [
67
+ [0, 1, [0, 0, -1], [0, 1, 0]],
68
+ [0, -1, [0, 0, 1], [0, 1, 0]],
69
+ [1, 1, [1, 0, 0], [0, 0, -1]],
70
+ [1, -1, [1, 0, 0], [0, 0, 1]],
71
+ [2, 1, [1, 0, 0], [0, 1, 0]],
72
+ [2, -1, [-1, 0, 0], [0, 1, 0]],
73
+ ];
74
+ for (const [axis, sign, au, av] of faces) {
75
+ const ua = au.findIndex((x) => x !== 0);
76
+ const va = av.findIndex((x) => x !== 0);
77
+ const us = coords(H[ua]).map((x) => x * au[ua]);
78
+ const vs = coords(H[va]).map((x) => x * av[va]);
79
+ const n = us.length;
80
+ const base = b.vertexCount;
81
+ for (let j = 0; j < n; j++) {
82
+ for (let i = 0; i < n; i++) {
83
+ p[axis] = sign * H[axis];
84
+ p[ua] = us[i];
85
+ p[va] = vs[j];
86
+ const c0 = Math.max(-(H[0] - r), Math.min(H[0] - r, p[0]));
87
+ const c1 = Math.max(-(H[1] - r), Math.min(H[1] - r, p[1]));
88
+ const c2 = Math.max(-(H[2] - r), Math.min(H[2] - r, p[2]));
89
+ let nx = p[0] - c0, ny = p[1] - c1, nz = p[2] - c2;
90
+ const l = Math.hypot(nx, ny, nz);
91
+ if (l > 1e-12) {
92
+ nx /= l;
93
+ ny /= l;
94
+ nz /= l;
95
+ } else {
96
+ // only reachable for r = 0: use the face normal
97
+ nx = axis === 0 ? sign : 0;
98
+ ny = axis === 1 ? sign : 0;
99
+ nz = axis === 2 ? sign : 0;
100
+ }
101
+ b.vertex(c0 + nx * r, c1 + ny * r, c2 + nz * r, nx, ny, nz, (us[i] / H[ua] / au[ua] + 1) / 2, (vs[j] / H[va] / av[va] + 1) / 2);
102
+ }
103
+ }
104
+ for (let j = 0; j < n - 1; j++) {
105
+ for (let i = 0; i < n - 1; i++) {
106
+ const a = base + j * n + i;
107
+ b.quad(a, a + 1, a + n + 1, a + n);
108
+ }
109
+ }
110
+ }
111
+ return b.build();
112
+ }
113
+
114
+ /** Unit-radius UV sphere. */
115
+ export function unitSphere(segments) {
116
+ const fast = primitivesWasm.sphere(segments);
117
+ if (fast) return new Mesh({ ...fast, kind: 'triangles' });
118
+ const w = Math.max(3, segments);
119
+ const hRings = Math.max(2, Math.round(w / 2));
120
+ // at the poles one triangle of each quad is degenerate and skipped
121
+ const b = new MeshBuilder('triangles', (w + 1) * (hRings + 1), w * (hRings - 1) * 6);
122
+ for (let j = 0; j <= hRings; j++) {
123
+ const th = (j / hRings) * Math.PI;
124
+ const st = Math.sin(th), ct = Math.cos(th);
125
+ for (let i = 0; i <= w; i++) {
126
+ const ph = ((i % w) / w) * TAU; // the seam column repeats column 0 exactly
127
+ const x = st * Math.cos(ph), y = ct, z = -st * Math.sin(ph);
128
+ b.vertex(x, y, z, x, y, z, i / w, 1 - j / hRings);
129
+ }
130
+ }
131
+ const row = w + 1;
132
+ for (let j = 0; j < hRings; j++) {
133
+ for (let i = 0; i < w; i++) {
134
+ const a = j * row + i, bb = (j + 1) * row + i, c = bb + 1, d = a + 1;
135
+ // at the poles one triangle of each quad is degenerate and skipped
136
+ if (j !== hRings - 1) b.tri(a, bb, c);
137
+ if (j !== 0) b.tri(a, c, d);
138
+ }
139
+ }
140
+ return b.build();
141
+ }
142
+
143
+ /** Cylinder / truncated cone of bottom radius 1, top radius `top`, height 1, centred on the origin. */
144
+ export function unitCylinder(top, segments, caps) {
145
+ const fast = primitivesWasm.cylinder(top, segments, caps);
146
+ if (fast) return new Mesh({ ...fast, kind: 'triangles' });
147
+ const s = Math.max(3, segments);
148
+ const b = new MeshBuilder(
149
+ 'triangles',
150
+ (s + 1) * 2 + (caps ? s + 2 + (top > 0 ? s + 2 : 0) : 0),
151
+ s * (top > 0 ? 6 : 3) + (caps ? s * 3 * (top > 0 ? 2 : 1) : 0),
152
+ );
153
+ const slope = 1 - top;
154
+ const ring = [];
155
+ for (let i = 0; i <= s; i++) {
156
+ const ph = ((i % s) / s) * TAU;
157
+ ring.push([Math.cos(ph), -Math.sin(ph)]);
158
+ }
159
+ const nrm = (c, sN) => {
160
+ const l = Math.hypot(c, slope, sN);
161
+ return [c / l, slope / l, sN / l];
162
+ };
163
+ const bottom = [];
164
+ const topV = [];
165
+ for (let i = 0; i <= s; i++) {
166
+ const [c, sN] = ring[i];
167
+ const n = nrm(c, sN);
168
+ bottom.push(b.vertex(c, -0.5, sN, n[0], n[1], n[2], i / s, 0));
169
+ if (top > 0) {
170
+ topV.push(b.vertex(c * top, 0.5, sN * top, n[0], n[1], n[2], i / s, 1));
171
+ } else {
172
+ // apex: use the normal halfway through the segment for smooth shading
173
+ const ph = ((i + 0.5) / s) * TAU;
174
+ const m = nrm(Math.cos(ph), -Math.sin(ph));
175
+ topV.push(b.vertex(0, 0.5, 0, m[0], m[1], m[2], (i + 0.5) / s, 1));
176
+ }
177
+ }
178
+ for (let i = 0; i < s; i++) {
179
+ if (top > 0) b.quad(bottom[i], bottom[i + 1], topV[i + 1], topV[i]);
180
+ else b.tri(bottom[i], bottom[i + 1], topV[i]);
181
+ }
182
+ if (caps) {
183
+ const cb = b.vertex(0, -0.5, 0, 0, -1, 0, 0.5, 0.5);
184
+ const rb = [];
185
+ for (let i = 0; i <= s; i++) rb.push(b.vertex(ring[i][0], -0.5, ring[i][1], 0, -1, 0, ring[i][0] * 0.5 + 0.5, ring[i][1] * 0.5 + 0.5));
186
+ for (let i = 0; i < s; i++) b.tri(cb, rb[i + 1], rb[i]);
187
+ if (top > 0) {
188
+ const ct = b.vertex(0, 0.5, 0, 0, 1, 0, 0.5, 0.5);
189
+ const rt = [];
190
+ for (let i = 0; i <= s; i++) rt.push(b.vertex(ring[i][0] * top, 0.5, ring[i][1] * top, 0, 1, 0, ring[i][0] * 0.5 + 0.5, ring[i][1] * 0.5 + 0.5));
191
+ for (let i = 0; i < s; i++) b.tri(ct, rt[i], rt[i + 1]);
192
+ }
193
+ }
194
+ return b.build();
195
+ }
196
+
197
+ /** Capsule of radius 1 whose straight section has length L (total height L + 2). */
198
+ export function unitCapsule(L, segments) {
199
+ const fast = primitivesWasm.capsule(L, segments);
200
+ if (fast) return new Mesh({ ...fast, kind: 'triangles' });
201
+ const w = Math.max(3, segments);
202
+ const hr = Math.max(1, Math.round(w / 4));
203
+ const rings = [];
204
+ for (let k = 0; k <= hr; k++) rings.push([(k / hr) * (Math.PI / 2), L / 2]);
205
+ for (let k = 0; k <= hr; k++) rings.push([Math.PI / 2 + (k / hr) * (Math.PI / 2), -L / 2]);
206
+ const total = L + 2;
207
+ const nRings = rings.length;
208
+ const b = new MeshBuilder('triangles', nRings * (w + 1), w * 3 * (2 * (nRings - 1) - 2));
209
+ for (let j = 0; j < rings.length; j++) {
210
+ const [th, yo] = rings[j];
211
+ const st = Math.sin(th), ct = Math.cos(th);
212
+ const y = ct + yo;
213
+ for (let i = 0; i <= w; i++) {
214
+ const ph = ((i % w) / w) * TAU;
215
+ const x = st * Math.cos(ph), z = -st * Math.sin(ph);
216
+ b.vertex(x, y, z, x, ct, z, i / w, (y + total / 2) / total);
217
+ }
218
+ }
219
+ const row = w + 1;
220
+ const nr = rings.length;
221
+ for (let j = 0; j < nr - 1; j++) {
222
+ for (let i = 0; i < w; i++) {
223
+ const a = j * row + i, bb = (j + 1) * row + i, c = bb + 1, d = a + 1;
224
+ if (j !== nr - 2) b.tri(a, bb, c);
225
+ if (j !== 0) b.tri(a, c, d);
226
+ }
227
+ }
228
+ return b.build();
229
+ }
230
+
231
+ /** Unit plane in XZ facing +Y. */
232
+ export function unitPlane() {
233
+ const b = new MeshBuilder('triangles', 4, 6);
234
+ const a = b.vertex(-0.5, 0, 0.5, 0, 1, 0, 0, 0);
235
+ const c = b.vertex(0.5, 0, 0.5, 0, 1, 0, 1, 0);
236
+ const d = b.vertex(0.5, 0, -0.5, 0, 1, 0, 1, 1);
237
+ const e = b.vertex(-0.5, 0, -0.5, 0, 1, 0, 0, 1);
238
+ b.quad(a, c, d, e);
239
+ return b.build();
240
+ }
241
+
242
+ /** Torus with major radius 1 and tube radius t, around Y, swept over `arcDeg`. */
243
+ export function unitTorus(t, arcDeg, segments, tubeSegments) {
244
+ const fast = primitivesWasm.torus(t, arcDeg, segments, tubeSegments);
245
+ if (fast) return new Mesh({ ...fast, kind: 'triangles' });
246
+ const full = arcDeg >= 360 - 1e-9;
247
+ const arc = (Math.min(arcDeg, 360) * Math.PI) / 180;
248
+ const ts = Math.max(3, Math.round(segments * (full ? 1 : arcDeg / 360)) || 3);
249
+ const rs = Math.max(3, tubeSegments);
250
+ const b = new MeshBuilder('triangles', (ts + 1) * (rs + 1) + (full ? 0 : 2 * (rs + 2)), ts * rs * 6 + (full ? 0 : 2 * rs * 3));
251
+ for (let i = 0; i <= ts; i++) {
252
+ const ph = full ? ((i % ts) / ts) * TAU : (i / ts) * arc;
253
+ const cp = Math.cos(ph), sp = Math.sin(ph);
254
+ for (let j = 0; j <= rs; j++) {
255
+ const th = ((j % rs) / rs) * TAU;
256
+ const ct = Math.cos(th), st = Math.sin(th);
257
+ const R = 1 + t * ct;
258
+ b.vertex(R * cp, t * st, -R * sp, ct * cp, st, -ct * sp, i / ts, j / rs);
259
+ }
260
+ }
261
+ const row = rs + 1;
262
+ for (let i = 0; i < ts; i++) {
263
+ for (let j = 0; j < rs; j++) {
264
+ const a = i * row + j;
265
+ b.quad(a, a + row, a + row + 1, a + 1);
266
+ }
267
+ }
268
+ if (!full) {
269
+ // end caps (discs) so a partial torus is closed
270
+ for (const [ph, dir] of [[0, 1], [arc, -1]]) {
271
+ const cp = Math.cos(ph), sp = Math.sin(ph);
272
+ // outward normal: opposite to the sweep direction at the start, along it at the end
273
+ const tx = -sp, tz = -cp;
274
+ const nx = -tx * dir, nz = -tz * dir;
275
+ const center = b.vertex(cp, 0, -sp, nx, 0, nz, 0.5, 0.5);
276
+ const rim = [];
277
+ for (let j = 0; j <= rs; j++) {
278
+ const th = ((j % rs) / rs) * TAU;
279
+ const R = 1 + t * Math.cos(th);
280
+ rim.push(b.vertex(R * cp, t * Math.sin(th), -R * sp, nx, 0, nz, 0.5 + Math.cos(th) * 0.5, 0.5 + Math.sin(th) * 0.5));
281
+ }
282
+ for (let j = 0; j < rs; j++) {
283
+ if (dir > 0) b.tri(center, rim[j], rim[j + 1]);
284
+ else b.tri(center, rim[j + 1], rim[j]);
285
+ }
286
+ }
287
+ }
288
+ return b.build();
289
+ }
290
+
291
+ /** Hairline polyline through 3D points (rendered with the line pipeline). */
292
+ export function lineMesh(points, closed) {
293
+ const n = points.length / 3;
294
+ const b = new MeshBuilder('lines', n, 2 * (n - 1) + (closed && n > 2 ? 2 : 0));
295
+ for (let i = 0; i < n; i++) b.vertex(points[i * 3], points[i * 3 + 1], points[i * 3 + 2], 0, 0, 0, i / Math.max(1, n - 1), 0);
296
+ for (let i = 0; i < n - 1; i++) b.line(i, i + 1);
297
+ if (closed && n > 2) b.line(n - 1, 0);
298
+ return b.build();
299
+ }