Skip to content

Commit 995b48e

Browse files
authored
Merge pull request #84 from armano2/fix/handle-zero-radi-arcs
fix(math): handle zero radius arcs by converting to "lines"
2 parents 20c4332 + b3a0358 commit 995b48e

File tree

2 files changed

+61
-23
lines changed

2 files changed

+61
-23
lines changed

src/mathUtils.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,16 @@ export function annotateArcCommand(c: CommandA, x1: number, y1: number) {
3838
let { rX, rY } = c;
3939
const { x, y } = c;
4040

41+
if (Math.abs(rX) < 1e-10 || Math.abs(rY) < 1e-10) {
42+
c.rX = 0;
43+
c.rY = 0;
44+
c.cX = (x1 + x) / 2;
45+
c.cY = (y1 + y) / 2;
46+
c.phi1 = 0;
47+
c.phi2 = 0;
48+
return;
49+
}
50+
4151
rX = Math.abs(c.rX);
4252
rY = Math.abs(c.rY);
4353
const [x1_, y1_] = rotate([(x1 - x) / 2, (y1 - y) / 2], (-c.xRot / 180) * PI);
@@ -130,6 +140,9 @@ export function arcAt(c: number, x1: number, x2: number, phiDeg: number) {
130140

131141
export function bezierRoot(x0: number, x1: number, x2: number, x3: number) {
132142
const EPS = 1e-6;
143+
// Coefficients for the derivative of a cubic Bezier curve
144+
// B'(t) = 3(1-t)²(P₁-P₀) + 6(1-t)t(P₂-P₁) + 3t²(P₃-P₂)
145+
// When rearranged to at² + bt + c:
133146
const x01 = x1 - x0;
134147
const x12 = x2 - x1;
135148
const x23 = x3 - x2;
@@ -139,8 +152,8 @@ export function bezierRoot(x0: number, x1: number, x2: number, x3: number) {
139152
// solve a * t² + b * t + c = 0
140153

141154
if (Math.abs(a) < EPS) {
142-
// equivalent to b * t + c =>
143-
return [-c / b];
155+
// For near-zero a, it becomes a linear equation: b * t + c = 0
156+
return Math.abs(b) < EPS ? [] : [-c / b];
144157
}
145158
return pqFormula(b / a, c / a, EPS);
146159
}
@@ -152,7 +165,10 @@ export function bezierAt(
152165
x3: number,
153166
t: number,
154167
) {
155-
// console.log(x0, y0, x1, y1, x2, y2, x3, y3, t)
168+
// Calculates a point on a cubic Bezier curve at parameter t.
169+
// B(t) = (1-t)³P₀ + 3(1-t)²tP₁ + 3(1-t)t²P₂ + t³P₃
170+
// Which is equivalent to:
171+
// B(t) = (s³)P₀ + (3s²t)P₁ + (3st²)P₂ + (t³)P₃ where s = 1-t
156172
const s = 1 - t;
157173
const c0 = s * s * s;
158174
const c1 = 3 * s * s * t;
@@ -181,6 +197,22 @@ export function a2c(arc: CommandA, x0: number, y0: number): CommandC[] {
181197
annotateArcCommand(arc, x0, y0);
182198
}
183199

200+
// Handle zero radius case - convert to a straight line represented as a curve
201+
if (Math.abs(arc.rX) < 1e-10 || Math.abs(arc.rY) < 1e-10) {
202+
return [
203+
{
204+
relative: arc.relative,
205+
type: SVGPathData.CURVE_TO,
206+
x1: x0 + (arc.x - x0) / 3,
207+
y1: y0 + (arc.y - y0) / 3,
208+
x2: x0 + (2 * (arc.x - x0)) / 3,
209+
y2: y0 + (2 * (arc.y - y0)) / 3,
210+
x: arc.x,
211+
y: arc.y,
212+
},
213+
];
214+
}
215+
184216
const phiMin = Math.min(arc.phi1!, arc.phi2!),
185217
phiMax = Math.max(arc.phi1!, arc.phi2!),
186218
deltaPhi = phiMax - phiMin;

src/tests/arctocurve.test.ts

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,59 +6,65 @@ import { SVGPathData } from '../index.js';
66
// Here we have to round output before testing since there is some lil
77
// differences across browsers.
88

9+
function testArcToCurve(input: string) {
10+
return new SVGPathData(input).aToC().round().encode();
11+
}
12+
913
describe('Converting elliptical arc commands to curves', () => {
1014
test('should work sweepFlag on 0 and largeArcFlag on 0', () => {
1115
expect(
12-
new SVGPathData('M80 80 A 45 45, 0, 0, 0, 125 125 L 125 80 Z')
13-
.aToC()
14-
.round()
15-
.encode(),
16+
testArcToCurve('M80 80 A 45 45, 0, 0, 0, 125 125 L 125 80 Z'),
1617
).toEqual(
1718
'M80 80C80 104.8528137423857 100.1471862576143 125 125 125L125 80z',
1819
);
1920
});
2021

2122
test('should work sweepFlag on 1 and largeArcFlag on 0', () => {
2223
expect(
23-
new SVGPathData('M230 80 A 45 45, 0, 1, 0, 275 125 L 275 80 Z')
24-
.aToC()
25-
.round()
26-
.encode(),
24+
testArcToCurve('M230 80 A 45 45, 0, 1, 0, 275 125 L 275 80 Z'),
2725
).toEqual(
2826
'M230 80C205.1471862576143 80 185 100.1471862576143 185 125C185 149.8528137423857 205.1471862576143 170 230 170C254.8528137423857 170 275 149.8528137423857 275 125L275 80z',
2927
);
3028
});
3129

3230
test('should work sweepFlag on 0 and largeArcFlag on 1', () => {
3331
expect(
34-
new SVGPathData('M80 230 A 45 45, 0, 0, 1, 125 275 L 125 230 Z')
35-
.aToC()
36-
.round()
37-
.encode(),
32+
testArcToCurve('M80 230 A 45 45, 0, 0, 1, 125 275 L 125 230 Z'),
3833
).toEqual(
3934
'M80 230C104.8528137423857 230 125 250.1471862576143 125 275L125 230z',
4035
);
4136
});
4237

4338
test('should work sweepFlag on 1 and largeArcFlag on 1', () => {
4439
expect(
45-
new SVGPathData('M230 230 A 45 45, 0, 1, 1, 275 275 L 275 230 Z')
46-
.aToC()
47-
.round()
48-
.encode(),
40+
testArcToCurve('M230 230 A 45 45, 0, 1, 1, 275 275 L 275 230 Z'),
4941
).toEqual(
5042
'M230 230C230 205.1471862576143 250.1471862576143 185 275 185C299.8528137423857 185 320 205.1471862576143 320 230C320 254.8528137423857 299.8528137423857 275 275 275L275 230z',
5143
);
5244
});
5345

5446
test('should work sweepFlag on 0 and largeArcFlag on 0 with relative arc', () => {
5547
expect(
56-
new SVGPathData('M80 80 a 45 45, 0, 0, 0, 125 125 L 125 80 Z')
57-
.aToC()
58-
.round()
59-
.encode(),
48+
testArcToCurve('M80 80 a 45 45, 0, 0, 0, 125 125 L 125 80 Z'),
6049
).toEqual(
6150
'M80 80c-34.5177968644246 34.5177968644246 -34.5177968644246 90.4822031355754 0 125c34.5177968644246 34.5177968644246 90.4822031355754 34.5177968644246 125 0L125 80z',
6251
);
6352
});
53+
54+
test('should handle zero radius arcs by converting to lines', () => {
55+
// Zero y-radius
56+
expect(testArcToCurve('M0 0A80 0 0 0 0 125 125')).toEqual(
57+
'M0 0C41.6666666666667 41.6666666666667 83.3333333333333 83.3333333333333 125 125',
58+
);
59+
60+
// Zero x-radius
61+
expect(testArcToCurve('M0 0A0 80 0 0 0 125 125')).toEqual(
62+
'M0 0C41.6666666666667 41.6666666666667 83.3333333333333 83.3333333333333 125 125',
63+
);
64+
65+
// Both x and y radius zero
66+
expect(testArcToCurve('M0 0A0 0 0 0 0 125 125')).toEqual(
67+
'M0 0C41.6666666666667 41.6666666666667 83.3333333333333 83.3333333333333 125 125',
68+
);
69+
});
6470
});

0 commit comments

Comments
 (0)