Skip to content

Commit 72ad8cf

Browse files
authored
fix: correctly offset relative arcs that produce more than one bezier (#91)
* fix: correctly offset relative arcs that produce more than one bezier * chore: remove debug script * fix: update test case to properly use f=1,l=1
1 parent a2258f2 commit 72ad8cf

File tree

2 files changed

+91
-36
lines changed

2 files changed

+91
-36
lines changed

src/mathUtils.ts

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -224,38 +224,42 @@ export function a2c(arc: CommandA, x0: number, y0: number): CommandC[] {
224224
const partCount = Math.ceil(deltaPhi / 90);
225225

226226
const result: CommandC[] = new Array(partCount);
227-
let prevX = x0,
228-
prevY = y0;
227+
let prevX = x0;
228+
let prevY = y0;
229+
230+
const transform = (x: number, y: number): Point => {
231+
const [xTemp, yTemp] = rotate([x * arc.rX, y * arc.rY], xRotRad);
232+
return [arc.cX! + xTemp, arc.cY! + yTemp];
233+
};
234+
229235
for (let i = 0; i < partCount; i++) {
230236
const phiStart = lerp(arc.phi1!, arc.phi2!, i / partCount);
231237
const phiEnd = lerp(arc.phi1!, arc.phi2!, (i + 1) / partCount);
232238
const deltaPhi = phiEnd - phiStart;
233239
const f = (4 / 3) * Math.tan((deltaPhi * DEG) / 4);
234240
// x1/y1, x2/y2 and x/y coordinates on the unit circle for phiStart/phiEnd
235-
const [x1, y1] = [
236-
Math.cos(phiStart * DEG) - f * Math.sin(phiStart * DEG),
237-
Math.sin(phiStart * DEG) + f * Math.cos(phiStart * DEG),
238-
];
239-
const [x, y] = [Math.cos(phiEnd * DEG), Math.sin(phiEnd * DEG)];
240-
const [x2, y2] = [
241-
x + f * Math.sin(phiEnd * DEG),
242-
y - f * Math.cos(phiEnd * DEG),
243-
];
244-
245-
const command: Partial<CommandC> = {
241+
const x1 = Math.cos(phiStart * DEG) - f * Math.sin(phiStart * DEG);
242+
const y1 = Math.sin(phiStart * DEG) + f * Math.cos(phiStart * DEG);
243+
const x = Math.cos(phiEnd * DEG);
244+
const y = Math.sin(phiEnd * DEG);
245+
const x2 = x + f * y;
246+
const y2 = y - f * x;
247+
248+
const cp1 = transform(x1, y1);
249+
const cp2 = transform(x2, y2);
250+
const end = transform(x, y);
251+
252+
const command: CommandC = {
246253
relative: arc.relative,
247254
type: SVGPathData.CURVE_TO,
255+
x: end[0],
256+
y: end[1],
257+
x1: cp1[0],
258+
y1: cp1[1],
259+
x2: cp2[0],
260+
y2: cp2[1],
248261
};
249262

250-
const transform = (x: number, y: number) => {
251-
const [xTemp, yTemp] = rotate([x * arc.rX, y * arc.rY], xRotRad);
252-
return [arc.cX! + xTemp, arc.cY! + yTemp];
253-
};
254-
255-
[command.x1, command.y1] = transform(x1, y1);
256-
[command.x2, command.y2] = transform(x2, y2);
257-
[command.x, command.y] = transform(x, y);
258-
259263
if (arc.relative) {
260264
command.x1 -= prevX;
261265
command.y1 -= prevY;
@@ -264,9 +268,10 @@ export function a2c(arc: CommandA, x0: number, y0: number): CommandC[] {
264268
command.x -= prevX;
265269
command.y -= prevY;
266270
}
267-
[prevX, prevY] = [command.x, command.y];
271+
prevX = end[0];
272+
prevY = end[1];
268273

269-
result[i] = command as CommandC;
274+
result[i] = command;
270275
}
271276
return result;
272277
}

src/tests/arctocurve.test.ts

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,60 +13,83 @@ function testArcToCurve(input: string) {
1313

1414
describe('Converting elliptical arc commands to curves', () => {
1515
test('should work sweepFlag on 0 and largeArcFlag on 0', () => {
16+
// Absolute
1617
expect(
1718
testArcToCurve('M80 80 A 45 45, 0, 0, 0, 125 125 L 125 80 Z'),
1819
).toEqual(
1920
'M80 80C80 104.8528137423857 100.1471862576143 125 125 125L125 80z',
2021
);
22+
23+
// Relative
24+
expect(
25+
testArcToCurve('M80 80 a 45 45, 0, 0, 0, 125 125 L 125 80 Z'),
26+
).toEqual(
27+
'M80 80c-34.5177968644246 34.5177968644246 -34.5177968644246 90.4822031355754 0 125c34.5177968644246 34.5177968644246 90.4822031355754 34.5177968644246 125 0L125 80z',
28+
);
2129
});
2230

2331
test('should work sweepFlag on 1 and largeArcFlag on 0', () => {
32+
// Absolute
2433
expect(
2534
testArcToCurve('M230 80 A 45 45, 0, 1, 0, 275 125 L 275 80 Z'),
2635
).toEqual(
2736
'M230 80C205.1471862576143 80 185 100.1471862576143 185 125C185 149.8528137423857 205.1471862576143 170 230 170C254.8528137423857 170 275 149.8528137423857 275 125L275 80z',
2837
);
38+
39+
// Relative
40+
expect(testArcToCurve('M230 80 a 45 45 0 1 0 45 45')).toEqual(
41+
'M230 80c-24.8528137423857 0 -45 20.1471862576143 -45 45c0 24.8528137423857 20.1471862576143 45 45 45c24.8528137423857 0 45 -20.1471862576143 45 -45',
42+
);
2943
});
3044

3145
test('should work sweepFlag on 0 and largeArcFlag on 1', () => {
46+
// Absolute
3247
expect(
3348
testArcToCurve('M80 230 A 45 45, 0, 0, 1, 125 275 L 125 230 Z'),
3449
).toEqual(
3550
'M80 230C104.8528137423857 230 125 250.1471862576143 125 275L125 230z',
3651
);
52+
53+
// Relative
54+
expect(testArcToCurve('M80 230 a 45 45 0 0 1 45 45')).toEqual(
55+
'M80 230c24.8528137423857 0 45 20.1471862576143 45 45',
56+
);
3757
});
3858

3959
test('should work sweepFlag on 1 and largeArcFlag on 1', () => {
60+
// Absolute
4061
expect(
4162
testArcToCurve('M230 230 A 45 45, 0, 1, 1, 275 275 L 275 230 Z'),
4263
).toEqual(
4364
'M230 230C230 205.1471862576143 250.1471862576143 185 275 185C299.8528137423857 185 320 205.1471862576143 320 230C320 254.8528137423857 299.8528137423857 275 275 275L275 230z',
4465
);
45-
});
4666

47-
test('should work sweepFlag on 0 and largeArcFlag on 0 with relative arc', () => {
48-
expect(
49-
testArcToCurve('M80 80 a 45 45, 0, 0, 0, 125 125 L 125 80 Z'),
50-
).toEqual(
51-
'M80 80c-34.5177968644246 34.5177968644246 -34.5177968644246 90.4822031355754 0 125c34.5177968644246 34.5177968644246 90.4822031355754 34.5177968644246 125 0L125 80z',
67+
// Relative
68+
expect(testArcToCurve('M230 230 a 45 45 0 1 1 45 45')).toEqual(
69+
'M230 230c0 -24.8528137423857 20.1471862576143 -45 45 -45c24.8528137423857 0 45 20.1471862576143 45 45c0 24.8528137423857 -20.1471862576143 45 -45 45',
5270
);
5371
});
5472

5573
test('should handle zero radius arcs by converting to lines', () => {
56-
// Zero y-radius
74+
// Zero y-radius (absolute)
5775
expect(testArcToCurve('M0 0A80 0 0 0 0 125 125')).toEqual(
5876
'M0 0C41.6666666666667 41.6666666666667 83.3333333333333 83.3333333333333 125 125',
5977
);
6078

61-
// Zero x-radius
79+
// Zero x-radius (absolute)
6280
expect(testArcToCurve('M0 0A0 80 0 0 0 125 125')).toEqual(
6381
'M0 0C41.6666666666667 41.6666666666667 83.3333333333333 83.3333333333333 125 125',
6482
);
6583

66-
// Both x and y radius zero
84+
// Both x and y radius zero (absolute)
6785
expect(testArcToCurve('M0 0A0 0 0 0 0 125 125')).toEqual(
6886
'M0 0C41.6666666666667 41.6666666666667 83.3333333333333 83.3333333333333 125 125',
6987
);
88+
89+
// Both x and y radius zero (relative)
90+
expect(testArcToCurve('M0 0 a 0 80 0 0 0 125 125')).toEqual(
91+
'M0 0c41.6666666666667 41.6666666666667 83.3333333333333 83.3333333333333 125 125',
92+
);
7093
});
7194

7295
test('should convert to correct arc', () => {
@@ -76,20 +99,30 @@ describe('Converting elliptical arc commands to curves', () => {
7699
});
77100

78101
test('should correctly handle rotated arcs', () => {
79-
// 45 degree rotation
102+
// 45 degree rotation (absolute)
80103
expect(testArcToCurve('M 50 50 A 30 15 45 0 1 100 100')).toEqual(
81104
'M50 50C56.9035593728849 43.0964406271151 73.6928812542302 48.6928812542302 87.5 62.5C101.3071187457698 76.3071187457698 106.9035593728849 93.0964406271151 100 100',
82105
);
83106

84-
// 90 degree rotation
107+
// 90 degree rotation (absolute)
85108
expect(testArcToCurve('M 30 30 A 30 15 90 0 1 80 80')).toEqual(
86109
'M30 30C36.9035593728849 2.3857625084603 53.6928812542302 -8.8071187457698 67.5 5C81.3071187457698 18.8071187457698 86.9035593728849 52.3857625084603 80 80',
87110
);
88111

89-
// 180 degree rotation (equivalent to flipping x and y radii)
112+
// 180 degree rotation (absolute, equivalent to flipping x and y radii)
90113
expect(testArcToCurve('M 30 30 A 30 15 180 0 1 80 80')).toEqual(
91114
'M30 30C57.6142374125551 23.0964408852183 91.1928806985313 28.6928815616988 104.999999309466 42.5000001726335C118.8071179204008 56.3071187835682 107.6142370311837 73.096440503847 80 80',
92115
);
116+
117+
// 45 degree rotation (relative)
118+
expect(testArcToCurve('M 50 50 a 30 15 45 0 1 50 50')).toEqual(
119+
'M50 50c6.9035593728849 -6.9035593728849 23.6928812542302 -1.3071187457698 37.5 12.5c13.8071187457698 13.8071187457698 19.4035593728849 30.5964406271151 12.5 37.5',
120+
);
121+
122+
// 90 degree rotation (relative)
123+
expect(testArcToCurve('M 30 30 a 30 15 90 0 1 50 50')).toEqual(
124+
'M30 30c6.9035593728849 -27.6142374915397 23.6928812542302 -38.8071187457698 37.5 -25c13.8071187457698 13.8071187457698 19.4035593728849 47.3857625084603 12.5 75',
125+
);
93126
});
94127

95128
test('should handle different radius combinations', () => {
@@ -144,4 +177,21 @@ describe('Converting elliptical arc commands to curves', () => {
144177
'M0 0C3.3568621862997 3.3097211449502 6.6902788550498 6.6431378137003 10 10',
145178
);
146179
});
180+
181+
test('should split arcs to mutiple curves', () => {
182+
// Half circle (relative)
183+
expect(testArcToCurve('M4.6 20 a 1.556 1.556 0 1 0 -2.2 -2.2z')).toEqual(
184+
'M4.6 20c0.4073109040792 -0.3900354880021 0.5717037274325 -0.9699266164694 0.4296821520769 -1.5156918834261c-0.1420215753557 -0.5457652669567 -0.568225001694 -0.9719686932951 -1.1139902686508 -1.1139902686508c-0.5457652669567 -0.1420215753557 -1.125656395424 0.0223712479977 -1.5156918834261 0.4296821520769z',
185+
);
186+
187+
// Almost full circle (absolute)
188+
expect(testArcToCurve('M100 100 A50 50 0 1 1 101 100')).toEqual(
189+
'M100 100C72.484720600949 99.7248334473379 50.363040023335 77.2688082346382 50.5006250195323 49.7524969373664C50.6382100157295 22.2361856400946 72.9833447337881 0.0025000625031 100.5 0.0025000625031C128.0166552662119 0.0025000625031 150.3617899842705 22.2361856400946 150.4993749804677 49.7524969373664C150.636959976665 77.2688082346382 128.5152793990511 99.7248334473379 101 100',
190+
);
191+
192+
// Almost full circle (relative)
193+
expect(testArcToCurve('M100 100 a50 50 0 1 1 1 0')).toEqual(
194+
'M100 100c-27.515279399051 -0.2751665526621 -49.636959976665 -22.7311917653618 -49.4993749804677 -50.2475030626336c0.1375849961973 -27.5163112972718 22.4827197142558 -49.7499968748633 49.9993749804677 -49.7499968748633c27.5166552662119 0 49.8617899842705 22.2336855775915 49.9993749804677 49.7499968748633c0.1375849961973 27.5163112972718 -21.9840955814167 49.9723365099715 -49.4993749804677 50.2475030626336',
195+
);
196+
});
147197
});

0 commit comments

Comments
 (0)