ttrpg-tools/src/components/utils/3mf-generator.ts

119 lines
3.0 KiB
TypeScript
Raw Normal View History

2026-03-15 22:35:38 +08:00
import * as THREE from "three";
import { exportTo3MF } from "three-3mf-exporter";
2026-03-17 12:19:15 +08:00
import type { TraceResult } from "./image-tracer";
2026-03-17 12:49:49 +08:00
import {Vector3} from "three";
2026-03-15 22:35:38 +08:00
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;
2026-03-17 12:49:49 +08:00
let boundingBox = new THREE.Box3();
2026-03-15 22:35:38 +08:00
for (const layerSetting of settings.layers) {
const layer = traceResult.layers.find((l) => l.id === layerSetting.id);
if (!layer) continue;
// 创建挤压几何体
const extrudeSettings: THREE.ExtrudeGeometryOptions = {
depth: layerSetting.thickness,
curveSegments: 36,
2026-03-17 12:49:49 +08:00
bevelEnabled: true,
bevelSize: 0.4
2026-03-15 22:35:38 +08:00
};
2026-03-17 12:19:15 +08:00
const geometry: THREE.ExtrudeGeometry = new THREE.ExtrudeGeometry(layer.paths, extrudeSettings);
2026-03-17 12:49:49 +08:00
geometry.computeBoundingBox();
2026-03-15 22:35:38 +08:00
// 为该图层生成颜色
const color = generateLayerColor(layerIndex);
const material = new THREE.MeshStandardMaterial({
color,
metalness: 0.3,
roughness: 0.7,
});
2026-03-17 12:19:15 +08:00
const mesh = new THREE.Mesh(geometry, material);
2026-03-17 12:49:49 +08:00
boundingBox.expandByObject(mesh);
2026-03-15 22:35:38 +08:00
// 设置图层高度(堆叠)
mesh.position.y = currentHeight;
scene.add(mesh);
meshes.push({
id: layerSetting.id,
mesh,
thickness: layerSetting.thickness,
color,
});
currentHeight += layerSetting.thickness;
layerIndex++;
}
2026-03-17 12:49:49 +08:00
const center = boundingBox.getCenter(new Vector3());
for(const mesh of meshes){
mesh.mesh.position.sub(center);
}
2026-03-15 22:35:38 +08:00
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;
2026-03-17 12:19:15 +08:00
}