@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.
- package/LICENSE +201 -201
- package/README.md +95 -86
- package/docs/SPEC.md +426 -417
- package/package.json +1 -1
- package/scripts/es3d-cli.mjs +390 -377
- package/src/cache/geometry-store.js +392 -392
- package/src/cache/mesh-bundle.js +123 -123
- package/src/cache/sha256.js +73 -73
- package/src/format/ast.js +238 -238
- package/src/format/colors.js +93 -93
- package/src/format/compact-parser.js +171 -171
- package/src/format/compact-tokenizer.js +165 -165
- package/src/format/errors.js +247 -247
- package/src/format/json.js +55 -55
- package/src/format/limits.js +14 -14
- package/src/format/parse.js +132 -92
- package/src/format/schema.js +617 -617
- package/src/format/serializer.js +333 -333
- package/src/format/validator.js +330 -330
- package/src/format/value-error.js +12 -12
- package/src/format/value-format.js +314 -314
- package/src/format/values.js +459 -459
- package/src/format/xml-parser.js +136 -136
- package/src/format/xml-tokenizer.js +237 -237
- package/src/geometry/contour.js +86 -86
- package/src/geometry/extrude.js +175 -175
- package/src/geometry/gear.js +85 -85
- package/src/geometry/geometry-cache.js +159 -159
- package/src/geometry/lod-error.js +178 -178
- package/src/geometry/mesh.js +458 -458
- package/src/geometry/primitives.js +299 -299
- package/src/geometry/registry.js +645 -645
- package/src/geometry/revolve.js +136 -136
- package/src/geometry/sweep.js +441 -441
- package/src/index.js +47 -47
- package/src/io/compression.js +67 -67
- package/src/io/convert.js +236 -236
- package/src/io/download.js +13 -13
- package/src/io/external-scene.js +127 -127
- package/src/io/file-loader.js +28 -28
- package/src/io/gltf-pack.js +1461 -0
- package/src/io/gltf.js +630 -630
- package/src/io/mesh-import.js +104 -104
- package/src/io/obj.js +352 -352
- package/src/io/step-brep.js +439 -439
- package/src/io/step-exact.js +1149 -1149
- package/src/io/step.js +1418 -1418
- package/src/io/stl.js +193 -193
- package/src/io/svg-import.js +787 -787
- package/src/material/materials.js +154 -154
- package/src/math/mat4.js +475 -475
- package/src/math/random.js +30 -30
- package/src/math/trig.js +65 -65
- package/src/math/vec3.js +81 -81
- package/src/optimize/optimizer.js +604 -579
- package/src/path/path-flatten.js +322 -322
- package/src/path/path-parser.js +332 -332
- package/src/path/shapes.js +129 -129
- package/src/path/triangulate.js +517 -517
- package/src/pipeline.js +74 -74
- package/src/render/camera.js +168 -168
- package/src/render/frame-builder.js +362 -362
- package/src/render/geometry-pool.js +149 -149
- package/src/render/lighting.js +149 -149
- package/src/render/orbit-controls.js +237 -237
- package/src/render/renderer.js +258 -249
- package/src/render/scroll-timeline.js +104 -104
- package/src/render/shaders/glsl.js +285 -285
- package/src/render/shaders/wgsl.js +252 -252
- package/src/render/stage.js +365 -352
- package/src/render/texture-cache.js +40 -40
- package/src/render/textures.js +223 -223
- package/src/render/webgl2/webgl2-backend.js +586 -532
- package/src/render/webgpu/webgpu-backend.js +482 -480
- package/src/scene/flatten.js +754 -754
- package/src/scene/picking.js +161 -161
- package/src/scene/scene-graph.js +320 -320
- package/src/transform/transform.js +211 -211
- package/src/wasm/kernels-binary.js +3 -3
- package/src/wasm/kernels.js +385 -385
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
/** Error raised by attribute value parsers; offsets are relative to the attribute value. */
|
|
2
|
-
export class ValueError extends Error {
|
|
3
|
-
constructor(message, offset = 0, length = 0, extra = {}) {
|
|
4
|
-
super(message);
|
|
5
|
-
this.name = 'ValueError';
|
|
6
|
-
this.offset = offset;
|
|
7
|
-
this.length = length;
|
|
8
|
-
this.hint = extra.hint || null;
|
|
9
|
-
this.suggestion = extra.suggestion || null;
|
|
10
|
-
this.code = extra.code || null;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
1
|
+
/** Error raised by attribute value parsers; offsets are relative to the attribute value. */
|
|
2
|
+
export class ValueError extends Error {
|
|
3
|
+
constructor(message, offset = 0, length = 0, extra = {}) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = 'ValueError';
|
|
6
|
+
this.offset = offset;
|
|
7
|
+
this.length = length;
|
|
8
|
+
this.hint = extra.hint || null;
|
|
9
|
+
this.suggestion = extra.suggestion || null;
|
|
10
|
+
this.code = extra.code || null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -1,314 +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
|
-
}
|
|
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
|
+
}
|