234 lines
5.9 KiB
TypeScript
234 lines
5.9 KiB
TypeScript
|
|
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<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[] = [];
|
|||
|
|
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;
|
|||
|
|
}
|