@es3d/es3d 1.0.0 → 1.0.2

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 (80) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +95 -86
  3. package/docs/SPEC.md +426 -417
  4. package/package.json +1 -1
  5. package/scripts/es3d-cli.mjs +390 -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 +132 -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-pack.js +1461 -0
  42. package/src/io/gltf.js +630 -630
  43. package/src/io/mesh-import.js +104 -104
  44. package/src/io/obj.js +352 -352
  45. package/src/io/step-brep.js +439 -439
  46. package/src/io/step-exact.js +1149 -1149
  47. package/src/io/step.js +1418 -1418
  48. package/src/io/stl.js +193 -193
  49. package/src/io/svg-import.js +787 -787
  50. package/src/material/materials.js +154 -154
  51. package/src/math/mat4.js +475 -475
  52. package/src/math/random.js +30 -30
  53. package/src/math/trig.js +65 -65
  54. package/src/math/vec3.js +81 -81
  55. package/src/optimize/optimizer.js +604 -579
  56. package/src/path/path-flatten.js +322 -322
  57. package/src/path/path-parser.js +332 -332
  58. package/src/path/shapes.js +129 -129
  59. package/src/path/triangulate.js +517 -517
  60. package/src/pipeline.js +74 -74
  61. package/src/render/camera.js +168 -168
  62. package/src/render/frame-builder.js +362 -362
  63. package/src/render/geometry-pool.js +149 -149
  64. package/src/render/lighting.js +149 -149
  65. package/src/render/orbit-controls.js +237 -237
  66. package/src/render/renderer.js +258 -249
  67. package/src/render/scroll-timeline.js +104 -104
  68. package/src/render/shaders/glsl.js +285 -285
  69. package/src/render/shaders/wgsl.js +252 -252
  70. package/src/render/stage.js +365 -352
  71. package/src/render/texture-cache.js +40 -40
  72. package/src/render/textures.js +223 -223
  73. package/src/render/webgl2/webgl2-backend.js +586 -532
  74. package/src/render/webgpu/webgpu-backend.js +482 -480
  75. package/src/scene/flatten.js +754 -754
  76. package/src/scene/picking.js +161 -161
  77. package/src/scene/scene-graph.js +320 -320
  78. package/src/transform/transform.js +211 -211
  79. package/src/wasm/kernels-binary.js +3 -3
  80. 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
+ }