@es3d/es3d 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -201
- package/README.md +95 -86
- package/docs/SPEC.md +426 -417
- package/package.json +1 -1
- package/scripts/es3d-cli.mjs +390 -377
- package/src/cache/geometry-store.js +392 -392
- package/src/cache/mesh-bundle.js +123 -123
- package/src/cache/sha256.js +73 -73
- package/src/format/ast.js +238 -238
- package/src/format/colors.js +93 -93
- package/src/format/compact-parser.js +171 -171
- package/src/format/compact-tokenizer.js +165 -165
- package/src/format/errors.js +247 -247
- package/src/format/json.js +55 -55
- package/src/format/limits.js +14 -14
- package/src/format/parse.js +132 -92
- package/src/format/schema.js +617 -617
- package/src/format/serializer.js +333 -333
- package/src/format/validator.js +330 -330
- package/src/format/value-error.js +12 -12
- package/src/format/value-format.js +314 -314
- package/src/format/values.js +459 -459
- package/src/format/xml-parser.js +136 -136
- package/src/format/xml-tokenizer.js +237 -237
- package/src/geometry/contour.js +86 -86
- package/src/geometry/extrude.js +175 -175
- package/src/geometry/gear.js +85 -85
- package/src/geometry/geometry-cache.js +159 -159
- package/src/geometry/lod-error.js +178 -178
- package/src/geometry/mesh.js +458 -458
- package/src/geometry/primitives.js +299 -299
- package/src/geometry/registry.js +645 -645
- package/src/geometry/revolve.js +136 -136
- package/src/geometry/sweep.js +441 -441
- package/src/index.js +47 -47
- package/src/io/compression.js +67 -67
- package/src/io/convert.js +236 -236
- package/src/io/download.js +13 -13
- package/src/io/external-scene.js +127 -127
- package/src/io/file-loader.js +28 -28
- package/src/io/gltf-pack.js +1461 -0
- package/src/io/gltf.js +630 -630
- package/src/io/mesh-import.js +104 -104
- package/src/io/obj.js +352 -352
- package/src/io/step-brep.js +439 -439
- package/src/io/step-exact.js +1149 -1149
- package/src/io/step.js +1418 -1418
- package/src/io/stl.js +193 -193
- package/src/io/svg-import.js +787 -787
- package/src/material/materials.js +154 -154
- package/src/math/mat4.js +475 -475
- package/src/math/random.js +30 -30
- package/src/math/trig.js +65 -65
- package/src/math/vec3.js +81 -81
- package/src/optimize/optimizer.js +604 -579
- package/src/path/path-flatten.js +322 -322
- package/src/path/path-parser.js +332 -332
- package/src/path/shapes.js +129 -129
- package/src/path/triangulate.js +517 -517
- package/src/pipeline.js +74 -74
- package/src/render/camera.js +168 -168
- package/src/render/frame-builder.js +362 -362
- package/src/render/geometry-pool.js +149 -149
- package/src/render/lighting.js +149 -149
- package/src/render/orbit-controls.js +237 -237
- package/src/render/renderer.js +258 -249
- package/src/render/scroll-timeline.js +104 -104
- package/src/render/shaders/glsl.js +285 -285
- package/src/render/shaders/wgsl.js +252 -252
- package/src/render/stage.js +365 -352
- package/src/render/texture-cache.js +40 -40
- package/src/render/textures.js +223 -223
- package/src/render/webgl2/webgl2-backend.js +586 -532
- package/src/render/webgpu/webgpu-backend.js +482 -480
- package/src/scene/flatten.js +754 -754
- package/src/scene/picking.js +161 -161
- package/src/scene/scene-graph.js +320 -320
- package/src/transform/transform.js +211 -211
- package/src/wasm/kernels-binary.js +3 -3
- package/src/wasm/kernels.js +385 -385
|
@@ -1,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 '&' 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 < > & " ' 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 '<' 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 '&' 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 < > & " ' 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 '<' 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);
|
package/src/geometry/contour.js
CHANGED
|
@@ -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
|
+
}
|