@es3d/es3d 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +95 -86
  3. package/docs/SPEC.md +426 -417
  4. package/package.json +1 -1
  5. package/scripts/es3d-cli.mjs +390 -377
  6. package/src/cache/geometry-store.js +392 -392
  7. package/src/cache/mesh-bundle.js +123 -123
  8. package/src/cache/sha256.js +73 -73
  9. package/src/format/ast.js +238 -238
  10. package/src/format/colors.js +93 -93
  11. package/src/format/compact-parser.js +171 -171
  12. package/src/format/compact-tokenizer.js +165 -165
  13. package/src/format/errors.js +247 -247
  14. package/src/format/json.js +55 -55
  15. package/src/format/limits.js +14 -14
  16. package/src/format/parse.js +132 -92
  17. package/src/format/schema.js +617 -617
  18. package/src/format/serializer.js +333 -333
  19. package/src/format/validator.js +330 -330
  20. package/src/format/value-error.js +12 -12
  21. package/src/format/value-format.js +314 -314
  22. package/src/format/values.js +459 -459
  23. package/src/format/xml-parser.js +136 -136
  24. package/src/format/xml-tokenizer.js +237 -237
  25. package/src/geometry/contour.js +86 -86
  26. package/src/geometry/extrude.js +175 -175
  27. package/src/geometry/gear.js +85 -85
  28. package/src/geometry/geometry-cache.js +159 -159
  29. package/src/geometry/lod-error.js +178 -178
  30. package/src/geometry/mesh.js +458 -458
  31. package/src/geometry/primitives.js +299 -299
  32. package/src/geometry/registry.js +645 -645
  33. package/src/geometry/revolve.js +136 -136
  34. package/src/geometry/sweep.js +441 -441
  35. package/src/index.js +47 -47
  36. package/src/io/compression.js +67 -67
  37. package/src/io/convert.js +236 -236
  38. package/src/io/download.js +13 -13
  39. package/src/io/external-scene.js +127 -127
  40. package/src/io/file-loader.js +28 -28
  41. package/src/io/gltf-pack.js +1461 -0
  42. package/src/io/gltf.js +630 -630
  43. package/src/io/mesh-import.js +104 -104
  44. package/src/io/obj.js +352 -352
  45. package/src/io/step-brep.js +439 -439
  46. package/src/io/step-exact.js +1149 -1149
  47. package/src/io/step.js +1418 -1418
  48. package/src/io/stl.js +193 -193
  49. package/src/io/svg-import.js +787 -787
  50. package/src/material/materials.js +154 -154
  51. package/src/math/mat4.js +475 -475
  52. package/src/math/random.js +30 -30
  53. package/src/math/trig.js +65 -65
  54. package/src/math/vec3.js +81 -81
  55. package/src/optimize/optimizer.js +604 -579
  56. package/src/path/path-flatten.js +322 -322
  57. package/src/path/path-parser.js +332 -332
  58. package/src/path/shapes.js +129 -129
  59. package/src/path/triangulate.js +517 -517
  60. package/src/pipeline.js +74 -74
  61. package/src/render/camera.js +168 -168
  62. package/src/render/frame-builder.js +362 -362
  63. package/src/render/geometry-pool.js +149 -149
  64. package/src/render/lighting.js +149 -149
  65. package/src/render/orbit-controls.js +237 -237
  66. package/src/render/renderer.js +258 -249
  67. package/src/render/scroll-timeline.js +104 -104
  68. package/src/render/shaders/glsl.js +285 -285
  69. package/src/render/shaders/wgsl.js +252 -252
  70. package/src/render/stage.js +365 -352
  71. package/src/render/texture-cache.js +40 -40
  72. package/src/render/textures.js +223 -223
  73. package/src/render/webgl2/webgl2-backend.js +586 -532
  74. package/src/render/webgpu/webgpu-backend.js +482 -480
  75. package/src/scene/flatten.js +754 -754
  76. package/src/scene/picking.js +161 -161
  77. package/src/scene/scene-graph.js +320 -320
  78. package/src/transform/transform.js +211 -211
  79. package/src/wasm/kernels-binary.js +3 -3
  80. package/src/wasm/kernels.js +385 -385
