Files
workinf_Blender_Wasm/web/protocol/stl-import.ts
mes123456 380cbed4ff
Some checks are pending
M6 deployable RC / quick (push) Waiting to run
M6 deployable RC / chromium (push) Blocked by required conditions
M6 deployable RC / release (push) Blocked by required conditions
Checkpoint web parity through Chromium input tasks
2026-08-19 10:39:03 -04:00

126 lines
5.7 KiB
TypeScript

export const STL_IMPORT_SCHEMA_VERSION = 1 as const;
export type STLVariant = "STL_BINARY" | "STL_ASCII";
export const STL_IMPORT_BUDGET = {
maxBytes: 512 * 1024,
maxTriangles: 65_536,
maxUnitScale: 1_000_000,
} as const;
export interface STLImportResult {
schemaVersion: typeof STL_IMPORT_SCHEMA_VERSION;
variant: STLVariant;
unitScale: number;
declaredTriangleCount: number;
triangleCount: number;
removedDegenerateTriangles: number;
normals: number[][];
vertices: number[][][];
bounds: { min: number[]; max: number[] };
}
function unitScale(value: number): number {
if (!Number.isFinite(value) || value <= 0 || value > STL_IMPORT_BUDGET.maxUnitScale) throw new Error("STL_UNIT_SCALE_INVALID");
return value;
}
function finite(values: number[], label: string): number[] {
if (values.some((value) => !Number.isFinite(value))) throw new Error(`STL_NUMBER_INVALID: ${label}`);
return values.map((value) => value === 0 ? 0 : value);
}
function degenerate(vertices: number[][]): boolean {
const left = vertices[1].map((value, index) => value - vertices[0][index]);
const right = vertices[2].map((value, index) => value - vertices[0][index]);
const cross = [left[1] * right[2] - left[2] * right[1], left[2] * right[0] - left[0] * right[2], left[0] * right[1] - left[1] * right[0]];
return cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2] <= 1e-20;
}
function finish(variant: STLVariant, scale: number, declaredTriangleCount: number, normals: number[][], rawVertices: number[][][]): STLImportResult {
const keptNormals: number[][] = [];
const vertices: number[][][] = [];
let removedDegenerateTriangles = 0;
for (let index = 0; index < rawVertices.length; index++) {
if (degenerate(rawVertices[index])) {
removedDegenerateTriangles++;
continue;
}
keptNormals.push(normals[index]);
vertices.push(rawVertices[index].map((vertex) => vertex.map((value) => value * scale)));
}
const flat = vertices.flat();
const bounds = flat.length > 0 ? {
min: [0, 1, 2].map((axis) => Math.min(...flat.map((vertex) => vertex[axis]))),
max: [0, 1, 2].map((axis) => Math.max(...flat.map((vertex) => vertex[axis]))),
} : { min: [0, 0, 0], max: [0, 0, 0] };
return {
schemaVersion: STL_IMPORT_SCHEMA_VERSION,
variant,
unitScale: scale,
declaredTriangleCount,
triangleCount: vertices.length,
removedDegenerateTriangles,
normals: keptNormals,
vertices,
bounds,
};
}
function parseBinary(bytes: ArrayBuffer, scale: number): STLImportResult {
if (bytes.byteLength < 84) throw new Error("STL_BINARY_TRUNCATED");
const view = new DataView(bytes);
const count = view.getUint32(80, true);
if (count > STL_IMPORT_BUDGET.maxTriangles) throw new Error("STL_IMPORT_BUDGET_EXCEEDED: triangles");
const expectedBytes = 84 + count * 50;
if (bytes.byteLength < expectedBytes) throw new Error("STL_BINARY_TRUNCATED");
if (bytes.byteLength > expectedBytes) throw new Error("STL_TRAILING_BYTES");
const normals: number[][] = [];
const vertices: number[][][] = [];
for (let triangle = 0; triangle < count; triangle++) {
const offset = 84 + triangle * 50;
normals.push(finite([view.getFloat32(offset, true), view.getFloat32(offset + 4, true), view.getFloat32(offset + 8, true)], `normal ${triangle}`));
const triangleVertices = [];
for (let vertex = 0; vertex < 3; vertex++) {
const vertexOffset = offset + 12 + vertex * 12;
triangleVertices.push(finite([view.getFloat32(vertexOffset, true), view.getFloat32(vertexOffset + 4, true), view.getFloat32(vertexOffset + 8, true)], `vertex ${triangle}/${vertex}`));
}
vertices.push(triangleVertices);
}
return finish("STL_BINARY", scale, count, normals, vertices);
}
function parseAscii(bytes: ArrayBuffer, scale: number): STLImportResult {
let source: string;
try { source = new TextDecoder("utf-8", { fatal: true }).decode(bytes); }
catch { throw new Error("STL_ASCII_INVALID"); }
const end = source.search(/^endsolid.*$/m);
if (!/^solid(?:\s|$)/.test(source) || end < 0) throw new Error("STL_ASCII_INVALID");
const endLine = source.indexOf("\n", end);
const trailing = source.slice(endLine < 0 ? source.length : endLine + 1);
if (trailing.trim()) throw new Error("STL_TRAILING_BYTES");
const facetPattern = /facet\s+normal\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+outer\s+loop\s+vertex\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+vertex\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+vertex\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+endloop\s+endfacet/g;
const normals: number[][] = [];
const vertices: number[][][] = [];
let match: RegExpExecArray | null;
while ((match = facetPattern.exec(source.slice(0, end)))) {
normals.push(finite(match.slice(1, 4).map(Number), `normal ${normals.length}`));
vertices.push([
finite(match.slice(4, 7).map(Number), `vertex ${vertices.length}/0`),
finite(match.slice(7, 10).map(Number), `vertex ${vertices.length}/1`),
finite(match.slice(10, 13).map(Number), `vertex ${vertices.length}/2`),
]);
if (vertices.length > STL_IMPORT_BUDGET.maxTriangles) throw new Error("STL_IMPORT_BUDGET_EXCEEDED: triangles");
}
if (vertices.length === 0) throw new Error("STL_ASCII_INVALID");
return finish("STL_ASCII", scale, vertices.length, normals, vertices);
}
export function importSTL(bytes: ArrayBuffer, options: { variant: STLVariant; unitScale: number }): STLImportResult {
if (bytes.byteLength > STL_IMPORT_BUDGET.maxBytes) throw new Error("STL_IMPORT_BUDGET_EXCEEDED: bytes");
const scale = unitScale(options.unitScale);
if (options.variant === "STL_BINARY") return parseBinary(bytes, scale);
if (options.variant === "STL_ASCII") return parseAscii(bytes, scale);
throw new Error("STL_VARIANT_REQUIRED");
}