svg-path-simplify 0.0.9 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +114 -46
- package/debug.cjs +11 -0
- package/dist/svg-path-simplify.esm.js +613 -98
- package/dist/svg-path-simplify.esm.min.js +1 -1
- package/dist/svg-path-simplify.js +613 -98
- package/dist/svg-path-simplify.min.js +1 -1
- package/dist/svg-path-simplify.poly.cjs +29 -0
- package/index.html +5 -1
- package/package.json +36 -16
- package/src/index-node.js +15 -0
- package/src/index-poly.js +27 -0
- package/src/index.js +1 -6
- package/src/pathSimplify-main.js +47 -14
- package/src/svgii/geometry.js +54 -4
- package/src/svgii/geometry_deduceRadius.js +50 -0
- package/src/svgii/pathData_convert.js +339 -5
- package/src/svgii/pathData_refine_round.js +222 -0
- package/src/svgii/pathData_remove_collinear.js +8 -3
- package/src/svgii/pathData_remove_short.js +66 -0
- package/src/svgii/pathData_reorder.js +27 -11
- package/src/svgii/pathData_simplify_refineCorners.js +57 -37
- package/src/svgii/svg_cleanup.js +28 -29
- package/src/svgii/visualize.js +2 -2
- package/testSVG.js +27 -20
- package/dist/svg-path-simplify.node.js +0 -5129
- package/dist/svg-path-simplify.node.min.js +0 -1
- package/src/dom-polyfill.js +0 -29
- package/src/dom-polyfill_back.js +0 -22
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { checkLineIntersection, getAngle, getDistAv, getDistance, getPointOnEllipse, getSquareDistance, pointAtT, rotatePoint } from "./geometry";
|
|
2
|
+
import { getPolygonArea } from "./geometry_area";
|
|
3
|
+
import { getArcFromPoly } from "./geometry_deduceRadius";
|
|
4
|
+
import { arcToBezierResolved, revertCubicQuadratic } from "./pathData_convert";
|
|
5
|
+
import { pathDataToD } from "./pathData_stringify";
|
|
6
|
+
import { renderPath, renderPoint, renderPoly } from "./visualize";
|
|
7
|
+
|
|
8
|
+
export function refineRoundSegments(pathData, {
|
|
9
|
+
threshold = 0,
|
|
10
|
+
tolerance = 1,
|
|
11
|
+
// take arcs or cubic beziers
|
|
12
|
+
toCubic = false,
|
|
13
|
+
debug = false
|
|
14
|
+
} = {}) {
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
// min size threshold for corners
|
|
18
|
+
threshold *= tolerance;
|
|
19
|
+
|
|
20
|
+
let l = pathData.length;
|
|
21
|
+
|
|
22
|
+
// add fist command
|
|
23
|
+
let pathDataN = [pathData[0]]
|
|
24
|
+
|
|
25
|
+
// just for debugging
|
|
26
|
+
let pathDataTest = []
|
|
27
|
+
|
|
28
|
+
for (let i = 1; i < l; i++) {
|
|
29
|
+
let com = pathData[i];
|
|
30
|
+
let { type } = com;
|
|
31
|
+
let comP = pathData[i - 1];
|
|
32
|
+
let comN = pathData[i + 1] ? pathData[i + 1] : null;
|
|
33
|
+
let comN2 = pathData[i + 2] ? pathData[i + 2] : null;
|
|
34
|
+
let comN3 = pathData[i + 3] ? pathData[i + 3] : null;
|
|
35
|
+
let comBez = null;
|
|
36
|
+
|
|
37
|
+
if ((com.type === 'C' || com.type === 'Q')) comBez = com;
|
|
38
|
+
else if (comN && (comN.type === 'C' || comN.type === 'Q')) comBez = comN;
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
let cpts = comBez ? (comBez.type === 'C' ? [comBez.p0, comBez.cp1, comBez.cp2, comBez.p] : [comBez.p0, comBez.cp1, comBez.p]) : []
|
|
42
|
+
|
|
43
|
+
let areaBez = 0;
|
|
44
|
+
let areaLines = 0;
|
|
45
|
+
let signChange = false;
|
|
46
|
+
let L1, L2;
|
|
47
|
+
let combine = false
|
|
48
|
+
|
|
49
|
+
let p0_S, p_S;
|
|
50
|
+
let poly = []
|
|
51
|
+
let pMid;
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
// 2. line-line-bezier-line-line
|
|
55
|
+
if (
|
|
56
|
+
comP.type === 'L' &&
|
|
57
|
+
type === 'L' &&
|
|
58
|
+
comBez &&
|
|
59
|
+
comN2.type === 'L' &&
|
|
60
|
+
comN3 && (comN3.type === 'L' || comN3.type === 'Z')
|
|
61
|
+
) {
|
|
62
|
+
|
|
63
|
+
L1 = [com.p0, com.p];
|
|
64
|
+
L2 = [comN2.p0, comN2.p];
|
|
65
|
+
p0_S = com.p0
|
|
66
|
+
p_S = comN2.p
|
|
67
|
+
|
|
68
|
+
// don't allow sign changes
|
|
69
|
+
areaBez = getPolygonArea(cpts, false)
|
|
70
|
+
areaLines = getPolygonArea([...L1, ...L2], false)
|
|
71
|
+
signChange = (areaBez < 0 && areaLines > 0) || (areaBez > 0 && areaLines < 0)
|
|
72
|
+
|
|
73
|
+
if (!signChange) {
|
|
74
|
+
|
|
75
|
+
// mid point of mid bezier
|
|
76
|
+
pMid = pointAtT(cpts, 0.5)
|
|
77
|
+
|
|
78
|
+
// add to poly
|
|
79
|
+
poly = [p0_S, pMid, p_S]
|
|
80
|
+
|
|
81
|
+
combine = true
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 1. line-bezier-bezier-line
|
|
87
|
+
else if ((type === 'C' || type === 'Q') && comP.type === 'L') {
|
|
88
|
+
|
|
89
|
+
// 1.2 next is cubic next is lineto
|
|
90
|
+
if ((comN.type === 'C' || comN.type === 'Q') && comN2.type === 'L') {
|
|
91
|
+
|
|
92
|
+
combine = true
|
|
93
|
+
|
|
94
|
+
L1 = [comP.p0, comP.p];
|
|
95
|
+
L2 = [comN2.p0, comN2.p];
|
|
96
|
+
p0_S = comP.p
|
|
97
|
+
p_S = comN2.p0
|
|
98
|
+
|
|
99
|
+
// mid point of mid bezier
|
|
100
|
+
pMid = comBez.p
|
|
101
|
+
|
|
102
|
+
// add to poly
|
|
103
|
+
poly = [p0_S, comBez.p, p_S]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* calculate either combined
|
|
112
|
+
* cubic or arc commands
|
|
113
|
+
*/
|
|
114
|
+
if (combine) {
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
// try to find center of arc
|
|
118
|
+
let arcProps = getArcFromPoly(poly)
|
|
119
|
+
if (arcProps) {
|
|
120
|
+
|
|
121
|
+
let { centroid, r, deltaAngle, startAngle, endAngle } = arcProps;
|
|
122
|
+
|
|
123
|
+
let xAxisRotation = 0;
|
|
124
|
+
let sweep = deltaAngle > 0 ? 1 : 0;
|
|
125
|
+
let largeArc = Math.abs(deltaAngle) > Math.PI ? 1 : 0;
|
|
126
|
+
|
|
127
|
+
let pCM = rotatePoint(p0_S, centroid.x, centroid.y, deltaAngle * 0.5)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
let dist2 = getDistAv(pCM, pMid)
|
|
131
|
+
let thresh = getDistAv(p0_S, p_S) * 0.05
|
|
132
|
+
let bezierCommands;
|
|
133
|
+
|
|
134
|
+
// point is close enough
|
|
135
|
+
if (dist2 < thresh) {
|
|
136
|
+
|
|
137
|
+
//toCubic = false;
|
|
138
|
+
|
|
139
|
+
bezierCommands = arcToBezierResolved(
|
|
140
|
+
{
|
|
141
|
+
p0: p0_S,
|
|
142
|
+
p: p_S,
|
|
143
|
+
centroid,
|
|
144
|
+
rx: r,
|
|
145
|
+
ry: r,
|
|
146
|
+
xAxisRotation,
|
|
147
|
+
sweep,
|
|
148
|
+
largeArc,
|
|
149
|
+
deltaAngle,
|
|
150
|
+
startAngle,
|
|
151
|
+
endAngle
|
|
152
|
+
}
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
if(bezierCommands.length === 1){
|
|
156
|
+
|
|
157
|
+
// prefer more compact quadratic - otherwise arcs
|
|
158
|
+
let comBezier = revertCubicQuadratic(p0_S, bezierCommands[0].cp1, bezierCommands[0].cp2, p_S)
|
|
159
|
+
|
|
160
|
+
if (comBezier.type === 'Q') {
|
|
161
|
+
toCubic = true
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
com = comBezier
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
// prefer arcs if 2 cubics are required
|
|
169
|
+
if (bezierCommands.length > 1) toCubic = false;
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
//toCubic = false
|
|
173
|
+
|
|
174
|
+
// return elliptic arc commands
|
|
175
|
+
if (!toCubic) {
|
|
176
|
+
// rewrite simplified command
|
|
177
|
+
com.type = 'A'
|
|
178
|
+
com.values = [r, r, xAxisRotation, largeArc, sweep, p_S.x, p_S.y];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
com.p0 = p0_S;
|
|
182
|
+
com.p = p_S;
|
|
183
|
+
com.extreme = false;
|
|
184
|
+
com.corner = false;
|
|
185
|
+
|
|
186
|
+
// test rendering
|
|
187
|
+
//debug=true
|
|
188
|
+
|
|
189
|
+
if (debug) {
|
|
190
|
+
// arcs
|
|
191
|
+
if (!toCubic) {
|
|
192
|
+
pathDataTest = [
|
|
193
|
+
{ type: 'M', values: [p0_S.x, p0_S.y] },
|
|
194
|
+
{ type: 'A', values: [r, r, xAxisRotation, largeArc, sweep, p_S.x, p_S.y] },
|
|
195
|
+
]
|
|
196
|
+
}
|
|
197
|
+
// cubics
|
|
198
|
+
else {
|
|
199
|
+
pathDataTest = [
|
|
200
|
+
{ type: 'M', values: [p0_S.x, p0_S.y] },
|
|
201
|
+
...bezierCommands
|
|
202
|
+
]
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let d = pathDataToD(pathDataTest);
|
|
206
|
+
renderPath(markers, d, 'orange', '0.5%', '0.5')
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
pathDataN.push(com);
|
|
210
|
+
i++
|
|
211
|
+
continue
|
|
212
|
+
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// pass through
|
|
218
|
+
pathDataN.push(com)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return pathDataN;
|
|
222
|
+
}
|
|
@@ -24,11 +24,14 @@ export function pathDataRemoveColinear(pathData, {
|
|
|
24
24
|
//let p1 = comN.type.toLowerCase() === 'z' ? M : { x: comN.values[comN.values.length - 2], y: comN.values[comN.values.length - 1] }
|
|
25
25
|
let p1 = comN.type.toLowerCase() === 'z' ? M : { x: comN.values[comN.values.length - 2], y: comN.values[comN.values.length - 1] }
|
|
26
26
|
|
|
27
|
+
|
|
27
28
|
let { type, values } = com;
|
|
28
29
|
let valsL = values.slice(-2)
|
|
29
30
|
p = type !== 'Z' ? { x: valsL[0], y: valsL[1] } : M;
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
|
|
33
|
+
let area = p1 ? getPolygonArea([p0, p, p1], true) : Infinity
|
|
34
|
+
|
|
32
35
|
|
|
33
36
|
//let distSquare0 = getSquareDistance(p0, p)
|
|
34
37
|
//let distSquare1 = getSquareDistance(p, p1)
|
|
@@ -56,14 +59,14 @@ export function pathDataRemoveColinear(pathData, {
|
|
|
56
59
|
type = "L"
|
|
57
60
|
com.type = "L"
|
|
58
61
|
com.values = valsL
|
|
59
|
-
//renderPoint(markers, p, 'cyan', '
|
|
62
|
+
//renderPoint(markers, p, 'cyan', '2%', '0.5')
|
|
60
63
|
}
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
|
|
64
67
|
// colinear – exclude arcs (as always =) as semicircles won't have an area
|
|
65
68
|
//&& comN.type==='L'
|
|
66
|
-
if ( isFlat && c < l - 1 && (type === 'L' || (flatBezierToLinetos && isFlatBez)) ) {
|
|
69
|
+
if ( isFlat && c < l - 1 && comN.type!=='A' && (type === 'L' || (flatBezierToLinetos && isFlatBez)) ) {
|
|
67
70
|
|
|
68
71
|
/*
|
|
69
72
|
console.log(area, distMax );
|
|
@@ -78,6 +81,8 @@ export function pathDataRemoveColinear(pathData, {
|
|
|
78
81
|
renderPoint(markers, p1, 'cyan', '0.5%', '1')
|
|
79
82
|
*/
|
|
80
83
|
|
|
84
|
+
//renderPoint(markers, p, 'blue', '1%', '1')
|
|
85
|
+
|
|
81
86
|
|
|
82
87
|
continue;
|
|
83
88
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { getDistAv } from "./geometry";
|
|
2
|
+
import { renderPoint } from "./visualize";
|
|
3
|
+
|
|
4
|
+
export function refineClosingCommand(pathData = [], {
|
|
5
|
+
threshold = 0,
|
|
6
|
+
} = {}) {
|
|
7
|
+
|
|
8
|
+
let l = pathData.length;
|
|
9
|
+
let comLast = pathData[l - 1]
|
|
10
|
+
let isClosed = comLast.type.toLowerCase() === 'z';
|
|
11
|
+
let idxPenultimate = isClosed ? l - 2 : l - 1
|
|
12
|
+
let comPenultimate = isClosed ? pathData[idxPenultimate] : pathData[idxPenultimate]
|
|
13
|
+
let valsPen = comPenultimate.values.slice(-2)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
let M = { x: pathData[0].values[0], y: pathData[0].values[1] }
|
|
17
|
+
let pPen = { x: valsPen[0], y: valsPen[1] }
|
|
18
|
+
let dist = getDistAv(M, pPen)
|
|
19
|
+
|
|
20
|
+
// adjust last coordinates for better reordering
|
|
21
|
+
if (dist && dist < threshold) {
|
|
22
|
+
//console.log('dist', dist, 'threshold', threshold, comPenultimate);
|
|
23
|
+
//renderPoint(markers, pPen)
|
|
24
|
+
|
|
25
|
+
let valsLast = pathData[idxPenultimate].values
|
|
26
|
+
let valsLastLen = valsLast.length;
|
|
27
|
+
pathData[idxPenultimate].values[valsLastLen - 2] = M.x
|
|
28
|
+
pathData[idxPenultimate].values[valsLastLen - 1] = M.y
|
|
29
|
+
|
|
30
|
+
// adjust cpts
|
|
31
|
+
let comFirst = pathData[1]
|
|
32
|
+
//console.log(comFirst, comPenultimate);
|
|
33
|
+
|
|
34
|
+
if (comFirst.type === 'C' && comPenultimate.type === 'C') {
|
|
35
|
+
let dx1 = Math.abs(comFirst.values[0] - comPenultimate.values[2])
|
|
36
|
+
let dy1 = Math.abs(comFirst.values[1] - comPenultimate.values[3])
|
|
37
|
+
|
|
38
|
+
let dx2 = Math.abs(pathData[1].values[0] - comFirst.values[0])
|
|
39
|
+
let dy2 = Math.abs(pathData[1].values[1] - comFirst.values[1])
|
|
40
|
+
|
|
41
|
+
let dx3 = Math.abs(pathData[1].values[0] - comPenultimate.values[2])
|
|
42
|
+
let dy3 = Math.abs(pathData[1].values[1] - comPenultimate.values[3])
|
|
43
|
+
|
|
44
|
+
let ver = dx2 < threshold && dx3 < threshold && dy1;
|
|
45
|
+
let hor = (dy2 < threshold && dy3 < threshold) && dx1;
|
|
46
|
+
|
|
47
|
+
//console.log(dy1);
|
|
48
|
+
if (dx1 && dx1 < threshold && ver) {
|
|
49
|
+
//console.log('adjust v');
|
|
50
|
+
pathData[1].values[0] = M.x
|
|
51
|
+
pathData[idxPenultimate].values[2] = M.x
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (dy1 && dy1 < threshold && hor) {
|
|
55
|
+
//console.log('should be y extreme');
|
|
56
|
+
pathData[1].values[1] = M.y
|
|
57
|
+
pathData[idxPenultimate].values[3] = M.y
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return pathData;
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
}
|
|
@@ -47,28 +47,44 @@ export function pathDataToTopLeft(pathData) {
|
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
|
|
50
|
-
export function optimizeClosePath(pathData, removeFinalLineto = true,
|
|
50
|
+
export function optimizeClosePath(pathData, {removeFinalLineto = true, autoClose = true}={}) {
|
|
51
51
|
|
|
52
52
|
let pathDataNew = [];
|
|
53
|
-
let
|
|
53
|
+
let l = pathData.length;
|
|
54
54
|
let M = { x: +pathData[0].values[0].toFixed(8), y: +pathData[0].values[1].toFixed(8) }
|
|
55
|
-
let isClosed = pathData[
|
|
55
|
+
let isClosed = pathData[l - 1].type.toLowerCase() === 'z'
|
|
56
56
|
|
|
57
57
|
let linetos = pathData.filter(com => com.type === 'L')
|
|
58
58
|
|
|
59
|
-
//return pathData;
|
|
60
|
-
|
|
61
59
|
|
|
62
60
|
// check if order is ideal
|
|
63
|
-
let
|
|
61
|
+
let idxPenultimate = isClosed ? l-2 : l-1
|
|
62
|
+
|
|
63
|
+
let penultimateCom = pathData[idxPenultimate];
|
|
64
64
|
let penultimateType = penultimateCom.type;
|
|
65
65
|
let penultimateComCoords = penultimateCom.values.slice(-2).map(val => +val.toFixed(8))
|
|
66
66
|
|
|
67
67
|
// last L command ends at M
|
|
68
68
|
let isClosingCommand = penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y
|
|
69
69
|
|
|
70
|
+
// add closepath Z to enable order optimizations
|
|
71
|
+
if(!isClosed && autoClose && isClosingCommand){
|
|
72
|
+
|
|
73
|
+
/*
|
|
74
|
+
// adjust final coords
|
|
75
|
+
let valsLast = pathData[idxPenultimate].values
|
|
76
|
+
let valsLastLen = valsLast.length;
|
|
77
|
+
pathData[idxPenultimate].values[valsLastLen-2] = M.x
|
|
78
|
+
pathData[idxPenultimate].values[valsLastLen-1] = M.y
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
pathData.push({type:'Z', values:[]})
|
|
82
|
+
isClosed = true;
|
|
83
|
+
l++
|
|
84
|
+
}
|
|
85
|
+
|
|
70
86
|
// if last segment is not closing or a lineto
|
|
71
|
-
let skipReorder = pathData[1].type !== 'L' && (!isClosingCommand ||
|
|
87
|
+
let skipReorder = pathData[1].type !== 'L' && (!isClosingCommand || penultimateCom.type === 'L')
|
|
72
88
|
skipReorder = false
|
|
73
89
|
|
|
74
90
|
|
|
@@ -82,7 +98,7 @@ export function optimizeClosePath(pathData, removeFinalLineto = true, reorder =
|
|
|
82
98
|
if (!skipReorder) {
|
|
83
99
|
//get top most index
|
|
84
100
|
let indices = [];
|
|
85
|
-
for (let i = 0
|
|
101
|
+
for (let i = 0; i < l; i++) {
|
|
86
102
|
let com = pathData[i];
|
|
87
103
|
let { type, values } = com;
|
|
88
104
|
if (values.length) {
|
|
@@ -122,10 +138,10 @@ export function optimizeClosePath(pathData, removeFinalLineto = true, reorder =
|
|
|
122
138
|
|
|
123
139
|
M = { x: +pathData[0].values[0].toFixed(8), y: +pathData[0].values[1].toFixed(8) }
|
|
124
140
|
|
|
125
|
-
|
|
141
|
+
l = pathData.length
|
|
126
142
|
|
|
127
143
|
// remove last lineto
|
|
128
|
-
penultimateCom = pathData[
|
|
144
|
+
penultimateCom = pathData[l - 2];
|
|
129
145
|
penultimateType = penultimateCom.type;
|
|
130
146
|
penultimateComCoords = penultimateCom.values.slice(-2).map(val=>+val.toFixed(8))
|
|
131
147
|
|
|
@@ -134,7 +150,7 @@ export function optimizeClosePath(pathData, removeFinalLineto = true, reorder =
|
|
|
134
150
|
//console.log('penultimateCom', isClosingCommand, penultimateCom.values, M);
|
|
135
151
|
|
|
136
152
|
if (removeFinalLineto && isClosingCommand) {
|
|
137
|
-
pathData.splice(
|
|
153
|
+
pathData.splice(l - 2, 1)
|
|
138
154
|
}
|
|
139
155
|
|
|
140
156
|
pathDataNew.push(...pathData);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { checkLineIntersection, getDistAv, interpolate, pointAtT } from "./geometry";
|
|
1
|
+
import { checkLineIntersection, getDistAv, getSquareDistance, interpolate, pointAtT } from "./geometry";
|
|
2
2
|
import { getPolygonArea } from "./geometry_area";
|
|
3
3
|
import { commandIsFlat } from "./geometry_flatness";
|
|
4
4
|
import { renderPoint } from "./visualize";
|
|
@@ -8,12 +8,20 @@ export function refineRoundedCorners(pathData, {
|
|
|
8
8
|
tolerance = 1
|
|
9
9
|
} = {}) {
|
|
10
10
|
|
|
11
|
+
|
|
12
|
+
// min size threshold for corners
|
|
13
|
+
threshold *= tolerance;
|
|
14
|
+
|
|
11
15
|
let l = pathData.length;
|
|
12
16
|
|
|
13
17
|
// add fist command
|
|
14
18
|
let pathDataN = [pathData[0]]
|
|
15
19
|
|
|
16
20
|
let isClosed = pathData[l - 1].type.toLowerCase() === 'z';
|
|
21
|
+
let zIsLineto = isClosed ?
|
|
22
|
+
(pathData[l-1].p.x === pathData[0].p0.x && pathData[l-1].p.y === pathData[0].p0.y)
|
|
23
|
+
: false ;
|
|
24
|
+
|
|
17
25
|
let lastOff = isClosed ? 2 : 1;
|
|
18
26
|
|
|
19
27
|
let comLast = pathData[l - lastOff];
|
|
@@ -24,7 +32,9 @@ export function refineRoundedCorners(pathData, {
|
|
|
24
32
|
|
|
25
33
|
//console.log('lastIsLine', lastIsLine, 'firstIsLine', firstIsLine, 'lastIsBez', lastIsBez, 'firstIsBez', firstIsBez, 'isClosed', isClosed, 'comLast1', comLast1);
|
|
26
34
|
|
|
27
|
-
let normalizeClose = isClosed && firstIsBez;
|
|
35
|
+
let normalizeClose = isClosed && firstIsBez && (lastIsLine || zIsLineto);
|
|
36
|
+
let adjustStart = false
|
|
37
|
+
//normalizeClose = false
|
|
28
38
|
//console.log('normalizeClose', normalizeClose);
|
|
29
39
|
|
|
30
40
|
// normalize closepath to lineto
|
|
@@ -42,9 +52,8 @@ export function refineRoundedCorners(pathData, {
|
|
|
42
52
|
// search small cubic segments enclosed by linetos
|
|
43
53
|
if ((type === 'L' && comN && comN.type === 'C') ||
|
|
44
54
|
(type === 'C' && comN && comN.type === 'L')
|
|
45
|
-
|
|
46
55
|
) {
|
|
47
|
-
let comL0 = com;
|
|
56
|
+
let comL0 = type==='L' ? com : null;
|
|
48
57
|
let comL1 = null;
|
|
49
58
|
let comBez = [];
|
|
50
59
|
let offset = 0;
|
|
@@ -54,7 +63,12 @@ export function refineRoundedCorners(pathData, {
|
|
|
54
63
|
comBez = [pathData[1]]
|
|
55
64
|
comL0 = pathData[l - 1]
|
|
56
65
|
comL1 = comN
|
|
57
|
-
//renderPoint(markers, com.p, '
|
|
66
|
+
//renderPoint(markers, com.p, 'purple')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if(!comL0) {
|
|
70
|
+
pathDataN.push(com)
|
|
71
|
+
continue
|
|
58
72
|
}
|
|
59
73
|
|
|
60
74
|
// closing corner to start
|
|
@@ -93,49 +107,50 @@ export function refineRoundedCorners(pathData, {
|
|
|
93
107
|
// check concaveness by area sign change
|
|
94
108
|
let area1 = getPolygonArea([comL0.p0, comL0.p, comL1.p0, comL1.p], false)
|
|
95
109
|
let area2 = getPolygonArea([comBez[0].p0, comBez[0].cp1, comBez[0].cp2, comBez[0].p], false)
|
|
110
|
+
//let isFlatBezier = area2 < getSquareDistance(comL0.p, comL1.p)*0.001
|
|
96
111
|
|
|
97
112
|
let signChange = (area1 < 0 && area2 > 0) || (area1 > 0 && area2 < 0)
|
|
98
113
|
|
|
99
|
-
|
|
114
|
+
// exclude mid bezier segments that are larger than surrounding linetos
|
|
115
|
+
let bezThresh = len3*0.5 * tolerance
|
|
116
|
+
let isSmall = bezThresh < len1 && bezThresh < len2 ;
|
|
100
117
|
|
|
101
|
-
|
|
102
|
-
|
|
118
|
+
//len1 > len3 && len2 > len3
|
|
119
|
+
if (comBez.length && !signChange && isSmall ) {
|
|
103
120
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
let dist2 = getDistAv(ptQ, comL1.p0)
|
|
107
|
-
let diff = Math.abs(dist1-dist2)
|
|
108
|
-
let rat = diff/Math.max(dist1, dist2)
|
|
109
|
-
console.log('rat', rat);
|
|
110
|
-
*/
|
|
121
|
+
let isFlatBezier = Math.abs(area2) <= getSquareDistance(comBez[0].p0, comBez[0].p)*0.005
|
|
122
|
+
let ptQ = !isFlatBezier ? checkLineIntersection(comL0.p0, comL0.p, comL1.p0, comL1.p, false) : null
|
|
111
123
|
|
|
112
|
-
|
|
113
|
-
// adjust curve start and end to meet original
|
|
114
|
-
let t = 1
|
|
124
|
+
if (!isFlatBezier && ptQ) {
|
|
115
125
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
comL0.values = [p0_2.x, p0_2.y]
|
|
126
|
+
// final check: mid point proximity
|
|
127
|
+
let ptM = pointAtT([comL0.p, ptQ, comL1.p0], 0.5)
|
|
128
|
+
//renderPoint(markers, ptM, 'red', '0.5%', '0.5')
|
|
120
129
|
|
|
121
|
-
let
|
|
122
|
-
//renderPoint(markers, p_2, 'orange', '1%', '0.5')
|
|
123
|
-
comL1.p0 = p_2
|
|
130
|
+
let ptM_bez = comBez.length===1 ? pointAtT( [comBez[0].p0, comBez[0].cp1, comBez[0].cp2, comBez[0].p], 0.5 ) : comBez[0].p ;
|
|
124
131
|
|
|
125
|
-
|
|
126
|
-
//renderPoint(markers, ptQ, 'magenta')
|
|
127
|
-
*/
|
|
132
|
+
let dist1 = getDistAv(ptM, ptM_bez)
|
|
128
133
|
|
|
134
|
+
// not in tolerance – rturn original command
|
|
135
|
+
if(dist1>len3){
|
|
136
|
+
//renderPoint(markers, ptM_bez, 'cyan', '0.5%', '0.5')
|
|
137
|
+
//renderPoint(markers, ptQ, 'magenta', '0.5%', '0.5')
|
|
138
|
+
pathDataN.push(com);
|
|
139
|
+
} else{
|
|
129
140
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
141
|
+
//renderPoint(markers, ptQ, 'magenta', '0.5%', '0.5')
|
|
142
|
+
|
|
143
|
+
let comQ = { type: 'Q', values: [ptQ.x, ptQ.y, comL1.p0.x, comL1.p0.y] }
|
|
144
|
+
comQ.p0 = comL0.p;
|
|
145
|
+
comQ.cp1 = ptQ;
|
|
146
|
+
comQ.p = comL1.p0;
|
|
147
|
+
|
|
148
|
+
// add quadratic command
|
|
149
|
+
pathDataN.push(comL0, comQ);
|
|
150
|
+
i += offset;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
134
153
|
|
|
135
|
-
// add quadratic command
|
|
136
|
-
pathDataN.push(comL0, comQ);
|
|
137
|
-
i += offset;
|
|
138
|
-
continue;
|
|
139
154
|
}
|
|
140
155
|
}
|
|
141
156
|
}
|
|
@@ -150,11 +165,16 @@ export function refineRoundedCorners(pathData, {
|
|
|
150
165
|
|
|
151
166
|
}
|
|
152
167
|
|
|
168
|
+
|
|
169
|
+
|
|
153
170
|
// revert close path normalization
|
|
154
|
-
if (normalizeClose) {
|
|
171
|
+
if (normalizeClose || (isClosed && pathDataN[pathDataN.length-1].type!=='Z') ) {
|
|
155
172
|
pathDataN.push({ type: 'Z', values: [] })
|
|
156
173
|
}
|
|
157
174
|
|
|
175
|
+
|
|
176
|
+
//console.log(pathDataN);
|
|
177
|
+
|
|
158
178
|
return pathDataN;
|
|
159
179
|
|
|
160
180
|
}
|
package/src/svgii/svg_cleanup.js
CHANGED
|
@@ -1,54 +1,53 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
|
|
4
3
|
export function removeEmptySVGEls(svg) {
|
|
5
4
|
let els = svg.querySelectorAll('g, defs');
|
|
6
5
|
els.forEach(el => {
|
|
7
|
-
|
|
6
|
+
if (!el.children.length) el.remove()
|
|
8
7
|
})
|
|
9
8
|
}
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
//const DOMParserPoly = globalThis.DOMParser;
|
|
12
11
|
|
|
13
12
|
export function cleanUpSVG(svgMarkup, {
|
|
14
|
-
returnDom=false,
|
|
15
|
-
removeHidden=true,
|
|
16
|
-
removeUnused=true,
|
|
17
|
-
}={}) {
|
|
13
|
+
returnDom = false,
|
|
14
|
+
removeHidden = true,
|
|
15
|
+
removeUnused = true,
|
|
16
|
+
} = {}) {
|
|
18
17
|
svgMarkup = cleanSvgPrologue(svgMarkup);
|
|
19
|
-
|
|
18
|
+
|
|
20
19
|
// replace namespaced refs
|
|
21
20
|
svgMarkup = svgMarkup.replaceAll("xlink:href=", "href=");
|
|
22
|
-
|
|
21
|
+
|
|
23
22
|
let svg = new DOMParser()
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
let allowed=['viewBox', 'xmlns', 'width', 'height', 'id', 'class', 'fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin'];
|
|
23
|
+
.parseFromString(svgMarkup, "text/html")
|
|
24
|
+
.querySelector("svg");
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
let allowed = ['viewBox', 'xmlns', 'width', 'height', 'id', 'class', 'fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin'];
|
|
29
28
|
removeExcludedAttribues(svg, allowed)
|
|
30
|
-
|
|
29
|
+
|
|
31
30
|
let removeEls = ['metadata', 'script']
|
|
32
|
-
|
|
31
|
+
|
|
33
32
|
let els = svg.querySelectorAll('*')
|
|
34
|
-
els.forEach(el=>{
|
|
35
|
-
let name = el.nodeName;
|
|
33
|
+
els.forEach(el => {
|
|
34
|
+
let name = el.nodeName;
|
|
36
35
|
// remove hidden elements
|
|
37
36
|
let style = el.getAttribute('style') || ''
|
|
38
37
|
let isHiddenByStyle = style ? style.trim().includes('display:none') : false;
|
|
39
38
|
let isHidden = (el.getAttribute('display') && el.getAttribute('display') === 'none') || isHiddenByStyle;
|
|
40
|
-
if(name.includes(':') || removeEls.includes(name) || (removeHidden && isHidden
|
|
39
|
+
if (name.includes(':') || removeEls.includes(name) || (removeHidden && isHidden)) {
|
|
41
40
|
el.remove();
|
|
42
|
-
}else{
|
|
41
|
+
} else {
|
|
43
42
|
// remove BS elements
|
|
44
43
|
removeNameSpaceAtts(el)
|
|
45
44
|
}
|
|
46
45
|
})
|
|
47
46
|
|
|
48
|
-
if(returnDom) return svg
|
|
47
|
+
if (returnDom) return svg
|
|
49
48
|
|
|
50
49
|
let markup = stringifySVG(svg)
|
|
51
|
-
console.log(markup);
|
|
50
|
+
//console.log(markup);
|
|
52
51
|
|
|
53
52
|
return markup;
|
|
54
53
|
}
|
|
@@ -67,7 +66,7 @@ function cleanSvgPrologue(svgString) {
|
|
|
67
66
|
);
|
|
68
67
|
}
|
|
69
68
|
|
|
70
|
-
function removeExcludedAttribues(el, allowed=['viewBox', 'xmlns', 'width', 'height', 'id', 'class']){
|
|
69
|
+
function removeExcludedAttribues(el, allowed = ['viewBox', 'xmlns', 'width', 'height', 'id', 'class']) {
|
|
71
70
|
let atts = [...el.attributes].map((att) => att.name);
|
|
72
71
|
atts.forEach((att) => {
|
|
73
72
|
if (!allowed.includes(att)) {
|
|
@@ -86,13 +85,13 @@ function removeNameSpaceAtts(el) {
|
|
|
86
85
|
});
|
|
87
86
|
}
|
|
88
87
|
|
|
89
|
-
export function stringifySVG(svg){
|
|
90
|
-
|
|
88
|
+
export function stringifySVG(svg) {
|
|
89
|
+
let markup = new XMLSerializer().serializeToString(svg);
|
|
91
90
|
markup = markup
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
91
|
+
.replace(/\t/g, "")
|
|
92
|
+
.replace(/[\n\r|]/g, "\n")
|
|
93
|
+
.replace(/\n\s*\n/g, '\n')
|
|
94
|
+
.replace(/ +/g, ' ')
|
|
96
95
|
|
|
97
96
|
return markup
|
|
98
97
|
}
|