@es3d/es3d 0.0.0-stage → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +85 -2
  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 +258 -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 +364 -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 +586 -0
  74. package/src/render/webgpu/webgpu-backend.js +482 -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,314 @@
1
+ // Formatting of typed values back to text: canonical numbers, quantization,
2
+ // number lists, transform lists and a path data minifier (shortest of
3
+ // absolute/relative commands, implicit repetition, minimal separators).
4
+
5
+ import { colorToShortHex } from './colors.js';
6
+
7
+ /**
8
+ * Shortest decimal representation of x.
9
+ * @param {number} x
10
+ * @param {number|null} precision decimals to keep (quantization), null = exact
11
+ * @param {boolean} compact drop the leading zero ("0.5" -> ".5")
12
+ */
13
+ export function formatNumber(x, precision = null, compact = false) {
14
+ if (precision !== null && precision !== undefined) {
15
+ const f = 10 ** precision;
16
+ x = Math.round(x * f) / f;
17
+ }
18
+ if (x === 0 || Object.is(x, -0)) return '0';
19
+ let s = String(x);
20
+ if (s.indexOf('e') >= 0) {
21
+ s = s.replace('e+', 'e');
22
+ // prefer plain notation when it is not longer
23
+ const plain = toPlain(x);
24
+ if (plain && plain.length <= s.length) s = plain;
25
+ }
26
+ if (compact) {
27
+ if (s.startsWith('0.')) s = s.slice(1);
28
+ else if (s.startsWith('-0.')) s = '-' + s.slice(2);
29
+ }
30
+ return s;
31
+ }
32
+
33
+ /**
34
+ * Shortest decimal that converts back to the same float32 value (mesh arrays
35
+ * are float32 data: printing their double expansion would add noise digits).
36
+ */
37
+ export function formatFloat32(x, precision = null, compact = false) {
38
+ if (precision !== null && precision !== undefined) return formatNumber(x, precision, compact);
39
+ if (x === 0 || !Number.isFinite(x)) return formatNumber(x, null, compact);
40
+ // toPrecision(9) always round-trips; binary search for the fewest digits
41
+ let lo = 1, hi = 9;
42
+ while (lo < hi) {
43
+ const mid = (lo + hi) >> 1;
44
+ if (Math.fround(Number(x.toPrecision(mid))) === x) hi = mid;
45
+ else lo = mid + 1;
46
+ }
47
+ return formatNumber(Number(x.toPrecision(lo)), null, compact);
48
+ }
49
+
50
+ function toPlain(x) {
51
+ const abs = Math.abs(x);
52
+ if (abs >= 1e21 || abs < 1e-12) return null;
53
+ let s = abs.toFixed(20).replace(/0+$/, '').replace(/\.$/, '');
54
+ // guard against binary noise introduced by toFixed
55
+ if (Number(s) !== abs) return null;
56
+ return (x < 0 ? '-' : '') + s;
57
+ }
58
+
59
+ /** Join numbers with the minimal separators allowed by the SVG path grammar. */
60
+ export function joinNumbersMinimal(strs, prevLast = null) {
61
+ let out = '';
62
+ let prev = prevLast;
63
+ for (const s of strs) {
64
+ if (prev === null) out += s;
65
+ else if (s[0] === '-' || (s[0] === '.' && (prev.includes('.') || prev.includes('e')))) out += s;
66
+ else out += ' ' + s;
67
+ prev = s;
68
+ }
69
+ return out;
70
+ }
71
+
72
+ /**
73
+ * Format a flat numeric list.
74
+ * group > 1 writes "x,y,z x,y,z" (points); group === 1 with sep writes "a,b,c" or "a b c".
75
+ */
76
+ export function formatNumberList(list, { group = 1, sep = ',', precision = null, compact = false } = {}) {
77
+ const parts = new Array(list.length);
78
+ const fmt = list instanceof Float32Array ? formatFloat32 : formatNumber;
79
+ for (let i = 0; i < list.length; i++) parts[i] = fmt(list[i], precision, compact);
80
+ if (group <= 1) return parts.join(sep);
81
+ const groups = [];
82
+ for (let i = 0; i < parts.length; i += group) groups.push(parts.slice(i, i + group).join(','));
83
+ return groups.join(' ');
84
+ }
85
+
86
+ export function formatColor(c) {
87
+ return colorToShortHex(c);
88
+ }
89
+
90
+ /** Transform ops -> "translate(1,2,3) rotate(0,45,0)" */
91
+ export function formatTransformOps(ops, precision = null, compact = false) {
92
+ return ops.map((op) => `${op.fn}(${op.args.map((a) => formatNumber(a, precision, compact)).join(',')})`).join(' ');
93
+ }
94
+
95
+ const EPS = 1e-9;
96
+
97
+ /**
98
+ * Serialize absolute path segments (from parsePathData) as compact path data.
99
+ * @param {object[]} segments
100
+ * @param {{precision?: number|null, minify?: boolean, dim?: 2|3}} opts
101
+ */
102
+ export function formatPathData(segments, { precision = null, minify = true, dim = 2 } = {}) {
103
+ const fmt = (v) => formatNumber(v, precision, minify);
104
+ const q = (v) => (precision === null || precision === undefined ? v : Math.round(v * 10 ** precision) / 10 ** precision);
105
+ if (!minify) return formatPathPretty(segments, fmt, dim);
106
+
107
+ let out = '';
108
+ let lastCmd = '';
109
+ let lastNum = null;
110
+ // reconstructed (decoder side) current point and subpath start
111
+ let cx = 0, cy = 0, cz = 0;
112
+ let sx = 0, sy = 0, sz = 0;
113
+ let prevC2 = null; // reflected control point candidates
114
+ let prevQ1 = null;
115
+ let prevType = '';
116
+
117
+ const emit = (cmd, nums) => {
118
+ const strs = nums.map(fmt);
119
+ // a repeated command (other than moveto/closepath) can omit its letter, and
120
+ // so can a lineto directly after a moveto of the same relativity
121
+ const omit =
122
+ (cmd === lastCmd && cmd !== 'M' && cmd !== 'm') ||
123
+ (lastCmd === 'M' && cmd === 'L') ||
124
+ (lastCmd === 'm' && cmd === 'l');
125
+ out += omit ? joinNumbersMinimal(strs, lastNum) : cmd + joinNumbersMinimal(strs, null);
126
+ if (strs.length) lastNum = strs[strs.length - 1];
127
+ lastCmd = cmd;
128
+ };
129
+ const pick = (absCmd, absNums, relCmd, relNums) => {
130
+ const a = absCmd + absNums.map(fmt).join(' ');
131
+ const r = relCmd + relNums.map(fmt).join(' ');
132
+ return r.length < a.length ? [relCmd, relNums] : [absCmd, absNums];
133
+ };
134
+
135
+ for (const s of segments) {
136
+ switch (s.c) {
137
+ case 'M': {
138
+ if (s.implicit) break;
139
+ const x = q(s.x), y = q(s.y), z = q(s.z || 0);
140
+ const abs = dim === 3 ? [x, y, z] : [x, y];
141
+ const rel = dim === 3 ? [q(x - cx), q(y - cy), q(z - cz)] : [q(x - cx), q(y - cy)];
142
+ const [cmd, nums] = out === '' ? ['M', abs] : pick('M', abs, 'm', rel);
143
+ emit(cmd, nums);
144
+ if (cmd === 'm') {
145
+ cx = cx + nums[0];
146
+ cy = cy + nums[1];
147
+ if (dim === 3) cz = cz + nums[2];
148
+ } else {
149
+ cx = x;
150
+ cy = y;
151
+ cz = z;
152
+ }
153
+ sx = cx; sy = cy; sz = cz;
154
+ prevType = 'M';
155
+ break;
156
+ }
157
+ case 'L': {
158
+ const x = q(s.x), y = q(s.y), z = q(s.z || 0);
159
+ let cmd, nums;
160
+ if (dim === 2 && Math.abs(y - cy) < EPS && Math.abs(x - cx) >= EPS) {
161
+ [cmd, nums] = pick('H', [x], 'h', [q(x - cx)]);
162
+ } else if (dim === 2 && Math.abs(x - cx) < EPS && Math.abs(y - cy) >= EPS) {
163
+ [cmd, nums] = pick('V', [y], 'v', [q(y - cy)]);
164
+ } else {
165
+ const abs = dim === 3 ? [x, y, z] : [x, y];
166
+ const rel = dim === 3 ? [q(x - cx), q(y - cy), q(z - cz)] : [q(x - cx), q(y - cy)];
167
+ [cmd, nums] = pick('L', abs, 'l', rel);
168
+ }
169
+ emit(cmd, nums);
170
+ const up = cmd.toUpperCase();
171
+ const relative = cmd !== up;
172
+ if (up === 'H') cx = relative ? cx + nums[0] : nums[0];
173
+ else if (up === 'V') cy = relative ? cy + nums[0] : nums[0];
174
+ else if (relative) {
175
+ cx += nums[0];
176
+ cy += nums[1];
177
+ if (dim === 3) cz += nums[2];
178
+ } else {
179
+ cx = nums[0];
180
+ cy = nums[1];
181
+ if (dim === 3) cz = nums[2];
182
+ }
183
+ prevType = 'L';
184
+ break;
185
+ }
186
+ case 'C': {
187
+ const p1 = [q(s.x1), q(s.y1), q(s.z1 || 0)];
188
+ const p2 = [q(s.x2), q(s.y2), q(s.z2 || 0)];
189
+ const p = [q(s.x), q(s.y), q(s.z || 0)];
190
+ const d = dim;
191
+ const take = (v) => v.slice(0, d);
192
+ const rel = (v) => [q(v[0] - cx), q(v[1] - cy), q(v[2] - cz)].slice(0, d);
193
+ let reflect = false;
194
+ if (prevC2 && (prevType === 'C')) {
195
+ const rx = 2 * cx - prevC2[0], ry = 2 * cy - prevC2[1], rz = 2 * cz - prevC2[2];
196
+ reflect = Math.abs(rx - p1[0]) < 1e-7 && Math.abs(ry - p1[1]) < 1e-7 && (d === 2 || Math.abs(rz - p1[2]) < 1e-7);
197
+ } else if (prevType !== 'C') {
198
+ reflect = Math.abs(cx - p1[0]) < 1e-7 && Math.abs(cy - p1[1]) < 1e-7 && (d === 2 || Math.abs(cz - p1[2]) < 1e-7);
199
+ }
200
+ let cmd, nums;
201
+ if (reflect) [cmd, nums] = pick('S', [...take(p2), ...take(p)], 's', [...rel(p2), ...rel(p)]);
202
+ else [cmd, nums] = pick('C', [...take(p1), ...take(p2), ...take(p)], 'c', [...rel(p1), ...rel(p2), ...rel(p)]);
203
+ emit(cmd, nums);
204
+ const relative = cmd === 'c' || cmd === 's';
205
+ const k = nums.length;
206
+ // reconstruct as the decoder will
207
+ const base = relative ? [cx, cy, cz] : [0, 0, 0];
208
+ const endP = [base[0] + nums[k - d], base[1] + nums[k - d + 1], d === 3 ? base[2] + nums[k - 1] : 0];
209
+ const c2 = [base[0] + nums[k - 2 * d], base[1] + nums[k - 2 * d + 1], d === 3 ? base[2] + nums[k - d - 1] : 0];
210
+ prevC2 = c2;
211
+ cx = endP[0];
212
+ cy = endP[1];
213
+ cz = endP[2];
214
+ prevType = 'C';
215
+ break;
216
+ }
217
+ case 'Q': {
218
+ const p1 = [q(s.x1), q(s.y1), q(s.z1 || 0)];
219
+ const p = [q(s.x), q(s.y), q(s.z || 0)];
220
+ const d = dim;
221
+ const take = (v) => v.slice(0, d);
222
+ const rel = (v) => [q(v[0] - cx), q(v[1] - cy), q(v[2] - cz)].slice(0, d);
223
+ let reflect = false;
224
+ if (prevQ1 && prevType === 'Q') {
225
+ const rx = 2 * cx - prevQ1[0], ry = 2 * cy - prevQ1[1], rz = 2 * cz - prevQ1[2];
226
+ reflect = Math.abs(rx - p1[0]) < 1e-7 && Math.abs(ry - p1[1]) < 1e-7 && (d === 2 || Math.abs(rz - p1[2]) < 1e-7);
227
+ }
228
+ let cmd, nums;
229
+ if (reflect) [cmd, nums] = pick('T', take(p), 't', rel(p));
230
+ else [cmd, nums] = pick('Q', [...take(p1), ...take(p)], 'q', [...rel(p1), ...rel(p)]);
231
+ emit(cmd, nums);
232
+ const relative = cmd === 'q' || cmd === 't';
233
+ const base = relative ? [cx, cy, cz] : [0, 0, 0];
234
+ const k = nums.length;
235
+ const endP = [base[0] + nums[k - d], base[1] + nums[k - d + 1], d === 3 ? base[2] + nums[k - 1] : 0];
236
+ prevQ1 = reflect ? [2 * cx - prevQ1[0], 2 * cy - prevQ1[1], 2 * cz - prevQ1[2]] : [base[0] + nums[0], base[1] + nums[1], d === 3 ? base[2] + nums[2] : 0];
237
+ cx = endP[0];
238
+ cy = endP[1];
239
+ cz = endP[2];
240
+ prevType = 'Q';
241
+ break;
242
+ }
243
+ case 'A': {
244
+ const x = q(s.x), y = q(s.y);
245
+ const absNums = [s.rx, s.ry, s.rot, s.large, s.sweep, x, y];
246
+ const relNums = [s.rx, s.ry, s.rot, s.large, s.sweep, q(x - cx), q(y - cy)];
247
+ const a = 'A' + absNums.map(fmt).join(' ');
248
+ const r = 'a' + relNums.map(fmt).join(' ');
249
+ const useRel = r.length < a.length;
250
+ const cmd = useRel ? 'a' : 'A';
251
+ const nums = useRel ? relNums : absNums;
252
+ // arc flags can be packed: radii, rotation, then flags without separators
253
+ const strs = [fmt(nums[0]), fmt(nums[1]), fmt(nums[2])];
254
+ const head = joinNumbersMinimal(strs, cmd === lastCmd ? lastNum : null);
255
+ const tail = joinNumbersMinimal([fmt(nums[5]), fmt(nums[6])], null);
256
+ const body = `${head} ${nums[3]}${nums[4]}${tail}`;
257
+ out += cmd === lastCmd ? body : cmd + body;
258
+ lastCmd = cmd;
259
+ lastNum = fmt(nums[6]);
260
+ if (useRel) {
261
+ cx += nums[5];
262
+ cy += nums[6];
263
+ } else {
264
+ cx = x;
265
+ cy = y;
266
+ }
267
+ prevType = 'A';
268
+ break;
269
+ }
270
+ case 'Z':
271
+ out += 'Z';
272
+ lastCmd = 'Z';
273
+ lastNum = null;
274
+ cx = sx;
275
+ cy = sy;
276
+ cz = sz;
277
+ prevType = 'Z';
278
+ break;
279
+ default:
280
+ break;
281
+ }
282
+ }
283
+ return out;
284
+ }
285
+
286
+ function formatPathPretty(segments, fmt, dim) {
287
+ const parts = [];
288
+ const pt = (x, y, z) => (dim === 3 ? `${fmt(x)},${fmt(y)},${fmt(z || 0)}` : `${fmt(x)},${fmt(y)}`);
289
+ for (const s of segments) {
290
+ switch (s.c) {
291
+ case 'M':
292
+ if (!s.implicit) parts.push('M' + pt(s.x, s.y, s.z));
293
+ break;
294
+ case 'L':
295
+ parts.push('L' + pt(s.x, s.y, s.z));
296
+ break;
297
+ case 'C':
298
+ parts.push(`C${pt(s.x1, s.y1, s.z1)} ${pt(s.x2, s.y2, s.z2)} ${pt(s.x, s.y, s.z)}`);
299
+ break;
300
+ case 'Q':
301
+ parts.push(`Q${pt(s.x1, s.y1, s.z1)} ${pt(s.x, s.y, s.z)}`);
302
+ break;
303
+ case 'A':
304
+ parts.push(`A${fmt(s.rx)},${fmt(s.ry)} ${fmt(s.rot)} ${s.large},${s.sweep} ${pt(s.x, s.y)}`);
305
+ break;
306
+ case 'Z':
307
+ parts.push('Z');
308
+ break;
309
+ default:
310
+ break;
311
+ }
312
+ }
313
+ return parts.join(' ');
314
+ }