@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,385 +1,385 @@
1
- // WebAssembly versions of the reader's hottest loops: number lists, normals,
2
- // sweeps, extrusions, triangulation, primitives and the instance rows of
3
- // <repeat> and <scatter>. They are built from wasm/ by `npm run wasm` and
4
- // compute exactly what the JavaScript versions compute (the same IEEE
5
- // operations in the same order; sin, cos, atan2 and hypot come from this
6
- // page's Math). Where WebAssembly is missing or blocked, for example by a
7
- // content security policy without 'wasm-unsafe-eval', every caller keeps
8
- // using its JavaScript version.
9
-
10
- import BINARY, { INSTANCES } from './kernels-binary.js';
11
-
12
- let text = ''; // the number list being read, for the rare numbers the kernel hands back to Number()
13
- let material = null; // resolves an instance row's material code (see instancesWasm)
14
-
15
- const imports = {
16
- env: {
17
- sin: Math.sin,
18
- cos: Math.cos,
19
- atan2: Math.atan2,
20
- tan: Math.tan,
21
- acos: Math.acos,
22
- hypot2: Math.hypot,
23
- hypot3: Math.hypot,
24
- number: (start, end) => Number(text.slice(start, end)),
25
- material: (code) => material(code),
26
- },
27
- };
28
-
29
- function decode(base64) {
30
- // native decoders where there is one (newer browsers, Node); a loop otherwise
31
- if (typeof Uint8Array.fromBase64 === 'function') return Uint8Array.fromBase64(base64);
32
- if (typeof Buffer === 'function') return Buffer.from(base64, 'base64');
33
- const s = atob(base64);
34
- const bytes = new Uint8Array(s.length);
35
- for (let i = 0; i < s.length; i++) bytes[i] = s.charCodeAt(i);
36
- return bytes;
37
- }
38
-
39
- async function instantiate(base64) {
40
- try {
41
- if (typeof WebAssembly !== 'object') return null;
42
- const { instance } = await WebAssembly.instantiate(decode(base64), imports);
43
- return instance.exports;
44
- } catch {
45
- return null;
46
- }
47
- }
48
-
49
- // the instance kernel is a module of its own: it needs WebAssembly SIMD
50
- const [wasm, simd] = await Promise.all([instantiate(BINARY), instantiate(INSTANCES)]);
51
- let enabled = wasm !== null;
52
-
53
- /**
54
- * Whether the module's hypot (V8's algorithm) gives this engine's Math.hypot
55
- * results. The special cases (infinities, NaN, zeros) are fixed by the
56
- * language; the rounding of finite values is the engine's, so values over
57
- * many magnitudes are compared. Where they differ the kernels keep calling
58
- * Math.hypot, which is slower (every call leaves WebAssembly and allocates).
59
- */
60
- function hypotMatches() {
61
- const edge = [0, -0, 3, -4, 5e-324, 1e308, Infinity, NaN];
62
- for (const x of edge) for (const y of edge) if (!Object.is(wasm.hypot2(x, y), Math.hypot(x, y))) return false;
63
- let seed = 12345;
64
- const next = () => {
65
- seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
66
- return (seed / 4294967296 - 0.5) * 10 ** ((seed % 41) - 20);
67
- };
68
- for (let i = 0; i < 100; i++) {
69
- const x = next(), y = next(), z = next();
70
- if (!Object.is(wasm.hypot3(x, y, z), Math.hypot(x, y, z)) || !Object.is(wasm.hypot2(x, y), Math.hypot(x, y))) return false;
71
- }
72
- return true;
73
- }
74
- if (wasm) wasm.set_native_hypot(hypotMatches() ? 1 : 0);
75
-
76
- /** Whether the kernels are in use. */
77
- export function wasmEnabled() {
78
- return enabled;
79
- }
80
-
81
- /** Whether the instance kernel is in use (it needs WebAssembly SIMD as well). */
82
- export function instanceKernelEnabled() {
83
- return enabled && simd !== null;
84
- }
85
-
86
- /** Turn the kernels off (or back on, when they loaded) to compare with the JavaScript versions. */
87
- export function setWasmEnabled(on) {
88
- enabled = Boolean(on) && wasm !== null;
89
- }
90
-
91
- const heap = () => wasm.memory.buffer;
92
-
93
- /** Copy a typed array into a module's memory; returns its address (0 when memory ran out). */
94
- function put(a, module = wasm) {
95
- const p = module.alloc(Math.max(1, a.byteLength));
96
- if (p) new Uint8Array(module.memory.buffer, p, a.byteLength).set(new Uint8Array(a.buffer, a.byteOffset, a.byteLength));
97
- return p;
98
- }
99
-
100
- const encoder = new TextEncoder();
101
- const AS = { f64: [0, Float64Array], f32: [1, Float32Array], index: [2, Uint32Array] };
102
-
103
- /**
104
- * A number list read into a typed array (`as`: 'f64', 'f32' or 'index'), or
105
- * null when the JavaScript reader must handle it (the kernels are off, the
106
- * text is not ASCII, or it has an error, which JavaScript then reports).
107
- */
108
- export function readNumbersWasm(raw, as) {
109
- if (!enabled) return null;
110
- const [kind, Type] = AS[as];
111
- const n = raw.length;
112
- wasm.reset();
113
- const sp = wasm.alloc(Math.max(1, n));
114
- const op = sp && wasm.alloc((Math.floor(n / 2) + 1) * Type.BYTES_PER_ELEMENT);
115
- if (!op) return null;
116
- const r = encoder.encodeInto(raw, new Uint8Array(heap(), sp, n));
117
- if (r.read !== n || r.written !== n) return null;
118
- text = raw;
119
- const count = wasm.numbers(sp, n, op, kind);
120
- text = '';
121
- if (count < 0) return null;
122
- return new Type(heap(), op, count).slice();
123
- }
124
-
125
- /**
126
- * computeNormals for Float32Array positions and uvs and Uint32Array indices,
127
- * as {positions, normals, uvs, indices}, or null to use the JavaScript version.
128
- */
129
- export function computeNormalsWasm(positions, indices, crease, cosCrease, uvs) {
130
- if (!enabled || !(positions instanceof Float32Array) || !(indices instanceof Uint32Array) || (uvs && !(uvs instanceof Float32Array))) return null;
131
- wasm.reset();
132
- const pp = put(positions), ip = put(indices), tp = uvs ? put(uvs) : 0;
133
- if (!pp || !ip || (uvs && !tp)) return null;
134
- const res = wasm.normals(pp, positions.length / 3, ip, indices.length, tp, crease, cosCrease);
135
- if (!res) return null;
136
- const [vc, ic, p, nrm, t, i] = new Uint32Array(heap(), res, 6);
137
- return {
138
- positions: new Float32Array(heap(), p, vc * 3).slice(),
139
- normals: new Float32Array(heap(), nrm, vc * 3).slice(),
140
- uvs: new Float32Array(heap(), t, vc * 2).slice(),
141
- indices: new Uint32Array(heap(), i, ic).slice(),
142
- };
143
- }
144
-
145
- const f64 = (a) => (a instanceof Float64Array ? a : Float64Array.from(a));
146
- const u32 = (a) => (a instanceof Uint32Array ? a : Uint32Array.from(a));
147
- const u8 = (a) => (a instanceof Uint8Array ? a : Uint8Array.from(a));
148
-
149
- /**
150
- * Writes contours ({points, smooth, isWall}) as {n, points, smooth, isWall}
151
- * records; returns the table's address, or 0 when memory ran out.
152
- */
153
- function contourTable(list) {
154
- const rows = new Int32Array(list.length * 4);
155
- for (let k = 0; k < list.length; k++) {
156
- const c = list[k];
157
- const p = put(f64(c.points));
158
- const sm = c.smooth ? put(u8(c.smooth)) : 0;
159
- if (!p || (c.smooth && !sm)) return 0;
160
- rows[k * 4] = c.points.length / 2;
161
- rows[k * 4 + 1] = p;
162
- rows[k * 4 + 2] = sm;
163
- rows[k * 4 + 3] = c.isWall === false ? 0 : 1;
164
- }
165
- return put(rows);
166
- }
167
-
168
- /** {first contour, contour count} per region, appending each region's contours to `list`. */
169
- function regionRows(regions, list) {
170
- const rows = new Int32Array(Math.max(1, regions.length * 2));
171
- regions.forEach((r, k) => {
172
- rows[k * 2] = list.length;
173
- rows[k * 2 + 1] = 1 + r.holes.length;
174
- list.push(r.outer, ...r.holes);
175
- });
176
- return rows;
177
- }
178
-
179
- /** A kernel's mesh, copied out of kernel memory into one buffer (one allocation instead of four). */
180
- const meshOut = (res, extra) => {
181
- const [vc, ic, p, nrm, t, i] = new Uint32Array(heap(), res, 6);
182
- const out = new ArrayBuffer((vc * 8 + ic) * 4);
183
- const positions = new Float32Array(out, 0, vc * 3);
184
- const normals = new Float32Array(out, vc * 12, vc * 3);
185
- const uvs = new Float32Array(out, vc * 24, vc * 2);
186
- const indices = new Uint32Array(out, vc * 32, ic);
187
- positions.set(new Float32Array(heap(), p, vc * 3));
188
- normals.set(new Float32Array(heap(), nrm, vc * 3));
189
- uvs.set(new Float32Array(heap(), t, vc * 2));
190
- indices.set(new Uint32Array(heap(), i, ic));
191
- return { positions, normals, uvs, indices, ...extra };
192
- };
193
-
194
- /**
195
- * sweepMesh after its profile is known: spines [{points, closed}], the
196
- * profile contours, the circle's points (circle profiles) or the regions to
197
- * cap (shaped profiles). Returns {positions, normals, uvs, indices, open,
198
- * degenerate: one flag per spine, errors: triangulation error per cap
199
- * region}, or null to use the JavaScript version.
200
- */
201
- export function sweepWasm(spines, contours, circle, regions, twist, taper, caps) {
202
- if (!enabled) return null;
203
- wasm.reset();
204
- const list = [...contours];
205
- const regionTab = circle ? null : regionRows(regions, list);
206
- const table = contourTable(list);
207
- const spineRows = new Int32Array(Math.max(1, spines.length * 3));
208
- for (let k = 0; k < spines.length; k++) {
209
- spineRows[k * 3] = put(f64(spines[k].points));
210
- spineRows[k * 3 + 1] = spines[k].points.length / 3;
211
- spineRows[k * 3 + 2] = spines[k].closed ? 1 : 0;
212
- if (!spineRows[k * 3]) return null;
213
- }
214
- const profile = put(Int32Array.from(contours, (_, k) => k));
215
- const spineTab = put(spineRows);
216
- const capData = circle ? put(f64(circle)) : put(regionTab);
217
- const header = put(
218
- Int32Array.of(spines.length, spineTab, list.length, table, contours.length, profile, circle ? 1 : 2, circle ? circle.length / 2 : regions.length, capData, caps ? 1 : 0),
219
- );
220
- if (!table || !profile || !spineTab || !capData || !header) return null;
221
- const res = wasm.sweep(header, twist, taper);
222
- if (!res) return null;
223
- const [open, deg, errs] = new Uint32Array(heap(), res + 24, 3);
224
- return meshOut(res, {
225
- open: open !== 0,
226
- degenerate: Array.from(new Uint8Array(heap(), deg, spines.length), (d) => d !== 0),
227
- errors: circle ? [] : Array.from(new Float64Array(heap(), errs, regions.length)),
228
- });
229
- }
230
-
231
- /**
232
- * triangulate(data, holeIndices) as an array of vertex indices, or null to
233
- * use the JavaScript version (also when two holes tie for the sort order).
234
- */
235
- export function triangulateWasm(data, holeIndices) {
236
- if (!enabled) return null;
237
- wasm.reset();
238
- const dp = put(f64(data));
239
- const holes = holeIndices && holeIndices.length ? Int32Array.from(holeIndices) : null;
240
- const hp = holes ? put(holes) : 0;
241
- if (!dp || (holes && !hp)) return null;
242
- let res;
243
- try {
244
- res = wasm.triangulate(dp, data.length, hp, holes ? holes.length : 0);
245
- } catch {
246
- return null; // stack exhausted on pathological input: JavaScript has a deeper stack
247
- }
248
- if (!res) return null;
249
- const [count, p] = new Uint32Array(heap(), res, 2);
250
- return count ? Array.from(new Uint32Array(heap(), p, count)) : [];
251
- }
252
-
253
- /**
254
- * extrudeMesh from a shape ({regions, walls}) with the bevel profile rings
255
- * {inset, z, nr, nz} and bounds from JavaScript. Returns {positions,
256
- * normals, uvs, indices, errors: triangulation error per region}, or null
257
- * to use the JavaScript version.
258
- */
259
- export function extrudeWasm(shape, bevel, rings, z0, zTop, minX, minY, S) {
260
- if (!enabled) return null;
261
- wasm.reset();
262
- const list = [];
263
- const regionTab = put(regionRows(shape.regions, list));
264
- const walls = put(Int32Array.from(shape.walls, (_, k) => list.length + k));
265
- list.push(...shape.walls);
266
- const table = contourTable(list);
267
- const ringData = new Float64Array(Math.max(1, rings.length * 4));
268
- rings.forEach((r, k) => ringData.set([r.inset, r.z, r.nr, r.nz], k * 4));
269
- const ringPtr = put(ringData);
270
- const header = put(Int32Array.of(list.length, table, shape.regions.length, regionTab, shape.walls.length, walls, rings.length, ringPtr));
271
- if (!regionTab || !walls || !table || !ringPtr || !header) return null;
272
- const res = wasm.extrude(header, bevel, z0, zTop, minX, minY, S);
273
- if (!res) return null;
274
- const errs = new Uint32Array(heap(), res + 24, 1)[0];
275
- return meshOut(res, { errors: Array.from(new Float64Array(heap(), errs, shape.regions.length)) });
276
- }
277
-
278
- /** measureSphereDeviation(w) for w >= 3, or null to use the JavaScript version. */
279
- export function sphereDeviationWasm(w) {
280
- if (!enabled) return null;
281
- wasm.reset();
282
- const v = wasm.sphere_deviation(w);
283
- return v < 0 ? null : v;
284
- }
285
-
286
- const whole = (...v) => v.every((x) => Number.isInteger(x) && x >= 0 && x < 2 ** 30);
287
- const prim = (res) => (res ? meshOut(res) : null);
288
-
289
- /** unitSphere, unitCylinder, unitCapsule, unitTorus and roundedBox ({positions, normals, uvs, indices}), or null. */
290
- export const primitivesWasm = {
291
- sphere: (segments) => (enabled && whole(segments) ? (wasm.reset(), prim(wasm.sphere(segments))) : null),
292
- cylinder: (top, segments, caps) => (enabled && whole(segments) ? (wasm.reset(), prim(wasm.cylinder(top, segments, caps ? 1 : 0))) : null),
293
- capsule: (L, segments) => (enabled && whole(segments) ? (wasm.reset(), prim(wasm.capsule(L, segments))) : null),
294
- torus: (t, arcDeg, segments, tubeSegments) => (enabled && whole(segments, tubeSegments) ? (wasm.reset(), prim(wasm.torus(t, arcDeg, segments, tubeSegments))) : null),
295
- roundedBox: (w, h, d, r, seg) => (enabled && whole(seg) && seg >= 1 ? (wasm.reset(), prim(wasm.rounded_box(w, h, d, r, seg))) : null),
296
- };
297
-
298
- /** catmullRom for two or more points (a Float64Array), or null. */
299
- export function catmullRomWasm(points, closed, subdivisions) {
300
- if (!enabled || !whole(subdivisions) || points.length < 6 || points.length % 3) return null;
301
- wasm.reset();
302
- const p = put(f64(points));
303
- const res = p && wasm.catmull_rom(p, points.length / 3, closed ? 1 : 0, subdivisions);
304
- if (!res) return null;
305
- const [count, out] = new Uint32Array(heap(), res, 2);
306
- return new Float64Array(heap(), out, count * 3).slice();
307
- }
308
-
309
- /** gearOutline ({outline: {points, smooth, closed}, outerRadius, rootRadius}), or null. */
310
- export function gearOutlineWasm(teeth, pressureAngleDeg, flankSamples, arcSamples) {
311
- if (!enabled || !whole(teeth, flankSamples, arcSamples) || teeth < 1) return null;
312
- wasm.reset();
313
- const res = wasm.gear_outline(teeth, pressureAngleDeg, flankSamples, arcSamples);
314
- if (!res) return null;
315
- const [count, pts, smooth, radii] = new Uint32Array(heap(), res, 4);
316
- const [outerRadius, rootRadius] = new Float64Array(heap(), radii, 2);
317
- return {
318
- outline: { points: new Float64Array(heap(), pts, count * 2).slice(), smooth: new Uint8Array(heap(), smooth, count).slice(), closed: true },
319
- outerRadius,
320
- rootRadius,
321
- };
322
- }
323
-
324
- // Header layouts of the instance kernel (the enums in wasm/simd/instances.c).
325
- export const INSTANCE_HEADER = {
326
- KIND: 0, FIRST: 1, COUNT: 2, STATE: 3, MODE: 4, NX: 5, NZ: 6, RANDOM: 7, CHOICES: 8,
327
- OPS: 9, NOPS: 10, MATS: 11, ROWS: 12, NROWS: 13, BOUNDS: 14, MATERIALS: 15, LODS: 16, OUTER_LOD: 17, SWITCH: 18, SLOTS: 19,
328
- OUT: 20, SIZE: 29,
329
- };
330
- export const INSTANCE_PARAMS = { WORLD: 0, ACC: 16, STEP: 32, SPACING: 48, JITTER: 50, RADIUS: 51, AREA: 52, ROTATION: 54, SCALE: 60, BOUNDS: 66, SIZE: 72 };
331
-
332
- /**
333
- * Instance rows of placements [FIRST, FIRST + COUNT) of a <repeat> or
334
- * <scatter> whose subtree was recorded as `template` ({ops, mats, table,
335
- * bounds}, see recordTemplate in src/scene/flatten.js). The headers `h`
336
- * (Int32Array) and `f` (Float64Array) and the table `materials` (material
337
- * index per material code, -1 until resolveMaterial(code) has given it)
338
- * carry the generator state, the accumulated repeat step, the scene bounds
339
- * and the materials from one call to the next. Returns views of kernel
340
- * memory, valid until the next kernel call, or null.
341
- */
342
- export function instancesWasm(h, f, template, materials, resolveMaterial) {
343
- if (!enabled || !simd) return null;
344
- simd.reset();
345
- const H = INSTANCE_HEADER;
346
- h[H.OPS] = put(template.ops, simd);
347
- h[H.MATS] = put(template.mats, simd);
348
- h[H.ROWS] = put(template.table, simd);
349
- h[H.BOUNDS] = put(template.bounds, simd);
350
- h[H.MATERIALS] = put(materials, simd);
351
- const hp = put(h, simd), fp = put(f, simd);
352
- if (!h[H.OPS] || !h[H.MATS] || !h[H.ROWS] || !h[H.BOUNDS] || !h[H.MATERIALS] || !hp || !fp) return null;
353
- material = resolveMaterial;
354
- try {
355
- if (!simd.instances(hp, fp)) return null;
356
- } finally {
357
- material = null;
358
- }
359
- const buf = simd.memory.buffer;
360
- h.set(new Int32Array(buf, hp, H.SIZE));
361
- f.set(new Float64Array(buf, fp, INSTANCE_PARAMS.SIZE));
362
- materials.set(new Int32Array(buf, h[H.MATERIALS], materials.length));
363
- const n = h[H.COUNT] * h[H.NROWS];
364
- const o = H.OUT;
365
- return {
366
- matrices: new Float32Array(buf, h[o], n * 16),
367
- spheres: new Float32Array(buf, h[o + 1], n * 4),
368
- geom: new Uint32Array(buf, h[o + 2], n),
369
- mat: new Uint32Array(buf, h[o + 3], n),
370
- uid: new Uint32Array(buf, h[o + 4], n),
371
- lodNode: new Int32Array(buf, h[o + 5], n),
372
- lodChild: new Uint16Array(buf, h[o + 6], n),
373
- flags: new Uint8Array(buf, h[o + 7], n),
374
- switches: new Float64Array(buf, h[o + 8], h[H.COUNT] * h[H.LODS] * 4),
375
- };
376
- }
377
-
378
- /** es3dCos(x) and es3dSin(x) as the instance kernel computes them (for the tests), or null. */
379
- export function cosSinWasm(x) {
380
- if (!enabled || !simd) return null;
381
- simd.reset();
382
- const p = simd.alloc(16);
383
- simd.cos_sin(x, p);
384
- return Array.from(new Float64Array(simd.memory.buffer, p, 2));
385
- }
1
+ // WebAssembly versions of the reader's hottest loops: number lists, normals,
2
+ // sweeps, extrusions, triangulation, primitives and the instance rows of
3
+ // <repeat> and <scatter>. They are built from wasm/ by `npm run wasm` and
4
+ // compute exactly what the JavaScript versions compute (the same IEEE
5
+ // operations in the same order; sin, cos, atan2 and hypot come from this
6
+ // page's Math). Where WebAssembly is missing or blocked, for example by a
7
+ // content security policy without 'wasm-unsafe-eval', every caller keeps
8
+ // using its JavaScript version.
9
+
10
+ import BINARY, { INSTANCES } from './kernels-binary.js';
11
+
12
+ let text = ''; // the number list being read, for the rare numbers the kernel hands back to Number()
13
+ let material = null; // resolves an instance row's material code (see instancesWasm)
14
+
15
+ const imports = {
16
+ env: {
17
+ sin: Math.sin,
18
+ cos: Math.cos,
19
+ atan2: Math.atan2,
20
+ tan: Math.tan,
21
+ acos: Math.acos,
22
+ hypot2: Math.hypot,
23
+ hypot3: Math.hypot,
24
+ number: (start, end) => Number(text.slice(start, end)),
25
+ material: (code) => material(code),
26
+ },
27
+ };
28
+
29
+ function decode(base64) {
30
+ // native decoders where there is one (newer browsers, Node); a loop otherwise
31
+ if (typeof Uint8Array.fromBase64 === 'function') return Uint8Array.fromBase64(base64);
32
+ if (typeof Buffer === 'function') return Buffer.from(base64, 'base64');
33
+ const s = atob(base64);
34
+ const bytes = new Uint8Array(s.length);
35
+ for (let i = 0; i < s.length; i++) bytes[i] = s.charCodeAt(i);
36
+ return bytes;
37
+ }
38
+
39
+ async function instantiate(base64) {
40
+ try {
41
+ if (typeof WebAssembly !== 'object') return null;
42
+ const { instance } = await WebAssembly.instantiate(decode(base64), imports);
43
+ return instance.exports;
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ // the instance kernel is a module of its own: it needs WebAssembly SIMD
50
+ const [wasm, simd] = await Promise.all([instantiate(BINARY), instantiate(INSTANCES)]);
51
+ let enabled = wasm !== null;
52
+
53
+ /**
54
+ * Whether the module's hypot (V8's algorithm) gives this engine's Math.hypot
55
+ * results. The special cases (infinities, NaN, zeros) are fixed by the
56
+ * language; the rounding of finite values is the engine's, so values over
57
+ * many magnitudes are compared. Where they differ the kernels keep calling
58
+ * Math.hypot, which is slower (every call leaves WebAssembly and allocates).
59
+ */
60
+ function hypotMatches() {
61
+ const edge = [0, -0, 3, -4, 5e-324, 1e308, Infinity, NaN];
62
+ for (const x of edge) for (const y of edge) if (!Object.is(wasm.hypot2(x, y), Math.hypot(x, y))) return false;
63
+ let seed = 12345;
64
+ const next = () => {
65
+ seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
66
+ return (seed / 4294967296 - 0.5) * 10 ** ((seed % 41) - 20);
67
+ };
68
+ for (let i = 0; i < 100; i++) {
69
+ const x = next(), y = next(), z = next();
70
+ if (!Object.is(wasm.hypot3(x, y, z), Math.hypot(x, y, z)) || !Object.is(wasm.hypot2(x, y), Math.hypot(x, y))) return false;
71
+ }
72
+ return true;
73
+ }
74
+ if (wasm) wasm.set_native_hypot(hypotMatches() ? 1 : 0);
75
+
76
+ /** Whether the kernels are in use. */
77
+ export function wasmEnabled() {
78
+ return enabled;
79
+ }
80
+
81
+ /** Whether the instance kernel is in use (it needs WebAssembly SIMD as well). */
82
+ export function instanceKernelEnabled() {
83
+ return enabled && simd !== null;
84
+ }
85
+
86
+ /** Turn the kernels off (or back on, when they loaded) to compare with the JavaScript versions. */
87
+ export function setWasmEnabled(on) {
88
+ enabled = Boolean(on) && wasm !== null;
89
+ }
90
+
91
+ const heap = () => wasm.memory.buffer;
92
+
93
+ /** Copy a typed array into a module's memory; returns its address (0 when memory ran out). */
94
+ function put(a, module = wasm) {
95
+ const p = module.alloc(Math.max(1, a.byteLength));
96
+ if (p) new Uint8Array(module.memory.buffer, p, a.byteLength).set(new Uint8Array(a.buffer, a.byteOffset, a.byteLength));
97
+ return p;
98
+ }
99
+
100
+ const encoder = new TextEncoder();
101
+ const AS = { f64: [0, Float64Array], f32: [1, Float32Array], index: [2, Uint32Array] };
102
+
103
+ /**
104
+ * A number list read into a typed array (`as`: 'f64', 'f32' or 'index'), or
105
+ * null when the JavaScript reader must handle it (the kernels are off, the
106
+ * text is not ASCII, or it has an error, which JavaScript then reports).
107
+ */
108
+ export function readNumbersWasm(raw, as) {
109
+ if (!enabled) return null;
110
+ const [kind, Type] = AS[as];
111
+ const n = raw.length;
112
+ wasm.reset();
113
+ const sp = wasm.alloc(Math.max(1, n));
114
+ const op = sp && wasm.alloc((Math.floor(n / 2) + 1) * Type.BYTES_PER_ELEMENT);
115
+ if (!op) return null;
116
+ const r = encoder.encodeInto(raw, new Uint8Array(heap(), sp, n));
117
+ if (r.read !== n || r.written !== n) return null;
118
+ text = raw;
119
+ const count = wasm.numbers(sp, n, op, kind);
120
+ text = '';
121
+ if (count < 0) return null;
122
+ return new Type(heap(), op, count).slice();
123
+ }
124
+
125
+ /**
126
+ * computeNormals for Float32Array positions and uvs and Uint32Array indices,
127
+ * as {positions, normals, uvs, indices}, or null to use the JavaScript version.
128
+ */
129
+ export function computeNormalsWasm(positions, indices, crease, cosCrease, uvs) {
130
+ if (!enabled || !(positions instanceof Float32Array) || !(indices instanceof Uint32Array) || (uvs && !(uvs instanceof Float32Array))) return null;
131
+ wasm.reset();
132
+ const pp = put(positions), ip = put(indices), tp = uvs ? put(uvs) : 0;
133
+ if (!pp || !ip || (uvs && !tp)) return null;
134
+ const res = wasm.normals(pp, positions.length / 3, ip, indices.length, tp, crease, cosCrease);
135
+ if (!res) return null;
136
+ const [vc, ic, p, nrm, t, i] = new Uint32Array(heap(), res, 6);
137
+ return {
138
+ positions: new Float32Array(heap(), p, vc * 3).slice(),
139
+ normals: new Float32Array(heap(), nrm, vc * 3).slice(),
140
+ uvs: new Float32Array(heap(), t, vc * 2).slice(),
141
+ indices: new Uint32Array(heap(), i, ic).slice(),
142
+ };
143
+ }
144
+
145
+ const f64 = (a) => (a instanceof Float64Array ? a : Float64Array.from(a));
146
+ const u32 = (a) => (a instanceof Uint32Array ? a : Uint32Array.from(a));
147
+ const u8 = (a) => (a instanceof Uint8Array ? a : Uint8Array.from(a));
148
+
149
+ /**
150
+ * Writes contours ({points, smooth, isWall}) as {n, points, smooth, isWall}
151
+ * records; returns the table's address, or 0 when memory ran out.
152
+ */
153
+ function contourTable(list) {
154
+ const rows = new Int32Array(list.length * 4);
155
+ for (let k = 0; k < list.length; k++) {
156
+ const c = list[k];
157
+ const p = put(f64(c.points));
158
+ const sm = c.smooth ? put(u8(c.smooth)) : 0;
159
+ if (!p || (c.smooth && !sm)) return 0;
160
+ rows[k * 4] = c.points.length / 2;
161
+ rows[k * 4 + 1] = p;
162
+ rows[k * 4 + 2] = sm;
163
+ rows[k * 4 + 3] = c.isWall === false ? 0 : 1;
164
+ }
165
+ return put(rows);
166
+ }
167
+
168
+ /** {first contour, contour count} per region, appending each region's contours to `list`. */
169
+ function regionRows(regions, list) {
170
+ const rows = new Int32Array(Math.max(1, regions.length * 2));
171
+ regions.forEach((r, k) => {
172
+ rows[k * 2] = list.length;
173
+ rows[k * 2 + 1] = 1 + r.holes.length;
174
+ list.push(r.outer, ...r.holes);
175
+ });
176
+ return rows;
177
+ }
178
+
179
+ /** A kernel's mesh, copied out of kernel memory into one buffer (one allocation instead of four). */
180
+ const meshOut = (res, extra) => {
181
+ const [vc, ic, p, nrm, t, i] = new Uint32Array(heap(), res, 6);
182
+ const out = new ArrayBuffer((vc * 8 + ic) * 4);
183
+ const positions = new Float32Array(out, 0, vc * 3);
184
+ const normals = new Float32Array(out, vc * 12, vc * 3);
185
+ const uvs = new Float32Array(out, vc * 24, vc * 2);
186
+ const indices = new Uint32Array(out, vc * 32, ic);
187
+ positions.set(new Float32Array(heap(), p, vc * 3));
188
+ normals.set(new Float32Array(heap(), nrm, vc * 3));
189
+ uvs.set(new Float32Array(heap(), t, vc * 2));
190
+ indices.set(new Uint32Array(heap(), i, ic));
191
+ return { positions, normals, uvs, indices, ...extra };
192
+ };
193
+
194
+ /**
195
+ * sweepMesh after its profile is known: spines [{points, closed}], the
196
+ * profile contours, the circle's points (circle profiles) or the regions to
197
+ * cap (shaped profiles). Returns {positions, normals, uvs, indices, open,
198
+ * degenerate: one flag per spine, errors: triangulation error per cap
199
+ * region}, or null to use the JavaScript version.
200
+ */
201
+ export function sweepWasm(spines, contours, circle, regions, twist, taper, caps) {
202
+ if (!enabled) return null;
203
+ wasm.reset();
204
+ const list = [...contours];
205
+ const regionTab = circle ? null : regionRows(regions, list);
206
+ const table = contourTable(list);
207
+ const spineRows = new Int32Array(Math.max(1, spines.length * 3));
208
+ for (let k = 0; k < spines.length; k++) {
209
+ spineRows[k * 3] = put(f64(spines[k].points));
210
+ spineRows[k * 3 + 1] = spines[k].points.length / 3;
211
+ spineRows[k * 3 + 2] = spines[k].closed ? 1 : 0;
212
+ if (!spineRows[k * 3]) return null;
213
+ }
214
+ const profile = put(Int32Array.from(contours, (_, k) => k));
215
+ const spineTab = put(spineRows);
216
+ const capData = circle ? put(f64(circle)) : put(regionTab);
217
+ const header = put(
218
+ Int32Array.of(spines.length, spineTab, list.length, table, contours.length, profile, circle ? 1 : 2, circle ? circle.length / 2 : regions.length, capData, caps ? 1 : 0),
219
+ );
220
+ if (!table || !profile || !spineTab || !capData || !header) return null;
221
+ const res = wasm.sweep(header, twist, taper);
222
+ if (!res) return null;
223
+ const [open, deg, errs] = new Uint32Array(heap(), res + 24, 3);
224
+ return meshOut(res, {
225
+ open: open !== 0,
226
+ degenerate: Array.from(new Uint8Array(heap(), deg, spines.length), (d) => d !== 0),
227
+ errors: circle ? [] : Array.from(new Float64Array(heap(), errs, regions.length)),
228
+ });
229
+ }
230
+
231
+ /**
232
+ * triangulate(data, holeIndices) as an array of vertex indices, or null to
233
+ * use the JavaScript version (also when two holes tie for the sort order).
234
+ */
235
+ export function triangulateWasm(data, holeIndices) {
236
+ if (!enabled) return null;
237
+ wasm.reset();
238
+ const dp = put(f64(data));
239
+ const holes = holeIndices && holeIndices.length ? Int32Array.from(holeIndices) : null;
240
+ const hp = holes ? put(holes) : 0;
241
+ if (!dp || (holes && !hp)) return null;
242
+ let res;
243
+ try {
244
+ res = wasm.triangulate(dp, data.length, hp, holes ? holes.length : 0);
245
+ } catch {
246
+ return null; // stack exhausted on pathological input: JavaScript has a deeper stack
247
+ }
248
+ if (!res) return null;
249
+ const [count, p] = new Uint32Array(heap(), res, 2);
250
+ return count ? Array.from(new Uint32Array(heap(), p, count)) : [];
251
+ }
252
+
253
+ /**
254
+ * extrudeMesh from a shape ({regions, walls}) with the bevel profile rings
255
+ * {inset, z, nr, nz} and bounds from JavaScript. Returns {positions,
256
+ * normals, uvs, indices, errors: triangulation error per region}, or null
257
+ * to use the JavaScript version.
258
+ */
259
+ export function extrudeWasm(shape, bevel, rings, z0, zTop, minX, minY, S) {
260
+ if (!enabled) return null;
261
+ wasm.reset();
262
+ const list = [];
263
+ const regionTab = put(regionRows(shape.regions, list));
264
+ const walls = put(Int32Array.from(shape.walls, (_, k) => list.length + k));
265
+ list.push(...shape.walls);
266
+ const table = contourTable(list);
267
+ const ringData = new Float64Array(Math.max(1, rings.length * 4));
268
+ rings.forEach((r, k) => ringData.set([r.inset, r.z, r.nr, r.nz], k * 4));
269
+ const ringPtr = put(ringData);
270
+ const header = put(Int32Array.of(list.length, table, shape.regions.length, regionTab, shape.walls.length, walls, rings.length, ringPtr));
271
+ if (!regionTab || !walls || !table || !ringPtr || !header) return null;
272
+ const res = wasm.extrude(header, bevel, z0, zTop, minX, minY, S);
273
+ if (!res) return null;
274
+ const errs = new Uint32Array(heap(), res + 24, 1)[0];
275
+ return meshOut(res, { errors: Array.from(new Float64Array(heap(), errs, shape.regions.length)) });
276
+ }
277
+
278
+ /** measureSphereDeviation(w) for w >= 3, or null to use the JavaScript version. */
279
+ export function sphereDeviationWasm(w) {
280
+ if (!enabled) return null;
281
+ wasm.reset();
282
+ const v = wasm.sphere_deviation(w);
283
+ return v < 0 ? null : v;
284
+ }
285
+
286
+ const whole = (...v) => v.every((x) => Number.isInteger(x) && x >= 0 && x < 2 ** 30);
287
+ const prim = (res) => (res ? meshOut(res) : null);
288
+
289
+ /** unitSphere, unitCylinder, unitCapsule, unitTorus and roundedBox ({positions, normals, uvs, indices}), or null. */
290
+ export const primitivesWasm = {
291
+ sphere: (segments) => (enabled && whole(segments) ? (wasm.reset(), prim(wasm.sphere(segments))) : null),
292
+ cylinder: (top, segments, caps) => (enabled && whole(segments) ? (wasm.reset(), prim(wasm.cylinder(top, segments, caps ? 1 : 0))) : null),
293
+ capsule: (L, segments) => (enabled && whole(segments) ? (wasm.reset(), prim(wasm.capsule(L, segments))) : null),
294
+ torus: (t, arcDeg, segments, tubeSegments) => (enabled && whole(segments, tubeSegments) ? (wasm.reset(), prim(wasm.torus(t, arcDeg, segments, tubeSegments))) : null),
295
+ roundedBox: (w, h, d, r, seg) => (enabled && whole(seg) && seg >= 1 ? (wasm.reset(), prim(wasm.rounded_box(w, h, d, r, seg))) : null),
296
+ };
297
+
298
+ /** catmullRom for two or more points (a Float64Array), or null. */
299
+ export function catmullRomWasm(points, closed, subdivisions) {
300
+ if (!enabled || !whole(subdivisions) || points.length < 6 || points.length % 3) return null;
301
+ wasm.reset();
302
+ const p = put(f64(points));
303
+ const res = p && wasm.catmull_rom(p, points.length / 3, closed ? 1 : 0, subdivisions);
304
+ if (!res) return null;
305
+ const [count, out] = new Uint32Array(heap(), res, 2);
306
+ return new Float64Array(heap(), out, count * 3).slice();
307
+ }
308
+
309
+ /** gearOutline ({outline: {points, smooth, closed}, outerRadius, rootRadius}), or null. */
310
+ export function gearOutlineWasm(teeth, pressureAngleDeg, flankSamples, arcSamples) {
311
+ if (!enabled || !whole(teeth, flankSamples, arcSamples) || teeth < 1) return null;
312
+ wasm.reset();
313
+ const res = wasm.gear_outline(teeth, pressureAngleDeg, flankSamples, arcSamples);
314
+ if (!res) return null;
315
+ const [count, pts, smooth, radii] = new Uint32Array(heap(), res, 4);
316
+ const [outerRadius, rootRadius] = new Float64Array(heap(), radii, 2);
317
+ return {
318
+ outline: { points: new Float64Array(heap(), pts, count * 2).slice(), smooth: new Uint8Array(heap(), smooth, count).slice(), closed: true },
319
+ outerRadius,
320
+ rootRadius,
321
+ };
322
+ }
323
+
324
+ // Header layouts of the instance kernel (the enums in wasm/simd/instances.c).
325
+ export const INSTANCE_HEADER = {
326
+ KIND: 0, FIRST: 1, COUNT: 2, STATE: 3, MODE: 4, NX: 5, NZ: 6, RANDOM: 7, CHOICES: 8,
327
+ OPS: 9, NOPS: 10, MATS: 11, ROWS: 12, NROWS: 13, BOUNDS: 14, MATERIALS: 15, LODS: 16, OUTER_LOD: 17, SWITCH: 18, SLOTS: 19,
328
+ OUT: 20, SIZE: 29,
329
+ };
330
+ export const INSTANCE_PARAMS = { WORLD: 0, ACC: 16, STEP: 32, SPACING: 48, JITTER: 50, RADIUS: 51, AREA: 52, ROTATION: 54, SCALE: 60, BOUNDS: 66, SIZE: 72 };
331
+
332
+ /**
333
+ * Instance rows of placements [FIRST, FIRST + COUNT) of a <repeat> or
334
+ * <scatter> whose subtree was recorded as `template` ({ops, mats, table,
335
+ * bounds}, see recordTemplate in src/scene/flatten.js). The headers `h`
336
+ * (Int32Array) and `f` (Float64Array) and the table `materials` (material
337
+ * index per material code, -1 until resolveMaterial(code) has given it)
338
+ * carry the generator state, the accumulated repeat step, the scene bounds
339
+ * and the materials from one call to the next. Returns views of kernel
340
+ * memory, valid until the next kernel call, or null.
341
+ */
342
+ export function instancesWasm(h, f, template, materials, resolveMaterial) {
343
+ if (!enabled || !simd) return null;
344
+ simd.reset();
345
+ const H = INSTANCE_HEADER;
346
+ h[H.OPS] = put(template.ops, simd);
347
+ h[H.MATS] = put(template.mats, simd);
348
+ h[H.ROWS] = put(template.table, simd);
349
+ h[H.BOUNDS] = put(template.bounds, simd);
350
+ h[H.MATERIALS] = put(materials, simd);
351
+ const hp = put(h, simd), fp = put(f, simd);
352
+ if (!h[H.OPS] || !h[H.MATS] || !h[H.ROWS] || !h[H.BOUNDS] || !h[H.MATERIALS] || !hp || !fp) return null;
353
+ material = resolveMaterial;
354
+ try {
355
+ if (!simd.instances(hp, fp)) return null;
356
+ } finally {
357
+ material = null;
358
+ }
359
+ const buf = simd.memory.buffer;
360
+ h.set(new Int32Array(buf, hp, H.SIZE));
361
+ f.set(new Float64Array(buf, fp, INSTANCE_PARAMS.SIZE));
362
+ materials.set(new Int32Array(buf, h[H.MATERIALS], materials.length));
363
+ const n = h[H.COUNT] * h[H.NROWS];
364
+ const o = H.OUT;
365
+ return {
366
+ matrices: new Float32Array(buf, h[o], n * 16),
367
+ spheres: new Float32Array(buf, h[o + 1], n * 4),
368
+ geom: new Uint32Array(buf, h[o + 2], n),
369
+ mat: new Uint32Array(buf, h[o + 3], n),
370
+ uid: new Uint32Array(buf, h[o + 4], n),
371
+ lodNode: new Int32Array(buf, h[o + 5], n),
372
+ lodChild: new Uint16Array(buf, h[o + 6], n),
373
+ flags: new Uint8Array(buf, h[o + 7], n),
374
+ switches: new Float64Array(buf, h[o + 8], h[H.COUNT] * h[H.LODS] * 4),
375
+ };
376
+ }
377
+
378
+ /** es3dCos(x) and es3dSin(x) as the instance kernel computes them (for the tests), or null. */
379
+ export function cosSinWasm(x) {
380
+ if (!enabled || !simd) return null;
381
+ simd.reset();
382
+ const p = simd.alloc(16);
383
+ simd.cos_sin(x, p);
384
+ return Array.from(new Float64Array(simd.memory.buffer, p, 2));
385
+ }