mirror of
https://github.com/gittrahan/support-fins.git
synced 2026-10-19 02:10:52 -05:00
stl: review fixes -- bytes after the last triangle still read as binary, keywords any case, solid_name headers
Some exporters pad binary STLs; three.js reads them, so the command line must too. Tests pin padded binary (with and without a 'solid' header) and ASCII with CRLF, exponents, upper case, a BOM and a solid_name header.
This commit is contained in:
1 parent
4210035e54
commit
dcce14a185
3 files changed
+34
-7
No files matched your search
+3
-1
@@ -130,7 +130,9 @@ that aren't fin geometry, because the file format is equally part of the product
|
||||
use it; the site parses with three.js's `STLLoader`):
|
||||
- our own binary export **reads back** as the same triangles, and ASCII does too;
|
||||
- binary is decided by **size**, not by a leading `solid`: CAD exporters (SolidWorks)
|
||||
start binary headers with it, and reading one as text opens an empty part;
|
||||
start binary headers with it, and reading one as text opens an empty part; bytes
|
||||
left after the last triangle still read as binary, as three.js reads them;
|
||||
- ASCII with CRLF, exponents, upper-case keywords and a BOM reads;
|
||||
- a **truncated, empty or non-STL** file throws instead of opening blank.
|
||||
|
||||
**`step.test.js`** -- STEP import through the real vendored OpenCascade WASM (the
|
||||
|
||||
@@ -48,12 +48,32 @@ Deno.test('stl: a binary file whose header starts with "solid" is still binary',
|
||||
assert(got.length === CUBE.length, `read ${got.length / 9} faces as ASCII instead of binary`);
|
||||
});
|
||||
|
||||
Deno.test('stl: extra bytes after the last triangle still read as binary (three.js reads these)', async () => {
|
||||
for (const name of ['part', 'solid part']) {
|
||||
const bin = await bytesOf(triples(CUBE), name);
|
||||
const padded = new Uint8Array(bin.length + 2);
|
||||
padded.set(bin);
|
||||
const got = readSTL(padded);
|
||||
assert(got.length === CUBE.length, `header "${name}": read ${got.length / 9} faces`);
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test('stl: ASCII reads the same triangles', () => {
|
||||
const got = readSTL(ascii(CUBE));
|
||||
assert(got.length === CUBE.length, `length ${got.length} vs ${CUBE.length}`);
|
||||
for (let i = 0; i < CUBE.length; i++) assert(got[i] === CUBE[i], `value ${i}`);
|
||||
});
|
||||
|
||||
Deno.test('stl: ASCII with CRLF, exponents, upper case, a BOM and solid_name', () => {
|
||||
const text = new TextDecoder().decode(ascii(CUBE))
|
||||
.replace('solid cube', '\uFEFFSOLID_cube').replaceAll('vertex', 'VERTEX')
|
||||
.replace(/VERTEX (\S+)/g, (_, x) => `VERTEX ${Number(x).toExponential()}`)
|
||||
.replaceAll('\n', '\r\n');
|
||||
const got = readSTL(new TextEncoder().encode(text));
|
||||
assert(got.length === CUBE.length, `length ${got.length} vs ${CUBE.length}`);
|
||||
for (let i = 0; i < CUBE.length; i++) assert(got[i] === CUBE[i], `value ${i}`);
|
||||
});
|
||||
|
||||
Deno.test('stl: truncated, empty and non-STL files are refused', async () => {
|
||||
const bin = await bytesOf(triples(CUBE), 'cube');
|
||||
throws(() => readSTL(bin.subarray(0, bin.length - 7)), /not an STL/, 'truncated binary');
|
||||
|
||||
+11
-6
@@ -69,16 +69,22 @@ export function writeBinarySTL(tris, name = 'Support Fins') {
|
||||
*
|
||||
* Binary is decided by SIZE, not by the "solid" prefix: plenty of CAD exporters
|
||||
* (SolidWorks among them) start a binary file's header with "solid", and reading
|
||||
* one as text gives an empty part instead of an error.
|
||||
* one as text gives an empty part instead of an error. Some exporters also leave
|
||||
* bytes after the last triangle; three.js reads those files, so we do too: a file
|
||||
* at least as long as its triangle count says is binary unless it reads as ASCII
|
||||
* (a "solid" line and a "facet" soon after).
|
||||
*
|
||||
* @param bytes Uint8Array of the whole file
|
||||
* @returns Float32Array
|
||||
*/
|
||||
export function readSTL(bytes) {
|
||||
const head = new TextDecoder().decode(bytes.subarray(0, Math.min(bytes.length, 1024)));
|
||||
const looksAscii = /^\uFEFF?\s*solid/i.test(head) && /\bfacet\b/i.test(head);
|
||||
if (bytes.length >= HEADER + 4) {
|
||||
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||
const n = dv.getUint32(HEADER, true);
|
||||
if (bytes.length === HEADER + 4 + n * PER_TRI) {
|
||||
const need = HEADER + 4 + n * PER_TRI;
|
||||
if (bytes.length === need || (n > 0 && bytes.length > need && !looksAscii)) {
|
||||
if (n === 0) throw new Error('STL has no triangles');
|
||||
const pos = new Float32Array(n * 9);
|
||||
for (let f = 0; f < n; f++) {
|
||||
@@ -88,16 +94,15 @@ export function readSTL(bytes) {
|
||||
return pos;
|
||||
}
|
||||
}
|
||||
const text = new TextDecoder().decode(bytes.subarray(0, Math.min(bytes.length, 1024)));
|
||||
if (!/^\s*solid\b/.test(text)) {
|
||||
throw new Error('not an STL: wrong size for binary and no "solid" line for ASCII (truncated file?)');
|
||||
if (!/^\uFEFF?\s*solid/i.test(head)) {
|
||||
throw new Error('not an STL: too short for binary and no "solid" line for ASCII (truncated file?)');
|
||||
}
|
||||
return readAsciiSTL(new TextDecoder().decode(bytes));
|
||||
}
|
||||
|
||||
function readAsciiSTL(text) {
|
||||
const out = [];
|
||||
const re = /\bvertex\s+(\S+)\s+(\S+)\s+(\S+)/g;
|
||||
const re = /\bvertex\s+(\S+)\s+(\S+)\s+(\S+)/gi;
|
||||
for (let m; (m = re.exec(text));) {
|
||||
for (let k = 1; k <= 3; k++) {
|
||||
const v = Number(m[k]);
|
||||
|
||||
Reference in new issue
Block a user