fix: svg parsing

This commit is contained in:
hypercross 2026-03-16 09:29:32 +08:00
parent c99f602efa
commit cde2134b4b
2 changed files with 22 additions and 323 deletions

View File

@ -1,10 +1,13 @@
import { ImageTracer, type TraceData, type OutlinedArea, type SvgLineAttributes, Options } from "@image-tracer-ts/core";
//@ts-ignore
import {SVGLoader, SVGResult} from "three/examples/jsm/loaders/SVGLoader";
import {Color, ShapePath, Shape} from "three";
export interface TracedLayer {
id: string;
name: string;
color: string;
paths: PathData[];
color: Color;
paths: Shape[];
}
export interface PathData {
@ -74,239 +77,22 @@ export async function traceImage(
const svgString = tracer.traceImage(imageData);
// 解析 SVG 字符串
const paths = parseSVGString(svgString);
// 将路径按颜色分组为图层
const colorMap = new Map<string, PathData[]>();
for (const path of paths) {
if (!colorMap.has(path.color)) {
colorMap.set(path.color, []);
}
colorMap.get(path.color)!.push(path.path);
}
const layers: TracedLayer[] = [];
let layerIndex = 0;
for (const [color, pathDatas] of colorMap.entries()) {
layers.push({
id: `layer-${layerIndex}`,
name: `颜色层 ${layerIndex + 1}`,
color: color,
paths: pathDatas,
});
layerIndex++;
}
const loader = new SVGLoader();
const result = loader.parse(svgString) as SVGResult;
const paths: ShapePath[] = result.paths;
const layers: TracedLayer[] = paths.map((path, i,) => {
return {
id: `layer-${i}`,
name: `颜色层 ${i + 1}`,
color: path.color,
paths: SVGLoader.createShapes(path),
};
});
return {
width: canvas.width,
height: canvas.height,
layers,
};
}
/**
* SVG
*/
function parseSVGString(svgString: string): SvgPath[] {
const paths: SvgPath[] = [];
// 匹配所有 path 元素
const pathRegex = /<path[^>]*\/?>/g;
let match;
while ((match = pathRegex.exec(svgString)) !== null) {
const pathTag = match[0];
// 提取 d 属性(路径数据)
const dMatch = pathTag.match(/d="([^"]+)"/);
if (!dMatch) continue;
const d = dMatch[1];
// 提取 fill 颜色
const fillMatch = pathTag.match(/fill="([^"]+)"/);
let color = '#000000';
if (fillMatch) {
color = fillMatch[1];
// 处理 rgb() 格式
if (color.startsWith('rgb(')) {
const rgbValues = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
if (rgbValues) {
const r = parseInt(rgbValues[1]);
const g = parseInt(rgbValues[2]);
const b = parseInt(rgbValues[3]);
color = `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
}
}
// 解析路径数据
const pathDatas = parseSVGPathData(d);
// 为每个解析出的路径创建一个 SvgPath
for (const pathData of pathDatas) {
paths.push({ color, d, path: pathData });
}
}
return paths;
}
/**
* SVG
*/
function parseSVGPathData(d: string): PathData[] {
const paths: PathData[] = [];
// 分割路径命令
const commands = d.split(/(?=[MZLQCSHVTA])/g);
let currentPoints: Point[] = [];
let isClosed = false;
for (const cmd of commands) {
const type = cmd[0];
const values = cmd.slice(1)
.trim()
.split(/[\s,]+/)
.map(Number)
.filter(n => !isNaN(n));
if (type === 'M') {
// 如果有未完成的路径,保存它
if (currentPoints.length > 0) {
paths.push({ points: [...currentPoints], isClosed });
currentPoints = [];
isClosed = false;
}
// 移动到起点
for (let i = 0; i < values.length; i += 2) {
if (i + 1 < values.length) {
currentPoints.push({ x: values[i], y: values[i + 1] });
}
}
} else if (type === 'L') {
// 直线
for (let i = 0; i < values.length; i += 2) {
if (i + 1 < values.length) {
currentPoints.push({ x: values[i], y: values[i + 1] });
}
}
} else if (type === 'Q') {
// 二次贝塞尔曲线 - 简化处理
for (let i = 0; i < values.length; i += 4) {
if (i + 3 < values.length) {
const [cpX, cpY, endX, endY] = [values[i], values[i + 1], values[i + 2], values[i + 3]];
// 使用控制点和终点的中点作为近似
currentPoints.push({
x: (cpX + endX) / 2,
y: (cpY + endY) / 2,
});
}
}
} else if (type === 'C') {
// 三次贝塞尔曲线 - 简化处理
for (let i = 0; i < values.length; i += 6) {
if (i + 5 < values.length) {
const [cp1X, cp1Y, cp2X, cp2Y, endX, endY] = values.slice(i, i + 6);
// 使用两个控制点的中点作为近似
currentPoints.push({
x: (cp1X + cp2X) / 2,
y: (cp1Y + cp2Y) / 2,
});
currentPoints.push({ x: endX, y: endY });
}
}
} else if (type === 'Z' || type === 'z') {
isClosed = true;
}
}
// 保存最后一个路径
if (currentPoints.length > 0) {
paths.push({ points: currentPoints, isClosed });
}
return paths;
}
/**
* OutlinedArea
*/
function parseOutlinedArea(area: OutlinedArea): PathData | null {
const points: Point[] = [];
let isClosed = false;
if (!area.lineAttributes || area.lineAttributes.length === 0) {
return null;
}
// 收集所有唯一点
const pointMap = new Map<string, Point>();
const orderedPoints: Point[] = [];
for (const attr of area.lineAttributes) {
// 添加起点
const startKey = `${attr.x1},${attr.y1}`;
if (!pointMap.has(startKey)) {
const startPoint = { x: attr.x1, y: attr.y1 };
pointMap.set(startKey, startPoint);
orderedPoints.push(startPoint);
}
// 添加终点
const endKey = `${attr.x2},${attr.y2}`;
if (!pointMap.has(endKey)) {
const endPoint = { x: attr.x2, y: attr.y2 };
pointMap.set(endKey, endPoint);
orderedPoints.push(endPoint);
}
// 如果是 Q 类型,添加控制点相关的近似点
if (attr.type === 'Q' && 'x3' in attr) {
// 使用控制点和终点的中点作为近似
const midX = (attr.x1 + attr.x2 + (attr as any).x3) / 3;
const midY = (attr.y1 + attr.y2 + (attr as any).y3) / 3;
const midKey = `${midX},${midY}`;
if (!pointMap.has(midKey)) {
const midPoint = { x: midX, y: midY };
pointMap.set(midKey, midPoint);
orderedPoints.push(midPoint);
}
}
}
// 使用有序点
points.push(...orderedPoints);
// 检查是否闭合(起点和终点接近)
if (points.length >= 2) {
const first = points[0];
const last = points[points.length - 1];
const dist = Math.sqrt(
Math.pow(last.x - first.x, 2) + Math.pow(last.y - first.y, 2)
);
isClosed = dist < 5; // 距离小于 5 像素视为闭合
}
if (points.length === 0) {
return null;
}
return {
points,
isClosed,
};
}
function toHex(n: number): string {
const hex = Math.round(Math.min(255, Math.max(0, n))).toString(16);
return hex.length === 1 ? "0" + hex : hex;
}
/**
* RGB
*/
function rgbToHex(rgb: { r: number; g: number; b: number; a?: number }): string {
return `#${toHex(rgb.r)}${toHex(rgb.g)}${toHex(rgb.b)}`;
}
}

View File

@ -46,45 +46,19 @@ export async function generateSTL(
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;
// 创建挤压几何体
// 创建挤压几何体 - Three.js 支持直接传入 shape 数组
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);
}
// 直接将所有 shape 传给 ExtrudeGeometry它会自动处理多个 shape
const geometry = new THREE.ExtrudeGeometry(layer.paths, extrudeSettings);
// 创建网格并设置位置
const material = new THREE.MeshBasicMaterial({ color: 0x808080 });
const mesh = new THREE.Mesh(combinedGeometry, material);
const mesh = new THREE.Mesh(geometry, material);
// 设置图层高度(堆叠)
mesh.position.y = currentHeight;
@ -154,67 +128,6 @@ function createShapeFromPath(
return shape;
}
/**
*
*/
function mergeGeometries(
geometries: THREE.ExtrudeGeometry[]
): THREE.ExtrudeGeometry {
// 使用 Three.js 的 mergeGeometries 工具
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;
}
/**
* Three.js ASCII STL
*/