Checkpoint web parity through Chromium input tasks
This commit is contained in:
125
web/protocol/stl-import.ts
Normal file
125
web/protocol/stl-import.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
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");
|
||||
}
|
||||
Reference in New Issue
Block a user