geom-parse-stl
Version:
Parse a STL (StereoLithography) ASCII string, ArrayBuffer or ArrayBuffer with ASCII data, and return a simplicial complex.
141 lines (118 loc) • 4.27 kB
JavaScript
/** @module geom-parse-stl */
import typedArrayConstructor from "typed-array-constructor";
/**
* Parse a STL ASCII string and return a simplicial complex.
*
* @param {string} asciiString
* @returns {import("./types.js").SimplicialComplex}
*/
function parseStlAscii(asciiString) {
const facetCount = asciiString.split("endfacet").length - 1;
const size = facetCount * 3;
const positions = new Float32Array(size * 3);
const faceNormals = new Float32Array(size);
const cells = new (typedArrayConstructor(size))(size);
let name;
const lines = asciiString.split("\n");
let facetIndex = 0;
let facetOffset = 0;
let j = 0;
for (let i = 0; i < lines.length; i++) {
const [keyword, ...tokens] = lines[i]
.replaceAll(/[\s]+/g, " ")
.trim()
.split(" ")
.filter((part) => part !== "");
switch (keyword) {
case "solid":
name = tokens.join(" ");
break;
case "facet":
facetOffset = facetIndex * 3;
if (tokens[0] === "normal") {
faceNormals[facetOffset] = parseFloat(tokens[1]);
faceNormals[facetOffset + 1] = parseFloat(tokens[2]);
faceNormals[facetOffset + 2] = parseFloat(tokens[3]);
}
cells[facetOffset] = facetOffset;
cells[facetOffset + 1] = facetOffset + 1;
cells[facetOffset + 2] = facetOffset + 2;
break;
case "outer":
j = 0;
break;
case "vertex": {
const vertexOffset = facetOffset * 3 + j * 3;
positions[vertexOffset] = parseFloat(tokens[0]);
positions[vertexOffset + 1] = parseFloat(tokens[1]);
positions[vertexOffset + 2] = parseFloat(tokens[2]);
j++;
break;
}
case "endfacet":
facetIndex++;
break;
case "endloop":
case "endsolid":
break;
default:
console.warn(`Unrecognized keyword "${keyword}"`);
}
}
return { positions, faceNormals, cells, name };
}
const HEADER_SIZE = 80;
const FACET_COUNT_SIZE = 4;
const FACET_SIZE = 50;
const readUInt32LE = (dataView, offset = 0) => dataView.getUint32(offset, true);
const readFloat32LE = (dataView, offset = 0) =>
dataView.getFloat32(offset, true);
/**
* Parse a STL ArrayBuffer or ArrayBuffer with ASCII data and return a simplicial complex.
*
* @param {ArrayBuffer} arrayBuffer
* @returns {import("./types.js").SimplicialComplex}
*/
function parseStlBinary(arrayBuffer) {
const dataView = new DataView(arrayBuffer);
const facetCount = readUInt32LE(dataView, HEADER_SIZE);
let byteOffset = HEADER_SIZE + FACET_COUNT_SIZE;
if (byteOffset + facetCount * FACET_SIZE !== arrayBuffer.byteLength) {
return parseStlAscii(new TextDecoder("utf-8").decode(arrayBuffer));
}
const size = facetCount * 3;
const positions = new Float32Array(size * 3);
const faceNormals = new Float32Array(size);
const cells = new (typedArrayConstructor(size))(size);
let facetIndex = 0;
for (let i = 0; i < facetCount; i++) {
const facetOffset = facetIndex * 3;
faceNormals[facetOffset] = readFloat32LE(dataView, byteOffset);
faceNormals[facetOffset + 1] = readFloat32LE(dataView, byteOffset + 4);
faceNormals[facetOffset + 2] = readFloat32LE(dataView, byteOffset + 8);
byteOffset += 12;
for (let j = 0; j < 3; j++) {
const vertexOffset = facetOffset * 3 + j * 3;
positions[vertexOffset] = readFloat32LE(dataView, byteOffset);
positions[vertexOffset + 1] = readFloat32LE(dataView, byteOffset + 4);
positions[vertexOffset + 2] = readFloat32LE(dataView, byteOffset + 8);
byteOffset += 12;
}
cells[facetOffset] = facetOffset;
cells[facetOffset + 1] = facetOffset + 1;
cells[facetOffset + 2] = facetOffset + 2;
byteOffset += 2;
facetIndex++;
}
return { positions, faceNormals, cells };
}
/**
* Parse a STL (StereoLithography) ASCII string, ArrayBuffer or ArrayBuffer with ASCII data, and return a simplicial complex.
*
* @see https://paulbourke.net/dataformats/stl/
* @param {string|ArrayBuffer} stl
* @returns {import("./types.js").SimplicialComplex}
*/
const parseStl = (stl) =>
typeof stl === "string" ? parseStlAscii(stl) : parseStlBinary(stl);
export { parseStl, parseStlAscii, parseStlBinary };