@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.
Files changed (79) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +86 -86
  3. package/docs/SPEC.md +417 -417
  4. package/package.json +1 -1
  5. package/scripts/es3d-cli.mjs +377 -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 +92 -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.js +630 -630
  42. package/src/io/mesh-import.js +104 -104
  43. package/src/io/obj.js +352 -352
  44. package/src/io/step-brep.js +439 -439
  45. package/src/io/step-exact.js +1149 -1149
  46. package/src/io/step.js +1418 -1418
  47. package/src/io/stl.js +193 -193
  48. package/src/io/svg-import.js +787 -787
  49. package/src/material/materials.js +154 -154
  50. package/src/math/mat4.js +475 -475
  51. package/src/math/random.js +30 -30
  52. package/src/math/trig.js +65 -65
  53. package/src/math/vec3.js +81 -81
  54. package/src/optimize/optimizer.js +579 -579
  55. package/src/path/path-flatten.js +322 -322
  56. package/src/path/path-parser.js +332 -332
  57. package/src/path/shapes.js +129 -129
  58. package/src/path/triangulate.js +517 -517
  59. package/src/pipeline.js +74 -74
  60. package/src/render/camera.js +168 -168
  61. package/src/render/frame-builder.js +362 -362
  62. package/src/render/geometry-pool.js +149 -149
  63. package/src/render/lighting.js +149 -149
  64. package/src/render/orbit-controls.js +237 -237
  65. package/src/render/renderer.js +258 -249
  66. package/src/render/scroll-timeline.js +104 -104
  67. package/src/render/shaders/glsl.js +285 -285
  68. package/src/render/shaders/wgsl.js +252 -252
  69. package/src/render/stage.js +364 -352
  70. package/src/render/texture-cache.js +40 -40
  71. package/src/render/textures.js +223 -223
  72. package/src/render/webgl2/webgl2-backend.js +586 -532
  73. package/src/render/webgpu/webgpu-backend.js +482 -480
  74. package/src/scene/flatten.js +754 -754
  75. package/src/scene/picking.js +161 -161
  76. package/src/scene/scene-graph.js +320 -320
  77. package/src/transform/transform.js +211 -211
  78. package/src/wasm/kernels-binary.js +3 -3
  79. package/src/wasm/kernels.js +385 -385
@@ -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
+ }
@@ -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
+ }
@@ -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;