I am working on creating a custom converter from svg to dxf and need a math wizard to help me figure out where my logic is flawed.
Here is my current code:
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;
}
Here is what the svg looks like using
d="M10,10L20,25A1,3,0,0,0,50,50L90,90V80Z"
SVG:
As you can see, the ry and rx aren't being taken into account. And when I change my path to add a xRotationalAxis, my dxf breaks even more: d="M10,10L20,25A1,3,45,0,0,50,50L90,90V80Z"
:
svg with path M10,10L20,25A1,3,45,0,0,50,50L90,90V80Z
Dxf conversion of M10,10L20,25A1,3,45,0,0,50,50L90,90V80Z
I've spent 12 hours trying to tweek this and figure out mathmatically how to make it work (with a lot of help from ChatGPT) So any help I can get on this would be really nice!
I'm afraid there are multiple errors in the calculation helper.
Most importantly you need to calculate the actual rx
and ry
values via parametrisation.
In other words A 1,3, 45,0,0,50,50
doesn't contain the absolute values for rx/ry – rx=1 and ry=3 are relative values.
Here's an example based on cuixiping's answer "Calculate center of SVG arc"
// arc command values
let p0 = {
x: 20,
y: 25
}
let values = [1, 3, 45, 0, 0, 50, 50]
let vertices = 12;
let polylinePts = arcToPolyline(p0, values, vertices)
//render polyline
polyline.setAttribute('points', polylinePts.map(pt => {
return `${pt.x} ${pt.y}`
}))
function arcToPolyline(p0, values, vertices = 12) {
let [rxC, ryC, xAxisRotation, largeArc, sweep, x, y] = values;
// parametrize arc command to get actual rx,ry and center
let param = svgArcToCenterParam(p0.x, p0.y, rxC, ryC, xAxisRotation, largeArc, sweep, x, y);
let {
cx,
cy,
rx,
ry,
startAngle,
deltaAngle,
endAngle
} = param;
let splitAngle = deltaAngle / vertices;
let pts = []
for (let i = 0; i <= vertices; i++) {
let angle = startAngle - splitAngle * i;
let xAxisRotation_radian = xAxisRotation * Math.PI / 180;
let pt = getEllipsePointForAngle(cx, cy, rx, ry, xAxisRotation_radian, deltaAngle + angle)
pts.push(pt)
}
return pts;
}
/**
* based on @cuixiping;
* https://stackoverflow.com/questions/9017100/calculate-center-of-svg-arc/12329083#12329083
*/
function svgArcToCenterParam(p0x, p0y, rx, ry, angle, largeArc, sweep, px, py) {
const radian = (ux, uy, vx, vy) => {
let dot = ux * vx + uy * vy;
let mod = Math.sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy));
let rad = Math.acos(dot / mod);
if (ux * vy - uy * vx < 0) {
rad = -rad;
}
return rad;
};
// degree to radian – if rx equals ry the x-axis rotation has no effect
let phi = rx === ry ? 0 : (angle * Math.PI) / 180;
let cx, cy, startAngle, deltaAngle, endAngle;
let PI = Math.PI;
//let PIpx = PI * 2;
// invalid arguments
if (rx == 0 || ry == 0) {
throw Error("rx and ry can not be 0");
}
if (rx < 0) {
rx = -rx;
}
if (ry < 0) {
ry = -ry;
}
let s_phi = Math.sin(phi);
let c_phi = Math.cos(phi);
let hd_x = (p0x - px) / 2; // half diff of x
let hd_y = (p0y - py) / 2; // half diff of y
let hs_x = (p0x + px) / 2; // half sum of x
let hs_y = (p0y + py) / 2; // half sum of y
// F6.5.1
let p0x_ = c_phi * hd_x + s_phi * hd_y;
let p0y_ = c_phi * hd_y - s_phi * hd_x;
// F.6.6 Correction of out-of-range radii
// Step 3: Ensure radii are large enough
let lambda = (p0x_ * p0x_) / (rx * rx) + (p0y_ * p0y_) / (ry * ry);
if (lambda > 1) {
rx = rx * Math.sqrt(lambda);
ry = ry * Math.sqrt(lambda);
}
let rxry = rx * ry;
let rxp0y_ = rx * p0y_;
let ryp0x_ = ry * p0x_;
let sum_of_sq = rxp0y_ * rxp0y_ + ryp0x_ * ryp0x_; // sum of square
if (!sum_of_sq) {
console.log("start point can not be same as end point");
}
let coe = Math.sqrt(Math.abs((rxry * rxry - sum_of_sq) / sum_of_sq));
if (largeArc == sweep) {
coe = -coe;
}
// F6.5.2
let cx_ = (coe * rxp0y_) / ry;
let cy_ = (-coe * ryp0x_) / rx;
// F6.5.3
cx = c_phi * cx_ - s_phi * cy_ + hs_x;
cy = s_phi * cx_ + c_phi * cy_ + hs_y;
let xcr1 = (p0x_ - cx_) / rx;
let xcr2 = (p0x_ + cx_) / rx;
let ycr1 = (p0y_ - cy_) / ry;
let ycr2 = (p0y_ + cy_) / ry;
// F6.5.5
startAngle = radian(1, 0, xcr1, ycr1);
// F6.5.6
deltaAngle = radian(xcr1, ycr1, -xcr2, -ycr2);
if (deltaAngle > PI * 2) {
deltaAngle -= PI * 2;
} else if (deltaAngle < 0) {
deltaAngle += PI * 2;
}
if (sweep == false || sweep == 0) {
deltaAngle -= PI * 2;
}
endAngle = startAngle + deltaAngle;
if (endAngle > PI * 2) {
endAngle -= PI * 2;
} else if (endAngle < 0) {
endAngle += PI * 2;
}
let outputObj = {
cx: cx,
cy: cy,
rx: rx,
ry: ry,
startAngle: startAngle,
deltaAngle: deltaAngle,
endAngle: endAngle,
sweep: sweep
};
return outputObj;
}
/*
* based on
* https://source.chromium.org/chromium/chromium/src/+/main:third_party/skia/modules/canvaskit/htmlcanvas/path2d.js
* and https://observablehq.com/@toja/ellipse-and-elliptical-arc-conversion
*/
function getEllipsePointForAngle(cx, cy, rx, ry, rotation_angle, angle) {
let M = Math.abs(rx) * Math.cos(angle),
N = Math.abs(ry) * Math.sin(angle);
return {
x: cx + Math.cos(rotation_angle) * M - Math.sin(rotation_angle) * N,
y: cy + Math.sin(rotation_angle) * M + Math.cos(rotation_angle) * N
};
}
svg {
overflow: visible;
border: 1px solid #ccc;
}
<svg id="svg" viewBox="-9 0 100 100">
<path id="path" d="M 10 10
L 20 25
A 1 3 45 0 0 50 50
L 90 90
V 80
Z" fill="none" stroke="#000" />
<polyline id="polyline" fill="none" stroke="red"></polyline>
</svg>
svgArcToCenterParam(p0x, p0y, rx, ry, angle, largeArc, sweep, px, py)
returns the parametrized ellipse parametersgetEllipsePointForAngle(cx, cy, rx, ry, rotation_angle, angle)
For a more convenient conversion you may just use maker.js as it includes an dxf export method out-of-the-box
var m = require("makerjs");
var pathData = `M10,10L20,25 A1,3,0,0,0,50,50L90,90V80Z`;
var path = m.importer.fromSVGPathData(pathData)
// convert to maker.js model object
var model = {models: {path}}
//export
var dxf = m.exporter.toDXF(model)
outputDxf.value=dxf;
textarea{
display:block;
width:100%;
min-height:50vmin
}
<script src="https://cdn.jsdelivr.net/npm/makerjs@0/target/js/browser.maker.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bezier-js@2/bezier.js"></script>
<h3>DXF</h3>
<textarea id="outputDxf"></textarea>
You may also take inspiration from my path-to-polygon helper (or simple example). Its based on a custom path data parser and calculates lengths for each path segment. This way we can calculate polygon vertices respecting the command on-path-points.