diff --git a/js/exporter.js b/js/exporter.js index 6e881b7..403aa47 100644 --- a/js/exporter.js +++ b/js/exporter.js @@ -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] */ export function exportSTL(geometry, filename = 'textured.stl') { - // Geometry is already in the original Z-up orientation (the loader never rotates it; - // the viewer uses a Z-up camera instead). Export as-is so slicers receive the correct pose. - const mesh = new THREE.Mesh(geometry, new THREE.MeshBasicMaterial()); - const result = exporter.parse(mesh, { binary: true }); + const posArr = geometry.attributes.position.array; + const norArr = geometry.attributes.normal + ? geometry.attributes.normal.array + : null; + const triCount = (posArr.length / 9) | 0; - // result is an ArrayBuffer in binary mode - const blob = new Blob([result], { type: 'application/octet-stream' }); + // Binary STL: 80-byte header + 4-byte tri count + 50 bytes per triangle + 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 a = document.createElement('a'); + const a = document.createElement('a'); a.href = url; a.download = filename; a.style.display = 'none'; document.body.appendChild(a); a.click(); document.body.removeChild(a); - - // Revoke after a short delay so the download has time to start setTimeout(() => URL.revokeObjectURL(url), 10000); }