You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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;
}

核心缺陷分析

  1. 错误的缩放逻辑:svgArcToLWPolyline中计算的scaleX/scaleY强制将椭圆弧缩放至两点距离的一半,完全篡改了原始SVG的rx/ry参数,这是rx/ry未被正确识别的根本原因。
  2. 角度方向处理错误:svgArcToOvalArc中未根据sweepFlag调整角度范围,导致跨0度的弧或逆时针弧的角度增量计算错误,进而影响LWPOLYLINE的点序列和bulge值。
  3. 弧长计算不准确:calculateNumberOfPoints使用(rx+ry)/2近似椭圆半径,对于扁率大的椭圆误差较大,可能导致点数量不合理。
  4. 旋转应用逻辑错误:原代码未正确处理椭圆绕中心的旋转,导致带旋转参数的弧输出异常。

修复后的代码

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;
}

修复要点说明

  1. 移除错误缩放:删除scaleX/scaleY计算,直接使用原始rx/ry参数,确保SVG的椭圆参数被正确保留。
  2. 修正角度方向:根据sweepFlag调整startAngle和endAngle的范围,确保角度增量方向与弧的绘制方向(顺时针/逆时针)一致。
  3. Bulge值修正:遵循DXF规范,顺时针弧的bulge设为负,逆时针设为正,保证弧的方向正确。
  4. 正确应用旋转:椭圆的旋转是绕自身中心进行的,修复点坐标的旋转计算逻辑,解决带旋转参数的弧输出异常问题。
  5. 弧长计算优化:使用Ramanujan近似公式计算椭圆弧长,提高点数量的准确性,避免扁率大的椭圆出现点过少或过多的情况。
  6. 边界情况处理:当椭圆无法容纳起点和终点时,自动缩放rx/ry(符合SVG规范),避免radicand为负导致的计算错误。

内容的提问来源于stack exchange,提问作者Andrew Giles

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 14:42:01