@es3d/es3d 0.0.0-stage → 1.0.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.
Files changed (82) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +86 -3
  3. package/docs/REFERENCE.md +581 -0
  4. package/docs/SPEC.md +417 -0
  5. package/package.json +50 -4
  6. package/scripts/es3d-cli.mjs +377 -0
  7. package/src/cache/geometry-store.js +392 -0
  8. package/src/cache/mesh-bundle.js +123 -0
  9. package/src/cache/sha256.js +73 -0
  10. package/src/format/ast.js +238 -0
  11. package/src/format/colors.js +93 -0
  12. package/src/format/compact-parser.js +171 -0
  13. package/src/format/compact-tokenizer.js +165 -0
  14. package/src/format/errors.js +247 -0
  15. package/src/format/json.js +55 -0
  16. package/src/format/limits.js +14 -0
  17. package/src/format/parse.js +92 -0
  18. package/src/format/schema.js +617 -0
  19. package/src/format/serializer.js +333 -0
  20. package/src/format/validator.js +330 -0
  21. package/src/format/value-error.js +12 -0
  22. package/src/format/value-format.js +314 -0
  23. package/src/format/values.js +459 -0
  24. package/src/format/xml-parser.js +136 -0
  25. package/src/format/xml-tokenizer.js +237 -0
  26. package/src/geometry/contour.js +86 -0
  27. package/src/geometry/extrude.js +175 -0
  28. package/src/geometry/gear.js +85 -0
  29. package/src/geometry/geometry-cache.js +159 -0
  30. package/src/geometry/lod-error.js +178 -0
  31. package/src/geometry/mesh.js +458 -0
  32. package/src/geometry/primitives.js +299 -0
  33. package/src/geometry/registry.js +645 -0
  34. package/src/geometry/revolve.js +136 -0
  35. package/src/geometry/sweep.js +441 -0
  36. package/src/index.js +47 -0
  37. package/src/io/compression.js +67 -0
  38. package/src/io/convert.js +236 -0
  39. package/src/io/download.js +13 -0
  40. package/src/io/external-scene.js +127 -0
  41. package/src/io/file-loader.js +28 -0
  42. package/src/io/gltf.js +630 -0
  43. package/src/io/mesh-import.js +104 -0
  44. package/src/io/obj.js +352 -0
  45. package/src/io/step-brep.js +439 -0
  46. package/src/io/step-exact.js +1149 -0
  47. package/src/io/step.js +1418 -0
  48. package/src/io/stl.js +193 -0
  49. package/src/io/svg-import.js +787 -0
  50. package/src/material/materials.js +154 -0
  51. package/src/math/mat4.js +475 -0
  52. package/src/math/random.js +30 -0
  53. package/src/math/trig.js +65 -0
  54. package/src/math/vec3.js +81 -0
  55. package/src/optimize/optimizer.js +579 -0
  56. package/src/path/path-flatten.js +322 -0
  57. package/src/path/path-parser.js +332 -0
  58. package/src/path/shapes.js +129 -0
  59. package/src/path/triangulate.js +517 -0
  60. package/src/pipeline.js +74 -0
  61. package/src/render/camera.js +168 -0
  62. package/src/render/frame-builder.js +362 -0
  63. package/src/render/geometry-pool.js +149 -0
  64. package/src/render/lighting.js +149 -0
  65. package/src/render/orbit-controls.js +237 -0
  66. package/src/render/renderer.js +249 -0
  67. package/src/render/scroll-timeline.js +104 -0
  68. package/src/render/shaders/glsl.js +285 -0
  69. package/src/render/shaders/wgsl.js +252 -0
  70. package/src/render/stage.js +352 -0
  71. package/src/render/texture-cache.js +40 -0
  72. package/src/render/textures.js +223 -0
  73. package/src/render/webgl2/webgl2-backend.js +532 -0
  74. package/src/render/webgpu/webgpu-backend.js +480 -0
  75. package/src/scene/animation.js +171 -0
  76. package/src/scene/camera-constraints.js +117 -0
  77. package/src/scene/flatten.js +754 -0
  78. package/src/scene/picking.js +161 -0
  79. package/src/scene/scene-graph.js +320 -0
  80. package/src/transform/transform.js +211 -0
  81. package/src/wasm/kernels-binary.js +3 -0
  82. package/src/wasm/kernels.js +385 -0
@@ -0,0 +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
+ }