SVG路径圆弧转DXF自定义转换器逻辑故障求助:rx/ry与旋转轴异常
SVG转DXF转换器:椭圆弧转LWPOLYLINE逻辑缺陷排查
问题概述
我正在开发自定义SVG转DXF转换器,当前实现的椭圆弧转LWPOLYLINE逻辑存在缺陷:
- 使用路径
d="M10,10L20,25A1,3,0,0,0,50,50L90,90V80Z"时,SVG与DXF输出对比显示rx、ry未被正确识别; - 添加x旋转轴(路径改为
d="M10,10L20,25A1,3,45,0,0,50,50L90,90V80Z")后,DXF输出异常更严重; - 已花费12小时调试(含ChatGPT辅助),仍未解决问题。
当前实现代码
function svgArcToLWPolyline(rx, ry, rotation, largeArcFlag, sweepFlag, x1, y1, x2, y2) { const scaleX = Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)) / (2 * rx); const scaleY = Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)) / (2 * ry); const arcInfo = svgArcToOvalArc(rx * scaleX, ry * scaleY, rotation, largeArcFlag, sweepFlag, x1, y1, x2, y2); const numPoints = calculateNumberOfPoints(rx * scaleX, ry * scaleY, arcInfo.startAngle, arcInfo.endAngle); // Calculate bulge factors const bulgeFactors = []; const angleIncrement = (arcInfo.endAngle - arcInfo.startAngle) / (numPoints - 1); let currentAngle = arcInfo.startAngle; for (let i = 0; i < numPoints - 1; i++) { const nextAngle = currentAngle + angleIncrement; const bulge = Math.tan((nextAngle - currentAngle) * Math.PI / 360); bulgeFactors.push(bulge); currentAngle = nextAngle; } bulgeFactors.push(0); // Last point has zero bulge // Construct LWPOLYLINE points const lwpolylinePoints = []; for (let i = 0; i < numPoints; i++) { const angle = arcInfo.startAngle + i * angleIncrement; const x = arcInfo.cx + rx * scaleX * Math.cos(angle * Math.PI / 180); const y = arcInfo.cy + ry * scaleY * Math.sin(angle * Math.PI / 180); lwpolylinePoints.push([x, y, bulgeFactors[i]]); } return lwpolylinePoints; } function svgArcToOvalArc(rx, ry, rotation, largeArcFlag, sweepFlag, x1, y1, x2, y2) { // Convert rotation angle to radians const angle = rotation * Math.PI / 180; // Calculate intermediate values const dx = (x1 - x2) / 2; const dy = (y1 - y2) / 2; const x1p = Math.cos(angle) * dx + Math.sin(angle) * dy; const y1p = -Math.sin(angle) * dx + Math.cos(angle) * dy; const rxSq = rx * rx; const rySq = ry * ry; const x1pSq = x1p * x1p; const y1pSq = y1p * y1p; let radicand = (rxSq * rySq - rxSq * y1pSq - rySq * x1pSq) / (rxSq * y1pSq + rySq * x1pSq); // Ensure non-negative radicand if (radicand < 0) { radicand = 0; } // Calculate root let root = Math.sqrt(radicand); if (largeArcFlag === sweepFlag) { root = -root; } const cxp = root * rx * y1p / ry; const cyp = -root * ry * x1p / rx; // Calculate center const cx = Math.cos(angle) * cxp - Math.sin(angle) * cyp + (x1 + x2) / 2; const cy = Math.sin(angle) * cxp + Math.cos(angle) * cyp + (y1 + y2) / 2; // Calculate start and end angles let startAngle = Math.atan2((y1p - cyp) / ry, (x1p - cxp) / rx); let endAngle = Math.atan2((-y1p - cyp) / ry, (-x1p - cxp) / rx); // Convert angles to degrees startAngle *= 180 / Math.PI; endAngle *= 180 / Math.PI; // Adjust angles to be in the range [0, 360] if (startAngle < 0) { startAngle += 360; } if (endAngle < 0) { endAngle += 360; } return { cx: cx, cy: cy, startAngle: startAngle, endAngle: endAngle }; } function calculateNumberOfPoints(rx, ry, startAngle, endAngle) { // Calculate arc length let arcLength; if (startAngle <= endAngle) { arcLength = (endAngle - startAngle) * Math.PI / 180; } else { arcLength = (360 - startAngle + endAngle) * Math.PI / 180; } arcLength *= (rx + ry) / 2; // Choose a fixed length for each segment const segmentLength = 1.0; // Adjust as needed // Calculate the number of points const numPoints = Math.max(Math.floor(arcLength / segmentLength), 2); // Minimum of 2 points return numPoints; }
核心缺陷分析
- 错误的缩放逻辑:
svgArcToLWPolyline中计算的scaleX/scaleY强制将椭圆弧缩放至两点距离的一半,完全篡改了原始SVG的rx/ry参数,这是rx/ry未被正确识别的根本原因。 - 角度方向处理错误:
svgArcToOvalArc中未根据sweepFlag调整角度范围,导致跨0度的弧或逆时针弧的角度增量计算错误,进而影响LWPOLYLINE的点序列和bulge值。 - 弧长计算不准确:
calculateNumberOfPoints使用(rx+ry)/2近似椭圆半径,对于扁率大的椭圆误差较大,可能导致点数量不合理。 - 旋转应用逻辑错误:原代码未正确处理椭圆绕中心的旋转,导致带旋转参数的弧输出异常。
修复后的代码
function svgArcToLWPolyline(rx, ry, rotation, largeArcFlag, sweepFlag, x1, y1, x2, y2) { // 移除错误的缩放逻辑,直接使用原始rx/ry const arcInfo = svgArcToOvalArc(rx, ry, rotation, largeArcFlag, sweepFlag, x1, y1, x2, y2); const numPoints = calculateNumberOfPoints(rx, ry, arcInfo.startAngle, arcInfo.endAngle, sweepFlag); // 计算角度增量,处理跨0度和方向问题 let angleDiff = arcInfo.endAngle - arcInfo.startAngle; if (sweepFlag === 1 && angleDiff < 0) { angleDiff += 360; } else if (sweepFlag === 0 && angleDiff > 0) { angleDiff -= 360; } const angleIncrement = angleDiff / (numPoints - 1); // Calculate bulge factors const bulgeFactors = []; let currentAngle = arcInfo.startAngle; for (let i = 0; i < numPoints - 1; i++) { const nextAngle = (currentAngle + angleIncrement + 360) % 360; // 计算bulge,注意角度差的绝对值 const angleDelta = Math.abs(nextAngle - currentAngle); const bulge = Math.tan((angleDelta % 360) * Math.PI / 360); // DXF规范:顺时针弧bulge为负,逆时针为正 bulgeFactors.push(sweepFlag === 1 ? -bulge : bulge); currentAngle = nextAngle; } bulgeFactors.push(0); // Last point has zero bulge // Construct LWPOLYLINE points const lwpolylinePoints = []; const rotationRad = rotation * Math.PI / 180; const cosRot = Math.cos(rotationRad); const sinRot = Math.sin(rotationRad); for (let i = 0; i < numPoints; i++) { const angle = (arcInfo.startAngle + i * angleIncrement + 360) % 360; const angleRad = angle * Math.PI / 180; // 计算椭圆上的点(未旋转) const xOval = arcInfo.cx + rx * Math.cos(angleRad); const yOval = arcInfo.cy + ry * Math.sin(angleRad); // 绕椭圆中心应用旋转 const x = cosRot * (xOval - arcInfo.cx) - sinRot * (yOval - arcInfo.cy) + arcInfo.cx; const y = sinRot * (xOval - arcInfo.cx) + cosRot * (yOval - arcInfo.cy) + arcInfo.cy; lwpolylinePoints.push([x, y, bulgeFactors[i]]); } return lwpolylinePoints; } function svgArcToOvalArc(rx, ry, rotation, largeArcFlag, sweepFlag, x1, y1, x2, y2) { // Convert rotation angle to radians const angle = rotation * Math.PI / 180; const cosRot = Math.cos(angle); const sinRot = Math.sin(angle); // Calculate intermediate values const dx = (x1 - x2) / 2; const dy = (y1 - y2) / 2; // 将起点和终点转换到旋转后的坐标系 const x1p = cosRot * dx + sinRot * dy; const y1p = -sinRot * dx + cosRot * dy; const rxSq = rx * rx; const rySq = ry * ry; const x1pSq = x1p * x1p; const y1pSq = y1p * y1p; // 修正radicand计算,避免除以0 const denominator = rxSq * y1pSq + rySq * x1pSq; let radicand = (rxSq * rySq - rxSq * y1pSq - rySq * x1pSq) / (denominator || 1); // 当椭圆无法容纳两点时,按SVG规范缩放rx/ry if (radicand < 0) { const scale = Math.sqrt((rxSq * y1pSq + rySq * x1pSq) / (rxSq * rySq)); rx *= scale; ry *= scale; radicand = 0; } // Calculate root,修正符号判断符合SVG规范 let root = Math.sqrt(radicand); if (largeArcFlag !== sweepFlag) { root = -root; } const cxp = root * rx * y1p / ry; const cyp = -root * ry * x1p / rx; // Calculate center(转换回原始坐标系) const cx = cosRot * cxp - sinRot * cyp + (x1 + x2) / 2; const cy = sinRot * cxp + cosRot * cyp + (y1 + y2) / 2; // Calculate start and end angles(在旋转后的坐标系中) let startAngle = Math.atan2((y1p - cyp) / ry, (x1p - cxp) / rx); let endAngle = Math.atan2((-y1p - cyp) / ry, (-x1p - cxp) / rx); // Convert angles to degrees startAngle *= 180 / Math.PI; endAngle *= 180 / Math.PI; // 根据sweepFlag调整角度范围,确保方向正确 if (sweepFlag === 1 && endAngle <= startAngle) { endAngle += 360; } else if (sweepFlag === 0 && endAngle >= startAngle) { endAngle -= 360; } // 转换为[0, 360)范围 startAngle = (startAngle + 360) % 360; endAngle = (endAngle + 360) % 360; return { cx: cx, cy: cy, startAngle: startAngle, endAngle: endAngle }; } function calculateNumberOfPoints(rx, ry, startAngle, endAngle, sweepFlag) { // 计算椭圆弧长(使用Ramanujan近似公式,提高准确性) let angleDiff = Math.abs(endAngle - startAngle); if (sweepFlag === 1 && endAngle < startAngle) { angleDiff = 360 - angleDiff; } else if (sweepFlag === 0 && endAngle > startAngle) { angleDiff = 360 - angleDiff; } const angleRad = angleDiff * Math.PI / 180; // Ramanujan椭圆周长近似公式,取对应弧长比例 const h = Math.pow((rx - ry) / (rx + ry), 2); const fullPerimeter = Math.PI * (rx + ry) * (1 + (3 * h) / (10 + Math.sqrt(4 - 3 * h))); const arcLength = fullPerimeter * (angleRad / (2 * Math.PI)); // Choose a fixed length for each segment const segmentLength = 1.0; // Adjust as needed // Calculate the number of points,用ceil确保覆盖整个弧长 const numPoints = Math.max(Math.ceil(arcLength / segmentLength), 2); return numPoints; }
修复要点说明
- 移除错误缩放:删除
scaleX/scaleY计算,直接使用原始rx/ry参数,确保SVG的椭圆参数被正确保留。 - 修正角度方向:根据
sweepFlag调整startAngle和endAngle的范围,确保角度增量方向与弧的绘制方向(顺时针/逆时针)一致。 - Bulge值修正:遵循DXF规范,顺时针弧的bulge设为负,逆时针设为正,保证弧的方向正确。
- 正确应用旋转:椭圆的旋转是绕自身中心进行的,修复点坐标的旋转计算逻辑,解决带旋转参数的弧输出异常问题。
- 弧长计算优化:使用Ramanujan近似公式计算椭圆弧长,提高点数量的准确性,避免扁率大的椭圆出现点过少或过多的情况。
- 边界情况处理:当椭圆无法容纳起点和终点时,自动缩放rx/ry(符合SVG规范),避免radicand为负导致的计算错误。
内容的提问来源于stack exchange,提问作者Andrew Giles
相关产品推荐
相关产品推荐