@@ -1,237 +1,237 @@
1
- // Tokenizer for the ES3D markup syntax (an XML subset).
2
- //
3
- // A hand written, allocation-light scanner: the parser pulls tokens one at a
4
- // time with next(). Every token carries character offsets so the parser and
5
- // validator can report exact line/column positions and the editor can perform
6
- // lossless text edits.
7
-
8
- import { Diagnostic, ES3DError } from './errors.js';
9
-
10
- export const Tok = Object.freeze({
11
- OPEN: 'open', // <name
12
- ATTR: 'attr', // name="value"
13
- OPEN_END: 'openEnd', // > or />
14
- CLOSE: 'close', // </name>
15
- TEXT: 'text',
16
- COMMENT: 'comment',
17
- CDATA: 'cdata',
18
- PI: 'pi', // <?xml ... ?>
19
- DOCTYPE: 'doctype',
20
- EOF: 'eof',
21
- });
22
-
23
- const LT = 60;
24
- const GT = 62;
25
- const SLASH = 47;
26
- const EQ = 61;
27
- const DQ = 34;
28
- const SQ = 39;
29
- const BANG = 33;
30
- const QM = 63;
31
- const AMP = 38;
32
-
33
- function isSpace(c) {
34
- return c === 32 || c === 10 || c === 9 || c === 13;
35
- }
36
-
37
- function isNameStart(c) {
38
- return (c >= 65 && c <= 90) || (c >= 97 && c <= 122) || c === 95 || c === 58 || c > 127;
39
- }
40
-
41
- function isNameChar(c) {
42
- return isNameStart(c) || (c >= 48 && c <= 57) || c === 45 || c === 46;
43
- }
44
-
45
- const NAMED_ENTITIES = { lt: '<', gt: '>', amp: '&', quot: '"', apos: "'" };
46
-
47
- /** Decode XML entity references; reports bad references with exact offsets. */
48
- export function decodeEntities(raw, baseOffset, fail) {
49
- if (raw.indexOf('&') < 0) return raw;
50
- let out = '';
51
- let i = 0;
52
- while (i < raw.length) {
53
- const amp = raw.indexOf('&', i);
54
- if (amp < 0) {
55
- out += raw.slice(i);
56
- break;
57
- }
58
- out += raw.slice(i, amp);
59
- const semi = raw.indexOf(';', amp);
60
- if (semi < 0 || semi - amp > 12) {
61
- fail('E_BAD_ENTITY', "Unescaped '&'. Write '&amp;' for a literal ampersand.", baseOffset + amp, 1);
62
- }
63
- const name = raw.slice(amp + 1, semi);
64
- let ch;
65
- if (name[0] === '#') {
66
- const code = name[1] === 'x' || name[1] === 'X' ? parseInt(name.slice(2), 16) : parseInt(name.slice(1), 10);
67
- if (!Number.isFinite(code) || code < 0 || code > 0x10ffff) {
68
- fail('E_BAD_ENTITY', `Invalid character reference '&${name};'.`, baseOffset + amp, semi - amp + 1);
69
- }
70
- ch = String.fromCodePoint(code);
71
- } else {
72
- ch = NAMED_ENTITIES[name];
73
- if (ch === undefined) {
74
- fail('E_BAD_ENTITY', `Unknown entity '&${name};'. Only &lt; &gt; &amp; &quot; &apos; and numeric references are allowed.`, baseOffset + amp, semi - amp + 1);
75
- }
76
- }
77
- out += ch;
78
- i = semi + 1;
79
- }
80
- return out;
81
- }
82
-
83
- export class XmlTokenizer {
84
- constructor(text, lineIndex = null) {
85
- this.s = text;
86
- this.i = 0;
87
- this.inTag = false;
88
- this.lineIndex = lineIndex;
89
- this.n = text.length;
90
- // Skip a UTF-8 byte order mark.
91
- if (text.charCodeAt(0) === 0xfeff) this.i = 1;
92
- }
93
-
94
- fail(code, message, offset, length = 1, extra = {}) {
95
- const d = new Diagnostic({ code, message, offset, length, ...extra });
96
- d.locate(this.lineIndex);
97
- throw new ES3DError([d], this.lineIndex);
98
- }
99
-
100
- next() {
101
- return this.inTag ? this.nextInTag() : this.nextContent();
102
- }
103
-
104
- nextContent() {
105
- const s = this.s;
106
- const start = this.i;
107
- if (start >= this.n) return { type: Tok.EOF, start, end: start };
108
- if (s.charCodeAt(start) !== LT) {
109
- let end = s.indexOf('<', start);
110
- if (end < 0) end = this.n;
111
- this.i = end;
112
- const raw = s.slice(start, end);
113
- return { type: Tok.TEXT, raw, text: null, start, end };
114
- }
115
- const c1 = s.charCodeAt(start + 1);
116
- if (c1 === BANG) {
117
- if (s.startsWith('<!--', start)) {
118
- const end = s.indexOf('-->', start + 4);
119
- if (end < 0) this.fail('E_UNTERMINATED_COMMENT', "Unterminated comment: missing '-->'.", start, 4);
120
- this.i = end + 3;
121
- return { type: Tok.COMMENT, text: s.slice(start + 4, end), start, end: end + 3 };
122
- }
123
- if (s.startsWith('<![CDATA[', start)) {
124
- const end = s.indexOf(']]>', start + 9);
125
- if (end < 0) this.fail('E_UNTERMINATED_CDATA', "Unterminated CDATA section: missing ']]>'.", start, 9);
126
- this.i = end + 3;
127
- return { type: Tok.CDATA, text: s.slice(start + 9, end), start, end: end + 3 };
128
- }
129
- if (s.startsWith('<!DOCTYPE', start) || s.startsWith('<!doctype', start)) {
130
- const end = s.indexOf('>', start);
131
- if (end < 0) this.fail('E_UNTERMINATED_DOCTYPE', "Unterminated DOCTYPE: missing '>'.", start, 9);
132
- this.i = end + 1;
133
- return { type: Tok.DOCTYPE, start, end: end + 1 };
134
- }
135
- this.fail('E_SYNTAX', "Unexpected '<!'. Comments are written <!-- like this -->.", start, 2);
136
- }
137
- if (c1 === QM) {
138
- const end = s.indexOf('?>', start + 2);
139
- if (end < 0) this.fail('E_UNTERMINATED_PI', "Unterminated processing instruction: missing '?>'.", start, 2);
140
- this.i = end + 2;
141
- let j = start + 2;
142
- while (j < end && isNameChar(s.charCodeAt(j))) j++;
143
- return { type: Tok.PI, target: s.slice(start + 2, j), start, end: end + 2 };
144
- }
145
- if (c1 === SLASH) {
146
- const nameStart = start + 2;
147
- let j = nameStart;
148
- if (!isNameStart(s.charCodeAt(j))) this.fail('E_SYNTAX', "Expected an element name after '</'.", j, 1);
149
- while (j < this.n && isNameChar(s.charCodeAt(j))) j++;
150
- const nameEnd = j;
151
- while (j < this.n && isSpace(s.charCodeAt(j))) j++;
152
- if (s.charCodeAt(j) !== GT) this.fail('E_SYNTAX', `Expected '>' to finish closing tag </${s.slice(nameStart, nameEnd)}>.`, j, 1);
153
- this.i = j + 1;
154
- return { type: Tok.CLOSE, name: s.slice(nameStart, nameEnd), start, end: j + 1, nameStart, nameEnd };
155
- }
156
- const nameStart = start + 1;
157
- if (!isNameStart(c1)) {
158
- this.fail('E_SYNTAX', "Unexpected '<'. Element names must start with a letter; write '&lt;' for a literal '<'.", start, 1);
159
- }
160
- let j = nameStart + 1;
161
- while (j < this.n && isNameChar(s.charCodeAt(j))) j++;
162
- this.i = j;
163
- this.inTag = true;
164
- return { type: Tok.OPEN, name: s.slice(nameStart, j), start, nameStart, nameEnd: j };
165
- }
166
-
167
- nextInTag() {
168
- const s = this.s;
169
- let i = this.i;
170
- const hadSpace = i < this.n && isSpace(s.charCodeAt(i));
171
- while (i < this.n && isSpace(s.charCodeAt(i))) i++;
172
- if (i >= this.n) this.fail('E_UNEXPECTED_EOF', "Unexpected end of file inside a tag: missing '>'.", i, 0);
173
- const c = s.charCodeAt(i);
174
- if (c === GT) {
175
- this.i = i + 1;
176
- this.inTag = false;
177
- return { type: Tok.OPEN_END, selfClosing: false, start: i, end: i + 1 };
178
- }
179
- if (c === SLASH) {
180
- if (s.charCodeAt(i + 1) !== GT) this.fail('E_SYNTAX', "Expected '/>' to close the tag.", i, 1);
181
- this.i = i + 2;
182
- this.inTag = false;
183
- return { type: Tok.OPEN_END, selfClosing: true, start: i, end: i + 2 };
184
- }
185
- if (!isNameStart(c)) {
186
- if (c === DQ || c === SQ) this.fail('E_SYNTAX', 'Unexpected quoted value: an attribute name is missing.', i, 1);
187
- if (c === LT) this.fail('E_SYNTAX', "Unexpected '<': the previous tag is missing its '>'.", i, 1);
188
- this.fail('E_SYNTAX', `Unexpected character '${s[i]}' in tag.`, i, 1);
189
- }
190
- if (!hadSpace) this.fail('E_SYNTAX', 'Attributes must be separated by whitespace.', i, 1);
191
- const nameStart = i;
192
- while (i < this.n && isNameChar(s.charCodeAt(i))) i++;
193
- const nameEnd = i;
194
- const name = s.slice(nameStart, nameEnd);
195
- while (i < this.n && isSpace(s.charCodeAt(i))) i++;
196
- if (s.charCodeAt(i) !== EQ) {
197
- this.fail('E_SYNTAX', `Attribute '${name}' has no value. Write ${name}="...".`, nameStart, nameEnd - nameStart);
198
- }
199
- i++;
200
- while (i < this.n && isSpace(s.charCodeAt(i))) i++;
201
- const q = s.charCodeAt(i);
202
- if (q !== DQ && q !== SQ) {
203
- this.fail('E_UNQUOTED_ATTRIBUTE', `The value of attribute '${name}' must be quoted.`, i, 1, { hint: `Write ${name}="value".` });
204
- }
205
- const valueStart = i + 1;
206
- const close = s.indexOf(q === DQ ? '"' : "'", valueStart);
207
- if (close < 0) {
208
- this.fail('E_UNTERMINATED_ATTRIBUTE', `Unterminated value for attribute '${name}': missing closing quote.`, i, 1);
209
- }
210
- const lt = s.indexOf('<', valueStart);
211
- if (lt >= 0 && lt < close) {
212
- this.fail('E_UNTERMINATED_ATTRIBUTE', `Unterminated value for attribute '${name}': found '<' before the closing quote.`, i, 1);
213
- }
214
- this.i = close + 1;
215
- const raw = s.slice(valueStart, close);
216
- let value = raw;
217
- let decoded = false;
218
- if (raw.indexOf('&') >= 0) {
219
- value = decodeEntities(raw, valueStart, (code, msg, off, len) => this.fail(code, msg, off, len));
220
- decoded = true;
221
- }
222
- return {
223
- type: Tok.ATTR,
224
- name,
225
- nameStart,
226
- nameEnd,
227
- value,
228
- valueStart,
229
- valueEnd: close,
230
- quote: q === DQ ? '"' : "'",
231
- decoded,
232
- };
233
- }
234
- }
235
-
236
- // keeps the tokenizer's hidden class alive between documents (see MeshBuilder.shapeKeeper)
237
- XmlTokenizer.shapeKeeper = new XmlTokenizer('', null);
1
+ // Tokenizer for the ES3D markup syntax (an XML subset).
2
+ //
3
+ // A hand written, allocation-light scanner: the parser pulls tokens one at a
4
+ // time with next(). Every token carries character offsets so the parser and
5
+ // validator can report exact line/column positions and the editor can perform
6
+ // lossless text edits.
7
+
8
+ import { Diagnostic, ES3DError } from './errors.js';
9
+
10
+ export const Tok = Object.freeze({
11
+ OPEN: 'open', // <name
12
+ ATTR: 'attr', // name="value"
13
+ OPEN_END: 'openEnd', // > or />
14
+ CLOSE: 'close', // </name>
15
+ TEXT: 'text',
16
+ COMMENT: 'comment',
17
+ CDATA: 'cdata',
18
+ PI: 'pi', // <?xml ... ?>
19
+ DOCTYPE: 'doctype',
20
+ EOF: 'eof',
21
+ });
22
+
23
+ const LT = 60;
24
+ const GT = 62;
25
+ const SLASH = 47;
26
+ const EQ = 61;
27
+ const DQ = 34;
28
+ const SQ = 39;
29
+ const BANG = 33;
30
+ const QM = 63;
31
+ const AMP = 38;
32
+
33
+ function isSpace(c) {
34
+ return c === 32 || c === 10 || c === 9 || c === 13;
35
+ }
36
+
37
+ function isNameStart(c) {
38
+ return (c >= 65 && c <= 90) || (c >= 97 && c <= 122) || c === 95 || c === 58 || c > 127;
39
+ }
40
+
41
+ function isNameChar(c) {
42
+ return isNameStart(c) || (c >= 48 && c <= 57) || c === 45 || c === 46;
43
+ }
44
+
45
+ const NAMED_ENTITIES = { lt: '<', gt: '>', amp: '&', quot: '"', apos: "'" };
46
+
47
+ /** Decode XML entity references; reports bad references with exact offsets. */
48
+ export function decodeEntities(raw, baseOffset, fail) {
49
+ if (raw.indexOf('&') < 0) return raw;
50
+ let out = '';
51
+ let i = 0;
52
+ while (i < raw.length) {
53
+ const amp = raw.indexOf('&', i);
54
+ if (amp < 0) {
55
+ out += raw.slice(i);
56
+ break;
57
+ }
58
+ out += raw.slice(i, amp);
59
+ const semi = raw.indexOf(';', amp);
60
+ if (semi < 0 || semi - amp > 12) {
61
+ fail('E_BAD_ENTITY', "Unescaped '&'. Write '&amp;' for a literal ampersand.", baseOffset + amp, 1);
62
+ }
63
+ const name = raw.slice(amp + 1, semi);
64
+ let ch;
65
+ if (name[0] === '#') {
66
+ const code = name[1] === 'x' || name[1] === 'X' ? parseInt(name.slice(2), 16) : parseInt(name.slice(1), 10);
67
+ if (!Number.isFinite(code) || code < 0 || code > 0x10ffff) {
68
+ fail('E_BAD_ENTITY', `Invalid character reference '&${name};'.`, baseOffset + amp, semi - amp + 1);
69
+ }
70
+ ch = String.fromCodePoint(code);
71
+ } else {
72
+ ch = NAMED_ENTITIES[name];
73
+ if (ch === undefined) {
74
+ fail('E_BAD_ENTITY', `Unknown entity '&${name};'. Only &lt; &gt; &amp; &quot; &apos; and numeric references are allowed.`, baseOffset + amp, semi - amp + 1);
75
+ }
76
+ }
77
+ out += ch;
78
+ i = semi + 1;
79
+ }
80
+ return out;
81
+ }
82
+
83
+ export class XmlTokenizer {
84
+ constructor(text, lineIndex = null) {
85
+ this.s = text;
86
+ this.i = 0;
87
+ this.inTag = false;
88
+ this.lineIndex = lineIndex;
89
+ this.n = text.length;
90
+ // Skip a UTF-8 byte order mark.
91
+ if (text.charCodeAt(0) === 0xfeff) this.i = 1;
92
+ }
93
+
94
+ fail(code, message, offset, length = 1, extra = {}) {
95
+ const d = new Diagnostic({ code, message, offset, length, ...extra });
96
+ d.locate(this.lineIndex);
97
+ throw new ES3DError([d], this.lineIndex);
98
+ }
99
+
100
+ next() {
101
+ return this.inTag ? this.nextInTag() : this.nextContent();
102
+ }
103
+
104
+ nextContent() {
105
+ const s = this.s;
106
+ const start = this.i;
107
+ if (start >= this.n) return { type: Tok.EOF, start, end: start };
108
+ if (s.charCodeAt(start) !== LT) {
109
+ let end = s.indexOf('<', start);
110
+ if (end < 0) end = this.n;
111
+ this.i = end;
112
+ const raw = s.slice(start, end);
113
+ return { type: Tok.TEXT, raw, text: null, start, end };
114
+ }
115
+ const c1 = s.charCodeAt(start + 1);
116
+ if (c1 === BANG) {
117
+ if (s.startsWith('<!--', start)) {
118
+ const end = s.indexOf('-->', start + 4);
119
+ if (end < 0) this.fail('E_UNTERMINATED_COMMENT', "Unterminated comment: missing '-->'.", start, 4);
120
+ this.i = end + 3;
121
+ return { type: Tok.COMMENT, text: s.slice(start + 4, end), start, end: end + 3 };
122
+ }
123
+ if (s.startsWith('<![CDATA[', start)) {
124
+ const end = s.indexOf(']]>', start + 9);
125
+ if (end < 0) this.fail('E_UNTERMINATED_CDATA', "Unterminated CDATA section: missing ']]>'.", start, 9);
126
+ this.i = end + 3;
127
+ return { type: Tok.CDATA, text: s.slice(start + 9, end), start, end: end + 3 };
128
+ }
129
+ if (s.startsWith('<!DOCTYPE', start) || s.startsWith('<!doctype', start)) {
130
+ const end = s.indexOf('>', start);
131
+ if (end < 0) this.fail('E_UNTERMINATED_DOCTYPE', "Unterminated DOCTYPE: missing '>'.", start, 9);
132
+ this.i = end + 1;
133
+ return { type: Tok.DOCTYPE, start, end: end + 1 };
134
+ }
135
+ this.fail('E_SYNTAX', "Unexpected '<!'. Comments are written <!-- like this -->.", start, 2);
136
+ }
137
+ if (c1 === QM) {
138
+ const end = s.indexOf('?>', start + 2);
139
+ if (end < 0) this.fail('E_UNTERMINATED_PI', "Unterminated processing instruction: missing '?>'.", start, 2);
140
+ this.i = end + 2;
141
+ let j = start + 2;
142
+ while (j < end && isNameChar(s.charCodeAt(j))) j++;
143
+ return { type: Tok.PI, target: s.slice(start + 2, j), start, end: end + 2 };
144
+ }
145
+ if (c1 === SLASH) {
146
+ const nameStart = start + 2;
147
+ let j = nameStart;
148
+ if (!isNameStart(s.charCodeAt(j))) this.fail('E_SYNTAX', "Expected an element name after '</'.", j, 1);
149
+ while (j < this.n && isNameChar(s.charCodeAt(j))) j++;
150
+ const nameEnd = j;
151
+ while (j < this.n && isSpace(s.charCodeAt(j))) j++;
152
+ if (s.charCodeAt(j) !== GT) this.fail('E_SYNTAX', `Expected '>' to finish closing tag </${s.slice(nameStart, nameEnd)}>.`, j, 1);
153
+ this.i = j + 1;
154
+ return { type: Tok.CLOSE, name: s.slice(nameStart, nameEnd), start, end: j + 1, nameStart, nameEnd };
155
+ }
156
+ const nameStart = start + 1;
157
+ if (!isNameStart(c1)) {
158
+ this.fail('E_SYNTAX', "Unexpected '<'. Element names must start with a letter; write '&lt;' for a literal '<'.", start, 1);
159
+ }
160
+ let j = nameStart + 1;
161
+ while (j < this.n && isNameChar(s.charCodeAt(j))) j++;
162
+ this.i = j;
163
+ this.inTag = true;
164
+ return { type: Tok.OPEN, name: s.slice(nameStart, j), start, nameStart, nameEnd: j };
165
+ }
166
+
167
+ nextInTag() {
168
+ const s = this.s;
169
+ let i = this.i;
170
+ const hadSpace = i < this.n && isSpace(s.charCodeAt(i));
171
+ while (i < this.n && isSpace(s.charCodeAt(i))) i++;
172
+ if (i >= this.n) this.fail('E_UNEXPECTED_EOF', "Unexpected end of file inside a tag: missing '>'.", i, 0);
173
+ const c = s.charCodeAt(i);
174
+ if (c === GT) {
175
+ this.i = i + 1;
176
+ this.inTag = false;
177
+ return { type: Tok.OPEN_END, selfClosing: false, start: i, end: i + 1 };
178
+ }
179
+ if (c === SLASH) {
180
+ if (s.charCodeAt(i + 1) !== GT) this.fail('E_SYNTAX', "Expected '/>' to close the tag.", i, 1);
181
+ this.i = i + 2;
182
+ this.inTag = false;
183
+ return { type: Tok.OPEN_END, selfClosing: true, start: i, end: i + 2 };
184
+ }
185
+ if (!isNameStart(c)) {
186
+ if (c === DQ || c === SQ) this.fail('E_SYNTAX', 'Unexpected quoted value: an attribute name is missing.', i, 1);
187
+ if (c === LT) this.fail('E_SYNTAX', "Unexpected '<': the previous tag is missing its '>'.", i, 1);
188
+ this.fail('E_SYNTAX', `Unexpected character '${s[i]}' in tag.`, i, 1);
189
+ }
190
+ if (!hadSpace) this.fail('E_SYNTAX', 'Attributes must be separated by whitespace.', i, 1);
191
+ const nameStart = i;
192
+ while (i < this.n && isNameChar(s.charCodeAt(i))) i++;
193
+ const nameEnd = i;
194
+ const name = s.slice(nameStart, nameEnd);
195
+ while (i < this.n && isSpace(s.charCodeAt(i))) i++;
196
+ if (s.charCodeAt(i) !== EQ) {
197
+ this.fail('E_SYNTAX', `Attribute '${name}' has no value. Write ${name}="...".`, nameStart, nameEnd - nameStart);
198
+ }
199
+ i++;
200
+ while (i < this.n && isSpace(s.charCodeAt(i))) i++;
201
+ const q = s.charCodeAt(i);
202
+ if (q !== DQ && q !== SQ) {
203
+ this.fail('E_UNQUOTED_ATTRIBUTE', `The value of attribute '${name}' must be quoted.`, i, 1, { hint: `Write ${name}="value".` });
204
+ }
205
+ const valueStart = i + 1;
206
+ const close = s.indexOf(q === DQ ? '"' : "'", valueStart);
207
+ if (close < 0) {
208
+ this.fail('E_UNTERMINATED_ATTRIBUTE', `Unterminated value for attribute '${name}': missing closing quote.`, i, 1);
209
+ }
210
+ const lt = s.indexOf('<', valueStart);
211
+ if (lt >= 0 && lt < close) {
212
+ this.fail('E_UNTERMINATED_ATTRIBUTE', `Unterminated value for attribute '${name}': found '<' before the closing quote.`, i, 1);
213
+ }
214
+ this.i = close + 1;
215
+ const raw = s.slice(valueStart, close);
216
+ let value = raw;
217
+ let decoded = false;
218
+ if (raw.indexOf('&') >= 0) {
219
+ value = decodeEntities(raw, valueStart, (code, msg, off, len) => this.fail(code, msg, off, len));
220
+ decoded = true;
221
+ }
222
+ return {
223
+ type: Tok.ATTR,
224
+ name,
225
+ nameStart,
226
+ nameEnd,
227
+ value,
228
+ valueStart,
229
+ valueEnd: close,
230
+ quote: q === DQ ? '"' : "'",
231
+ decoded,
232
+ };
233
+ }
234
+ }
235
+
236
+ // keeps the tokenizer's hidden class alive between documents (see MeshBuilder.shapeKeeper)
237
+ XmlTokenizer.shapeKeeper = new XmlTokenizer('', null);
@@ -1,86 +1,86 @@
1
- // Per-vertex data for 2D contours shared by the extrusion, sweep and revolve
2
- // generators: edge normals, smoothed vertex normals, miter (inset) vectors and
3
- // cumulative arc length. Contours are oriented with the filled side on the
4
- // left, so the right-hand perpendicular of an edge is its outward normal.
5
-
6
- const MAX_MITER = 4;
7
-
8
- export function contourFrames(points, smooth, closed = true) {
9
- const n = points.length / 2;
10
- const edges = closed ? n : n - 1;
11
- const edgeN = new Float64Array(edges * 2);
12
- for (let i = 0; i < edges; i++) {
13
- const j = (i + 1) % n;
14
- const dx = points[j * 2] - points[i * 2];
15
- const dy = points[j * 2 + 1] - points[i * 2 + 1];
16
- const l = Math.hypot(dx, dy) || 1;
17
- edgeN[i * 2] = dy / l;
18
- edgeN[i * 2 + 1] = -dx / l;
19
- }
20
- const vertN = new Float64Array(n * 2);
21
- const miter = new Float64Array(n * 2);
22
- const corner = new Uint8Array(n);
23
- for (let i = 0; i < n; i++) {
24
- const hasPrev = closed || i > 0;
25
- const hasNext = closed || i < n - 1;
26
- const pe = hasPrev ? (i - 1 + edges) % edges : -1;
27
- const ne = hasNext ? i % edges : -1;
28
- let ax, ay, bx, by;
29
- if (pe >= 0) {
30
- ax = edgeN[pe * 2];
31
- ay = edgeN[pe * 2 + 1];
32
- }
33
- if (ne >= 0) {
34
- bx = edgeN[ne * 2];
35
- by = edgeN[ne * 2 + 1];
36
- }
37
- if (pe < 0) {
38
- ax = bx;
39
- ay = by;
40
- }
41
- if (ne < 0) {
42
- bx = ax;
43
- by = ay;
44
- }
45
- let mx = ax + bx, my = ay + by;
46
- let ml = Math.hypot(mx, my);
47
- if (ml < 1e-9) {
48
- mx = ax;
49
- my = ay;
50
- ml = 1;
51
- }
52
- mx /= ml;
53
- my /= ml;
54
- vertN[i * 2] = mx;
55
- vertN[i * 2 + 1] = my;
56
- const cosHalf = Math.max(mx * ax + my * ay, 1 / MAX_MITER);
57
- miter[i * 2] = mx / cosHalf;
58
- miter[i * 2 + 1] = my / cosHalf;
59
- corner[i] = smooth && smooth[i] ? 0 : 1;
60
- }
61
- const arc = new Float64Array(n + 1);
62
- for (let i = 1; i <= n; i++) {
63
- if (!closed && i === n) {
64
- arc[i] = arc[i - 1];
65
- break;
66
- }
67
- const a = (i - 1) % n, b = i % n;
68
- arc[i] = arc[i - 1] + Math.hypot(points[b * 2] - points[a * 2], points[b * 2 + 1] - points[a * 2 + 1]);
69
- }
70
- return { n, edges, edgeN, vertN, miter, corner, arc, length: arc[closed ? n : n - 1] };
71
- }
72
-
73
- /** Bounds of a set of 2D point arrays. */
74
- export function bounds2(list) {
75
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
76
- for (const pts of list) {
77
- for (let i = 0; i < pts.length; i += 2) {
78
- const x = pts[i], y = pts[i + 1];
79
- if (x < minX) minX = x;
80
- if (y < minY) minY = y;
81
- if (x > maxX) maxX = x;
82
- if (y > maxY) maxY = y;
83
- }
84
- }
85
- return { minX, minY, maxX, maxY, size: Math.max(maxX - minX, maxY - minY) || 1 };
86
- }
1
+ // Per-vertex data for 2D contours shared by the extrusion, sweep and revolve
2
+ // generators: edge normals, smoothed vertex normals, miter (inset) vectors and
3
+ // cumulative arc length. Contours are oriented with the filled side on the
4
+ // left, so the right-hand perpendicular of an edge is its outward normal.
5
+
6
+ const MAX_MITER = 4;
7
+
8
+ export function contourFrames(points, smooth, closed = true) {
9
+ const n = points.length / 2;
10
+ const edges = closed ? n : n - 1;
11
+ const edgeN = new Float64Array(edges * 2);
12
+ for (let i = 0; i < edges; i++) {
13
+ const j = (i + 1) % n;
14
+ const dx = points[j * 2] - points[i * 2];
15
+ const dy = points[j * 2 + 1] - points[i * 2 + 1];
16
+ const l = Math.hypot(dx, dy) || 1;
17
+ edgeN[i * 2] = dy / l;
18
+ edgeN[i * 2 + 1] = -dx / l;
19
+ }
20
+ const vertN = new Float64Array(n * 2);
21
+ const miter = new Float64Array(n * 2);
22
+ const corner = new Uint8Array(n);
23
+ for (let i = 0; i < n; i++) {
24
+ const hasPrev = closed || i > 0;
25
+ const hasNext = closed || i < n - 1;
26
+ const pe = hasPrev ? (i - 1 + edges) % edges : -1;
27
+ const ne = hasNext ? i % edges : -1;
28
+ let ax, ay, bx, by;
29
+ if (pe >= 0) {
30
+ ax = edgeN[pe * 2];
31
+ ay = edgeN[pe * 2 + 1];
32
+ }
33
+ if (ne >= 0) {
34
+ bx = edgeN[ne * 2];
35
+ by = edgeN[ne * 2 + 1];
36
+ }
37
+ if (pe < 0) {
38
+ ax = bx;
39
+ ay = by;
40
+ }
41
+ if (ne < 0) {
42
+ bx = ax;
43
+ by = ay;
44
+ }
45
+ let mx = ax + bx, my = ay + by;
46
+ let ml = Math.hypot(mx, my);
47
+ if (ml < 1e-9) {
48
+ mx = ax;
49
+ my = ay;
50
+ ml = 1;
51
+ }
52
+ mx /= ml;
53
+ my /= ml;
54
+ vertN[i * 2] = mx;
55
+ vertN[i * 2 + 1] = my;
56
+ const cosHalf = Math.max(mx * ax + my * ay, 1 / MAX_MITER);
57
+ miter[i * 2] = mx / cosHalf;
58
+ miter[i * 2 + 1] = my / cosHalf;
59
+ corner[i] = smooth && smooth[i] ? 0 : 1;
60
+ }
61
+ const arc = new Float64Array(n + 1);
62
+ for (let i = 1; i <= n; i++) {
63
+ if (!closed && i === n) {
64
+ arc[i] = arc[i - 1];
65
+ break;
66
+ }
67
+ const a = (i - 1) % n, b = i % n;
68
+ arc[i] = arc[i - 1] + Math.hypot(points[b * 2] - points[a * 2], points[b * 2 + 1] - points[a * 2 + 1]);
69
+ }
70
+ return { n, edges, edgeN, vertN, miter, corner, arc, length: arc[closed ? n : n - 1] };
71
+ }
72
+
73
+ /** Bounds of a set of 2D point arrays. */
74
+ export function bounds2(list) {
75
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
76
+ for (const pts of list) {
77
+ for (let i = 0; i < pts.length; i += 2) {
78
+ const x = pts[i], y = pts[i + 1];
79
+ if (x < minX) minX = x;
80
+ if (y < minY) minY = y;
81
+ if (x > maxX) maxX = x;
82
+ if (y > maxY) maxY = y;
83
+ }
84
+ }
85
+ return { minX, minY, maxX, maxY, size: Math.max(maxX - minX, maxY - minY) || 1 };
86
+ }