@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,136 @@
1
+ // Revolution (lathe) of a 2D profile around the Y axis.
2
+ //
3
+ // The profile lives in the XY plane: x is the distance from the axis, y the
4
+ // height. Open profiles are oriented automatically so their normals face away
5
+ // from the axis region they enclose; closed profiles become solids of
6
+ // revolution and get end caps when the angle is below 360 degrees.
7
+
8
+ import { MeshBuilder } from './mesh.js';
9
+ import { signedArea, reverseContour } from '../path/path-flatten.js';
10
+ import { contourFrames } from './contour.js';
11
+ import { triangulate } from '../path/triangulate.js';
12
+
13
+ /** Orient an open profile so that (profile + axis) forms a counter-clockwise loop. */
14
+ function orientOpen(c) {
15
+ const p = c.points;
16
+ const n = p.length / 2;
17
+ const loop = new Float64Array(p.length + 4);
18
+ loop.set(p);
19
+ loop[p.length] = 0;
20
+ loop[p.length + 1] = p[(n - 1) * 2 + 1];
21
+ loop[p.length + 2] = 0;
22
+ loop[p.length + 3] = p[1];
23
+ return signedArea(loop) >= 0 ? c : reverseContour(c);
24
+ }
25
+
26
+ /**
27
+ * @param {object[]} contours flattened 2D contours
28
+ * @param {{angle:number, segments:number}} o angle in degrees
29
+ * @returns {{mesh: import('./mesh.js').Mesh, doubleSided: boolean}}
30
+ */
31
+ export function revolveMesh(contours, o, report) {
32
+ const full = o.angle >= 360 - 1e-9;
33
+ const A = (Math.min(o.angle, 360) * Math.PI) / 180;
34
+ const S = Math.max(2, o.segments);
35
+ let open = false;
36
+
37
+ // profiles first, so the mesh can be allocated once at its final size
38
+ const profiles = [];
39
+ for (const raw of contours) {
40
+ const pts0 = raw.points;
41
+ for (let i = 0; i < pts0.length; i += 2) {
42
+ if (pts0[i] < -1e-9) {
43
+ if (report) report('error', 'E_REVOLVE_AXIS', `The revolve profile crosses the axis: point (${pts0[i]}, ${pts0[i + 1]}) has x < 0. Profiles must stay at x ≥ 0.`);
44
+ break;
45
+ }
46
+ }
47
+ const closed = raw.closed;
48
+ if (!closed) open = true;
49
+ const c = closed ? (signedArea(raw.points) > 0 ? raw : reverseContour(raw)) : orientOpen(raw);
50
+ const pts = c.points;
51
+ const n = pts.length / 2;
52
+ if (n < 2) continue;
53
+ const f = contourFrames(pts, c.smooth, closed);
54
+ const total = f.length || 1;
55
+ // profile entries (x, y, nx, ny, v): per vertex, entryIn (end of the
56
+ // previous edge) and entryOut (start of the next edge)
57
+ const entries = [];
58
+ const entryIn = new Int32Array(n).fill(-1);
59
+ const entryOut = new Int32Array(n).fill(-1);
60
+ const add = (x, y, nx, ny, v) => entries.push(x, y, nx, ny, v) / 5 - 1;
61
+ for (let i = 0; i < n; i++) {
62
+ const pe = closed ? (i - 1 + n) % n : i - 1;
63
+ const ne = closed || i < n - 1 ? i : -1;
64
+ const x = Math.max(0, pts[i * 2]), y = pts[i * 2 + 1];
65
+ const v = f.arc[i] / total;
66
+ if (pe >= 0 && ne >= 0 && f.corner[i]) {
67
+ entryIn[i] = add(x, y, f.edgeN[pe * 2], f.edgeN[pe * 2 + 1], v);
68
+ entryOut[i] = add(x, y, f.edgeN[ne * 2], f.edgeN[ne * 2 + 1], v);
69
+ } else {
70
+ const e = ne >= 0 ? ne : pe;
71
+ const k = f.corner[i] ? add(x, y, f.edgeN[e * 2], f.edgeN[e * 2 + 1], v) : add(x, y, f.vertN[i * 2], f.vertN[i * 2 + 1], v);
72
+ entryIn[i] = k;
73
+ entryOut[i] = k;
74
+ }
75
+ }
76
+ const tri = !full && closed ? triangulate(pts) : null;
77
+ profiles.push({ closed, pts, n, entries, entryIn, entryOut, tri });
78
+ }
79
+ let vertices = 0, indices = 0;
80
+ for (const p of profiles) {
81
+ vertices += (p.entries.length / 5) * (S + 1);
82
+ indices += (p.closed ? p.n : p.n - 1) * S * 6;
83
+ if (p.tri) {
84
+ vertices += 2 * p.n;
85
+ indices += 2 * p.tri.length;
86
+ }
87
+ }
88
+ const b = new MeshBuilder('triangles', vertices, indices);
89
+ // the angle of each column is the same for every profile entry
90
+ const cosCol = new Float64Array(S + 1), sinCol = new Float64Array(S + 1);
91
+ for (let j = 0; j <= S; j++) {
92
+ const th = (A * j) / S;
93
+ cosCol[j] = Math.cos(th);
94
+ sinCol[j] = Math.sin(th);
95
+ }
96
+
97
+ for (const { closed, pts, n, entries, entryIn, entryOut, tri } of profiles) {
98
+ // closing seam of closed contours: v wraps to 1 at the last edge end
99
+ const base = b.vertexCount;
100
+ const cols = S + 1;
101
+ for (let k = 0; k < entries.length; k += 5) {
102
+ const x = entries[k], y = entries[k + 1], nx = entries[k + 2], ny = entries[k + 3], v = entries[k + 4];
103
+ for (let j = 0; j <= S; j++) {
104
+ const ct = cosCol[j], st = sinCol[j];
105
+ b.vertex(x * ct, y, -x * st, nx * ct, ny, -nx * st, j / S, v);
106
+ }
107
+ }
108
+ const edges = closed ? n : n - 1;
109
+ for (let e = 0; e < edges; e++) {
110
+ const s0 = entryOut[e];
111
+ const t0 = entryIn[(e + 1) % n];
112
+ for (let j = 0; j < S; j++) {
113
+ const a = base + s0 * cols + j;
114
+ const d = base + t0 * cols + j;
115
+ b.quad(a, a + 1, d + 1, d);
116
+ }
117
+ }
118
+ // caps for partial revolutions of closed profiles
119
+ if (tri) {
120
+ for (const [th, sign] of [[0, 1], [A, -1]]) {
121
+ const ct = Math.cos(th), st = Math.sin(th);
122
+ // start cap faces +Z (against the sweep); the end cap faces the sweep direction (-sin A, 0, -cos A)
123
+ const cb = b.vertexCount;
124
+ for (let i = 0; i < n; i++) {
125
+ const x = Math.max(0, pts[i * 2]), y = pts[i * 2 + 1];
126
+ b.vertex(x * ct, y, -x * st, sign > 0 ? 0 : -st, 0, sign > 0 ? 1 : -ct, x, y);
127
+ }
128
+ for (let i = 0; i < tri.length; i += 3) {
129
+ if (sign > 0) b.tri(cb + tri[i], cb + tri[i + 1], cb + tri[i + 2]);
130
+ else b.tri(cb + tri[i], cb + tri[i + 2], cb + tri[i + 1]);
131
+ }
132
+ }
133
+ }
134
+ }
135
+ return { mesh: b.build(), doubleSided: open };
136
+ }
@@ -0,0 +1,441 @@
1
+ // Sweeping a 2D profile along a 3D spine.
2
+ //
3
+ // Frames along the spine are rotation minimising (double reflection method,
4
+ // Wang et al. 2008), so tubes do not twist unexpectedly; closed spines get the
5
+ // residual twist distributed evenly so the seam matches. Sharp spine corners
6
+ // are mitered (the profile is stretched along the bend so the tube keeps its
7
+ // thickness). Optional twist and taper are applied along the arc length.
8
+
9
+ import { Mesh, MeshBuilder } from './mesh.js';
10
+ import { triangulateRegions, reportTriangulation } from './extrude.js';
11
+ import { contourFrames } from './contour.js';
12
+ import { flattenPath } from '../path/path-flatten.js';
13
+ import { perpendicular } from '../math/vec3.js';
14
+ import { sweepWasm, catmullRomWasm } from '../wasm/kernels.js';
15
+
16
+ const TAU = Math.PI * 2;
17
+
18
+ /** Helix around +Y starting at (r, 0, 0). */
19
+ export function helixPoints(radius, pitch, turns, segmentsPerTurn) {
20
+ const n = Math.max(2, Math.ceil(turns * segmentsPerTurn));
21
+ const pts = new Float64Array((n + 1) * 3);
22
+ const total = turns * TAU;
23
+ for (let i = 0; i <= n; i++) {
24
+ const t = (i / n) * total;
25
+ pts[i * 3] = radius * Math.cos(t);
26
+ pts[i * 3 + 1] = (pitch * t) / TAU;
27
+ pts[i * 3 + 2] = -radius * Math.sin(t);
28
+ }
29
+ return pts;
30
+ }
31
+
32
+ /** Centripetal Catmull-Rom spline through 3D points. */
33
+ export function catmullRom(points, closed, subdivisions) {
34
+ const n = points.length / 3;
35
+ if (n < 2) return Float64Array.from(points);
36
+ const fast = catmullRomWasm(points, closed, subdivisions);
37
+ if (fast) return fast;
38
+ const P = (i) => {
39
+ if (closed) {
40
+ const k = ((i % n) + n) % n;
41
+ return [points[k * 3], points[k * 3 + 1], points[k * 3 + 2]];
42
+ }
43
+ if (i < 0) {
44
+ const a = P(0), b = P(1);
45
+ return [2 * a[0] - b[0], 2 * a[1] - b[1], 2 * a[2] - b[2]];
46
+ }
47
+ if (i > n - 1) {
48
+ const a = P(n - 1), b = P(n - 2);
49
+ return [2 * a[0] - b[0], 2 * a[1] - b[1], 2 * a[2] - b[2]];
50
+ }
51
+ return [points[i * 3], points[i * 3 + 1], points[i * 3 + 2]];
52
+ };
53
+ const out = [];
54
+ const segs = closed ? n : n - 1;
55
+ const knot = (a, b) => Math.max(Math.sqrt(Math.hypot(b[0] - a[0], b[1] - a[1], b[2] - a[2])), 1e-6);
56
+ for (let s = 0; s < segs; s++) {
57
+ const p0 = P(s - 1), p1 = P(s), p2 = P(s + 1), p3 = P(s + 2);
58
+ const t0 = 0;
59
+ const t1 = t0 + knot(p0, p1);
60
+ const t2 = t1 + knot(p1, p2);
61
+ const t3 = t2 + knot(p2, p3);
62
+ for (let k = 0; k < subdivisions; k++) {
63
+ const u = t1 + ((t2 - t1) * k) / subdivisions;
64
+ const lerp = (a, b, ta, tb) => {
65
+ const w = (u - ta) / (tb - ta);
66
+ return [a[0] + (b[0] - a[0]) * w, a[1] + (b[1] - a[1]) * w, a[2] + (b[2] - a[2]) * w];
67
+ };
68
+ const A1 = lerp(p0, p1, t0, t1);
69
+ const A2 = lerp(p1, p2, t1, t2);
70
+ const A3 = lerp(p2, p3, t2, t3);
71
+ const B1 = lerp(A1, A2, t0, t2);
72
+ const B2 = lerp(A2, A3, t1, t3);
73
+ const C = lerp(B1, B2, t1, t2);
74
+ out.push(C[0], C[1], C[2]);
75
+ }
76
+ }
77
+ if (!closed) {
78
+ const last = P(n - 1);
79
+ out.push(last[0], last[1], last[2]);
80
+ }
81
+ return Float64Array.from(out);
82
+ }
83
+
84
+ /** Spine polylines from a parsed `along` value. Returns [{points, closed}]. */
85
+ export function spinePolylines(spine, { tolerance, segmentsPerTurn, smooth, subdivisions }) {
86
+ if (spine.kind === 'helix') {
87
+ return [{ points: helixPoints(spine.radius, spine.pitch, spine.turns, spine.segmentsPerTurn || segmentsPerTurn), closed: false }];
88
+ }
89
+ if (spine.kind === 'path') {
90
+ return flattenPath(spine.segments, { dim: 3, tolerance }).map((c) => ({ points: c.points, closed: c.closed }));
91
+ }
92
+ let pts = spine.points;
93
+ const n = pts.length / 3;
94
+ let closed = false;
95
+ if (n > 2 && Math.hypot(pts[0] - pts[(n - 1) * 3], pts[1] - pts[(n - 1) * 3 + 1], pts[2] - pts[(n - 1) * 3 + 2]) < 1e-12) {
96
+ closed = true;
97
+ pts = pts.slice(0, (n - 1) * 3);
98
+ }
99
+ if (smooth) pts = catmullRom(pts, closed, subdivisions);
100
+ return [{ points: pts, closed }];
101
+ }
102
+
103
+ function dedupe(points, closed) {
104
+ const out = [];
105
+ const n = points.length / 3;
106
+ for (let i = 0; i < n; i++) {
107
+ const x = points[i * 3], y = points[i * 3 + 1], z = points[i * 3 + 2];
108
+ const k = out.length;
109
+ if (k && Math.hypot(out[k - 3] - x, out[k - 2] - y, out[k - 1] - z) < 1e-9) continue;
110
+ out.push(x, y, z);
111
+ }
112
+ if (closed && out.length > 6) {
113
+ const k = out.length;
114
+ if (Math.hypot(out[0] - out[k - 3], out[1] - out[k - 2], out[2] - out[k - 1]) < 1e-9) out.length -= 3;
115
+ }
116
+ return out;
117
+ }
118
+
119
+ /**
120
+ * Stations along a spine: position, tangent T, normal N, binormal B, arc length,
121
+ * and miter data (bend axis K and stretch factor) at each point. Vectors are
122
+ * flat arrays, x y z per station.
123
+ */
124
+ export function spineFrames(points, closed) {
125
+ const p = dedupe(points, closed);
126
+ const n = p.length / 3;
127
+ if (n < 2) return null;
128
+ const count = closed ? n + 1 : n;
129
+ const pos = new Float64Array(count * 3);
130
+ for (let i = 0; i < count; i++) {
131
+ const k = (i % n) * 3;
132
+ pos[i * 3] = p[k];
133
+ pos[i * 3 + 1] = p[k + 1];
134
+ pos[i * 3 + 2] = p[k + 2];
135
+ }
136
+ const nd = count - 1;
137
+ const dirs = new Float64Array(nd * 3);
138
+ for (let i = 0; i < nd; i++) normalizeInto(dirs, i * 3, pos[i * 3 + 3] - pos[i * 3], pos[i * 3 + 4] - pos[i * 3 + 1], pos[i * 3 + 5] - pos[i * 3 + 2]);
139
+ const T = new Float64Array(count * 3);
140
+ const K = new Float64Array(count * 3);
141
+ const stretch = new Float64Array(count);
142
+ for (let i = 0; i < count; i++) {
143
+ const d0 = 3 * (closed ? (i - 1 + nd) % nd : Math.max(0, i - 1));
144
+ const d1 = 3 * (closed ? i % nd : Math.min(nd - 1, i));
145
+ const o = i * 3;
146
+ normalizeInto(T, o, dirs[d0] + dirs[d1], dirs[d0 + 1] + dirs[d1 + 1], dirs[d0 + 2] + dirs[d1 + 2]);
147
+ if (T[o] === 0 && T[o + 1] === 0 && T[o + 2] === 0) {
148
+ T[o] = dirs[d1];
149
+ T[o + 1] = dirs[d1 + 1];
150
+ T[o + 2] = dirs[d1 + 2];
151
+ }
152
+ const c = Math.max(T[o] * dirs[d1] + T[o + 1] * dirs[d1 + 1] + T[o + 2] * dirs[d1 + 2], 0.25);
153
+ normalizeInto(K, o, dirs[d1] - dirs[d0], dirs[d1 + 1] - dirs[d0 + 1], dirs[d1 + 2] - dirs[d0 + 2]);
154
+ stretch[i] = 1 / c;
155
+ }
156
+ // initial normal: the world up direction projected onto the plane of T0
157
+ const N = new Float64Array(count * 3);
158
+ const tx = T[0], ty = T[1], tz = T[2];
159
+ const ux = Math.abs(ty) < 0.99 ? 0 : 1, uy = Math.abs(ty) < 0.99 ? 1 : 0, uz = 0;
160
+ const wx = ty * uz - tz * uy, wy = tz * ux - tx * uz, wz = tx * uy - ty * ux;
161
+ normalizeInto(N, 0, wy * tz - wz * ty, wz * tx - wx * tz, wx * ty - wy * tx);
162
+ if (N[0] === 0 && N[1] === 0 && N[2] === 0) N.set(perpendicular([tx, ty, tz]));
163
+ // rotation minimising frames: double reflection
164
+ for (let i = 0; i < count - 1; i++) {
165
+ const o = i * 3;
166
+ const v1x = pos[o + 3] - pos[o], v1y = pos[o + 4] - pos[o + 1], v1z = pos[o + 5] - pos[o + 2];
167
+ const c1 = v1x * v1x + v1y * v1y + v1z * v1z;
168
+ let rx = N[o], ry = N[o + 1], rz = N[o + 2];
169
+ let lx = T[o], ly = T[o + 1], lz = T[o + 2];
170
+ if (c1 > 0) {
171
+ const sr = (2 / c1) * (v1x * rx + v1y * ry + v1z * rz);
172
+ rx = rx - v1x * sr;
173
+ ry = ry - v1y * sr;
174
+ rz = rz - v1z * sr;
175
+ const st = (2 / c1) * (v1x * lx + v1y * ly + v1z * lz);
176
+ lx = lx - v1x * st;
177
+ ly = ly - v1y * st;
178
+ lz = lz - v1z * st;
179
+ }
180
+ const v2x = T[o + 3] - lx, v2y = T[o + 4] - ly, v2z = T[o + 5] - lz;
181
+ const c2 = v2x * v2x + v2y * v2y + v2z * v2z;
182
+ if (c2 > 1e-20) {
183
+ const s2 = (2 / c2) * (v2x * rx + v2y * ry + v2z * rz);
184
+ rx = rx - v2x * s2;
185
+ ry = ry - v2y * s2;
186
+ rz = rz - v2z * s2;
187
+ }
188
+ normalizeInto(N, o + 3, rx, ry, rz);
189
+ }
190
+ if (closed) {
191
+ // distribute the residual rotation so the last frame matches the first
192
+ const o = (count - 1) * 3;
193
+ const ax = N[o + 1] * N[2] - N[o + 2] * N[1], ay = N[o + 2] * N[0] - N[o] * N[2], az = N[o] * N[1] - N[o + 1] * N[0];
194
+ const ang = Math.atan2(ax * T[0] + ay * T[1] + az * T[2], N[o] * N[0] + N[o + 1] * N[1] + N[o + 2] * N[2]);
195
+ for (let i = 0; i < count; i++) {
196
+ // Rodrigues rotation of N[i] around T[i]
197
+ const angle = (ang * i) / (count - 1);
198
+ const c = Math.cos(angle), s = Math.sin(angle);
199
+ const k = i * 3;
200
+ const vx = N[k], vy = N[k + 1], vz = N[k + 2];
201
+ const kx = T[k], ky = T[k + 1], kz = T[k + 2];
202
+ const cx = ky * vz - kz * vy, cy = kz * vx - kx * vz, cz = kx * vy - ky * vx;
203
+ const kd = (kx * vx + ky * vy + kz * vz) * (1 - c);
204
+ N[k] = vx * c + cx * s + kx * kd;
205
+ N[k + 1] = vy * c + cy * s + ky * kd;
206
+ N[k + 2] = vz * c + cz * s + kz * kd;
207
+ }
208
+ N[o] = N[0];
209
+ N[o + 1] = N[1];
210
+ N[o + 2] = N[2];
211
+ }
212
+ const B = new Float64Array(count * 3);
213
+ for (let i = 0; i < count * 3; i += 3) {
214
+ B[i] = T[i + 1] * N[i + 2] - T[i + 2] * N[i + 1];
215
+ B[i + 1] = T[i + 2] * N[i] - T[i] * N[i + 2];
216
+ B[i + 2] = T[i] * N[i + 1] - T[i + 1] * N[i];
217
+ }
218
+ const arc = new Float64Array(count);
219
+ for (let i = 1; i < count; i++) arc[i] = arc[i - 1] + Math.hypot(pos[i * 3] - pos[i * 3 - 3], pos[i * 3 + 1] - pos[i * 3 - 2], pos[i * 3 + 2] - pos[i * 3 - 1]);
220
+ return { count, pos, T, N, B, K, stretch, arc, length: arc[count - 1], closed };
221
+ }
222
+
223
+ /** Unit vector of (x, y, z) into out[o..o+2] (zero when degenerate), as vec3 normalize. */
224
+ function normalizeInto(out, o, x, y, z) {
225
+ const l = Math.hypot(x, y, z);
226
+ if (l > 1e-20) {
227
+ out[o] = x / l;
228
+ out[o + 1] = y / l;
229
+ out[o + 2] = z / l;
230
+ } else out[o] = out[o + 1] = out[o + 2] = 0;
231
+ }
232
+
233
+ /** Position of profile point (px, py) at station i, written to out[o..o+2]. */
234
+ function placeAt(out, o, i, px, py, ks, cas, sas, Nf, Bf, Pf, Kf, stretch) {
235
+ const k = ks[i], ca = cas[i], sa = sas[i];
236
+ const x = (px * ca - py * sa) * k;
237
+ const y = (px * sa + py * ca) * k;
238
+ const i3 = i * 3;
239
+ let ox = Nf[i3] * x + Bf[i3] * y, oy = Nf[i3 + 1] * x + Bf[i3 + 1] * y, oz = Nf[i3 + 2] * x + Bf[i3 + 2] * y;
240
+ // miter stretch along the bend axis
241
+ const st = stretch[i];
242
+ if (st > 1.0001) {
243
+ const d = (ox * Kf[i3] + oy * Kf[i3 + 1] + oz * Kf[i3 + 2]) * (st - 1);
244
+ ox = ox + Kf[i3] * d;
245
+ oy = oy + Kf[i3 + 1] * d;
246
+ oz = oz + Kf[i3 + 2] * d;
247
+ }
248
+ out[o] = Pf[i3] + ox;
249
+ out[o + 1] = Pf[i3 + 1] + oy;
250
+ out[o + 2] = Pf[i3 + 2] + oz;
251
+ }
252
+
253
+ /** Normal at station i of profile point (px, py) whose 2D normal is (nx, ny), into out[0..2]. */
254
+ function stationNormal(out, i, px, py, nx, ny, cas, sas, Nf, Bf, Tf, dScale) {
255
+ const ca = cas[i], sa = sas[i];
256
+ const qx = nx * ca - ny * sa;
257
+ const qy = nx * sa + ny * ca;
258
+ const i3 = i * 3;
259
+ let n0 = Nf[i3] * qx + Bf[i3] * qy, n1 = Nf[i3 + 1] * qx + Bf[i3 + 1] * qy, n2 = Nf[i3 + 2] * qx + Bf[i3 + 2] * qy;
260
+ if (dScale !== 0) {
261
+ // taper tilts the surface along the spine
262
+ const slope = dScale * (px * nx + py * ny);
263
+ const a0 = n0 - Tf[i3] * slope, a1 = n1 - Tf[i3 + 1] * slope, a2 = n2 - Tf[i3 + 2] * slope;
264
+ const l = Math.hypot(a0, a1, a2);
265
+ if (l > 1e-20) {
266
+ n0 = a0 / l;
267
+ n1 = a1 / l;
268
+ n2 = a2 / l;
269
+ } else n0 = n1 = n2 = 0;
270
+ }
271
+ out[0] = n0;
272
+ out[1] = n1;
273
+ out[2] = n2;
274
+ }
275
+
276
+ /** Positions of every profile point at every station (station-major), into ring. */
277
+ function sweepRing(ring, pts, n, count, ks, cas, sas, Nf, Bf, Pf, Kf, stretch) {
278
+ for (let s = 0; s < count; s++) {
279
+ for (let j = 0; j < n; j++) placeAt(ring, (s * n + j) * 3, s, pts[j * 2], pts[j * 2 + 1], ks, cas, sas, Nf, Bf, Pf, Kf, stretch);
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Wall of one contour along one spine: for each profile edge, a strip of
285
+ * quads from the first station to the last. The hot loop of a sweep, in a
286
+ * function of its own so the engine optimizes it early and on its own.
287
+ */
288
+ function sweepWalls(b, ring, f, pts, count, arc, cas, sas, Nf, Bf, Tf, dScale, out) {
289
+ const n = f.n;
290
+ const perim = f.length || 1;
291
+ for (let e = 0; e < n; e++) {
292
+ const i0 = e, i1 = (e + 1) % n;
293
+ const c0 = f.corner[i0], c1 = f.corner[i1];
294
+ const n0x = c0 ? f.edgeN[e * 2] : f.vertN[i0 * 2], n0y = c0 ? f.edgeN[e * 2 + 1] : f.vertN[i0 * 2 + 1];
295
+ const n1x = c1 ? f.edgeN[e * 2] : f.vertN[i1 * 2], n1y = c1 ? f.edgeN[e * 2 + 1] : f.vertN[i1 * 2 + 1];
296
+ const u0 = f.arc[e] / perim, u1 = f.arc[e + 1] / perim;
297
+ const p0x = pts[i0 * 2], p0y = pts[i0 * 2 + 1], p1x = pts[i1 * 2], p1y = pts[i1 * 2 + 1];
298
+ let prevA = -1, prevB = -1;
299
+ for (let s = 0; s < count; s++) {
300
+ const v = arc[s] / perim;
301
+ stationNormal(out, s, p0x, p0y, n0x, n0y, cas, sas, Nf, Bf, Tf, dScale);
302
+ let o = (s * n + i0) * 3;
303
+ const a = b.vertex(ring[o], ring[o + 1], ring[o + 2], out[0], out[1], out[2], u0, v);
304
+ stationNormal(out, s, p1x, p1y, n1x, n1y, cas, sas, Nf, Bf, Tf, dScale);
305
+ o = (s * n + i1) * 3;
306
+ const c2 = b.vertex(ring[o], ring[o + 1], ring[o + 2], out[0], out[1], out[2], u1, v);
307
+ if (prevA >= 0) b.quad(prevA, prevB, c2, a);
308
+ prevA = a;
309
+ prevB = c2;
310
+ }
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Sweep a profile along spines.
316
+ * @param {object} profile {kind:'circle', radius, segments} | {kind:'shape', regions, walls}
317
+ * @param {{points:Float64Array, closed:boolean}[]} spines
318
+ * @param {{twist?:number, taper?:number, caps?:boolean}} o twist in degrees
319
+ */
320
+ export function sweepMesh(profile, spines, o, report) {
321
+ const twist = ((o.twist || 0) * Math.PI) / 180;
322
+ const taper = o.taper === undefined ? 1 : o.taper;
323
+ const caps = o.caps !== false;
324
+ let open = false;
325
+
326
+ // profile contours with per-vertex normals (2D)
327
+ let contours;
328
+ let capTris = null;
329
+ if (profile.kind === 'circle') {
330
+ const m = Math.max(3, profile.segments);
331
+ const pts = new Float64Array(m * 2);
332
+ for (let j = 0; j < m; j++) {
333
+ const a = (j / m) * TAU;
334
+ pts[j * 2] = Math.cos(a) * profile.radius;
335
+ pts[j * 2 + 1] = Math.sin(a) * profile.radius;
336
+ }
337
+ contours = [{ points: pts, smooth: new Uint8Array(m).fill(1), closed: true }];
338
+ capTris = { circle: true, pts };
339
+ } else contours = profile.walls;
340
+ const w = spines.every((sp) => sp.points.length % 3 === 0) ? sweepWasm(spines, contours, capTris && capTris.pts, profile.regions, twist, taper, caps) : null;
341
+ if (w) {
342
+ for (const e of w.errors) reportTriangulation(e, report);
343
+ for (const d of w.degenerate) if (d && report) report('error', 'E_DEGENERATE_SPINE', 'The sweep spine needs at least two distinct points.');
344
+ return { mesh: new Mesh({ positions: w.positions, normals: w.normals, uvs: w.uvs, indices: w.indices, kind: 'triangles' }), doubleSided: w.open };
345
+ }
346
+ if (!capTris) capTris = triangulateRegions(profile.regions, report);
347
+ const contourInfo = contours.map((c) => ({ pts: c.points, f: contourFrames(c.points, c.smooth, true) }));
348
+
349
+ // frames first, so the mesh can be allocated once at its final size
350
+ const frames = [];
351
+ let vertices = 0, indices = 0;
352
+ for (const sp of spines) {
353
+ const fr = spineFrames(sp.points, sp.closed);
354
+ if (!fr) {
355
+ if (report) report('error', 'E_DEGENERATE_SPINE', 'The sweep spine needs at least two distinct points.');
356
+ continue;
357
+ }
358
+ frames.push(fr);
359
+ for (const { f } of contourInfo) {
360
+ vertices += f.n * fr.count * 2;
361
+ indices += f.n * (fr.count - 1) * 6;
362
+ }
363
+ if (!fr.closed && caps) {
364
+ if (capTris.circle) {
365
+ const m = capTris.pts.length / 2;
366
+ vertices += 2 * (m + 1);
367
+ indices += 2 * m * 3;
368
+ } else {
369
+ for (const t of capTris) {
370
+ vertices += t.positions.length;
371
+ indices += 2 * t.indices.length;
372
+ }
373
+ }
374
+ }
375
+ }
376
+ const b = new MeshBuilder('triangles', vertices, indices);
377
+ const out = new Float64Array(3);
378
+
379
+ for (const fr of frames) {
380
+ const count = fr.count;
381
+ const L = fr.length || 1;
382
+ const dScale = (taper - 1) / L;
383
+ const Nf = fr.N, Bf = fr.B, Tf = fr.T, Pf = fr.pos, Kf = fr.K;
384
+ const stretch = fr.stretch, arc = fr.arc;
385
+ // twist and taper only depend on the station
386
+ const ks = new Float64Array(count), cas = new Float64Array(count), sas = new Float64Array(count);
387
+ for (let i = 0; i < count; i++) {
388
+ const s = arc[i] / L;
389
+ ks[i] = 1 + (taper - 1) * s;
390
+ const ang = twist * s;
391
+ cas[i] = Math.cos(ang);
392
+ sas[i] = Math.sin(ang);
393
+ }
394
+
395
+ for (const { pts, f } of contourInfo) {
396
+ // every profile point at every station, computed once for both of its edges
397
+ const ring = new Float64Array(count * f.n * 3);
398
+ sweepRing(ring, pts, f.n, count, ks, cas, sas, Nf, Bf, Pf, Kf, stretch);
399
+ sweepWalls(b, ring, f, pts, count, arc, cas, sas, Nf, Bf, Tf, dScale, out);
400
+ }
401
+
402
+ if (!fr.closed) {
403
+ if (!caps) open = true;
404
+ else {
405
+ for (const [s, dir] of [[0, -1], [count - 1, 1]]) {
406
+ const nx = Tf[s * 3] * dir, ny = Tf[s * 3 + 1] * dir, nz = Tf[s * 3 + 2] * dir;
407
+ if (capTris.circle) {
408
+ placeAt(out, 0, s, 0, 0, ks, cas, sas, Nf, Bf, Pf, Kf, stretch);
409
+ const ci = b.vertex(out[0], out[1], out[2], nx, ny, nz, 0.5, 0.5);
410
+ const m = capTris.pts.length / 2;
411
+ const first = b.vertexCount;
412
+ for (let j = 0; j < m; j++) {
413
+ placeAt(out, 0, s, capTris.pts[j * 2], capTris.pts[j * 2 + 1], ks, cas, sas, Nf, Bf, Pf, Kf, stretch);
414
+ b.vertex(out[0], out[1], out[2], nx, ny, nz, 0.5 + Math.cos((j / m) * TAU) / 2, 0.5 + Math.sin((j / m) * TAU) / 2);
415
+ }
416
+ for (let j = 0; j < m; j++) {
417
+ const a = first + j, c2 = first + ((j + 1) % m);
418
+ if (dir > 0) b.tri(ci, a, c2);
419
+ else b.tri(ci, c2, a);
420
+ }
421
+ } else {
422
+ for (const t of capTris) {
423
+ const base = b.vertexCount;
424
+ const p = t.positions;
425
+ for (let k = 0; k < p.length; k += 2) {
426
+ placeAt(out, 0, s, p[k], p[k + 1], ks, cas, sas, Nf, Bf, Pf, Kf, stretch);
427
+ b.vertex(out[0], out[1], out[2], nx, ny, nz, p[k], p[k + 1]);
428
+ }
429
+ const idx = t.indices;
430
+ for (let k = 0; k < idx.length; k += 3) {
431
+ if (dir > 0) b.tri(base + idx[k], base + idx[k + 1], base + idx[k + 2]);
432
+ else b.tri(base + idx[k], base + idx[k + 2], base + idx[k + 1]);
433
+ }
434
+ }
435
+ }
436
+ }
437
+ }
438
+ }
439
+ }
440
+ return { mesh: b.build(), doubleSided: open };
441
+ }
package/src/index.js ADDED
@@ -0,0 +1,47 @@
1
+ // Public API of the ES3D reference implementation.
2
+ //
3
+ // import { compile, createRenderer, Camera } from '@es3d/es3d';
4
+ //
5
+ // const result = compile(text); // parse + validate + build + flatten
6
+ // if (!result.ok) console.error(result.diagnostics.map((d) => d.format()).join('\n\n'));
7
+ // const renderer = await createRenderer(canvas); // WebGPU, else WebGL2
8
+ // renderer.setScene(result.scene, (entry, lod) => result.cache.mesh(entry, lod));
9
+ // const camera = new Camera().frame(result.scene.bounds);
10
+ // renderer.render(camera, { width: canvas.clientWidth, height: canvas.clientHeight });
11
+ //
12
+ // const svg = await importFiles([{ name: 'logo.svg', bytes }]); // SVG, OBJ, STL, STEP, glTF → ES3D
13
+ // const { files } = exportFormat(svg.text, 'glb'); // ES3D → glTF, OBJ, STL, STEP
14
+ //
15
+ // The format, scene, geometry and I/O modules run in Node as well; only the
16
+ // renderer and controls need a browser.
17
+
18
+ // documents
19
+ export { parse, parseStrict, detectSyntax, formatDiagnostics } from './format/parse.js';
20
+ export { serialize, SERIALIZE_PRESETS } from './format/serializer.js';
21
+ export { Document, Element, Attr, Comment, walk, h } from './format/ast.js';
22
+ export { Diagnostic, ES3DError } from './format/errors.js';
23
+ export { ELEMENTS, elementSpec, attrSpec } from './format/schema.js';
24
+
25
+ // scene pipeline
26
+ export { compile, generateGeometry, GeometryCache } from './pipeline.js';
27
+ export { openGeometryStore, indexedDBStore, memoryStore, restoreGeometry, saveGeometry, persistGeometry, geometryKey, GEOMETRY_REVISION } from './cache/geometry-store.js';
28
+ export { RenderScene, updateDynamic, FLAG_MIRRORED, FLAG_NO_COLLIDE } from './scene/flatten.js';
29
+ export { pick, raycast } from './scene/picking.js';
30
+ export { applyLimits, sweepCamera, constrainCamera } from './scene/camera-constraints.js';
31
+
32
+ // rendering (browser)
33
+ export { createRenderer, Renderer } from './render/renderer.js';
34
+ export { Camera } from './render/camera.js';
35
+ export { OrbitControls } from './render/orbit-controls.js';
36
+ export { ScrollTimeline } from './render/scroll-timeline.js';
37
+ export { createStage, ancestorNames } from './render/stage.js';
38
+
39
+ // conversion and optimization
40
+ export { importFiles, exportFormat, detectKind, INPUT_KINDS, OUTPUT_FORMATS } from './io/convert.js';
41
+ export { svgToES3D } from './io/svg-import.js';
42
+ export { exportOBJ, parseOBJ, objToScene } from './io/obj.js';
43
+ export { exportGLB, parseGLB, glbToScene, isGLB, parseGLTF, gltfToScene } from './io/gltf.js';
44
+ export { exportSTL, parseSTL, stlToScene } from './io/stl.js';
45
+ export { exportSTEP, parseSTEP, stepToScene, isSTEP } from './io/step.js';
46
+ export { sceneToMeshDocument } from './io/mesh-import.js';
47
+ export { optimize, expandProcedural } from './optimize/optimizer.js';
@@ -0,0 +1,67 @@
1
+ // Compression helpers that work in browsers (CompressionStream) and in Node
2
+ // (zlib, which also provides brotli). Used for file-size statistics and the
3
+ // benchmark, and for loading .gz files.
4
+
5
+ const isNode = typeof process !== 'undefined' && !!(process.versions && process.versions.node) && typeof window === 'undefined';
6
+
7
+ let zlibPromise = null;
8
+ function zlib() {
9
+ // Node only; the variable keeps bundlers from trying to resolve it for browsers
10
+ const specifier = 'node:zlib';
11
+ if (!zlibPromise) zlibPromise = import(/* @vite-ignore */ specifier);
12
+ return zlibPromise;
13
+ }
14
+
15
+ function toBytes(data) {
16
+ if (typeof data === 'string') return new TextEncoder().encode(data);
17
+ if (data instanceof Uint8Array) return data;
18
+ return new Uint8Array(data);
19
+ }
20
+
21
+ async function streamBytes(bytes, transform) {
22
+ const stream = new Blob([bytes]).stream().pipeThrough(transform);
23
+ return new Uint8Array(await new Response(stream).arrayBuffer());
24
+ }
25
+
26
+ /** gzip (level 9 in Node, browser default otherwise). */
27
+ export async function gzip(data) {
28
+ const bytes = toBytes(data);
29
+ if (isNode) {
30
+ const z = await zlib();
31
+ return new Uint8Array(z.gzipSync(bytes, { level: 9 }));
32
+ }
33
+ return streamBytes(bytes, new CompressionStream('gzip'));
34
+ }
35
+
36
+ export async function gunzip(data) {
37
+ const bytes = toBytes(data);
38
+ if (isNode) {
39
+ const z = await zlib();
40
+ return new Uint8Array(z.gunzipSync(bytes));
41
+ }
42
+ return streamBytes(bytes, new DecompressionStream('gzip'));
43
+ }
44
+
45
+ /** Brotli (quality 11). Returns null where unavailable (browsers lack a brotli compressor API). */
46
+ export async function brotli(data) {
47
+ const bytes = toBytes(data);
48
+ if (isNode) {
49
+ const z = await zlib();
50
+ return new Uint8Array(z.brotliCompressSync(bytes, { params: { [z.constants.BROTLI_PARAM_QUALITY]: 11, [z.constants.BROTLI_PARAM_SIZE_HINT]: bytes.length } }));
51
+ }
52
+ try {
53
+ return await streamBytes(bytes, new CompressionStream('br'));
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+
59
+ export async function compressedSizes(data) {
60
+ const bytes = toBytes(data);
61
+ const [g, b] = await Promise.all([gzip(bytes), brotli(bytes)]);
62
+ return { raw: bytes.length, gzip: g.length, brotli: b ? b.length : null };
63
+ }
64
+
65
+ export function isGzip(bytes) {
66
+ return bytes.length > 2 && bytes[0] === 0x1f && bytes[1] === 0x8b;
67
+ }