2026-03-15 18:45:20 +08:00
|
|
|
|
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<Blob> {
|
|
|
|
|
|
// 创建 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;
|
|
|
|
|
|
|
2026-03-16 09:29:32 +08:00
|
|
|
|
// 创建挤压几何体 - Three.js 支持直接传入 shape 数组
|
2026-03-15 18:45:20 +08:00
|
|
|
|
const extrudeSettings: THREE.ExtrudeGeometryOptions = {
|
|
|
|
|
|
depth: layerSetting.thickness,
|
2026-03-16 09:34:15 +08:00
|
|
|
|
curveSegments: 12,
|
2026-03-15 18:45:20 +08:00
|
|
|
|
bevelEnabled: false,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-16 09:29:32 +08:00
|
|
|
|
// 直接将所有 shape 传给 ExtrudeGeometry,它会自动处理多个 shape
|
|
|
|
|
|
const geometry = new THREE.ExtrudeGeometry(layer.paths, extrudeSettings);
|
2026-03-15 18:45:20 +08:00
|
|
|
|
|
|
|
|
|
|
// 创建网格并设置位置
|
|
|
|
|
|
const material = new THREE.MeshBasicMaterial({ color: 0x808080 });
|
2026-03-16 09:29:32 +08:00
|
|
|
|
const mesh = new THREE.Mesh(geometry, material);
|
2026-03-15 18:45:20 +08:00
|
|
|
|
|
|
|
|
|
|
// 设置图层高度(堆叠)
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|