@es3d/es3d 0.0.0-stage → 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 (82) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +85 -2
  3. package/docs/REFERENCE.md +581 -0
  4. package/docs/SPEC.md +417 -0
  5. package/package.json +50 -4
  6. package/scripts/es3d-cli.mjs +377 -0
  7. package/src/cache/geometry-store.js +392 -0
  8. package/src/cache/mesh-bundle.js +123 -0
  9. package/src/cache/sha256.js +73 -0
  10. package/src/format/ast.js +238 -0
  11. package/src/format/colors.js +93 -0
  12. package/src/format/compact-parser.js +171 -0
  13. package/src/format/compact-tokenizer.js +165 -0
  14. package/src/format/errors.js +247 -0
  15. package/src/format/json.js +55 -0
  16. package/src/format/limits.js +14 -0
  17. package/src/format/parse.js +92 -0
  18. package/src/format/schema.js +617 -0
  19. package/src/format/serializer.js +333 -0
  20. package/src/format/validator.js +330 -0
  21. package/src/format/value-error.js +12 -0
  22. package/src/format/value-format.js +314 -0
  23. package/src/format/values.js +459 -0
  24. package/src/format/xml-parser.js +136 -0
  25. package/src/format/xml-tokenizer.js +237 -0
  26. package/src/geometry/contour.js +86 -0
  27. package/src/geometry/extrude.js +175 -0
  28. package/src/geometry/gear.js +85 -0
  29. package/src/geometry/geometry-cache.js +159 -0
  30. package/src/geometry/lod-error.js +178 -0
  31. package/src/geometry/mesh.js +458 -0
  32. package/src/geometry/primitives.js +299 -0
  33. package/src/geometry/registry.js +645 -0
  34. package/src/geometry/revolve.js +136 -0
  35. package/src/geometry/sweep.js +441 -0
  36. package/src/index.js +47 -0
  37. package/src/io/compression.js +67 -0
  38. package/src/io/convert.js +236 -0
  39. package/src/io/download.js +13 -0
  40. package/src/io/external-scene.js +127 -0
  41. package/src/io/file-loader.js +28 -0
  42. package/src/io/gltf.js +630 -0
  43. package/src/io/mesh-import.js +104 -0
  44. package/src/io/obj.js +352 -0
  45. package/src/io/step-brep.js +439 -0
  46. package/src/io/step-exact.js +1149 -0
  47. package/src/io/step.js +1418 -0
  48. package/src/io/stl.js +193 -0
  49. package/src/io/svg-import.js +787 -0
  50. package/src/material/materials.js +154 -0
  51. package/src/math/mat4.js +475 -0
  52. package/src/math/random.js +30 -0
  53. package/src/math/trig.js +65 -0
  54. package/src/math/vec3.js +81 -0
  55. package/src/optimize/optimizer.js +579 -0
  56. package/src/path/path-flatten.js +322 -0
  57. package/src/path/path-parser.js +332 -0
  58. package/src/path/shapes.js +129 -0
  59. package/src/path/triangulate.js +517 -0
  60. package/src/pipeline.js +74 -0
  61. package/src/render/camera.js +168 -0
  62. package/src/render/frame-builder.js +362 -0
  63. package/src/render/geometry-pool.js +149 -0
  64. package/src/render/lighting.js +149 -0
  65. package/src/render/orbit-controls.js +237 -0
  66. package/src/render/renderer.js +258 -0
  67. package/src/render/scroll-timeline.js +104 -0
  68. package/src/render/shaders/glsl.js +285 -0
  69. package/src/render/shaders/wgsl.js +252 -0
  70. package/src/render/stage.js +364 -0
  71. package/src/render/texture-cache.js +40 -0
  72. package/src/render/textures.js +223 -0
  73. package/src/render/webgl2/webgl2-backend.js +586 -0
  74. package/src/render/webgpu/webgpu-backend.js +482 -0
  75. package/src/scene/animation.js +171 -0
  76. package/src/scene/camera-constraints.js +117 -0
  77. package/src/scene/flatten.js +754 -0
  78. package/src/scene/picking.js +161 -0
  79. package/src/scene/scene-graph.js +320 -0
  80. package/src/transform/transform.js +211 -0
  81. package/src/wasm/kernels-binary.js +3 -0
  82. package/src/wasm/kernels.js +385 -0
