@es3d/es3d 1.0.0 → 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.
- package/LICENSE +201 -201
- package/README.md +86 -86
- package/docs/SPEC.md +417 -417
- package/package.json +1 -1
- package/scripts/es3d-cli.mjs +377 -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 +92 -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.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 +579 -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 +364 -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
package/src/format/errors.js
CHANGED
|
@@ -1,247 +1,247 @@
|
|
|
1
|
-
// Diagnostics: every problem found while reading an ES3D document is reported
|
|
2
|
-
// as a Diagnostic with a precise source location, a human readable message and,
|
|
3
|
-
// where possible, a "did you mean" suggestion. Nothing is silently ignored.
|
|
4
|
-
|
|
5
|
-
/** Maps character offsets to 1-based line/column pairs. */
|
|
6
|
-
export class LineIndex {
|
|
7
|
-
constructor(text) {
|
|
8
|
-
this.text = text;
|
|
9
|
-
this.lineStarts = null;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/** Offset of every line start, found on first use: a valid document never asks. */
|
|
13
|
-
get starts() {
|
|
14
|
-
if (!this.lineStarts) {
|
|
15
|
-
const starts = [0];
|
|
16
|
-
for (let i = this.text.indexOf('\n'); i >= 0; i = this.text.indexOf('\n', i + 1)) starts.push(i + 1);
|
|
17
|
-
this.lineStarts = starts;
|
|
18
|
-
}
|
|
19
|
-
return this.lineStarts;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
position(offset) {
|
|
23
|
-
const starts = this.starts;
|
|
24
|
-
let lo = 0;
|
|
25
|
-
let hi = starts.length - 1;
|
|
26
|
-
while (lo < hi) {
|
|
27
|
-
const mid = (lo + hi + 1) >> 1;
|
|
28
|
-
if (starts[mid] <= offset) lo = mid;
|
|
29
|
-
else hi = mid - 1;
|
|
30
|
-
}
|
|
31
|
-
return { line: lo + 1, column: offset - starts[lo] + 1 };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
lineText(line) {
|
|
35
|
-
const start = this.starts[line - 1];
|
|
36
|
-
if (start === undefined) return '';
|
|
37
|
-
const end = line < this.starts.length ? this.starts[line] - 1 : this.text.length;
|
|
38
|
-
return this.text.slice(start, end).replace(/\r$/, '');
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
offsetOf(line, column) {
|
|
42
|
-
const start = this.starts[Math.max(0, Math.min(line - 1, this.starts.length - 1))];
|
|
43
|
-
return start + column - 1;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const PHASE_TITLES = {
|
|
48
|
-
parse: 'Parse',
|
|
49
|
-
reference: 'Reference',
|
|
50
|
-
geometry: 'Geometry',
|
|
51
|
-
render: 'Render',
|
|
52
|
-
io: 'File',
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
export class Diagnostic {
|
|
56
|
-
/**
|
|
57
|
-
* @param {object} o
|
|
58
|
-
* @param {'error'|'warning'|'info'} [o.severity]
|
|
59
|
-
* @param {'parse'|'reference'|'geometry'|'render'|'io'} [o.phase]
|
|
60
|
-
* @param {string} o.code stable machine readable code, e.g. E_UNKNOWN_ATTRIBUTE
|
|
61
|
-
* @param {string} o.message human readable message
|
|
62
|
-
* @param {number} [o.offset] character offset of the problem in the source text
|
|
63
|
-
* @param {number} [o.length] length of the offending text
|
|
64
|
-
* @param {string} [o.suggestion] "Did you mean" candidate
|
|
65
|
-
* @param {string} [o.hint] extra explanation
|
|
66
|
-
* @param {string} [o.path] structural path when no offsets exist (JSON / binary input)
|
|
67
|
-
*/
|
|
68
|
-
constructor(o) {
|
|
69
|
-
this.severity = o.severity || 'error';
|
|
70
|
-
this.phase = o.phase || 'parse';
|
|
71
|
-
this.code = o.code || 'E_GENERIC';
|
|
72
|
-
this.message = o.message;
|
|
73
|
-
this.offset = o.offset ?? -1;
|
|
74
|
-
this.length = o.length ?? 0;
|
|
75
|
-
this.suggestion = o.suggestion || null;
|
|
76
|
-
this.hint = o.hint || null;
|
|
77
|
-
this.path = o.path || null;
|
|
78
|
-
this.line = o.line ?? 0;
|
|
79
|
-
this.column = o.column ?? 0;
|
|
80
|
-
this.uid = o.uid ?? 0;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** Resolve line/column from an offset using the given LineIndex. */
|
|
84
|
-
locate(lineIndex) {
|
|
85
|
-
if (lineIndex && this.offset >= 0) {
|
|
86
|
-
const p = lineIndex.position(this.offset);
|
|
87
|
-
this.line = p.line;
|
|
88
|
-
this.column = p.column;
|
|
89
|
-
}
|
|
90
|
-
return this;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
get title() {
|
|
94
|
-
const phase = PHASE_TITLES[this.phase] || 'Parse';
|
|
95
|
-
return this.severity === 'warning' ? `ES3D ${phase} Warning` : this.severity === 'info' ? `ES3D ${phase} Note` : `ES3D ${phase} Error`;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** Multi-line report in the canonical ES3D format, optionally with a code frame. */
|
|
99
|
-
format(lineIndex) {
|
|
100
|
-
const lines = [this.title];
|
|
101
|
-
if (this.line > 0) lines.push(`line ${this.line}, column ${this.column}`);
|
|
102
|
-
else if (this.path) lines.push(`at ${this.path}`);
|
|
103
|
-
lines.push('', this.message);
|
|
104
|
-
if (this.suggestion) lines.push('', `Did you mean: ${this.suggestion}?`);
|
|
105
|
-
if (this.hint) lines.push('', this.hint);
|
|
106
|
-
if (lineIndex && this.line > 0) {
|
|
107
|
-
const src = lineIndex.lineText(this.line);
|
|
108
|
-
if (src.length < 400) {
|
|
109
|
-
const gutter = String(this.line).padStart(5) + ' | ';
|
|
110
|
-
const caretLen = Math.max(1, Math.min(this.length || 1, src.length - this.column + 1));
|
|
111
|
-
lines.push('', gutter + src, ' '.repeat(gutter.length + this.column - 1) + '^'.repeat(Math.max(1, caretLen)));
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
return lines.join('\n');
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
toJSON() {
|
|
118
|
-
return {
|
|
119
|
-
severity: this.severity,
|
|
120
|
-
phase: this.phase,
|
|
121
|
-
code: this.code,
|
|
122
|
-
message: this.message,
|
|
123
|
-
line: this.line,
|
|
124
|
-
column: this.column,
|
|
125
|
-
offset: this.offset,
|
|
126
|
-
length: this.length,
|
|
127
|
-
suggestion: this.suggestion,
|
|
128
|
-
hint: this.hint,
|
|
129
|
-
path: this.path,
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** Thrown when a document cannot be read at all (syntax errors). */
|
|
135
|
-
export class ES3DError extends Error {
|
|
136
|
-
constructor(diagnostics, lineIndex) {
|
|
137
|
-
const list = Array.isArray(diagnostics) ? diagnostics : [diagnostics];
|
|
138
|
-
super(list.length ? list[0].format(lineIndex) : 'ES3D Error');
|
|
139
|
-
this.name = 'ES3DError';
|
|
140
|
-
this.diagnostics = list;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/** Damerau-free Levenshtein distance with an early-exit bound. */
|
|
145
|
-
export function levenshtein(a, b, max = Infinity) {
|
|
146
|
-
if (a === b) return 0;
|
|
147
|
-
const la = a.length;
|
|
148
|
-
const lb = b.length;
|
|
149
|
-
if (Math.abs(la - lb) > max) return max + 1;
|
|
150
|
-
let prev = new Array(lb + 1);
|
|
151
|
-
let cur = new Array(lb + 1);
|
|
152
|
-
for (let j = 0; j <= lb; j++) prev[j] = j;
|
|
153
|
-
for (let i = 1; i <= la; i++) {
|
|
154
|
-
cur[0] = i;
|
|
155
|
-
let rowMin = cur[0];
|
|
156
|
-
const ca = a.charCodeAt(i - 1);
|
|
157
|
-
for (let j = 1; j <= lb; j++) {
|
|
158
|
-
const cost = ca === b.charCodeAt(j - 1) ? 0 : 1;
|
|
159
|
-
const v = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + cost);
|
|
160
|
-
cur[j] = v;
|
|
161
|
-
if (v < rowMin) rowMin = v;
|
|
162
|
-
}
|
|
163
|
-
if (rowMin > max) return max + 1;
|
|
164
|
-
const t = prev;
|
|
165
|
-
prev = cur;
|
|
166
|
-
cur = t;
|
|
167
|
-
}
|
|
168
|
-
return prev[lb];
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Best "did you mean" candidate for a misspelled name, or null.
|
|
173
|
-
* Case-insensitive matches and transposition-like typos are preferred.
|
|
174
|
-
*/
|
|
175
|
-
export function suggest(name, candidates) {
|
|
176
|
-
if (!name) return null;
|
|
177
|
-
const lower = name.toLowerCase();
|
|
178
|
-
let best = null;
|
|
179
|
-
let bestScore = Infinity;
|
|
180
|
-
const limit = Math.max(1, Math.min(3, Math.floor(name.length / 3) + 1));
|
|
181
|
-
for (const c of candidates) {
|
|
182
|
-
if (!c) continue;
|
|
183
|
-
const cl = c.toLowerCase();
|
|
184
|
-
if (cl === lower) return c;
|
|
185
|
-
let d = levenshtein(lower, cl, limit);
|
|
186
|
-
// A swapped pair of adjacent letters counts as one edit ("raduis" -> "radius").
|
|
187
|
-
if (d === 2 && lower.length === cl.length && isTransposition(lower, cl)) d = 1;
|
|
188
|
-
// Prefix matches are useful for abbreviations ("rough" -> "roughness")
|
|
189
|
-
// and for over-long names ("spotlight" -> "spot").
|
|
190
|
-
if (d > limit && ((cl.startsWith(lower) && lower.length >= 3) || (lower.startsWith(cl) && cl.length >= 3))) d = limit;
|
|
191
|
-
if (d <= limit && d < bestScore) {
|
|
192
|
-
best = c;
|
|
193
|
-
bestScore = d;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
return best;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function isTransposition(a, b) {
|
|
200
|
-
let first = -1;
|
|
201
|
-
for (let i = 0; i < a.length; i++) {
|
|
202
|
-
if (a[i] !== b[i]) {
|
|
203
|
-
if (first < 0) first = i;
|
|
204
|
-
else return i === first + 1 && a[first] === b[i] && a[i] === b[first] && a.slice(i + 1) === b.slice(i + 1);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
return false;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/** Collects diagnostics during a pass; keeps errors and warnings ordered by position. */
|
|
211
|
-
export class DiagnosticList {
|
|
212
|
-
constructor(lineIndex = null) {
|
|
213
|
-
this.lineIndex = lineIndex;
|
|
214
|
-
this.items = [];
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
add(o) {
|
|
218
|
-
const d = o instanceof Diagnostic ? o : new Diagnostic(o);
|
|
219
|
-
d.locate(this.lineIndex);
|
|
220
|
-
this.items.push(d);
|
|
221
|
-
return d;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
error(code, message, offset, length, extra = {}) {
|
|
225
|
-
return this.add({ severity: 'error', code, message, offset, length, ...extra });
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
warning(code, message, offset, length, extra = {}) {
|
|
229
|
-
return this.add({ severity: 'warning', code, message, offset, length, ...extra });
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
get errors() {
|
|
233
|
-
return this.items.filter((d) => d.severity === 'error');
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
get warnings() {
|
|
237
|
-
return this.items.filter((d) => d.severity === 'warning');
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
get hasErrors() {
|
|
241
|
-
return this.items.some((d) => d.severity === 'error');
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
sorted() {
|
|
245
|
-
return [...this.items].sort((a, b) => (a.offset - b.offset) || (a.severity === 'error' ? -1 : 1));
|
|
246
|
-
}
|
|
247
|
-
}
|
|
1
|
+
// Diagnostics: every problem found while reading an ES3D document is reported
|
|
2
|
+
// as a Diagnostic with a precise source location, a human readable message and,
|
|
3
|
+
// where possible, a "did you mean" suggestion. Nothing is silently ignored.
|
|
4
|
+
|
|
5
|
+
/** Maps character offsets to 1-based line/column pairs. */
|
|
6
|
+
export class LineIndex {
|
|
7
|
+
constructor(text) {
|
|
8
|
+
this.text = text;
|
|
9
|
+
this.lineStarts = null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Offset of every line start, found on first use: a valid document never asks. */
|
|
13
|
+
get starts() {
|
|
14
|
+
if (!this.lineStarts) {
|
|
15
|
+
const starts = [0];
|
|
16
|
+
for (let i = this.text.indexOf('\n'); i >= 0; i = this.text.indexOf('\n', i + 1)) starts.push(i + 1);
|
|
17
|
+
this.lineStarts = starts;
|
|
18
|
+
}
|
|
19
|
+
return this.lineStarts;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
position(offset) {
|
|
23
|
+
const starts = this.starts;
|
|
24
|
+
let lo = 0;
|
|
25
|
+
let hi = starts.length - 1;
|
|
26
|
+
while (lo < hi) {
|
|
27
|
+
const mid = (lo + hi + 1) >> 1;
|
|
28
|
+
if (starts[mid] <= offset) lo = mid;
|
|
29
|
+
else hi = mid - 1;
|
|
30
|
+
}
|
|
31
|
+
return { line: lo + 1, column: offset - starts[lo] + 1 };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
lineText(line) {
|
|
35
|
+
const start = this.starts[line - 1];
|
|
36
|
+
if (start === undefined) return '';
|
|
37
|
+
const end = line < this.starts.length ? this.starts[line] - 1 : this.text.length;
|
|
38
|
+
return this.text.slice(start, end).replace(/\r$/, '');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
offsetOf(line, column) {
|
|
42
|
+
const start = this.starts[Math.max(0, Math.min(line - 1, this.starts.length - 1))];
|
|
43
|
+
return start + column - 1;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const PHASE_TITLES = {
|
|
48
|
+
parse: 'Parse',
|
|
49
|
+
reference: 'Reference',
|
|
50
|
+
geometry: 'Geometry',
|
|
51
|
+
render: 'Render',
|
|
52
|
+
io: 'File',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export class Diagnostic {
|
|
56
|
+
/**
|
|
57
|
+
* @param {object} o
|
|
58
|
+
* @param {'error'|'warning'|'info'} [o.severity]
|
|
59
|
+
* @param {'parse'|'reference'|'geometry'|'render'|'io'} [o.phase]
|
|
60
|
+
* @param {string} o.code stable machine readable code, e.g. E_UNKNOWN_ATTRIBUTE
|
|
61
|
+
* @param {string} o.message human readable message
|
|
62
|
+
* @param {number} [o.offset] character offset of the problem in the source text
|
|
63
|
+
* @param {number} [o.length] length of the offending text
|
|
64
|
+
* @param {string} [o.suggestion] "Did you mean" candidate
|
|
65
|
+
* @param {string} [o.hint] extra explanation
|
|
66
|
+
* @param {string} [o.path] structural path when no offsets exist (JSON / binary input)
|
|
67
|
+
*/
|
|
68
|
+
constructor(o) {
|
|
69
|
+
this.severity = o.severity || 'error';
|
|
70
|
+
this.phase = o.phase || 'parse';
|
|
71
|
+
this.code = o.code || 'E_GENERIC';
|
|
72
|
+
this.message = o.message;
|
|
73
|
+
this.offset = o.offset ?? -1;
|
|
74
|
+
this.length = o.length ?? 0;
|
|
75
|
+
this.suggestion = o.suggestion || null;
|
|
76
|
+
this.hint = o.hint || null;
|
|
77
|
+
this.path = o.path || null;
|
|
78
|
+
this.line = o.line ?? 0;
|
|
79
|
+
this.column = o.column ?? 0;
|
|
80
|
+
this.uid = o.uid ?? 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Resolve line/column from an offset using the given LineIndex. */
|
|
84
|
+
locate(lineIndex) {
|
|
85
|
+
if (lineIndex && this.offset >= 0) {
|
|
86
|
+
const p = lineIndex.position(this.offset);
|
|
87
|
+
this.line = p.line;
|
|
88
|
+
this.column = p.column;
|
|
89
|
+
}
|
|
90
|
+
return this;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
get title() {
|
|
94
|
+
const phase = PHASE_TITLES[this.phase] || 'Parse';
|
|
95
|
+
return this.severity === 'warning' ? `ES3D ${phase} Warning` : this.severity === 'info' ? `ES3D ${phase} Note` : `ES3D ${phase} Error`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Multi-line report in the canonical ES3D format, optionally with a code frame. */
|
|
99
|
+
format(lineIndex) {
|
|
100
|
+
const lines = [this.title];
|
|
101
|
+
if (this.line > 0) lines.push(`line ${this.line}, column ${this.column}`);
|
|
102
|
+
else if (this.path) lines.push(`at ${this.path}`);
|
|
103
|
+
lines.push('', this.message);
|
|
104
|
+
if (this.suggestion) lines.push('', `Did you mean: ${this.suggestion}?`);
|
|
105
|
+
if (this.hint) lines.push('', this.hint);
|
|
106
|
+
if (lineIndex && this.line > 0) {
|
|
107
|
+
const src = lineIndex.lineText(this.line);
|
|
108
|
+
if (src.length < 400) {
|
|
109
|
+
const gutter = String(this.line).padStart(5) + ' | ';
|
|
110
|
+
const caretLen = Math.max(1, Math.min(this.length || 1, src.length - this.column + 1));
|
|
111
|
+
lines.push('', gutter + src, ' '.repeat(gutter.length + this.column - 1) + '^'.repeat(Math.max(1, caretLen)));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return lines.join('\n');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
toJSON() {
|
|
118
|
+
return {
|
|
119
|
+
severity: this.severity,
|
|
120
|
+
phase: this.phase,
|
|
121
|
+
code: this.code,
|
|
122
|
+
message: this.message,
|
|
123
|
+
line: this.line,
|
|
124
|
+
column: this.column,
|
|
125
|
+
offset: this.offset,
|
|
126
|
+
length: this.length,
|
|
127
|
+
suggestion: this.suggestion,
|
|
128
|
+
hint: this.hint,
|
|
129
|
+
path: this.path,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Thrown when a document cannot be read at all (syntax errors). */
|
|
135
|
+
export class ES3DError extends Error {
|
|
136
|
+
constructor(diagnostics, lineIndex) {
|
|
137
|
+
const list = Array.isArray(diagnostics) ? diagnostics : [diagnostics];
|
|
138
|
+
super(list.length ? list[0].format(lineIndex) : 'ES3D Error');
|
|
139
|
+
this.name = 'ES3DError';
|
|
140
|
+
this.diagnostics = list;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Damerau-free Levenshtein distance with an early-exit bound. */
|
|
145
|
+
export function levenshtein(a, b, max = Infinity) {
|
|
146
|
+
if (a === b) return 0;
|
|
147
|
+
const la = a.length;
|
|
148
|
+
const lb = b.length;
|
|
149
|
+
if (Math.abs(la - lb) > max) return max + 1;
|
|
150
|
+
let prev = new Array(lb + 1);
|
|
151
|
+
let cur = new Array(lb + 1);
|
|
152
|
+
for (let j = 0; j <= lb; j++) prev[j] = j;
|
|
153
|
+
for (let i = 1; i <= la; i++) {
|
|
154
|
+
cur[0] = i;
|
|
155
|
+
let rowMin = cur[0];
|
|
156
|
+
const ca = a.charCodeAt(i - 1);
|
|
157
|
+
for (let j = 1; j <= lb; j++) {
|
|
158
|
+
const cost = ca === b.charCodeAt(j - 1) ? 0 : 1;
|
|
159
|
+
const v = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + cost);
|
|
160
|
+
cur[j] = v;
|
|
161
|
+
if (v < rowMin) rowMin = v;
|
|
162
|
+
}
|
|
163
|
+
if (rowMin > max) return max + 1;
|
|
164
|
+
const t = prev;
|
|
165
|
+
prev = cur;
|
|
166
|
+
cur = t;
|
|
167
|
+
}
|
|
168
|
+
return prev[lb];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Best "did you mean" candidate for a misspelled name, or null.
|
|
173
|
+
* Case-insensitive matches and transposition-like typos are preferred.
|
|
174
|
+
*/
|
|
175
|
+
export function suggest(name, candidates) {
|
|
176
|
+
if (!name) return null;
|
|
177
|
+
const lower = name.toLowerCase();
|
|
178
|
+
let best = null;
|
|
179
|
+
let bestScore = Infinity;
|
|
180
|
+
const limit = Math.max(1, Math.min(3, Math.floor(name.length / 3) + 1));
|
|
181
|
+
for (const c of candidates) {
|
|
182
|
+
if (!c) continue;
|
|
183
|
+
const cl = c.toLowerCase();
|
|
184
|
+
if (cl === lower) return c;
|
|
185
|
+
let d = levenshtein(lower, cl, limit);
|
|
186
|
+
// A swapped pair of adjacent letters counts as one edit ("raduis" -> "radius").
|
|
187
|
+
if (d === 2 && lower.length === cl.length && isTransposition(lower, cl)) d = 1;
|
|
188
|
+
// Prefix matches are useful for abbreviations ("rough" -> "roughness")
|
|
189
|
+
// and for over-long names ("spotlight" -> "spot").
|
|
190
|
+
if (d > limit && ((cl.startsWith(lower) && lower.length >= 3) || (lower.startsWith(cl) && cl.length >= 3))) d = limit;
|
|
191
|
+
if (d <= limit && d < bestScore) {
|
|
192
|
+
best = c;
|
|
193
|
+
bestScore = d;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return best;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function isTransposition(a, b) {
|
|
200
|
+
let first = -1;
|
|
201
|
+
for (let i = 0; i < a.length; i++) {
|
|
202
|
+
if (a[i] !== b[i]) {
|
|
203
|
+
if (first < 0) first = i;
|
|
204
|
+
else return i === first + 1 && a[first] === b[i] && a[i] === b[first] && a.slice(i + 1) === b.slice(i + 1);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Collects diagnostics during a pass; keeps errors and warnings ordered by position. */
|
|
211
|
+
export class DiagnosticList {
|
|
212
|
+
constructor(lineIndex = null) {
|
|
213
|
+
this.lineIndex = lineIndex;
|
|
214
|
+
this.items = [];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
add(o) {
|
|
218
|
+
const d = o instanceof Diagnostic ? o : new Diagnostic(o);
|
|
219
|
+
d.locate(this.lineIndex);
|
|
220
|
+
this.items.push(d);
|
|
221
|
+
return d;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
error(code, message, offset, length, extra = {}) {
|
|
225
|
+
return this.add({ severity: 'error', code, message, offset, length, ...extra });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
warning(code, message, offset, length, extra = {}) {
|
|
229
|
+
return this.add({ severity: 'warning', code, message, offset, length, ...extra });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
get errors() {
|
|
233
|
+
return this.items.filter((d) => d.severity === 'error');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
get warnings() {
|
|
237
|
+
return this.items.filter((d) => d.severity === 'warning');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
get hasErrors() {
|
|
241
|
+
return this.items.some((d) => d.severity === 'error');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
sorted() {
|
|
245
|
+
return [...this.items].sort((a, b) => (a.offset - b.offset) || (a.severity === 'error' ? -1 : 1));
|
|
246
|
+
}
|
|
247
|
+
}
|
package/src/format/json.js
CHANGED
|
@@ -1,55 +1,55 @@
|
|
|
1
|
-
// JsonML-style JSON representation of the ES3D AST (experimental surface syntax).
|
|
2
|
-
//
|
|
3
|
-
// ["es3d", {"version": "1"},
|
|
4
|
-
// ["box", {"size": "2", "material": "red"}],
|
|
5
|
-
// ["#comment", " text "],
|
|
6
|
-
// ["title", {}, "Text content"]]
|
|
7
|
-
//
|
|
8
|
-
// Attribute values stay strings so the mapping to the markup syntax is exact.
|
|
9
|
-
|
|
10
|
-
import { Attr, Comment, Document, Element } from './ast.js';
|
|
11
|
-
import { Diagnostic, ES3DError } from './errors.js';
|
|
12
|
-
|
|
13
|
-
export function toJsonTree(doc, { comments = false } = {}) {
|
|
14
|
-
const conv = (el) => {
|
|
15
|
-
const attrs = {};
|
|
16
|
-
for (const a of el.attrs) attrs[a.name] = a.value;
|
|
17
|
-
const arr = [el.name, attrs];
|
|
18
|
-
if (el.text !== null && el.text !== undefined && el.text !== '') arr.push(el.text);
|
|
19
|
-
for (const c of el.children) {
|
|
20
|
-
if (c.type === 'element') arr.push(conv(c));
|
|
21
|
-
else if (comments) arr.push(['#comment', c.text]);
|
|
22
|
-
}
|
|
23
|
-
return arr;
|
|
24
|
-
};
|
|
25
|
-
return conv(doc.root);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function fromJsonTree(tree) {
|
|
29
|
-
const fail = (message, path) => {
|
|
30
|
-
throw new ES3DError([new Diagnostic({ code: 'E_JSON_STRUCTURE', message, path })]);
|
|
31
|
-
};
|
|
32
|
-
const conv = (node, path) => {
|
|
33
|
-
if (!Array.isArray(node) || typeof node[0] !== 'string') fail('Expected an element array ["name", {attributes}, ...children].', path);
|
|
34
|
-
if (node[0] === '#comment') return new Comment(String(node[1] ?? ''));
|
|
35
|
-
const el = new Element(node[0]);
|
|
36
|
-
let i = 1;
|
|
37
|
-
if (node.length > 1 && node[1] !== null && typeof node[1] === 'object' && !Array.isArray(node[1])) {
|
|
38
|
-
for (const [k, v] of Object.entries(node[1])) {
|
|
39
|
-
if (typeof v === 'object' && v !== null) fail(`Attribute '${k}' must be a string or number.`, `${path} > @${k}`);
|
|
40
|
-
el.attrs.push(new Attr(k, String(v)));
|
|
41
|
-
}
|
|
42
|
-
i = 2;
|
|
43
|
-
}
|
|
44
|
-
for (; i < node.length; i++) {
|
|
45
|
-
const c = node[i];
|
|
46
|
-
if (typeof c === 'string') el.text = (el.text || '') + c;
|
|
47
|
-
else el.appendChild(conv(c, `${path} > ${Array.isArray(c) ? c[0] : '?'}[${i}]`));
|
|
48
|
-
}
|
|
49
|
-
el.selfClosing = el.children.length === 0 && el.text === null;
|
|
50
|
-
return el;
|
|
51
|
-
};
|
|
52
|
-
const root = conv(tree, Array.isArray(tree) ? String(tree[0]) : 'root');
|
|
53
|
-
if (root.type !== 'element') fail('The root must be an element.', 'root');
|
|
54
|
-
return new Document({ root, syntax: 'json' });
|
|
55
|
-
}
|
|
1
|
+
// JsonML-style JSON representation of the ES3D AST (experimental surface syntax).
|
|
2
|
+
//
|
|
3
|
+
// ["es3d", {"version": "1"},
|
|
4
|
+
// ["box", {"size": "2", "material": "red"}],
|
|
5
|
+
// ["#comment", " text "],
|
|
6
|
+
// ["title", {}, "Text content"]]
|
|
7
|
+
//
|
|
8
|
+
// Attribute values stay strings so the mapping to the markup syntax is exact.
|
|
9
|
+
|
|
10
|
+
import { Attr, Comment, Document, Element } from './ast.js';
|
|
11
|
+
import { Diagnostic, ES3DError } from './errors.js';
|
|
12
|
+
|
|
13
|
+
export function toJsonTree(doc, { comments = false } = {}) {
|
|
14
|
+
const conv = (el) => {
|
|
15
|
+
const attrs = {};
|
|
16
|
+
for (const a of el.attrs) attrs[a.name] = a.value;
|
|
17
|
+
const arr = [el.name, attrs];
|
|
18
|
+
if (el.text !== null && el.text !== undefined && el.text !== '') arr.push(el.text);
|
|
19
|
+
for (const c of el.children) {
|
|
20
|
+
if (c.type === 'element') arr.push(conv(c));
|
|
21
|
+
else if (comments) arr.push(['#comment', c.text]);
|
|
22
|
+
}
|
|
23
|
+
return arr;
|
|
24
|
+
};
|
|
25
|
+
return conv(doc.root);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function fromJsonTree(tree) {
|
|
29
|
+
const fail = (message, path) => {
|
|
30
|
+
throw new ES3DError([new Diagnostic({ code: 'E_JSON_STRUCTURE', message, path })]);
|
|
31
|
+
};
|
|
32
|
+
const conv = (node, path) => {
|
|
33
|
+
if (!Array.isArray(node) || typeof node[0] !== 'string') fail('Expected an element array ["name", {attributes}, ...children].', path);
|
|
34
|
+
if (node[0] === '#comment') return new Comment(String(node[1] ?? ''));
|
|
35
|
+
const el = new Element(node[0]);
|
|
36
|
+
let i = 1;
|
|
37
|
+
if (node.length > 1 && node[1] !== null && typeof node[1] === 'object' && !Array.isArray(node[1])) {
|
|
38
|
+
for (const [k, v] of Object.entries(node[1])) {
|
|
39
|
+
if (typeof v === 'object' && v !== null) fail(`Attribute '${k}' must be a string or number.`, `${path} > @${k}`);
|
|
40
|
+
el.attrs.push(new Attr(k, String(v)));
|
|
41
|
+
}
|
|
42
|
+
i = 2;
|
|
43
|
+
}
|
|
44
|
+
for (; i < node.length; i++) {
|
|
45
|
+
const c = node[i];
|
|
46
|
+
if (typeof c === 'string') el.text = (el.text || '') + c;
|
|
47
|
+
else el.appendChild(conv(c, `${path} > ${Array.isArray(c) ? c[0] : '?'}[${i}]`));
|
|
48
|
+
}
|
|
49
|
+
el.selfClosing = el.children.length === 0 && el.text === null;
|
|
50
|
+
return el;
|
|
51
|
+
};
|
|
52
|
+
const root = conv(tree, Array.isArray(tree) ? String(tree[0]) : 'root');
|
|
53
|
+
if (root.type !== 'element') fail('The root must be an element.', 'root');
|
|
54
|
+
return new Document({ root, syntax: 'json' });
|
|
55
|
+
}
|
package/src/format/limits.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
// Resource limits every reader must enforce. Exceeding one is an error;
|
|
2
|
-
// the document is not drawn as if it were valid.
|
|
3
|
-
|
|
4
|
-
/** Expanded instances after repeat, scatter and use. */
|
|
5
|
-
export const MAX_INSTANCES = 2000000;
|
|
6
|
-
|
|
7
|
-
/** Nested elements, counting each use-to-target step. */
|
|
8
|
-
export const MAX_DEPTH = 64;
|
|
9
|
-
|
|
10
|
-
/** Commands in one path, after implicit command repetition. */
|
|
11
|
-
export const MAX_PATH_COMMANDS = 100000;
|
|
12
|
-
|
|
13
|
-
/** Copies in one repeat or scatter element (also the schema maximum). */
|
|
14
|
-
export const MAX_COPIES = 1000000;
|
|
1
|
+
// Resource limits every reader must enforce. Exceeding one is an error;
|
|
2
|
+
// the document is not drawn as if it were valid.
|
|
3
|
+
|
|
4
|
+
/** Expanded instances after repeat, scatter and use. */
|
|
5
|
+
export const MAX_INSTANCES = 2000000;
|
|
6
|
+
|
|
7
|
+
/** Nested elements, counting each use-to-target step. */
|
|
8
|
+
export const MAX_DEPTH = 64;
|
|
9
|
+
|
|
10
|
+
/** Commands in one path, after implicit command repetition. */
|
|
11
|
+
export const MAX_PATH_COMMANDS = 100000;
|
|
12
|
+
|
|
13
|
+
/** Copies in one repeat or scatter element (also the schema maximum). */
|
|
14
|
+
export const MAX_COPIES = 1000000;
|