import * as THREE from "three"; import type { TraceResult, PathData, Point } from "./image-tracer"; export interface ExtrusionSettings { size: number; // 模型整体尺寸 (mm) layers: Array<{ id: string; thickness: number; // 图层厚度 (mm) }>; } export interface LayerMesh { id: string; mesh: THREE.Mesh; thickness: number; } /** * 将矢量路径生成 STL 文件 * @param image - 原始图片 * @param traceResult - 矢量追踪结果 * @param settings - 挤压设置 * @returns STL Blob */ export async function generateSTL( image: HTMLImageElement, traceResult: TraceResult, settings: ExtrusionSettings ): Promise { // 创建 Three.js 场景 const scene = new THREE.Scene(); // 计算缩放比例,使模型适应指定尺寸 const maxDimension = Math.max(traceResult.width, traceResult.height); const scale = settings.size / maxDimension; // 中心偏移 const offsetX = -traceResult.width / 2; const offsetY = -traceResult.height / 2; // 为每个启用的图层创建网格 let currentHeight = 0; const meshes: LayerMesh[] = []; for (const layerSetting of settings.layers) { const layer = traceResult.layers.find((l) => l.id === layerSetting.id); if (!layer) continue; // 创建挤压几何体 - Three.js 支持直接传入 shape 数组 const extrudeSettings: THREE.ExtrudeGeometryOptions = { depth: layerSetting.thickness, curveSegments: 12, bevelEnabled: false, }; // 直接将所有 shape 传给 ExtrudeGeometry,它会自动处理多个 shape const geometry = new THREE.ExtrudeGeometry(layer.paths, extrudeSettings); // 创建网格并设置位置 const material = new THREE.MeshBasicMaterial({ color: 0x808080 }); const mesh = new THREE.Mesh(geometry, material); // 设置图层高度(堆叠) mesh.position.y = currentHeight; scene.add(mesh); meshes.push({ id: layerSetting.id, mesh, thickness: layerSetting.thickness, }); currentHeight += layerSetting.thickness; } if (meshes.length === 0) { throw new Error("没有可生成的图层"); } // 导出为 STL const stlString = exportToSTL(scene); const blob = new Blob([stlString], { type: "model/stl" }); // 清理 scene.clear(); meshes.forEach(({ mesh }) => { mesh.geometry.dispose(); (mesh.material as THREE.Material).dispose(); }); return blob; } /** * 从路径数据创建 Three.js 形状 */ function createShapeFromPath( path: PathData, scale: number, offsetX: number, offsetY: number ): THREE.Shape | null { if (path.points.length < 2) return null; const shape = new THREE.Shape(); // 移动到起点 const startPoint = path.points[0]; shape.moveTo( (startPoint.x + offsetX) * scale, (startPoint.y + offsetY) * scale ); // 绘制线段到后续点 for (let i = 1; i < path.points.length; i++) { const point = path.points[i]; shape.lineTo( (point.x + offsetX) * scale, (point.y + offsetY) * scale ); } // 如果是闭合路径,闭合形状 if (path.isClosed) { shape.closePath(); } return shape; } /** * 将 Three.js 场景导出为 ASCII STL 格式 */ function exportToSTL(scene: THREE.Scene): string { let output = "solid token\n"; scene.traverse((object) => { if (object instanceof THREE.Mesh && object.geometry) { const geometry = object.geometry as THREE.BufferGeometry; const positions = geometry.getAttribute("position") as THREE.BufferAttribute; const normals = geometry.getAttribute("normal") as THREE.BufferAttribute; for (let i = 0; i < positions.count; i += 3) { // 获取法线 let normalStr = ""; if (normals) { const nx = normals.getX(i); const ny = normals.getY(i); const nz = normals.getZ(i); normalStr = ` normal ${nx.toFixed(6)} ${ny.toFixed(6)} ${nz.toFixed(6)}`; } output += ` facet${normalStr}\n`; output += " outer loop\n"; // 获取三个顶点 for (let j = 0; j < 3; j++) { const x = positions.getX(i + j); const y = positions.getY(i + j); const z = positions.getZ(i + j); output += ` vertex ${x.toFixed(6)} ${y.toFixed(6)} ${z.toFixed(6)}\n`; } output += " endloop\n"; output += " endfacet\n"; } } }); output += "endsolid token\n"; return output; }