@@ -0,0 +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
+ }
@@ -0,0 +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
+ }
@@ -0,0 +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;
@@ -0,0 +1,92 @@
1
+ // Entry point for reading ES3D documents in any of the surface syntaxes:
2
+ // markup (XML-like, canonical) <es3d version="1"> ... </es3d>
3
+ // compact (ES3D-C) es3d version=1 box s=2 ...
4
+ // JSON (JsonML-style) ["es3d", {"version":"1"}, ...]
5
+ // All of them produce the same AST, which is then validated against the schema.
6
+
7
+ import { Document } from './ast.js';
8
+ import { Diagnostic, ES3DError, LineIndex } from './errors.js';
9
+ import { parseXml } from './xml-parser.js';
10
+ import { parseCompact } from './compact-parser.js';
11
+ import { validate } from './validator.js';
12
+ import { fromJsonTree } from './json.js';
13
+
14
+ /** Detect the surface syntax of a text document. */
15
+ export function detectSyntax(text) {
16
+ let i = 0;
17
+ const n = text.length;
18
+ if (text.charCodeAt(0) === 0xfeff) i = 1;
19
+ for (;;) {
20
+ while (i < n && /\s/.test(text[i])) i++;
21
+ if (text.startsWith('//', i)) {
22
+ const e = text.indexOf('\n', i);
23
+ i = e < 0 ? n : e + 1;
24
+ continue;
25
+ }
26
+ if (text.startsWith('/*', i)) {
27
+ const e = text.indexOf('*/', i + 2);
28
+ i = e < 0 ? n : e + 2;
29
+ continue;
30
+ }
31
+ break;
32
+ }
33
+ const c = text[i];
34
+ if (c === '<') return 'xml';
35
+ if (c === '[' || c === '{') return 'json';
36
+ return 'compact';
37
+ }
38
+
39
+ function failedDocument(err, syntax, text, lineIndex) {
40
+ const doc = new Document({ root: null, syntax, source: text });
41
+ doc.lineIndex = lineIndex;
42
+ doc.diagnostics = err instanceof ES3DError ? err.diagnostics : [new Diagnostic({ code: 'E_INTERNAL', message: String(err && err.message ? err.message : err) })];
43
+ return doc;
44
+ }
45
+
46
+ /**
47
+ * Parse (and by default validate) an ES3D document.
48
+ * Never throws for malformed input: problems are reported in doc.diagnostics.
49
+ * @param {string|ArrayBuffer|Uint8Array} input
50
+ * @param {{syntax?: 'xml'|'compact'|'json', validate?: boolean}} [opts]
51
+ * @returns {Document}
52
+ */
53
+ export function parse(input, opts = {}) {
54
+ if (typeof input !== 'string') input = new TextDecoder('utf-8').decode(input instanceof Uint8Array ? input : new Uint8Array(input));
55
+ const text = input;
56
+ const syntax = opts.syntax || detectSyntax(text);
57
+ const lineIndex = new LineIndex(text);
58
+ let doc;
59
+ try {
60
+ if (syntax === 'xml') doc = parseXml(text, lineIndex);
61
+ else if (syntax === 'compact') doc = parseCompact(text, lineIndex);
62
+ else if (syntax === 'json') {
63
+ let tree;
64
+ try {
65
+ tree = JSON.parse(text);
66
+ } catch (e) {
67
+ const m = /position (\d+)/.exec(e.message);
68
+ const d = new Diagnostic({ code: 'E_JSON', message: `Invalid JSON: ${e.message}`, offset: m ? Number(m[1]) : 0, length: 1 });
69
+ d.locate(lineIndex);
70
+ throw new ES3DError([d], lineIndex);
71
+ }
72
+ doc = fromJsonTree(tree);
73
+ doc.source = text;
74
+ } else throw new Error(`Unknown syntax '${syntax}'`);
75
+ } catch (e) {
76
+ return failedDocument(e, syntax, text, lineIndex);
77
+ }
78
+ if (!doc.lineIndex && syntax !== 'json') doc.lineIndex = lineIndex;
79
+ return opts.validate === false ? doc : validate(doc);
80
+ }
81
+
82
+ /** Parse and throw an ES3DError when the document has errors. */
83
+ export function parseStrict(input, opts = {}) {
84
+ const doc = parse(input, opts);
85
+ if (!doc.ok) throw new ES3DError(doc.errors, doc.lineIndex);
86
+ return doc;
87
+ }
88
+
89
+ /** Human readable report of all diagnostics. */
90
+ export function formatDiagnostics(doc) {
91
+ return doc.diagnostics.map((d) => d.format(doc.lineIndex)).join('\n\n');
92
+ }