@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,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
+ }