Skip to content

Commit 562152a

Browse files
authored
feat(convertPathData): use the sagitta of arcs to round and convert to lines when applicable (#1873)
1 parent 90406f7 commit 562152a

File tree

8 files changed

+77
-5
lines changed

8 files changed

+77
-5
lines changed

docs/03-plugins/convert-path-data.mdx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ svgo:
3030
transformPrecision:
3131
description: Number of decimal places to round to, using conventional rounding rules.
3232
default: 5
33+
smartArcRounding:
34+
description: Round the radius of circular arcs when the effective change is under the error. The effective change is determined using the <a href="https://wikipedia.org/wiki/Sagitta_(geometry)" target="_blank">sagitta</a> of the arc.
35+
default: true
3336
removeUseless:
3437
description: Remove redundant path commands that don't draw anything.
3538
default: true

plugins/convertPathData.js

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ let arcTolerance;
5050
* curveSmoothShorthands: boolean,
5151
* floatPrecision: number | false,
5252
* transformPrecision: number,
53+
* smartArcRounding: boolean,
5354
* removeUseless: boolean,
5455
* collapseRepeated: boolean,
5556
* utilizeAbsolute: boolean,
@@ -100,6 +101,7 @@ exports.fn = (root, params) => {
100101
curveSmoothShorthands = true,
101102
floatPrecision = 3,
102103
transformPrecision = 5,
104+
smartArcRounding = true,
103105
removeUseless = true,
104106
collapseRepeated = true,
105107
utilizeAbsolute = true,
@@ -122,6 +124,7 @@ exports.fn = (root, params) => {
122124
curveSmoothShorthands,
123125
floatPrecision,
124126
transformPrecision,
127+
smartArcRounding,
125128
removeUseless,
126129
collapseRepeated,
127130
utilizeAbsolute,
@@ -638,6 +641,24 @@ function filters(
638641
}
639642
}
640643

644+
// round arc radius more accurately
645+
// eg m 0 0 a 1234.567 1234.567 0 0 1 10 0 -> m 0 0 a 1235 1235 0 0 1 10 0
646+
const sagitta = command === 'a' ? calculateSagitta(data) : undefined;
647+
if (params.smartArcRounding && sagitta !== undefined && precision) {
648+
for (let precisionNew = precision; precisionNew >= 0; precisionNew--) {
649+
const radius = toFixed(data[0], precisionNew);
650+
const sagittaNew = /** @type {number} */ (
651+
calculateSagitta([radius, radius, ...data.slice(2)])
652+
);
653+
if (Math.abs(sagitta - sagittaNew) < error) {
654+
data[0] = radius;
655+
data[1] = radius;
656+
} else {
657+
break;
658+
}
659+
}
660+
}
661+
641662
// convert straight curves into lines segments
642663
if (params.straightCurves) {
643664
if (
@@ -660,7 +681,12 @@ function filters(
660681
) {
661682
command = 'l';
662683
data = data.slice(-2);
663-
} else if (command === 'a' && (data[0] === 0 || data[1] === 0)) {
684+
} else if (
685+
command === 'a' &&
686+
(data[0] === 0 ||
687+
data[1] === 0 ||
688+
(sagitta !== undefined && sagitta < error))
689+
) {
664690
command = 'l';
665691
data = data.slice(-2);
666692
}
@@ -1064,6 +1090,22 @@ function isCurveStraightLine(data) {
10641090
return true;
10651091
}
10661092

1093+
/**
1094+
* Calculates the sagitta of an arc if possible.
1095+
*
1096+
* @type {(data: number[]) => number | undefined}
1097+
* @see https://wikipedia.org/wiki/Sagitta_(geometry)#Formulas
1098+
*/
1099+
function calculateSagitta(data) {
1100+
if (data[3] === 1) return undefined;
1101+
1102+
const [rx, ry] = data;
1103+
if (Math.abs(rx - ry) > error) return undefined;
1104+
const chord = Math.sqrt(data[5] ** 2 + data[6] ** 2);
1105+
if (chord > rx * 2) return undefined;
1106+
return rx - Math.sqrt(rx ** 2 - 0.25 * chord ** 2);
1107+
}
1108+
10671109
/**
10681110
* Converts next curve from shorthand to full form using the current curve data.
10691111
*

plugins/plugins-types.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ type DefaultPlugins = {
4646
curveSmoothShorthands?: boolean;
4747
floatPrecision?: number | false;
4848
transformPrecision?: number;
49+
smartArcRounding?: boolean;
4950
removeUseless?: boolean;
5051
collapseRepeated?: boolean;
5152
utilizeAbsolute?: boolean;

test/plugins/convertPathData.14.svg

Lines changed: 2 additions & 2 deletions
Loading

test/plugins/convertPathData.30.svg

Lines changed: 1 addition & 1 deletion
Loading

test/plugins/convertPathData.31.svg

Lines changed: 1 addition & 1 deletion
Loading

test/plugins/convertPathData.32.svg

Lines changed: 13 additions & 0 deletions
Loading

test/plugins/convertPathData.33.svg

Lines changed: 13 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)