From cde2134b4b4f092b3092ae3db24561db489736d9 Mon Sep 17 00:00:00 2001 From: hypercross Date: Mon, 16 Mar 2026 09:29:32 +0800 Subject: [PATCH] fix: svg parsing --- src/components/utils/image-tracer.ts | 250 ++------------------------ src/components/utils/stl-generator.ts | 95 +--------- 2 files changed, 22 insertions(+), 323 deletions(-) diff --git a/src/components/utils/image-tracer.ts b/src/components/utils/image-tracer.ts index 9fcae30..d0e718b 100644 --- a/src/components/utils/image-tracer.ts +++ b/src/components/utils/image-tracer.ts @@ -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(); - 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 = /]*\/?>/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(); - 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)}`; -} +} \ No newline at end of file diff --git a/src/components/utils/stl-generator.ts b/src/components/utils/stl-generator.ts index bafb599..e667b88 100644 --- a/src/components/utils/stl-generator.ts +++ b/src/components/utils/stl-generator.ts @@ -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 格式 */