@es3d/es3d 1.0.1 → 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/README.md +11 -2
- package/docs/SPEC.md +9 -0
- package/package.json +1 -1
- package/scripts/es3d-cli.mjs +29 -16
- package/src/format/parse.js +43 -3
- package/src/index.js +2 -2
- package/src/io/gltf-pack.js +1461 -0
- package/src/optimize/optimizer.js +34 -9
- package/src/render/stage.js +2 -1
package/README.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# ES3D
|
|
2
2
|
|
|
3
|
+
<p align="center">
|
|
4
|
+
<a href="https://www.jsdelivr.com/package/npm/@es3d/es3d"><img src="https://img.shields.io/jsdelivr/npm/hm/@es3d/es3d?label=jsDelivr" alt="jsDelivr"></a>
|
|
5
|
+
<a href="https://www.npmjs.com/package/@es3d/es3d"><img src="https://img.shields.io/npm/v/@es3d/es3d" alt="npm version"></a>
|
|
6
|
+
<a href="https://www.npmjs.com/package/@es3d/es3d"><img src="https://img.shields.io/npm/dm/@es3d/es3d" alt="npm downloads"></a>
|
|
7
|
+
<a href="https://github.com/bitilia/es3d/stargazers"><img src="https://img.shields.io/github/stars/bitilia/es3d" alt="GitHub stars"></a>
|
|
8
|
+
<a href="https://github.com/bitilia/es3d/blob/main/LICENSE"><img src="https://img.shields.io/github/license/bitilia/es3d" alt="License"></a>
|
|
9
|
+
<a href="https://nodejs.org/"><img src="https://img.shields.io/node/v/@es3d/es3d" alt="Node.js"></a>
|
|
10
|
+
</p>
|
|
11
|
+
|
|
3
12
|
**A file format for 3D scenes you can write by hand, the way people write SVG.** The file describes objects, not triangles: a rounded box, an outline extruded into a solid, two thousand trees placed by a rule. The viewer builds the triangles, at the detail the screen needs.
|
|
4
13
|
|
|
5
14
|
This package is the reference implementation:
|
|
@@ -59,12 +68,12 @@ const { text } = await importFiles([{ name: 'logo.svg', bytes }]); // SVG, OBJ,
|
|
|
59
68
|
const { files } = exportFormat(text, 'glb'); // ES3D → glTF, OBJ, STL, STEP
|
|
60
69
|
```
|
|
61
70
|
|
|
62
|
-
Every error has a line, a column, a code and, where it can, a "did you mean" suggestion.
|
|
71
|
+
Every error has a line, a column, a code and, where it can, a "did you mean" suggestion. Repeated warnings that share a code, an element name and a message are printed once, with a count. `validate --json` still lists every warning.
|
|
63
72
|
|
|
64
73
|
## The command
|
|
65
74
|
|
|
66
75
|
```bash
|
|
67
|
-
npx @es3d/es3d validate scene.es3d #
|
|
76
|
+
npx @es3d/es3d validate scene.es3d # diagnostics, repeated warnings grouped; exit code 1 on errors
|
|
68
77
|
npx @es3d/es3d compact scene.es3d # convert to the compact syntax (also: markup, json, fmt, minify)
|
|
69
78
|
npx @es3d/es3d glb scene.es3d -o scene.glb # export (also: obj, stl, step)
|
|
70
79
|
npx @es3d/es3d import logo.svg -o logo.es3d # SVG, OBJ, STL, STEP or glTF to ES3D
|
package/docs/SPEC.md
CHANGED
|
@@ -403,11 +403,20 @@ The title names the phase (`Parse`, `Reference`, `Geometry`, `Render`, `File`) a
|
|
|
403
403
|
|
|
404
404
|
Unknown names get a suggestion when a known name is within a small edit distance or shares a prefix (`raduis` → `radius`, `spotlight` → `spot`, `sphear` → `sphere`).
|
|
405
405
|
|
|
406
|
+
The reference reader's human-readable report (`formatDiagnostics`, and the text from `validate`) shows a repeated warning once. Warnings group when they share a code, the element name written in the message (its first `<name>`), and the same message. The report keeps the first location and then says how many times that warning occurred:
|
|
407
|
+
|
|
408
|
+
```
|
|
409
|
+
Reported 906 times. Showing the first.
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
Every warning remains its own entry in the diagnostic list. `validate --json` prints that list. Errors are listed one by one.
|
|
413
|
+
|
|
406
414
|
## 16. Extensibility and versioning
|
|
407
415
|
|
|
408
416
|
- Elements whose names start with `x-` (and their subtrees) are vendor extensions: readers that do not understand them skip them with `W_EXTENSION`. Unknown elements without the prefix are errors, so typos are never mistaken for extensions.
|
|
409
417
|
- Unknown attributes are always errors. (A future version may reserve an `x-` attribute prefix as well.)
|
|
410
418
|
- `version="1.x"` documents must remain readable by version-1 readers; incompatible changes require version 2, which version-1 readers reject with `E_VERSION`.
|
|
419
|
+
- A processor that rewrites a document keeps `x-` elements, and keeps any element whose id is named by a token in an `x-` attribute. The reference optimizer treats a whitespace-separated token (an optional leading `#` is ignored) as a reference when that id exists, and updates the token when it merges a duplicate material.
|
|
411
420
|
|
|
412
421
|
## 17. File names and media types
|
|
413
422
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@es3d/es3d",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "ES3D (Extensible Scalable 3D): a declarative, SVG-inspired 3D scene format with a WebGPU/WebGL2 reference renderer, editor and converter.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
package/scripts/es3d-cli.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// npm run es3d -- validate scene.es3d
|
|
5
5
|
// npm run es3d -- fmt scene.es3d -o pretty.es3d
|
|
6
6
|
// npm run es3d -- stats scene.es3d
|
|
7
|
-
// npm run es3d -- glb scene.es3d
|
|
7
|
+
// npm run es3d -- glb scene.es3d -o scene.glb
|
|
8
8
|
// npm run es3d -- import drawing.svg -o drawing.es3d
|
|
9
9
|
//
|
|
10
10
|
// Run without arguments for the full command list. Input "-" reads stdin;
|
|
@@ -13,11 +13,10 @@
|
|
|
13
13
|
import fs from 'node:fs';
|
|
14
14
|
import path from 'node:path';
|
|
15
15
|
import zlib from 'node:zlib';
|
|
16
|
-
import { parse, detectSyntax } from '../src/format/parse.js';
|
|
16
|
+
import { parse, detectSyntax, formatDiagnostics } from '../src/format/parse.js';
|
|
17
17
|
import { serialize } from '../src/format/serializer.js';
|
|
18
18
|
import { compile } from '../src/pipeline.js';
|
|
19
19
|
import { exportOBJ } from '../src/io/obj.js';
|
|
20
|
-
import { exportGLB } from '../src/io/gltf.js';
|
|
21
20
|
import { exportSTL } from '../src/io/stl.js';
|
|
22
21
|
import { exportSTEP } from '../src/io/step.js';
|
|
23
22
|
import { detectKind, importFiles } from '../src/io/convert.js';
|
|
@@ -27,7 +26,8 @@ import { optimize, expandProcedural } from '../src/optimize/optimizer.js';
|
|
|
27
26
|
const HELP = `es3d <command> [options] <input>
|
|
28
27
|
|
|
29
28
|
Commands
|
|
30
|
-
validate parse, validate and build the scene; print
|
|
29
|
+
validate parse, validate and build the scene; print diagnostics (exit 1 on errors).
|
|
30
|
+
Repeated warnings are grouped; --json lists every one
|
|
31
31
|
fmt pretty-print, keeping the syntax (markup or compact)
|
|
32
32
|
minify minified markup (short attribute aliases, no comments)
|
|
33
33
|
compact convert to the compact syntax (--min for one line)
|
|
@@ -36,7 +36,7 @@ Commands
|
|
|
36
36
|
optimize run the optimizer and print its report to stderr
|
|
37
37
|
expand replace <repeat>/<scatter> with explicit elements
|
|
38
38
|
obj export Wavefront OBJ (+ .mtl next to -o) (--lod <0-3>)
|
|
39
|
-
glb
|
|
39
|
+
glb packed glTF binary (instances, LODs, lights, cameras, animation)
|
|
40
40
|
stl export STL for 3D printing, Z-up (--lod <0-3>, --ascii)
|
|
41
41
|
step export tessellated STEP (AP242), Z-up (--lod <0-3>)
|
|
42
42
|
import convert SVG, OBJ (+MTL), STL, STEP or glTF (.gltf + .bin, .glb) to ES3D:
|
|
@@ -48,9 +48,9 @@ Commands
|
|
|
48
48
|
Options
|
|
49
49
|
-o, --out <file> output file (default: stdout for text)
|
|
50
50
|
--precision <n> round numbers to n decimals when writing text
|
|
51
|
-
--lod <n> level of detail for
|
|
52
|
-
--instancing
|
|
53
|
-
--quantize
|
|
51
|
+
--lod <n> level of detail for obj, stl and step (default 0 = finest)
|
|
52
|
+
--instancing glb always writes EXT_mesh_gpu_instancing
|
|
53
|
+
--quantize glb always writes KHR_mesh_quantization
|
|
54
54
|
--min / --pretty force minified / pretty output
|
|
55
55
|
--json validate/stats: machine readable output
|
|
56
56
|
--ascii STL: write ASCII instead of binary
|
|
@@ -132,9 +132,7 @@ function write(opts, data) {
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
function report(doc) {
|
|
135
|
-
|
|
136
|
-
for (const d of doc.diagnostics) out.push(d.format(doc.lineIndex));
|
|
137
|
-
return out.join('\n\n');
|
|
135
|
+
return formatDiagnostics(doc);
|
|
138
136
|
}
|
|
139
137
|
|
|
140
138
|
/** Parse and validate, or exit with the diagnostics. */
|
|
@@ -144,7 +142,8 @@ function parsed(input) {
|
|
|
144
142
|
process.stderr.write(report(doc) + '\n');
|
|
145
143
|
process.exit(1);
|
|
146
144
|
}
|
|
147
|
-
|
|
145
|
+
const warnings = formatDiagnostics(doc.warnings || [], doc.lineIndex);
|
|
146
|
+
if (warnings) process.stderr.write(warnings + '\n\n');
|
|
148
147
|
return doc;
|
|
149
148
|
}
|
|
150
149
|
|
|
@@ -152,7 +151,7 @@ function compiled(input) {
|
|
|
152
151
|
const r = compile(input);
|
|
153
152
|
const errors = r.diagnostics.filter((d) => d.severity === 'error');
|
|
154
153
|
if (errors.length) {
|
|
155
|
-
process.stderr.write(
|
|
154
|
+
process.stderr.write(formatDiagnostics(r) + '\n');
|
|
156
155
|
process.exit(1);
|
|
157
156
|
}
|
|
158
157
|
return r;
|
|
@@ -264,7 +263,8 @@ async function main() {
|
|
|
264
263
|
const warnings = r.diagnostics.length - errors;
|
|
265
264
|
if (opts.json) console.log(JSON.stringify({ ok: errors === 0, diagnostics: r.diagnostics.map((d) => d.toJSON()) }, null, 2));
|
|
266
265
|
else {
|
|
267
|
-
|
|
266
|
+
const text = formatDiagnostics(r);
|
|
267
|
+
if (text) process.stderr.write(text + '\n\n');
|
|
268
268
|
console.log(`${opts.input}: ${errors} error${errors === 1 ? '' : 's'}, ${warnings} warning${warnings === 1 ? '' : 's'}${errors ? '' : `, ${r.scene.count.toLocaleString('en-US')} instances`}`);
|
|
269
269
|
}
|
|
270
270
|
process.exit(errors ? 1 : 0);
|
|
@@ -312,8 +312,21 @@ async function main() {
|
|
|
312
312
|
break;
|
|
313
313
|
}
|
|
314
314
|
case 'glb': {
|
|
315
|
-
|
|
316
|
-
|
|
315
|
+
let exportPackedGLB;
|
|
316
|
+
try {
|
|
317
|
+
({ exportPackedGLB } = await import('../src/io/gltf-pack.js'));
|
|
318
|
+
} catch (e) {
|
|
319
|
+
fail(`glb could not load the packer (${e.message})`, 1);
|
|
320
|
+
}
|
|
321
|
+
const baseDir = opts.input === '-' ? '' : path.dirname(path.resolve(opts.input));
|
|
322
|
+
const sourceName = opts.input === '-' ? 'stdin' : `${path.basename(path.dirname(opts.input))}/${path.basename(opts.input)}`;
|
|
323
|
+
let file;
|
|
324
|
+
try {
|
|
325
|
+
file = await exportPackedGLB(es3dInput(bytes), { baseDir, sourceName });
|
|
326
|
+
} catch (e) {
|
|
327
|
+
fail(e.message, 1);
|
|
328
|
+
}
|
|
329
|
+
write(opts, file);
|
|
317
330
|
break;
|
|
318
331
|
}
|
|
319
332
|
case 'stl': {
|
package/src/format/parse.js
CHANGED
|
@@ -86,7 +86,47 @@ export function parseStrict(input, opts = {}) {
|
|
|
86
86
|
return doc;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
/**
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
/** Element name written in a message, if it has one (`<x-anchors>`). */
|
|
90
|
+
function mentionedElement(message) {
|
|
91
|
+
const m = /<([^<>\s]+)>/.exec(message || '');
|
|
92
|
+
return m ? m[1] : '';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Human-readable report of diagnostics.
|
|
97
|
+
* Repeated warnings that share a code, the element name in the message, and
|
|
98
|
+
* the same message are shown once, at the first location, with a count.
|
|
99
|
+
* Errors are listed one by one. The diagnostic objects are not changed.
|
|
100
|
+
* @param {object|Array} source a document, a compile result, or a diagnostic list
|
|
101
|
+
* @param {import('./errors.js').LineIndex} [lineIndex]
|
|
102
|
+
*/
|
|
103
|
+
export function formatDiagnostics(source, lineIndex) {
|
|
104
|
+
if (!source) return '';
|
|
105
|
+
const list = Array.isArray(source) ? source : source.diagnostics || [];
|
|
106
|
+
const index = lineIndex || source.lineIndex || (source.doc && source.doc.lineIndex) || null;
|
|
107
|
+
const seen = new Map();
|
|
108
|
+
const items = [];
|
|
109
|
+
for (const d of list) {
|
|
110
|
+
if (!d || d.severity !== 'warning') {
|
|
111
|
+
items.push(d);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const key = `${d.code}\0${mentionedElement(d.message)}\0${d.message}`;
|
|
115
|
+
const prev = seen.get(key);
|
|
116
|
+
if (prev) {
|
|
117
|
+
prev.count++;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const slot = { d, count: 1 };
|
|
121
|
+
seen.set(key, slot);
|
|
122
|
+
items.push(slot);
|
|
123
|
+
}
|
|
124
|
+
return items
|
|
125
|
+
.map((item) => {
|
|
126
|
+
if (!item || !item.d) return item.format(index);
|
|
127
|
+
const text = item.d.format(index);
|
|
128
|
+
if (item.count < 2) return text;
|
|
129
|
+
return `${text}\n\nReported ${item.count} times. Showing the first.`;
|
|
130
|
+
})
|
|
131
|
+
.join('\n\n');
|
|
92
132
|
}
|
package/src/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// Public API of the ES3D reference implementation.
|
|
2
2
|
//
|
|
3
|
-
// import { compile, createRenderer, Camera } from '@es3d/es3d';
|
|
3
|
+
// import { compile, createRenderer, Camera, formatDiagnostics } from '@es3d/es3d';
|
|
4
4
|
//
|
|
5
5
|
// const result = compile(text); // parse + validate + build + flatten
|
|
6
|
-
// if (!result.ok) console.error(result
|
|
6
|
+
// if (!result.ok) console.error(formatDiagnostics(result));
|
|
7
7
|
// const renderer = await createRenderer(canvas); // WebGPU, else WebGL2
|
|
8
8
|
// renderer.setScene(result.scene, (entry, lod) => result.cache.mesh(entry, lod));
|
|
9
9
|
// const camera = new Camera().frame(result.scene.bounds);
|