@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.
- package/LICENSE +201 -201
- package/README.md +86 -86
- package/docs/SPEC.md +417 -417
- package/package.json +1 -1
- package/scripts/es3d-cli.mjs +377 -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 +92 -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.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 +579 -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 +364 -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
package/src/io/stl.js
CHANGED
|
@@ -1,193 +1,193 @@
|
|
|
1
|
-
// STL (stereolithography) import and export, binary and ASCII.
|
|
2
|
-
//
|
|
3
|
-
// STL stores unconnected triangles with one face normal each: no units, no
|
|
4
|
-
// colors, no shared vertices. Files from CAD and 3D-printing tools are Z-up
|
|
5
|
-
// and usually in millimetres. The importer welds identical corners and
|
|
6
|
-
// computes vertex normals with a crease angle, so flat faces stay flat and
|
|
7
|
-
// curved surfaces look round; the exporter writes world-space triangles of
|
|
8
|
-
// every instance, Z-up by default, as slicers expect.
|
|
9
|
-
|
|
10
|
-
import { computeNormals, weldMap } from '../geometry/mesh.js';
|
|
11
|
-
import { buildExternalScene, exportableInstances, InstanceTable } from './external-scene.js';
|
|
12
|
-
|
|
13
|
-
/** A binary STL is exactly 84 + 50 · n bytes long; that test is stronger than the header text. */
|
|
14
|
-
export function isBinarySTL(bytes) {
|
|
15
|
-
if (bytes.length < 84) return false;
|
|
16
|
-
const n = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(80, true);
|
|
17
|
-
return 84 + n * 50 === bytes.length;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** ASCII STL: "solid <name>" followed by the first facet (or an empty solid). */
|
|
21
|
-
export function isAsciiSTL(bytes) {
|
|
22
|
-
const head = new TextDecoder().decode(bytes.subarray(0, Math.min(bytes.length, 1024)));
|
|
23
|
-
return /^\s*solid\b[^\n]*\n\s*(facet|endsolid)\b/.test(head);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Parse an STL file.
|
|
28
|
-
* @param {Uint8Array} bytes
|
|
29
|
-
* @returns {{positions: Float32Array, triangles: number, name: string, format: 'binary'|'ascii'}}
|
|
30
|
-
* positions: 9 floats per triangle, in file order
|
|
31
|
-
*/
|
|
32
|
-
export function parseSTL(bytes) {
|
|
33
|
-
if (isBinarySTL(bytes)) {
|
|
34
|
-
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
35
|
-
const n = dv.getUint32(80, true);
|
|
36
|
-
const positions = new Float32Array(n * 9);
|
|
37
|
-
for (let t = 0; t < n; t++) {
|
|
38
|
-
const o = 84 + t * 50 + 12; // skip the face normal
|
|
39
|
-
for (let k = 0; k < 9; k++) positions[t * 9 + k] = dv.getFloat32(o + k * 4, true);
|
|
40
|
-
}
|
|
41
|
-
const header = new TextDecoder().decode(bytes.subarray(0, 80)).replace(/\0[\s\S]*$/, '').trim();
|
|
42
|
-
return { positions, triangles: n, name: header.replace(/^solid\s*/i, ''), format: 'binary' };
|
|
43
|
-
}
|
|
44
|
-
const text = new TextDecoder().decode(bytes);
|
|
45
|
-
if (!/^\s*solid\b/.test(text)) throw new Error('Not an STL file: binary STL size does not match its triangle count, and ASCII STL must start with "solid".');
|
|
46
|
-
const out = [];
|
|
47
|
-
const re = /\bvertex\s+(\S+)\s+(\S+)\s+(\S+)/g;
|
|
48
|
-
let m;
|
|
49
|
-
while ((m = re.exec(text))) {
|
|
50
|
-
const x = Number(m[1]), y = Number(m[2]), z = Number(m[3]);
|
|
51
|
-
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) {
|
|
52
|
-
const line = text.slice(0, m.index).split('\n').length;
|
|
53
|
-
throw new Error(`STL line ${line}: vertex coordinates must be numbers ("${m[0]}").`);
|
|
54
|
-
}
|
|
55
|
-
out.push(x, y, z);
|
|
56
|
-
}
|
|
57
|
-
if (out.length % 9 !== 0) throw new Error(`STL: found ${out.length / 3} vertices, which is not a multiple of 3 (every facet needs exactly 3).`);
|
|
58
|
-
const name = (/^\s*solid[ \t]*([^\r\n]*)/.exec(text) || [])[1] || '';
|
|
59
|
-
return { positions: Float32Array.from(out), triangles: out.length / 9, name: name.trim(), format: 'ascii' };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Weld identical corners of a triangle soup: unique positions plus an index list. */
|
|
63
|
-
export function weldTriangles(soup) {
|
|
64
|
-
const map = weldMap(soup);
|
|
65
|
-
const n = soup.length / 3;
|
|
66
|
-
const remap = new Int32Array(n).fill(-1);
|
|
67
|
-
const indices = new Uint32Array(n);
|
|
68
|
-
let unique = 0;
|
|
69
|
-
for (let i = 0; i < n; i++) {
|
|
70
|
-
const r = map[i];
|
|
71
|
-
if (remap[r] < 0) remap[r] = unique++;
|
|
72
|
-
indices[i] = remap[r];
|
|
73
|
-
}
|
|
74
|
-
const positions = new Float32Array(unique * 3);
|
|
75
|
-
for (let i = 0; i < n; i++) {
|
|
76
|
-
const v = indices[i] * 3;
|
|
77
|
-
positions[v] = soup[i * 3];
|
|
78
|
-
positions[v + 1] = soup[i * 3 + 1];
|
|
79
|
-
positions[v + 2] = soup[i * 3 + 2];
|
|
80
|
-
}
|
|
81
|
-
// drop triangles that collapsed to a line or a point
|
|
82
|
-
let w = 0;
|
|
83
|
-
for (let t = 0; t < indices.length; t += 3) {
|
|
84
|
-
const a = indices[t], b = indices[t + 1], c = indices[t + 2];
|
|
85
|
-
if (a === b || b === c || a === c) continue;
|
|
86
|
-
indices[w++] = a;
|
|
87
|
-
indices[w++] = b;
|
|
88
|
-
indices[w++] = c;
|
|
89
|
-
}
|
|
90
|
-
return { positions, indices: indices.slice(0, w), degenerate: (indices.length - w) / 3 };
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Z-up (STL) to Y-up (ES3D): a -90° rotation about X, column-major. */
|
|
94
|
-
export const Z_UP_TO_Y_UP = [1, 0, 0, 0, 0, 0, -1, 0, 0, 1, 0, 0, 0, 0, 0, 1];
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Convert an STL file into a RenderScene (one mesh, one instance).
|
|
98
|
-
* @param {Uint8Array} bytes
|
|
99
|
-
* @param {{up?: 'z'|'y', smooth?: number}} [opts] up: the file's up axis; smooth: crease angle in degrees
|
|
100
|
-
*/
|
|
101
|
-
export function stlToScene(bytes, { up = 'z', smooth = 30 } = {}) {
|
|
102
|
-
const parsed = parseSTL(bytes);
|
|
103
|
-
if (!parsed.triangles) throw new Error('The STL file contains no triangles.');
|
|
104
|
-
const { positions, indices, degenerate } = weldTriangles(parsed.positions);
|
|
105
|
-
const mesh = computeNormals(positions, indices, smooth);
|
|
106
|
-
const instances = new InstanceTable(1);
|
|
107
|
-
instances.push(0, 0, up === 'z' ? Z_UP_TO_Y_UP : [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
|
|
108
|
-
const materials = [{ name: 'stl', color: [0.72, 0.74, 0.78, 1], roughness: 0.45, metallic: 0 }];
|
|
109
|
-
const built = buildExternalScene({ geometries: [{ mesh }], materials, instances });
|
|
110
|
-
return { ...built, info: { name: parsed.name, format: parsed.format, triangles: parsed.triangles, vertices: positions.length / 3, degenerate, smooth } };
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Export the static scene as STL: every instance's triangles in world space.
|
|
115
|
-
* @param {import('../scene/flatten.js').RenderScene} scene
|
|
116
|
-
* @param {(entry: object, lod: number) => import('../geometry/mesh.js').Mesh} meshOf
|
|
117
|
-
* @param {{lod?: number, up?: 'z'|'y', binary?: boolean, name?: string, maxTriangles?: number}} [opts]
|
|
118
|
-
* @returns {{data: Uint8Array|string, triangles: number}}
|
|
119
|
-
*/
|
|
120
|
-
export function exportSTL(scene, meshOf, { lod = 0, up = 'z', binary = true, name = 'es3d', maxTriangles = 2e7 } = {}) {
|
|
121
|
-
const list = exportableInstances(scene);
|
|
122
|
-
let total = 0;
|
|
123
|
-
for (const i of list) {
|
|
124
|
-
const m = meshOf(scene.geometries[scene.geom[i]], lod);
|
|
125
|
-
if (m.kind === 'triangles') total += m.primitiveCount;
|
|
126
|
-
}
|
|
127
|
-
if (total > maxTriangles) throw new Error(`The scene has ${total.toLocaleString('en-US')} triangles; STL output is limited to ${maxTriangles.toLocaleString('en-US')}. Export a coarser level of detail.`);
|
|
128
|
-
const tri = new Float64Array(9);
|
|
129
|
-
const M = new Float64Array(16);
|
|
130
|
-
const zUp = up === 'z';
|
|
131
|
-
let dv = null, o = 84;
|
|
132
|
-
const lines = [];
|
|
133
|
-
if (binary) {
|
|
134
|
-
const out = new Uint8Array(84 + total * 50);
|
|
135
|
-
const head = new TextEncoder().encode(`ES3D export: ${name}`.slice(0, 79));
|
|
136
|
-
out.set(head);
|
|
137
|
-
dv = new DataView(out.buffer);
|
|
138
|
-
dv.setUint32(80, total, true);
|
|
139
|
-
} else lines.push(`solid ${name}`);
|
|
140
|
-
const f = (v) => {
|
|
141
|
-
if (Math.abs(v) < 1e-30) return '0';
|
|
142
|
-
const s = Math.fround(v).toPrecision(7);
|
|
143
|
-
return s.includes('.') ? s.replace(/0+(?=e|$)/, '').replace(/\.(?=e|$)/, '') : s;
|
|
144
|
-
};
|
|
145
|
-
for (const i of list) {
|
|
146
|
-
const mesh = meshOf(scene.geometries[scene.geom[i]], lod);
|
|
147
|
-
if (mesh.kind !== 'triangles') continue;
|
|
148
|
-
for (let k = 0; k < 16; k++) M[k] = scene.matrices[i * 16 + k];
|
|
149
|
-
const det = M[0] * (M[5] * M[10] - M[6] * M[9]) - M[4] * (M[1] * M[10] - M[2] * M[9]) + M[8] * (M[1] * M[6] - M[2] * M[5]);
|
|
150
|
-
const flip = det < 0;
|
|
151
|
-
const p = mesh.positions, idx = mesh.indices;
|
|
152
|
-
for (let t = 0; t < idx.length; t += 3) {
|
|
153
|
-
for (let c = 0; c < 3; c++) {
|
|
154
|
-
const v = idx[t + (flip && c ? 3 - c : c)] * 3;
|
|
155
|
-
const x = p[v], y = p[v + 1], z = p[v + 2];
|
|
156
|
-
const wx = M[0] * x + M[4] * y + M[8] * z + M[12];
|
|
157
|
-
const wy = M[1] * x + M[5] * y + M[9] * z + M[13];
|
|
158
|
-
const wz = M[2] * x + M[6] * y + M[10] * z + M[14];
|
|
159
|
-
// Y-up to Z-up: (x, y, z) -> (x, -z, y)
|
|
160
|
-
tri[c * 3] = wx;
|
|
161
|
-
tri[c * 3 + 1] = zUp ? -wz : wy;
|
|
162
|
-
tri[c * 3 + 2] = zUp ? wy : wz;
|
|
163
|
-
}
|
|
164
|
-
const e1x = tri[3] - tri[0], e1y = tri[4] - tri[1], e1z = tri[5] - tri[2];
|
|
165
|
-
const e2x = tri[6] - tri[0], e2y = tri[7] - tri[1], e2z = tri[8] - tri[2];
|
|
166
|
-
let nx = e1y * e2z - e1z * e2y, ny = e1z * e2x - e1x * e2z, nz = e1x * e2y - e1y * e2x;
|
|
167
|
-
const l = Math.hypot(nx, ny, nz) || 1;
|
|
168
|
-
nx /= l;
|
|
169
|
-
ny /= l;
|
|
170
|
-
nz /= l;
|
|
171
|
-
if (binary) {
|
|
172
|
-
dv.setFloat32(o, nx, true);
|
|
173
|
-
dv.setFloat32(o + 4, ny, true);
|
|
174
|
-
dv.setFloat32(o + 8, nz, true);
|
|
175
|
-
for (let k = 0; k < 9; k++) dv.setFloat32(o + 12 + k * 4, tri[k], true);
|
|
176
|
-
o += 50;
|
|
177
|
-
} else {
|
|
178
|
-
lines.push(
|
|
179
|
-
` facet normal ${f(nx)} ${f(ny)} ${f(nz)}`,
|
|
180
|
-
' outer loop',
|
|
181
|
-
` vertex ${f(tri[0])} ${f(tri[1])} ${f(tri[2])}`,
|
|
182
|
-
` vertex ${f(tri[3])} ${f(tri[4])} ${f(tri[5])}`,
|
|
183
|
-
` vertex ${f(tri[6])} ${f(tri[7])} ${f(tri[8])}`,
|
|
184
|
-
' endloop',
|
|
185
|
-
' endfacet',
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
if (binary) return { data: new Uint8Array(dv.buffer), triangles: total };
|
|
191
|
-
lines.push(`endsolid ${name}`);
|
|
192
|
-
return { data: lines.join('\n') + '\n', triangles: total };
|
|
193
|
-
}
|
|
1
|
+
// STL (stereolithography) import and export, binary and ASCII.
|
|
2
|
+
//
|
|
3
|
+
// STL stores unconnected triangles with one face normal each: no units, no
|
|
4
|
+
// colors, no shared vertices. Files from CAD and 3D-printing tools are Z-up
|
|
5
|
+
// and usually in millimetres. The importer welds identical corners and
|
|
6
|
+
// computes vertex normals with a crease angle, so flat faces stay flat and
|
|
7
|
+
// curved surfaces look round; the exporter writes world-space triangles of
|
|
8
|
+
// every instance, Z-up by default, as slicers expect.
|
|
9
|
+
|
|
10
|
+
import { computeNormals, weldMap } from '../geometry/mesh.js';
|
|
11
|
+
import { buildExternalScene, exportableInstances, InstanceTable } from './external-scene.js';
|
|
12
|
+
|
|
13
|
+
/** A binary STL is exactly 84 + 50 · n bytes long; that test is stronger than the header text. */
|
|
14
|
+
export function isBinarySTL(bytes) {
|
|
15
|
+
if (bytes.length < 84) return false;
|
|
16
|
+
const n = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(80, true);
|
|
17
|
+
return 84 + n * 50 === bytes.length;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** ASCII STL: "solid <name>" followed by the first facet (or an empty solid). */
|
|
21
|
+
export function isAsciiSTL(bytes) {
|
|
22
|
+
const head = new TextDecoder().decode(bytes.subarray(0, Math.min(bytes.length, 1024)));
|
|
23
|
+
return /^\s*solid\b[^\n]*\n\s*(facet|endsolid)\b/.test(head);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parse an STL file.
|
|
28
|
+
* @param {Uint8Array} bytes
|
|
29
|
+
* @returns {{positions: Float32Array, triangles: number, name: string, format: 'binary'|'ascii'}}
|
|
30
|
+
* positions: 9 floats per triangle, in file order
|
|
31
|
+
*/
|
|
32
|
+
export function parseSTL(bytes) {
|
|
33
|
+
if (isBinarySTL(bytes)) {
|
|
34
|
+
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
35
|
+
const n = dv.getUint32(80, true);
|
|
36
|
+
const positions = new Float32Array(n * 9);
|
|
37
|
+
for (let t = 0; t < n; t++) {
|
|
38
|
+
const o = 84 + t * 50 + 12; // skip the face normal
|
|
39
|
+
for (let k = 0; k < 9; k++) positions[t * 9 + k] = dv.getFloat32(o + k * 4, true);
|
|
40
|
+
}
|
|
41
|
+
const header = new TextDecoder().decode(bytes.subarray(0, 80)).replace(/\0[\s\S]*$/, '').trim();
|
|
42
|
+
return { positions, triangles: n, name: header.replace(/^solid\s*/i, ''), format: 'binary' };
|
|
43
|
+
}
|
|
44
|
+
const text = new TextDecoder().decode(bytes);
|
|
45
|
+
if (!/^\s*solid\b/.test(text)) throw new Error('Not an STL file: binary STL size does not match its triangle count, and ASCII STL must start with "solid".');
|
|
46
|
+
const out = [];
|
|
47
|
+
const re = /\bvertex\s+(\S+)\s+(\S+)\s+(\S+)/g;
|
|
48
|
+
let m;
|
|
49
|
+
while ((m = re.exec(text))) {
|
|
50
|
+
const x = Number(m[1]), y = Number(m[2]), z = Number(m[3]);
|
|
51
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) {
|
|
52
|
+
const line = text.slice(0, m.index).split('\n').length;
|
|
53
|
+
throw new Error(`STL line ${line}: vertex coordinates must be numbers ("${m[0]}").`);
|
|
54
|
+
}
|
|
55
|
+
out.push(x, y, z);
|
|
56
|
+
}
|
|
57
|
+
if (out.length % 9 !== 0) throw new Error(`STL: found ${out.length / 3} vertices, which is not a multiple of 3 (every facet needs exactly 3).`);
|
|
58
|
+
const name = (/^\s*solid[ \t]*([^\r\n]*)/.exec(text) || [])[1] || '';
|
|
59
|
+
return { positions: Float32Array.from(out), triangles: out.length / 9, name: name.trim(), format: 'ascii' };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Weld identical corners of a triangle soup: unique positions plus an index list. */
|
|
63
|
+
export function weldTriangles(soup) {
|
|
64
|
+
const map = weldMap(soup);
|
|
65
|
+
const n = soup.length / 3;
|
|
66
|
+
const remap = new Int32Array(n).fill(-1);
|
|
67
|
+
const indices = new Uint32Array(n);
|
|
68
|
+
let unique = 0;
|
|
69
|
+
for (let i = 0; i < n; i++) {
|
|
70
|
+
const r = map[i];
|
|
71
|
+
if (remap[r] < 0) remap[r] = unique++;
|
|
72
|
+
indices[i] = remap[r];
|
|
73
|
+
}
|
|
74
|
+
const positions = new Float32Array(unique * 3);
|
|
75
|
+
for (let i = 0; i < n; i++) {
|
|
76
|
+
const v = indices[i] * 3;
|
|
77
|
+
positions[v] = soup[i * 3];
|
|
78
|
+
positions[v + 1] = soup[i * 3 + 1];
|
|
79
|
+
positions[v + 2] = soup[i * 3 + 2];
|
|
80
|
+
}
|
|
81
|
+
// drop triangles that collapsed to a line or a point
|
|
82
|
+
let w = 0;
|
|
83
|
+
for (let t = 0; t < indices.length; t += 3) {
|
|
84
|
+
const a = indices[t], b = indices[t + 1], c = indices[t + 2];
|
|
85
|
+
if (a === b || b === c || a === c) continue;
|
|
86
|
+
indices[w++] = a;
|
|
87
|
+
indices[w++] = b;
|
|
88
|
+
indices[w++] = c;
|
|
89
|
+
}
|
|
90
|
+
return { positions, indices: indices.slice(0, w), degenerate: (indices.length - w) / 3 };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Z-up (STL) to Y-up (ES3D): a -90° rotation about X, column-major. */
|
|
94
|
+
export const Z_UP_TO_Y_UP = [1, 0, 0, 0, 0, 0, -1, 0, 0, 1, 0, 0, 0, 0, 0, 1];
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Convert an STL file into a RenderScene (one mesh, one instance).
|
|
98
|
+
* @param {Uint8Array} bytes
|
|
99
|
+
* @param {{up?: 'z'|'y', smooth?: number}} [opts] up: the file's up axis; smooth: crease angle in degrees
|
|
100
|
+
*/
|
|
101
|
+
export function stlToScene(bytes, { up = 'z', smooth = 30 } = {}) {
|
|
102
|
+
const parsed = parseSTL(bytes);
|
|
103
|
+
if (!parsed.triangles) throw new Error('The STL file contains no triangles.');
|
|
104
|
+
const { positions, indices, degenerate } = weldTriangles(parsed.positions);
|
|
105
|
+
const mesh = computeNormals(positions, indices, smooth);
|
|
106
|
+
const instances = new InstanceTable(1);
|
|
107
|
+
instances.push(0, 0, up === 'z' ? Z_UP_TO_Y_UP : [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
|
|
108
|
+
const materials = [{ name: 'stl', color: [0.72, 0.74, 0.78, 1], roughness: 0.45, metallic: 0 }];
|
|
109
|
+
const built = buildExternalScene({ geometries: [{ mesh }], materials, instances });
|
|
110
|
+
return { ...built, info: { name: parsed.name, format: parsed.format, triangles: parsed.triangles, vertices: positions.length / 3, degenerate, smooth } };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Export the static scene as STL: every instance's triangles in world space.
|
|
115
|
+
* @param {import('../scene/flatten.js').RenderScene} scene
|
|
116
|
+
* @param {(entry: object, lod: number) => import('../geometry/mesh.js').Mesh} meshOf
|
|
117
|
+
* @param {{lod?: number, up?: 'z'|'y', binary?: boolean, name?: string, maxTriangles?: number}} [opts]
|
|
118
|
+
* @returns {{data: Uint8Array|string, triangles: number}}
|
|
119
|
+
*/
|
|
120
|
+
export function exportSTL(scene, meshOf, { lod = 0, up = 'z', binary = true, name = 'es3d', maxTriangles = 2e7 } = {}) {
|
|
121
|
+
const list = exportableInstances(scene);
|
|
122
|
+
let total = 0;
|
|
123
|
+
for (const i of list) {
|
|
124
|
+
const m = meshOf(scene.geometries[scene.geom[i]], lod);
|
|
125
|
+
if (m.kind === 'triangles') total += m.primitiveCount;
|
|
126
|
+
}
|
|
127
|
+
if (total > maxTriangles) throw new Error(`The scene has ${total.toLocaleString('en-US')} triangles; STL output is limited to ${maxTriangles.toLocaleString('en-US')}. Export a coarser level of detail.`);
|
|
128
|
+
const tri = new Float64Array(9);
|
|
129
|
+
const M = new Float64Array(16);
|
|
130
|
+
const zUp = up === 'z';
|
|
131
|
+
let dv = null, o = 84;
|
|
132
|
+
const lines = [];
|
|
133
|
+
if (binary) {
|
|
134
|
+
const out = new Uint8Array(84 + total * 50);
|
|
135
|
+
const head = new TextEncoder().encode(`ES3D export: ${name}`.slice(0, 79));
|
|
136
|
+
out.set(head);
|
|
137
|
+
dv = new DataView(out.buffer);
|
|
138
|
+
dv.setUint32(80, total, true);
|
|
139
|
+
} else lines.push(`solid ${name}`);
|
|
140
|
+
const f = (v) => {
|
|
141
|
+
if (Math.abs(v) < 1e-30) return '0';
|
|
142
|
+
const s = Math.fround(v).toPrecision(7);
|
|
143
|
+
return s.includes('.') ? s.replace(/0+(?=e|$)/, '').replace(/\.(?=e|$)/, '') : s;
|
|
144
|
+
};
|
|
145
|
+
for (const i of list) {
|
|
146
|
+
const mesh = meshOf(scene.geometries[scene.geom[i]], lod);
|
|
147
|
+
if (mesh.kind !== 'triangles') continue;
|
|
148
|
+
for (let k = 0; k < 16; k++) M[k] = scene.matrices[i * 16 + k];
|
|
149
|
+
const det = M[0] * (M[5] * M[10] - M[6] * M[9]) - M[4] * (M[1] * M[10] - M[2] * M[9]) + M[8] * (M[1] * M[6] - M[2] * M[5]);
|
|
150
|
+
const flip = det < 0;
|
|
151
|
+
const p = mesh.positions, idx = mesh.indices;
|
|
152
|
+
for (let t = 0; t < idx.length; t += 3) {
|
|
153
|
+
for (let c = 0; c < 3; c++) {
|
|
154
|
+
const v = idx[t + (flip && c ? 3 - c : c)] * 3;
|
|
155
|
+
const x = p[v], y = p[v + 1], z = p[v + 2];
|
|
156
|
+
const wx = M[0] * x + M[4] * y + M[8] * z + M[12];
|
|
157
|
+
const wy = M[1] * x + M[5] * y + M[9] * z + M[13];
|
|
158
|
+
const wz = M[2] * x + M[6] * y + M[10] * z + M[14];
|
|
159
|
+
// Y-up to Z-up: (x, y, z) -> (x, -z, y)
|
|
160
|
+
tri[c * 3] = wx;
|
|
161
|
+
tri[c * 3 + 1] = zUp ? -wz : wy;
|
|
162
|
+
tri[c * 3 + 2] = zUp ? wy : wz;
|
|
163
|
+
}
|
|
164
|
+
const e1x = tri[3] - tri[0], e1y = tri[4] - tri[1], e1z = tri[5] - tri[2];
|
|
165
|
+
const e2x = tri[6] - tri[0], e2y = tri[7] - tri[1], e2z = tri[8] - tri[2];
|
|
166
|
+
let nx = e1y * e2z - e1z * e2y, ny = e1z * e2x - e1x * e2z, nz = e1x * e2y - e1y * e2x;
|
|
167
|
+
const l = Math.hypot(nx, ny, nz) || 1;
|
|
168
|
+
nx /= l;
|
|
169
|
+
ny /= l;
|
|
170
|
+
nz /= l;
|
|
171
|
+
if (binary) {
|
|
172
|
+
dv.setFloat32(o, nx, true);
|
|
173
|
+
dv.setFloat32(o + 4, ny, true);
|
|
174
|
+
dv.setFloat32(o + 8, nz, true);
|
|
175
|
+
for (let k = 0; k < 9; k++) dv.setFloat32(o + 12 + k * 4, tri[k], true);
|
|
176
|
+
o += 50;
|
|
177
|
+
} else {
|
|
178
|
+
lines.push(
|
|
179
|
+
` facet normal ${f(nx)} ${f(ny)} ${f(nz)}`,
|
|
180
|
+
' outer loop',
|
|
181
|
+
` vertex ${f(tri[0])} ${f(tri[1])} ${f(tri[2])}`,
|
|
182
|
+
` vertex ${f(tri[3])} ${f(tri[4])} ${f(tri[5])}`,
|
|
183
|
+
` vertex ${f(tri[6])} ${f(tri[7])} ${f(tri[8])}`,
|
|
184
|
+
' endloop',
|
|
185
|
+
' endfacet',
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (binary) return { data: new Uint8Array(dv.buffer), triangles: total };
|
|
191
|
+
lines.push(`endsolid ${name}`);
|
|
192
|
+
return { data: lines.join('\n') + '\n', triangles: total };
|
|
193
|
+
}
|