import * as THREE from "three"; import { exportTo3MF } from "three-3mf-exporter"; import type { TraceResult, PathData } 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; color: number; } /** * 从图层索引生成颜色(使用黄金角确保颜色分散) */ function generateLayerColor(index: number): number { const hue = (index * 137.508) % 360; // 黄金角 return new THREE.Color(`hsl(${hue}, 70%, 50%)`).getHex(); } /** * 将矢量路径生成 3MF 文件 * @param image - 原始图片 * @param traceResult - 矢量追踪结果 * @param settings - 挤压设置 * @returns 3MF Blob */ export async function generate3MF( 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[] = []; let layerIndex = 0; for (const layerSetting of settings.layers) { const layer = traceResult.layers.find((l) => l.id === layerSetting.id); if (!layer) continue; // 为该图层的所有路径创建形状 const shapes: THREE.Shape[] = []; for (const path of layer.paths) { if (path.points.length < 2) continue; const shape = createShapeFromPath(path, scale, offsetX, offsetY); if (shape) { shapes.push(shape); } } if (shapes.length === 0) continue; // 创建挤压几何体 const extrudeSettings: THREE.ExtrudeGeometryOptions = { depth: layerSetting.thickness, curveSegments: 36, bevelEnabled: false, }; // 如果有多个形状,创建多个几何体并合并 const geometries: THREE.ExtrudeGeometry[] = []; for (const shape of shapes) { const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings); geometries.push(geometry); } // 合并同一图层的几何体 let combinedGeometry; if (geometries.length === 1) { combinedGeometry = geometries[0]; } else { combinedGeometry = mergeGeometries(geometries); } // 为该图层生成颜色 const color = generateLayerColor(layerIndex); const material = new THREE.MeshStandardMaterial({ color, metalness: 0.3, roughness: 0.7, }); const mesh = new THREE.Mesh(combinedGeometry, material); // 设置图层高度(堆叠) mesh.position.y = currentHeight; scene.add(mesh); meshes.push({ id: layerSetting.id, mesh, thickness: layerSetting.thickness, color, }); currentHeight += layerSetting.thickness; layerIndex++; } if (meshes.length === 0) { throw new Error("没有可生成的图层"); } // 导出为 3MF const data = await exportTo3MF(scene); const blob = new Blob([data], { type: "model/3mf" }); // 清理 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; } /** * 合并多个几何体 */ function mergeGeometries( geometries: THREE.ExtrudeGeometry[] ): THREE.ExtrudeGeometry { const mergedGeometry = geometries[0].clone(); for (let i = 1; i < geometries.length; i++) { const geometry = geometries[i]; const positionAttribute = geometry.getAttribute("position"); const normalAttribute = geometry.getAttribute("normal"); const uvAttribute = geometry.getAttribute("uv"); if (positionAttribute) { const positions = mergedGeometry.getAttribute("position"); const newPositions = new Float32Array( positions.array.length + positionAttribute.array.length ); newPositions.set(positions.array); newPositions.set(positionAttribute.array, positions.array.length); mergedGeometry.setAttribute( "position", new THREE.BufferAttribute(newPositions, 3) ); } if (normalAttribute) { const normals = mergedGeometry.getAttribute("normal"); const newNormals = new Float32Array( normals.array.length + normalAttribute.array.length ); newNormals.set(normals.array); newNormals.set(normalAttribute.array, normals.array.length); mergedGeometry.setAttribute( "normal", new THREE.BufferAttribute(newNormals, 3) ); } if (uvAttribute) { const uvs = mergedGeometry.getAttribute("uv"); const newUvs = new Float32Array( uvs.array.length + uvAttribute.array.length ); newUvs.set(uvs.array); newUvs.set(uvAttribute.array, uvs.array.length); mergedGeometry.setAttribute( "uv", new THREE.BufferAttribute(newUvs, 2) ); } } mergedGeometry.computeVertexNormals(); return mergedGeometry; }