perf: replace Three.js STLExporter with direct binary writer

Write binary STL directly from BufferGeometry typed arrays using
Uint8Array.set() bulk copies. Eliminates the Three.js STLExporter
overhead: no Mesh/Material creation, no identity matrix multiplication,
no redundant normal recomputation, no per-float DataView calls.
This commit is contained in:
Avatarsia
2026-04-06 02:38:58 +02:00
parent 9b2fb68c47
commit 3c9bcfd75c
+56 -17
View File
@@ -1,32 +1,71 @@
import * as THREE from 'three';
import { STLExporter } from 'three/addons/exporters/STLExporter.js';
const exporter = new STLExporter();
/** /**
* Export a BufferGeometry as a binary STL file download. * Fast binary STL exporter — writes directly from BufferGeometry arrays.
* *
* @param {THREE.BufferGeometry} geometry * Eliminates Three.js STLExporter overhead:
* - No Mesh/Material creation
* - No identity matrix multiplication per vertex
* - No redundant normal recomputation
* - Bulk Uint8Array.set() instead of per-float DataView calls
*
* @param {THREE.BufferGeometry} geometry non-indexed with position + normal
* @param {string} [filename] * @param {string} [filename]
*/ */
export function exportSTL(geometry, filename = 'textured.stl') { export function exportSTL(geometry, filename = 'textured.stl') {
// Geometry is already in the original Z-up orientation (the loader never rotates it; const posArr = geometry.attributes.position.array;
// the viewer uses a Z-up camera instead). Export as-is so slicers receive the correct pose. const norArr = geometry.attributes.normal
const mesh = new THREE.Mesh(geometry, new THREE.MeshBasicMaterial()); ? geometry.attributes.normal.array
const result = exporter.parse(mesh, { binary: true }); : null;
const triCount = (posArr.length / 9) | 0;
// result is an ArrayBuffer in binary mode // Binary STL: 80-byte header + 4-byte tri count + 50 bytes per triangle
const blob = new Blob([result], { type: 'application/octet-stream' }); const bufLen = 84 + 50 * triCount;
const buffer = new ArrayBuffer(bufLen);
const bytes = new Uint8Array(buffer);
const view = new DataView(buffer);
// Header: 80 bytes (already zero-filled)
view.setUint32(80, triCount, true);
// Reinterpret source arrays as raw bytes for bulk copy
const posSrc = new Uint8Array(posArr.buffer, posArr.byteOffset, posArr.byteLength);
const norSrc = norArr
? new Uint8Array(norArr.buffer, norArr.byteOffset, norArr.byteLength)
: null;
for (let i = 0; i < triCount; i++) {
const dst = 84 + i * 50;
const srcOff = i * 36; // 9 floats * 4 bytes
if (norSrc) {
// Normal: copy first vertex normal (12 bytes) — flat shading, all 3 identical
bytes.set(norSrc.subarray(srcOff, srcOff + 12), dst);
} else {
// Compute face normal from cross product
const b = i * 9;
const ux = posArr[b+3]-posArr[b], uy = posArr[b+4]-posArr[b+1], uz = posArr[b+5]-posArr[b+2];
const vx = posArr[b+6]-posArr[b], vy = posArr[b+7]-posArr[b+1], vz = posArr[b+8]-posArr[b+2];
const nx = uy*vz-uz*vy, ny = uz*vx-ux*vz, nz = ux*vy-uy*vx;
const len = Math.sqrt(nx*nx + ny*ny + nz*nz) || 1;
view.setFloat32(dst, nx/len, true);
view.setFloat32(dst + 4, ny/len, true);
view.setFloat32(dst + 8, nz/len, true);
}
// Vertices: 36 bytes (3 vertices * 3 floats * 4 bytes)
bytes.set(posSrc.subarray(srcOff, srcOff + 36), dst + 12);
// Attribute byte count: 0 (already zero-filled)
}
// Download
const blob = new Blob([buffer], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement('a');
const a = document.createElement('a');
a.href = url; a.href = url;
a.download = filename; a.download = filename;
a.style.display = 'none'; a.style.display = 'none';
document.body.appendChild(a); document.body.appendChild(a);
a.click(); a.click();
document.body.removeChild(a); document.body.removeChild(a);
// Revoke after a short delay so the download has time to start
setTimeout(() => URL.revokeObjectURL(url), 10000); setTimeout(() => URL.revokeObjectURL(url), 10000);
} }