svg-path-simplify 0.4.3 → 0.4.4
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/CHANGELOG.md +11 -0
- package/README.md +1 -0
- package/dist/svg-path-simplify.esm.js +1610 -495
- package/dist/svg-path-simplify.esm.min.js +2 -2
- package/dist/svg-path-simplify.js +1611 -494
- package/dist/svg-path-simplify.min.js +2 -2
- package/dist/svg-path-simplify.pathdata.esm.js +893 -456
- package/dist/svg-path-simplify.pathdata.esm.min.js +2 -2
- package/dist/svg-path-simplify.poly.cjs +9 -8
- package/index.html +58 -17
- package/package.json +1 -1
- package/src/constants.js +4 -0
- package/src/detect_input.js +47 -29
- package/src/index.js +8 -0
- package/src/pathData_simplify_cubic.js +26 -16
- package/src/pathData_simplify_revertToquadratics.js +0 -1
- package/src/pathSimplify-main.js +75 -20
- package/src/pathSimplify-only-pathdata.js +7 -2
- package/src/pathSimplify-presets.js +15 -4
- package/src/svg-getAttributes.js +4 -2
- package/src/svgii/convert_units.js +1 -1
- package/src/svgii/geometry.js +140 -2
- package/src/svgii/geometry_bbox_element.js +1 -1
- package/src/svgii/geometry_deduceRadius.js +116 -27
- package/src/svgii/geometry_length.js +17 -1
- package/src/svgii/pathData_analyze.js +18 -0
- package/src/svgii/pathData_convert.js +188 -88
- package/src/svgii/pathData_fix_directions.js +10 -18
- package/src/svgii/pathData_reorder.js +122 -16
- package/src/svgii/pathData_simplify_refineCorners.js +130 -35
- package/src/svgii/pathData_simplify_refine_round.js +420 -0
- package/src/svgii/rounding.js +79 -78
- package/src/svgii/svg_cleanup.js +68 -20
- package/src/svgii/svg_cleanup_convertPathLength.js +22 -15
- package/src/svgii/svg_cleanup_remove_els_and_atts.js +6 -1
- package/src/svgii/svg_el_parse_style_props.js +13 -10
- package/src/svgii/svg_validate.js +220 -0
- package/tests/testSVG.js +14 -1
- package/src/svgii/pathData_refine_round.js +0 -222
|
@@ -1,47 +1,259 @@
|
|
|
1
|
+
const rad2Deg = 180/Math.PI;
|
|
2
|
+
const deg2rad = Math.PI/180;
|
|
3
|
+
|
|
4
|
+
function validateSVG(markup, allowed = {}) {
|
|
5
|
+
allowed = {
|
|
6
|
+
...{
|
|
7
|
+
|
|
8
|
+
useElsNested: 5000,
|
|
9
|
+
hasScripts: false,
|
|
10
|
+
hasEntity: false,
|
|
11
|
+
fileSizeKB: 10000,
|
|
12
|
+
isSymbolSprite: false,
|
|
13
|
+
isSvgFont: false
|
|
14
|
+
},
|
|
15
|
+
...allowed
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
let fileReport = analyzeSVG(markup, allowed);
|
|
19
|
+
let isValid = true;
|
|
20
|
+
let log = [];
|
|
21
|
+
|
|
22
|
+
if (!fileReport.hasEls) {
|
|
23
|
+
log.push("no elements");
|
|
24
|
+
isValid = false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (Object.keys(fileReport).length) {
|
|
28
|
+
if (fileReport.isBillionLaugh === true) {
|
|
29
|
+
log.push(`suspicious: might contain billion laugh attack`);
|
|
30
|
+
isValid = false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (let key in allowed) {
|
|
34
|
+
let val = allowed[key];
|
|
35
|
+
let valRep = fileReport[key];
|
|
36
|
+
if (typeof val === "number" && valRep > val) {
|
|
37
|
+
log.push(`allowed "${key}" exceeded: ${valRep} / ${val} `);
|
|
38
|
+
isValid = false;
|
|
39
|
+
}
|
|
40
|
+
if (valRep === true && val === false) {
|
|
41
|
+
log.push(`not allowed: "${key}" `);
|
|
42
|
+
isValid = false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
isValid = false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/*
|
|
50
|
+
if (!isValid) {
|
|
51
|
+
log = ["SVG not valid"].concat(log);
|
|
52
|
+
|
|
53
|
+
if (Object.keys(fileReport).length) {
|
|
54
|
+
console.warn(fileReport);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
return { isValid, log, fileReport };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function analyzeSVG(markup, allowed = {}) {
|
|
63
|
+
markup = markup.trim();
|
|
64
|
+
let doc, svg;
|
|
65
|
+
let fileSizeKB = +(markup.length / 1024).toFixed(3);
|
|
66
|
+
|
|
67
|
+
let fileReport = {
|
|
68
|
+
totalEls: 1,
|
|
69
|
+
hasEls: true,
|
|
70
|
+
hasDefs: false,
|
|
71
|
+
geometryEls: [],
|
|
72
|
+
useEls: 0,
|
|
73
|
+
useElsNested: 0,
|
|
74
|
+
nonsensePaths: 0,
|
|
75
|
+
isSuspicious: false,
|
|
76
|
+
isBillionLaugh: false,
|
|
77
|
+
hasScripts: false,
|
|
78
|
+
hasPrologue: false,
|
|
79
|
+
hasEntity: false,
|
|
80
|
+
isPathData:false,
|
|
81
|
+
fileSizeKB,
|
|
82
|
+
hasXmlns: markup.includes("http://www.w3.org/2000/svg"),
|
|
83
|
+
isSymbolSprite: false,
|
|
84
|
+
isSvgFont: markup.includes("<glyph>")
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
let maxNested = allowed.useElsNested ? allowed.useElsNested : 2000;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* analyze nestes use references
|
|
91
|
+
*/
|
|
92
|
+
const countUseRefs = (useEls, maxNested = 2000) => {
|
|
93
|
+
let nestedCount = 0;
|
|
94
|
+
|
|
95
|
+
for (let i = 0; i < useEls.length && nestedCount < maxNested; i++) {
|
|
96
|
+
let use = useEls[i];
|
|
97
|
+
let refId = use.getAttribute("xlink:href")
|
|
98
|
+
? use.getAttribute("xlink:href")
|
|
99
|
+
: use.getAttribute("href");
|
|
100
|
+
refId = refId ? refId.replace("#", "") : "";
|
|
101
|
+
|
|
102
|
+
use.setAttribute("href", "#" + refId);
|
|
103
|
+
|
|
104
|
+
let refEl = svg.getElementById(refId);
|
|
105
|
+
let nestedUse = refEl.querySelectorAll("use");
|
|
106
|
+
let nestedUseLength = nestedUse.length;
|
|
107
|
+
nestedCount += nestedUseLength;
|
|
108
|
+
|
|
109
|
+
// query nested use references
|
|
110
|
+
for (let n = 0; n < nestedUse.length && nestedCount < maxNested; n++) {
|
|
111
|
+
let nested = nestedUse[n];
|
|
112
|
+
let id1 = nested.getAttribute("href").replace("#", "");
|
|
113
|
+
let refEl1 = svg.getElementById(id1);
|
|
114
|
+
let nestedUse1 = refEl1.querySelectorAll("use");
|
|
115
|
+
nestedCount += nestedUse1.length;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
fileReport.useElsNested = nestedCount;
|
|
119
|
+
return nestedCount;
|
|
120
|
+
};
|
|
121
|
+
let hasEntity = /\<\!ENTITY/gi.test(markup);
|
|
122
|
+
let hasScripts = /\<script/gi.test(markup) ? true : false;
|
|
123
|
+
let hasUse = /\<use/gi.test(markup) ? true : false;
|
|
124
|
+
let hasEls = /[\<path|\<polygon|\<polyline|\<rect|\<circle|\<ellipse|\<line|\<text|\<foreignObject]/gi.test(markup);
|
|
125
|
+
let hasDefs = /[\<filter|\<linearGradient|\<radialGradient|\<pattern|\<animate|\<animateMotion|\<animateTransform|\<clipPath|\<mask|\<symbol|\<marker]/gi.test(markup);
|
|
126
|
+
|
|
127
|
+
let isPathData = (markup.startsWith('M') || markup.startsWith('m')) && !/[\<svg|\<\/svg]/gi.test(markup);
|
|
128
|
+
fileReport.isPathData = isPathData;
|
|
129
|
+
|
|
130
|
+
// seems OK
|
|
131
|
+
if (!hasEntity && !hasUse && !hasScripts && (hasEls || hasDefs) && fileSizeKB < allowed.fileSizeKB) {
|
|
132
|
+
fileReport.hasEls = hasEls;
|
|
133
|
+
fileReport.hasDefs = hasDefs;
|
|
134
|
+
|
|
135
|
+
return fileReport
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Contains xml entity definition: highly suspicious - stop parsing!
|
|
139
|
+
if (allowed.hasEntity === false && hasEntity) {
|
|
140
|
+
fileReport.hasEntity = true;
|
|
141
|
+
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* sanitizing for parsing:
|
|
146
|
+
* remove xml prologue and comments
|
|
147
|
+
*/
|
|
148
|
+
markup = markup
|
|
149
|
+
.replace(/\<\?xml.+\?\>|\<\!DOCTYPE.+]\>/g, "")
|
|
150
|
+
.replace(/(<!--.*?-->)|(<!--[\S\s]+?-->)|(<!--[\S\s]*?$)/g, "");
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Try to parse svg:
|
|
154
|
+
* invalid svg will return false via "catch"
|
|
155
|
+
*/
|
|
156
|
+
try {
|
|
157
|
+
|
|
158
|
+
doc = new DOMParser().parseFromString(markup, "text/html");
|
|
159
|
+
svg = doc.querySelector("svg");
|
|
160
|
+
|
|
161
|
+
// paths containing only a M command
|
|
162
|
+
let nonsensePaths = svg.querySelectorAll('path[d="M0,0"], path[d="M0 0"]').length;
|
|
163
|
+
let useEls = svg.querySelectorAll("use").length;
|
|
164
|
+
|
|
165
|
+
// create analyzing object
|
|
166
|
+
fileReport.totalEls = svg.querySelectorAll("*").length;
|
|
167
|
+
fileReport.geometryEls = svg.querySelectorAll(
|
|
168
|
+
"path, rect, circle, ellipse, polygon, polyline, line"
|
|
169
|
+
).length;
|
|
170
|
+
|
|
171
|
+
fileReport.hasScripts = hasScripts;
|
|
172
|
+
fileReport.useEls = useEls;
|
|
173
|
+
fileReport.nonsensePaths = nonsensePaths;
|
|
174
|
+
fileReport.isSuspicious = false;
|
|
175
|
+
fileReport.isBillionLaugh = false;
|
|
176
|
+
fileReport.hasXmlns = svg.getAttribute("xmlns")
|
|
177
|
+
? svg.getAttribute("xmlns") === "http://www.w3.org/2000/svg"
|
|
178
|
+
? true
|
|
179
|
+
: false
|
|
180
|
+
: false;
|
|
181
|
+
fileReport.isSymbolSprite =
|
|
182
|
+
svg.querySelectorAll("symbol").length &&
|
|
183
|
+
svg.querySelectorAll("use").length === 0
|
|
184
|
+
? true
|
|
185
|
+
: false;
|
|
186
|
+
fileReport.isSvgFont = svg.querySelectorAll("glyph").length ? true : false;
|
|
187
|
+
|
|
188
|
+
let totalEls = fileReport.totalEls;
|
|
189
|
+
let totalUseEls = fileReport.useEls;
|
|
190
|
+
let usePercentage = (100 / totalEls) * totalUseEls;
|
|
191
|
+
|
|
192
|
+
// if percentage of use elements is higher than 75% - suspicious
|
|
193
|
+
if (usePercentage > 75) {
|
|
194
|
+
fileReport.isSuspicious = true;
|
|
195
|
+
|
|
196
|
+
// check nested use references
|
|
197
|
+
let nestedCount = countUseRefs(svg.querySelectorAll("use"), maxNested);
|
|
198
|
+
if (nestedCount >= maxNested) {
|
|
199
|
+
fileReport.isBillionLaugh = true;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return fileReport;
|
|
204
|
+
} catch {
|
|
205
|
+
// svg file has malformed markup
|
|
206
|
+
console.warn("svg could not be parsed");
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
1
211
|
function detectInputType(input) {
|
|
2
|
-
let
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
212
|
+
let log = '';
|
|
213
|
+
let isValid = true;
|
|
214
|
+
|
|
215
|
+
let result = {
|
|
216
|
+
inputType:'',
|
|
217
|
+
isValid:true,
|
|
218
|
+
fileReport:{},
|
|
219
|
+
};
|
|
220
|
+
|
|
11
221
|
if (Array.isArray(input)) {
|
|
12
222
|
|
|
223
|
+
result.inputType = "array";
|
|
224
|
+
|
|
13
225
|
// nested array
|
|
14
226
|
if (Array.isArray(input[0])) {
|
|
15
227
|
|
|
16
228
|
if (input[0].length === 2) {
|
|
17
229
|
|
|
18
|
-
|
|
230
|
+
result.inputType = 'polyArray';
|
|
19
231
|
}
|
|
20
232
|
|
|
21
233
|
else if (Array.isArray(input[0][0]) && input[0][0].length === 2) {
|
|
22
234
|
|
|
23
|
-
|
|
235
|
+
result.inputType = 'polyComplexArray';
|
|
24
236
|
}
|
|
25
237
|
else if (input[0][0].x !== undefined && input[0][0].y !== undefined) {
|
|
26
238
|
|
|
27
|
-
|
|
239
|
+
result.inputType = 'polyComplexObjectArray';
|
|
28
240
|
}
|
|
241
|
+
|
|
29
242
|
}
|
|
30
243
|
|
|
31
244
|
// is point array
|
|
32
245
|
else if (input[0].x !== undefined && input[0].y !== undefined) {
|
|
33
246
|
|
|
34
|
-
|
|
247
|
+
result.inputType = 'polyObjectArray';
|
|
35
248
|
}
|
|
36
249
|
|
|
37
250
|
// path data array
|
|
38
251
|
else if (input[0]?.type && input[0]?.values
|
|
39
252
|
) {
|
|
40
|
-
|
|
41
|
-
|
|
253
|
+
result.inputType = "pathData";
|
|
42
254
|
}
|
|
43
255
|
|
|
44
|
-
return
|
|
256
|
+
return result;
|
|
45
257
|
}
|
|
46
258
|
|
|
47
259
|
if (typeof input === "string") {
|
|
@@ -53,36 +265,48 @@ function detectInputType(input) {
|
|
|
53
265
|
let isJson = isNumberJson(input);
|
|
54
266
|
|
|
55
267
|
if (isSVG) {
|
|
56
|
-
|
|
268
|
+
let validate = validateSVG(input);
|
|
269
|
+
({isValid, log} = validate) ;
|
|
270
|
+
if(!isValid){
|
|
271
|
+
|
|
272
|
+
result.inputType = 'invalid';
|
|
273
|
+
result.isValid=false,
|
|
274
|
+
|
|
275
|
+
result.log = log;
|
|
276
|
+
}else {
|
|
277
|
+
result.inputType = 'svgMarkup';
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
result.fileReport = validate.fileReport;
|
|
281
|
+
|
|
57
282
|
}
|
|
58
283
|
|
|
59
284
|
else if (isJson) {
|
|
60
|
-
|
|
285
|
+
result.inputType = 'json';
|
|
61
286
|
}
|
|
62
287
|
|
|
63
288
|
else if (isSymbol) {
|
|
64
|
-
|
|
289
|
+
result.inputType = 'symbol';
|
|
65
290
|
}
|
|
66
291
|
else if (isPathData) {
|
|
67
|
-
|
|
292
|
+
result.inputType = 'pathDataString';
|
|
68
293
|
}
|
|
69
294
|
else if (isPolyString) {
|
|
70
|
-
|
|
295
|
+
result.inputType = 'polyString';
|
|
71
296
|
}
|
|
72
297
|
|
|
73
298
|
else {
|
|
74
299
|
let url = /^(file:|https?:\/\/|\/|\.\/|\.\.\/)/.test(input);
|
|
75
300
|
let dataUrl = input.startsWith('data:image');
|
|
76
|
-
|
|
301
|
+
result.inputType = url || dataUrl ? "url" : "string";
|
|
77
302
|
}
|
|
78
303
|
|
|
79
|
-
return
|
|
304
|
+
return result
|
|
80
305
|
}
|
|
81
306
|
|
|
82
|
-
|
|
83
|
-
let constructor = input.constructor.name;
|
|
307
|
+
result.inputType = (input.constructor.name || typeof input ).toLowerCase();
|
|
84
308
|
|
|
85
|
-
return
|
|
309
|
+
return result;
|
|
86
310
|
}
|
|
87
311
|
|
|
88
312
|
function isNumberJson(str) {
|
|
@@ -100,9 +324,6 @@ function isNumberJson(str) {
|
|
|
100
324
|
|
|
101
325
|
}
|
|
102
326
|
|
|
103
|
-
const rad2Deg = 180/Math.PI;
|
|
104
|
-
const deg2rad = Math.PI/180;
|
|
105
|
-
|
|
106
327
|
function renderPoint(
|
|
107
328
|
svg,
|
|
108
329
|
coords,
|
|
@@ -130,18 +351,6 @@ function renderPoint(
|
|
|
130
351
|
}
|
|
131
352
|
}
|
|
132
353
|
|
|
133
|
-
function renderPath(svg, d = '', stroke = 'green', strokeWidth = '1%', opacity="1", render = true) {
|
|
134
|
-
|
|
135
|
-
let path = `<path d="${d}" fill="none" stroke="${stroke}" stroke-width="${strokeWidth}" stroke-opacity="${opacity}" /> `;
|
|
136
|
-
|
|
137
|
-
if (render) {
|
|
138
|
-
svg.insertAdjacentHTML("beforeend", path);
|
|
139
|
-
} else {
|
|
140
|
-
return path;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
|
|
145
354
|
/*
|
|
146
355
|
import {abs, acos, asin, atan, atan2, ceil, cos, exp, floor,
|
|
147
356
|
log, max, min, pow, random, round, sin, sqrt, tan, PI} from '/.constants.js';
|
|
@@ -358,6 +567,7 @@ function pointAtT(pts, t = 0.5, getTangent = false, getCpts = false, returnArray
|
|
|
358
567
|
let t1 = 1 - t;
|
|
359
568
|
|
|
360
569
|
// cubic beziers
|
|
570
|
+
/*
|
|
361
571
|
if (isCubic) {
|
|
362
572
|
pt = {
|
|
363
573
|
x:
|
|
@@ -373,11 +583,29 @@ function pointAtT(pts, t = 0.5, getTangent = false, getCpts = false, returnArray
|
|
|
373
583
|
};
|
|
374
584
|
|
|
375
585
|
}
|
|
586
|
+
*/
|
|
587
|
+
|
|
588
|
+
if (isCubic) {
|
|
589
|
+
pt = {
|
|
590
|
+
x:
|
|
591
|
+
t1 * t1 * t1 * p0.x +
|
|
592
|
+
3 * t1 * t1 * t * cp1.x +
|
|
593
|
+
3 * t1 * t * t * cp2.x +
|
|
594
|
+
t * t * t * p.x,
|
|
595
|
+
y:
|
|
596
|
+
t1 * t1 * t1 * p0.y +
|
|
597
|
+
3 * t1 * t1 * t * cp1.y +
|
|
598
|
+
3 * t1 * t * t * cp2.y +
|
|
599
|
+
t * t * t * p.y,
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
}
|
|
603
|
+
|
|
376
604
|
// quadratic beziers
|
|
377
605
|
else {
|
|
378
606
|
pt = {
|
|
379
|
-
x: t1 * t1 * p0.x + 2 * t1 * t * cp1.x + t
|
|
380
|
-
y: t1 * t1 * p0.y + 2 * t1 * t * cp1.y + t
|
|
607
|
+
x: t1 * t1 * p0.x + 2 * t1 * t * cp1.x + t * t * p.x,
|
|
608
|
+
y: t1 * t1 * p0.y + 2 * t1 * t * cp1.y + t * t * p.y,
|
|
381
609
|
};
|
|
382
610
|
}
|
|
383
611
|
|
|
@@ -693,12 +921,6 @@ function getBezierExtremeT(pts, { addExtremes = true, addSemiExtremes = false }
|
|
|
693
921
|
return tArr;
|
|
694
922
|
}
|
|
695
923
|
|
|
696
|
-
/**
|
|
697
|
-
* based on Nikos M.'s answer
|
|
698
|
-
* how-do-you-calculate-the-axis-aligned-bounding-box-of-an-ellipse
|
|
699
|
-
* https://stackoverflow.com/questions/87734/#75031511
|
|
700
|
-
* See also: https://github.com/foo123/Geometrize
|
|
701
|
-
*/
|
|
702
924
|
function getArcExtemes(p0, values) {
|
|
703
925
|
// compute point on ellipse from angle around ellipse (theta)
|
|
704
926
|
const arc = (theta, cx, cy, rx, ry, alpha) => {
|
|
@@ -1424,6 +1646,36 @@ function splitCommandAtTValues(p0, values, tArr, returnCommand = true) {
|
|
|
1424
1646
|
return segmentPoints;
|
|
1425
1647
|
}
|
|
1426
1648
|
|
|
1649
|
+
/**
|
|
1650
|
+
* round path data
|
|
1651
|
+
* either by explicit decimal value or
|
|
1652
|
+
* based on suggested accuracy in path data
|
|
1653
|
+
*/
|
|
1654
|
+
function roundPathData(pathData, decimalsGlobal = -1) {
|
|
1655
|
+
|
|
1656
|
+
if (decimalsGlobal < 0) return pathData;
|
|
1657
|
+
|
|
1658
|
+
let len = pathData.length;
|
|
1659
|
+
let decimals = decimalsGlobal;
|
|
1660
|
+
let decimalsArc = decimals < 3 ? decimals+2 : decimals;
|
|
1661
|
+
|
|
1662
|
+
for (let c = 0; c < len; c++) {
|
|
1663
|
+
let com = pathData[c];
|
|
1664
|
+
let { type, values } = com;
|
|
1665
|
+
let valLen = values.length;
|
|
1666
|
+
if (!valLen) continue
|
|
1667
|
+
|
|
1668
|
+
let isArc = type.toLowerCase() === 'a';
|
|
1669
|
+
|
|
1670
|
+
for (let v = 0; v < valLen; v++) {
|
|
1671
|
+
// allow higher accuracy for arc radii (... it's always arcs)
|
|
1672
|
+
pathData[c].values[v] = isArc && v < 2 ? roundTo(values[v], decimalsArc) : roundTo(values[v], decimals);
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
return pathData;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1427
1679
|
function detectAccuracy(pathData) {
|
|
1428
1680
|
let dims = [];
|
|
1429
1681
|
|
|
@@ -1457,39 +1709,29 @@ function detectAccuracy(pathData) {
|
|
|
1457
1709
|
|
|
1458
1710
|
}
|
|
1459
1711
|
|
|
1460
|
-
function roundTo(num = 0, decimals = 3) {
|
|
1461
|
-
if(decimals<=-1) return num;
|
|
1462
|
-
if (!decimals) return Math.round(num);
|
|
1463
|
-
let factor = 10 ** decimals;
|
|
1464
|
-
return Math.round(num * factor) / factor;
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
1712
|
/**
|
|
1468
|
-
*
|
|
1469
|
-
*
|
|
1470
|
-
*
|
|
1713
|
+
* rounding helper
|
|
1714
|
+
* allows for quantized rounding
|
|
1715
|
+
* e.g 0.5 decimals s
|
|
1471
1716
|
*/
|
|
1472
|
-
function
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
let len = pathData.length;
|
|
1477
|
-
|
|
1478
|
-
let decimals = decimalsGlobal;
|
|
1717
|
+
function roundTo(num = 0, decimals = 3) {
|
|
1718
|
+
if (decimals < 0) return num;
|
|
1719
|
+
// Normal integer rounding
|
|
1720
|
+
if (!decimals) return Math.round(num);
|
|
1479
1721
|
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
let {values} = com;
|
|
1722
|
+
// stepped rounding
|
|
1723
|
+
let intPart = Math.floor(decimals);
|
|
1483
1724
|
|
|
1484
|
-
|
|
1485
|
-
|
|
1725
|
+
if (intPart !== decimals) {
|
|
1726
|
+
let f = +(decimals - intPart).toFixed(2);
|
|
1727
|
+
f = f > 0.5 ? (Math.floor((f) / 0.5) * 0.5) : f;
|
|
1486
1728
|
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
}
|
|
1729
|
+
let step = 10 ** -intPart * f;
|
|
1730
|
+
return +(Math.round(num / step) * step).toFixed(8);
|
|
1490
1731
|
}
|
|
1491
1732
|
|
|
1492
|
-
|
|
1733
|
+
let factor = 10 ** decimals;
|
|
1734
|
+
return Math.round(num * factor) / factor;
|
|
1493
1735
|
}
|
|
1494
1736
|
|
|
1495
1737
|
/**
|
|
@@ -2299,9 +2541,9 @@ function combineCubicPairs(com1, com2, {
|
|
|
2299
2541
|
let comS = getExtrapolatedCommand(com1, com2, t);
|
|
2300
2542
|
|
|
2301
2543
|
// test new point-at-t against original mid segment starting point
|
|
2302
|
-
let
|
|
2544
|
+
let ptI = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t);
|
|
2303
2545
|
|
|
2304
|
-
let dist0 = getDistManhattan(com1.p,
|
|
2546
|
+
let dist0 = getDistManhattan(com1.p, ptI);
|
|
2305
2547
|
let dist1 = 0, dist2 = 0;
|
|
2306
2548
|
let close = dist0 < maxDist;
|
|
2307
2549
|
let success = false;
|
|
@@ -2316,29 +2558,40 @@ function combineCubicPairs(com1, com2, {
|
|
|
2316
2558
|
* to prevent distortions
|
|
2317
2559
|
*/
|
|
2318
2560
|
|
|
2319
|
-
//
|
|
2320
|
-
let
|
|
2561
|
+
// 1st segment mid
|
|
2562
|
+
let ptM_seg1 = pointAtT([com1.p0, com1.cp1, com1.cp2, com1.p], 0.5);
|
|
2321
2563
|
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
let
|
|
2325
|
-
dist1 = getDistManhattan(
|
|
2564
|
+
let t2 = t * 0.5;
|
|
2565
|
+
// combined interpolated mid point
|
|
2566
|
+
let ptI_seg1 = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t2);
|
|
2567
|
+
dist1 = getDistManhattan(ptM_seg1, ptI_seg1);
|
|
2326
2568
|
|
|
2327
2569
|
error += dist1;
|
|
2328
2570
|
|
|
2329
2571
|
if (dist1 < maxDist) {
|
|
2330
2572
|
|
|
2331
|
-
//
|
|
2332
|
-
let
|
|
2573
|
+
// 2nd segment mid
|
|
2574
|
+
let ptM_seg2 = pointAtT([com2.p0, com2.cp1, com2.cp2, com2.p], 0.5);
|
|
2333
2575
|
|
|
2334
|
-
|
|
2335
|
-
let
|
|
2336
|
-
|
|
2576
|
+
// simplified path
|
|
2577
|
+
let t3 = (1 + t) * 0.5;
|
|
2578
|
+
let ptI_seg2 = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t3);
|
|
2579
|
+
dist2 = getDistManhattan(ptM_seg2, ptI_seg2);
|
|
2337
2580
|
|
|
2338
2581
|
error += dist2;
|
|
2339
2582
|
|
|
2340
2583
|
if (error < maxDist) success = true;
|
|
2341
2584
|
|
|
2585
|
+
/*
|
|
2586
|
+
renderPoint(markers, ptM_seg1, 'cyan')
|
|
2587
|
+
renderPoint(markers, pt, 'orange', '1.5%', '1')
|
|
2588
|
+
renderPoint(markers, ptM_seg2, 'orange')
|
|
2589
|
+
|
|
2590
|
+
renderPoint(markers, com1.p, 'green')
|
|
2591
|
+
|
|
2592
|
+
renderPoint(markers, ptI_seg1, 'purple')
|
|
2593
|
+
*/
|
|
2594
|
+
|
|
2342
2595
|
}
|
|
2343
2596
|
|
|
2344
2597
|
} // end 1st try
|
|
@@ -2503,6 +2756,7 @@ function analyzePathData(pathData = [], {
|
|
|
2503
2756
|
let com = pathData[c - 1];
|
|
2504
2757
|
let { type, values, p0, p, cp1 = null, cp2 = null, squareDist = 0, cptArea = 0, dimA = 0 } = com;
|
|
2505
2758
|
|
|
2759
|
+
let comPrev = pathData[c-2];
|
|
2506
2760
|
let comN = pathData[c] || null;
|
|
2507
2761
|
|
|
2508
2762
|
// init properties
|
|
@@ -2521,6 +2775,7 @@ function analyzePathData(pathData = [], {
|
|
|
2521
2775
|
|
|
2522
2776
|
// bezier types
|
|
2523
2777
|
let isBezier = type === 'Q' || type === 'C';
|
|
2778
|
+
let isArc = type === 'A';
|
|
2524
2779
|
let isBezierN = comN && (comN.type === 'Q' || comN.type === 'C');
|
|
2525
2780
|
|
|
2526
2781
|
/**
|
|
@@ -2567,6 +2822,22 @@ function analyzePathData(pathData = [], {
|
|
|
2567
2822
|
}
|
|
2568
2823
|
}
|
|
2569
2824
|
|
|
2825
|
+
// check extremes introduce by small arcs
|
|
2826
|
+
else if(isArc && comN && ((comPrev.type==='C' || comPrev.type==='Q') || (comN.type==='C' || comN.type==='Q')) ){
|
|
2827
|
+
let distN = comN ? comN.dimA : 0;
|
|
2828
|
+
let isShort = com.dimA < (comPrev.dimA + distN) * 0.1;
|
|
2829
|
+
let smallRadius = com.values[0] === com.values[1] && (com.values[0] < 1);
|
|
2830
|
+
|
|
2831
|
+
if(isShort && smallRadius){
|
|
2832
|
+
let bb = getPolyBBox([comPrev.p0, comN.p]);
|
|
2833
|
+
if(p.x>bb.right || p.x<bb.x || p.y<bb.y || p.y>bb.bottom){
|
|
2834
|
+
hasExtremes = true;
|
|
2835
|
+
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2570
2841
|
if (hasExtremes) com.extreme = true;
|
|
2571
2842
|
|
|
2572
2843
|
// Corners and semi extremes
|
|
@@ -3120,50 +3391,10 @@ function parsePathDataString(d, debug = true, limit=0) {
|
|
|
3120
3391
|
|
|
3121
3392
|
}
|
|
3122
3393
|
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
toLonghands = true,
|
|
3128
|
-
|
|
3129
|
-
// not necessary unless you need cubics only
|
|
3130
|
-
quadraticToCubic = false,
|
|
3131
|
-
|
|
3132
|
-
// mostly a fallback if arc calculations fail
|
|
3133
|
-
arcToCubic = false,
|
|
3134
|
-
// arc to cubic precision - adds more segments for better precision
|
|
3135
|
-
arcAccuracy = 4,
|
|
3136
|
-
} = {}
|
|
3137
|
-
) {
|
|
3138
|
-
|
|
3139
|
-
// is already array
|
|
3140
|
-
let isArray = Array.isArray(d);
|
|
3141
|
-
|
|
3142
|
-
// normalize native pathData to regular array
|
|
3143
|
-
let hasConstructor = isArray && typeof d[0] === 'object' && typeof d[0].constructor === 'function';
|
|
3144
|
-
/*
|
|
3145
|
-
if (hasConstructor) {
|
|
3146
|
-
d = d.map(com => { return { type: com.type, values: com.values } })
|
|
3147
|
-
console.log('hasConstructor', hasConstructor, (typeof d[0].constructor), d);
|
|
3148
|
-
}
|
|
3149
|
-
*/
|
|
3150
|
-
|
|
3151
|
-
let pathDataObj = isArray ? d : parsePathDataString(d);
|
|
3152
|
-
|
|
3153
|
-
let { hasRelatives = true, hasShorthands = true, hasQuadratics = true, hasArcs = true } = pathDataObj;
|
|
3154
|
-
let pathData = hasConstructor ? pathDataObj : pathDataObj.pathData;
|
|
3155
|
-
|
|
3156
|
-
// normalize
|
|
3157
|
-
pathData = normalizePathData(pathData,
|
|
3158
|
-
{
|
|
3159
|
-
toAbsolute, toLonghands, quadraticToCubic, arcToCubic, arcAccuracy,
|
|
3160
|
-
hasRelatives, hasShorthands, hasQuadratics, hasArcs
|
|
3161
|
-
},
|
|
3162
|
-
);
|
|
3163
|
-
|
|
3164
|
-
return pathData;
|
|
3165
|
-
}
|
|
3166
|
-
|
|
3394
|
+
/**
|
|
3395
|
+
* wrapper function for
|
|
3396
|
+
* all path data conversion
|
|
3397
|
+
*/
|
|
3167
3398
|
function convertPathData(pathData, {
|
|
3168
3399
|
toShorthands = true,
|
|
3169
3400
|
toLonghands = false,
|
|
@@ -3215,22 +3446,24 @@ function convertPathData(pathData, {
|
|
|
3215
3446
|
|
|
3216
3447
|
if (hasQuadratics && quadraticToCubic) pathData = pathDataQuadraticToCubic(pathData);
|
|
3217
3448
|
|
|
3218
|
-
if(toMixed) toRelative = true;
|
|
3449
|
+
if (toMixed) toRelative = true;
|
|
3219
3450
|
|
|
3220
3451
|
// pre round - before relative conversion to minimize distortions
|
|
3221
3452
|
if (decimals > -1 && toRelative) pathData = roundPathData(pathData, decimals);
|
|
3222
3453
|
|
|
3223
3454
|
// clone absolute pathdata
|
|
3224
|
-
if(toMixed){
|
|
3455
|
+
if (toMixed) {
|
|
3225
3456
|
pathDataAbs = JSON.parse(JSON.stringify(pathData));
|
|
3226
3457
|
}
|
|
3227
3458
|
|
|
3228
3459
|
if (toRelative) pathData = pathDataToRelative(pathData);
|
|
3460
|
+
|
|
3461
|
+
// final rounding
|
|
3229
3462
|
if (decimals > -1) pathData = roundPathData(pathData, decimals);
|
|
3230
3463
|
|
|
3231
3464
|
// choose most compact commands: relative or absolute
|
|
3232
|
-
if(toMixed){
|
|
3233
|
-
for(let i=0; i<pathData.length; i++){
|
|
3465
|
+
if (toMixed) {
|
|
3466
|
+
for (let i = 0; i < pathData.length; i++) {
|
|
3234
3467
|
let com = pathData[i];
|
|
3235
3468
|
let comA = pathDataAbs[i];
|
|
3236
3469
|
// compare Lengths
|
|
@@ -3240,7 +3473,7 @@ function convertPathData(pathData, {
|
|
|
3240
3473
|
let lenR = comStr.length;
|
|
3241
3474
|
let lenA = comStrA.length;
|
|
3242
3475
|
|
|
3243
|
-
if(lenA<lenR){
|
|
3476
|
+
if (lenA < lenR) {
|
|
3244
3477
|
|
|
3245
3478
|
pathData[i] = pathDataAbs[i];
|
|
3246
3479
|
}
|
|
@@ -3250,6 +3483,50 @@ function convertPathData(pathData, {
|
|
|
3250
3483
|
return pathData
|
|
3251
3484
|
}
|
|
3252
3485
|
|
|
3486
|
+
function parsePathDataNormalized(d,
|
|
3487
|
+
{
|
|
3488
|
+
// necessary for most calculations
|
|
3489
|
+
toAbsolute = true,
|
|
3490
|
+
toLonghands = true,
|
|
3491
|
+
|
|
3492
|
+
// not necessary unless you need cubics only
|
|
3493
|
+
quadraticToCubic = false,
|
|
3494
|
+
|
|
3495
|
+
// mostly a fallback if arc calculations fail
|
|
3496
|
+
arcToCubic = false,
|
|
3497
|
+
// arc to cubic precision - adds more segments for better precision
|
|
3498
|
+
arcAccuracy = 4,
|
|
3499
|
+
} = {}
|
|
3500
|
+
) {
|
|
3501
|
+
|
|
3502
|
+
// is already array
|
|
3503
|
+
let isArray = Array.isArray(d);
|
|
3504
|
+
|
|
3505
|
+
// normalize native pathData to regular array
|
|
3506
|
+
let hasConstructor = isArray && typeof d[0] === 'object' && typeof d[0].constructor === 'function';
|
|
3507
|
+
/*
|
|
3508
|
+
if (hasConstructor) {
|
|
3509
|
+
d = d.map(com => { return { type: com.type, values: com.values } })
|
|
3510
|
+
console.log('hasConstructor', hasConstructor, (typeof d[0].constructor), d);
|
|
3511
|
+
}
|
|
3512
|
+
*/
|
|
3513
|
+
|
|
3514
|
+
let pathDataObj = isArray ? d : parsePathDataString(d);
|
|
3515
|
+
|
|
3516
|
+
let { hasRelatives = true, hasShorthands = true, hasQuadratics = true, hasArcs = true } = pathDataObj;
|
|
3517
|
+
let pathData = hasConstructor ? pathDataObj : pathDataObj.pathData;
|
|
3518
|
+
|
|
3519
|
+
// normalize
|
|
3520
|
+
pathData = normalizePathData(pathData,
|
|
3521
|
+
{
|
|
3522
|
+
toAbsolute, toLonghands, quadraticToCubic, arcToCubic, arcAccuracy,
|
|
3523
|
+
hasRelatives, hasShorthands, hasQuadratics, hasArcs
|
|
3524
|
+
},
|
|
3525
|
+
);
|
|
3526
|
+
|
|
3527
|
+
return pathData;
|
|
3528
|
+
}
|
|
3529
|
+
|
|
3253
3530
|
/**
|
|
3254
3531
|
*
|
|
3255
3532
|
* @param {*} pathData
|
|
@@ -3257,49 +3534,89 @@ function convertPathData(pathData, {
|
|
|
3257
3534
|
*/
|
|
3258
3535
|
|
|
3259
3536
|
function optimizeArcPathData(pathData = []) {
|
|
3537
|
+
let l = pathData.length;
|
|
3538
|
+
let pathDataN = [];
|
|
3260
3539
|
|
|
3261
|
-
let
|
|
3262
|
-
|
|
3263
|
-
pathData.forEach((com, i) => {
|
|
3540
|
+
for (let i = 0; i < l; i++) {
|
|
3541
|
+
let com = pathData[i];
|
|
3264
3542
|
let { type, values } = com;
|
|
3265
|
-
if (type === 'A') {
|
|
3266
|
-
let [rx, ry, largeArc, x, y] = [values[0], values[1], values[3], values[5], values[6]];
|
|
3267
|
-
let comPrev = pathData[i - 1];
|
|
3268
|
-
let [x0, y0] = [comPrev.values[comPrev.values.length - 2], comPrev.values[comPrev.values.length - 1]];
|
|
3269
|
-
let M = { x: x0, y: y0 };
|
|
3270
|
-
let p = { x, y };
|
|
3271
3543
|
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3544
|
+
if (type !== 'A') {
|
|
3545
|
+
pathDataN.push(com);
|
|
3546
|
+
continue
|
|
3547
|
+
}
|
|
3275
3548
|
|
|
3276
|
-
|
|
3549
|
+
let [rx, ry, largeArc, x, y] = [values[0], values[1], values[3], values[5], values[6]];
|
|
3550
|
+
let comPrev = pathData[i - 1];
|
|
3551
|
+
let [x0, y0] = [comPrev.values[comPrev.values.length - 2], comPrev.values[comPrev.values.length - 1]];
|
|
3552
|
+
let M = { x: x0, y: y0 };
|
|
3553
|
+
let p = { x, y };
|
|
3277
3554
|
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3555
|
+
if (rx === 0 || ry === 0) {
|
|
3556
|
+
pathData[i] = null;
|
|
3557
|
+
}
|
|
3281
3558
|
|
|
3282
|
-
|
|
3283
|
-
|
|
3559
|
+
// test for elliptic
|
|
3560
|
+
let rat = rx / ry;
|
|
3561
|
+
let error = rx !== ry ? Math.abs(1 - rat) : 0;
|
|
3284
3562
|
|
|
3285
|
-
|
|
3286
|
-
let pMid = interpolate(M, p, 0.5);
|
|
3287
|
-
let distM = getDistance(pMid, M);
|
|
3288
|
-
let rDiff = Math.abs(distM - rx) / rx;
|
|
3563
|
+
if (error > 0.01) {
|
|
3289
3564
|
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3565
|
+
pathDataN.push(com);
|
|
3566
|
+
continue
|
|
3567
|
+
|
|
3568
|
+
}
|
|
3569
|
+
|
|
3570
|
+
// xAxis rotation is futile for circular arcs - reset
|
|
3571
|
+
com.values[2] = 0;
|
|
3572
|
+
|
|
3573
|
+
/**
|
|
3574
|
+
* test semi circles
|
|
3575
|
+
* rx and ry are large enough
|
|
3576
|
+
*/
|
|
3577
|
+
|
|
3578
|
+
// 1. horizontal or vertical
|
|
3579
|
+
let thresh = getDistManhattan(M, p) * 0.001;
|
|
3580
|
+
let diffX = Math.abs(x - x0);
|
|
3581
|
+
let diffY = Math.abs(y - y0);
|
|
3582
|
+
|
|
3583
|
+
let isHorizontal = diffY < thresh;
|
|
3584
|
+
let isVertical = diffX < thresh;
|
|
3585
|
+
|
|
3586
|
+
// minify rx and ry
|
|
3587
|
+
if (isHorizontal || isVertical) {
|
|
3588
|
+
|
|
3589
|
+
// check if semi circle
|
|
3590
|
+
let needsTrueR = isHorizontal ? rx*1.9 > diffX : ry*1.9 > diffY;
|
|
3591
|
+
|
|
3592
|
+
// is semicircle we can simplify rx
|
|
3593
|
+
if (!needsTrueR) {
|
|
3594
|
+
|
|
3595
|
+
rx = rx >= 1 ? 1 : (rx > 0.5 ? 0.5 : rx);
|
|
3297
3596
|
}
|
|
3597
|
+
|
|
3598
|
+
com.values[0] = rx;
|
|
3599
|
+
com.values[1] = rx;
|
|
3600
|
+
pathDataN.push(com);
|
|
3601
|
+
continue
|
|
3602
|
+
|
|
3298
3603
|
}
|
|
3299
|
-
});
|
|
3300
3604
|
|
|
3301
|
-
|
|
3302
|
-
|
|
3605
|
+
// 2. get true radius - if rx ~= diameter/distance we have a semicircle
|
|
3606
|
+
let r = getDistance(M, p) * 0.5;
|
|
3607
|
+
error = rx / r;
|
|
3608
|
+
|
|
3609
|
+
if (error < 0.5) {
|
|
3610
|
+
rx = r >= 1 ? 1 : (r > 0.5 ? 0.5 : r);
|
|
3611
|
+
}
|
|
3612
|
+
|
|
3613
|
+
com.values[0] = rx;
|
|
3614
|
+
com.values[1] = rx;
|
|
3615
|
+
pathDataN.push(com);
|
|
3616
|
+
|
|
3617
|
+
}
|
|
3618
|
+
|
|
3619
|
+
return pathDataN;
|
|
3303
3620
|
}
|
|
3304
3621
|
|
|
3305
3622
|
/**
|
|
@@ -3323,52 +3640,11 @@ function normalizePathData(pathData = [],
|
|
|
3323
3640
|
return convertPathData(pathData, { toAbsolute, toLonghands, quadraticToCubic, arcToCubic, arcAccuracy, hasRelatives, hasShorthands, hasQuadratics, hasArcs, testTypes, decimals: -1 })
|
|
3324
3641
|
}
|
|
3325
3642
|
|
|
3326
|
-
|
|
3327
|
-
export function normalizePathData(pathData = [],
|
|
3328
|
-
{
|
|
3329
|
-
toAbsolute = true,
|
|
3330
|
-
toLonghands = true,
|
|
3331
|
-
quadraticToCubic = false,
|
|
3332
|
-
arcToCubic = false,
|
|
3333
|
-
arcAccuracy = 2,
|
|
3334
|
-
|
|
3335
|
-
// assume we need full normalization
|
|
3336
|
-
hasRelatives = true, hasShorthands = true, hasQuadratics = true, hasArcs = true, testTypes = false
|
|
3643
|
+
function revertCubicQuadratic(p0 = {}, cp1 = {}, cp2 = {}, p = {}, tolerance = 1) {
|
|
3337
3644
|
|
|
3338
|
-
|
|
3339
|
-
)
|
|
3340
|
-
|
|
3341
|
-
// pathdata properties - test= true adds a manual test
|
|
3342
|
-
if (testTypes) {
|
|
3343
|
-
|
|
3344
|
-
let commands = Array.from(new Set(pathData.map(com => com.type))).join('');
|
|
3345
|
-
hasRelatives = /[lcqamts]/gi.test(commands);
|
|
3346
|
-
hasQuadratics = /[qt]/gi.test(commands);
|
|
3347
|
-
hasArcs = /[a]/gi.test(commands);
|
|
3348
|
-
hasShorthands = /[vhst]/gi.test(commands);
|
|
3349
|
-
isPoly = /[mlz]/gi.test(commands);
|
|
3350
|
-
}
|
|
3351
|
-
|
|
3352
|
-
if ((hasQuadratics && quadraticToCubic) || (hasArcs && arcToCubic)) {
|
|
3353
|
-
toLonghands = true
|
|
3354
|
-
toAbsolute = true
|
|
3355
|
-
}
|
|
3356
|
-
|
|
3357
|
-
if (hasRelatives && toAbsolute) pathData = pathDataToAbsoluteOrRelative(pathData, false);
|
|
3358
|
-
if (hasShorthands && toLonghands) pathData = pathDataToLonghands(pathData, -1, false);
|
|
3359
|
-
if (hasArcs && arcToCubic) pathData = pathDataArcsToCubics(pathData, arcAccuracy);
|
|
3360
|
-
if (hasQuadratics && quadraticToCubic) pathData = pathDataQuadraticToCubic(pathData);
|
|
3361
|
-
|
|
3362
|
-
return pathData;
|
|
3363
|
-
|
|
3364
|
-
}
|
|
3365
|
-
*/
|
|
3366
|
-
|
|
3367
|
-
function revertCubicQuadratic(p0 = {}, cp1 = {}, cp2 = {}, p = {}, tolerance = 1) {
|
|
3368
|
-
|
|
3369
|
-
// test if cubic can be simplified to quadratic
|
|
3370
|
-
let cp1X = interpolate(p0, cp1, 1.5);
|
|
3371
|
-
let cp2X = interpolate(p, cp2, 1.5);
|
|
3645
|
+
// test if cubic can be simplified to quadratic
|
|
3646
|
+
let cp1X = interpolate(p0, cp1, 1.5);
|
|
3647
|
+
let cp2X = interpolate(p, cp2, 1.5);
|
|
3372
3648
|
|
|
3373
3649
|
let dist0 = getDistManhattan(p0, p);
|
|
3374
3650
|
let threshold = dist0 * 0.01 * tolerance;
|
|
@@ -4243,7 +4519,7 @@ function pathDataToTopLeft(pathData) {
|
|
|
4243
4519
|
let { type, values } = com;
|
|
4244
4520
|
let valsLen = values.length;
|
|
4245
4521
|
if (valsLen) {
|
|
4246
|
-
let p = { type: type, x: values[valsLen-2], y: values[valsLen-1], index: 0};
|
|
4522
|
+
let p = { type: type, x: values[valsLen - 2], y: values[valsLen - 1], index: 0 };
|
|
4247
4523
|
p.index = i;
|
|
4248
4524
|
indices.push(p);
|
|
4249
4525
|
}
|
|
@@ -4251,113 +4527,111 @@ function pathDataToTopLeft(pathData) {
|
|
|
4251
4527
|
|
|
4252
4528
|
// reorder to top left most
|
|
4253
4529
|
|
|
4254
|
-
indices = indices.sort((a, b) => +a.y.toFixed(8) - +b.y.toFixed(8) || a.x-b.x
|
|
4530
|
+
indices = indices.sort((a, b) => +a.y.toFixed(8) - +b.y.toFixed(8) || a.x - b.x);
|
|
4255
4531
|
newIndex = indices[0].index;
|
|
4256
4532
|
|
|
4257
|
-
return
|
|
4533
|
+
return newIndex ? shiftSvgStartingPoint(pathData, newIndex) : pathData;
|
|
4258
4534
|
}
|
|
4259
4535
|
|
|
4260
|
-
function optimizeClosePath(pathData, {removeFinalLineto = true, autoClose = true}={}) {
|
|
4536
|
+
function optimizeClosePath(pathData, { removeFinalLineto = true, autoClose = true } = {}) {
|
|
4261
4537
|
|
|
4262
|
-
let
|
|
4538
|
+
let pathDataN = pathData;
|
|
4263
4539
|
let l = pathData.length;
|
|
4264
4540
|
let M = { x: +pathData[0].values[0].toFixed(8), y: +pathData[0].values[1].toFixed(8) };
|
|
4265
4541
|
let isClosed = pathData[l - 1].type.toLowerCase() === 'z';
|
|
4266
4542
|
|
|
4267
|
-
let
|
|
4268
|
-
|
|
4269
|
-
// check if order is ideal
|
|
4270
|
-
let idxPenultimate = isClosed ? l-2 : l-1;
|
|
4543
|
+
let hasLinetos = false;
|
|
4271
4544
|
|
|
4545
|
+
// check if path is closed by explicit lineto
|
|
4546
|
+
let idxPenultimate = isClosed ? l - 2 : l - 1;
|
|
4272
4547
|
let penultimateCom = pathData[idxPenultimate];
|
|
4273
4548
|
let penultimateType = penultimateCom.type;
|
|
4274
4549
|
let penultimateComCoords = penultimateCom.values.slice(-2).map(val => +val.toFixed(8));
|
|
4275
4550
|
|
|
4276
4551
|
// last L command ends at M
|
|
4277
|
-
let
|
|
4278
|
-
|
|
4279
|
-
// add closepath Z to enable order optimizations
|
|
4280
|
-
if(!isClosed && autoClose && isClosingCommand){
|
|
4552
|
+
let hasClosingCommand = penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y;
|
|
4553
|
+
let lastIsLine = penultimateType === 'L';
|
|
4281
4554
|
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
let
|
|
4286
|
-
|
|
4287
|
-
pathData[idxPenultimate].values[valsLastLen-1] = M.y
|
|
4288
|
-
*/
|
|
4289
|
-
|
|
4290
|
-
pathData.push({type:'Z', values:[]});
|
|
4291
|
-
isClosed = true;
|
|
4292
|
-
l++;
|
|
4293
|
-
}
|
|
4555
|
+
// create index
|
|
4556
|
+
let indices = [];
|
|
4557
|
+
for (let i = 0; i < l; i++) {
|
|
4558
|
+
let com = pathData[i];
|
|
4559
|
+
let { type, values, p0, p } = com;
|
|
4294
4560
|
|
|
4295
|
-
|
|
4296
|
-
let skipReorder = pathData[1].type !== 'L' && (!isClosingCommand || penultimateCom.type === 'L');
|
|
4297
|
-
skipReorder = false;
|
|
4561
|
+
if(type==='L') hasLinetos = true;
|
|
4298
4562
|
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
}
|
|
4563
|
+
// exclude Z
|
|
4564
|
+
if (values.length) {
|
|
4565
|
+
values.slice(-2);
|
|
4303
4566
|
|
|
4304
|
-
|
|
4567
|
+
let x = Math.min(p0.x, p.x);
|
|
4568
|
+
let y = Math.min(p0.y, p.y);
|
|
4305
4569
|
|
|
4306
|
-
|
|
4570
|
+
let prevCom = pathData[i - 1] ? pathData[i - 1] : pathData[idxPenultimate];
|
|
4571
|
+
let prevType = prevCom.type;
|
|
4307
4572
|
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
let { type, values } = com;
|
|
4312
|
-
if (values.length) {
|
|
4313
|
-
let valsL = values.slice(-2);
|
|
4314
|
-
let prevL = pathData[i - 1] && pathData[i - 1].type === 'L';
|
|
4315
|
-
let nextL = pathData[i + 1] && pathData[i + 1].type === 'L';
|
|
4316
|
-
let prevCom = pathData[i - 1] ? pathData[i - 1].type.toUpperCase() : null;
|
|
4317
|
-
let nextCom = pathData[i + 1] ? pathData[i + 1].type.toUpperCase() : null;
|
|
4318
|
-
let p = { type: type, x: valsL[0], y: valsL[1], dist: 0, index: 0, prevL, nextL, prevCom, nextCom };
|
|
4319
|
-
p.index = i;
|
|
4320
|
-
indices.push(p);
|
|
4321
|
-
}
|
|
4573
|
+
let item = { type: type, x, y, index: 0, prevType };
|
|
4574
|
+
item.index = i;
|
|
4575
|
+
indices.push(item);
|
|
4322
4576
|
}
|
|
4323
4577
|
|
|
4324
|
-
|
|
4578
|
+
}
|
|
4325
4579
|
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4580
|
+
let xMin = Infinity;
|
|
4581
|
+
let yMin = Infinity;
|
|
4582
|
+
let idx_top = null;
|
|
4583
|
+
let len = indices.length;
|
|
4329
4584
|
|
|
4330
|
-
|
|
4585
|
+
for (let i = 0; i < len; i++) {
|
|
4586
|
+
let com = indices[i];
|
|
4587
|
+
let { type, index, x, y, prevType } = com;
|
|
4331
4588
|
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4589
|
+
if (hasLinetos && prevType === 'L') {
|
|
4590
|
+
if (x < xMin && y < yMin) {
|
|
4591
|
+
idx_top = index-1;
|
|
4592
|
+
}
|
|
4593
|
+
|
|
4594
|
+
if (y < yMin) {
|
|
4595
|
+
yMin = y;
|
|
4596
|
+
}
|
|
4338
4597
|
|
|
4339
|
-
|
|
4340
|
-
|
|
4598
|
+
if (x < xMin) {
|
|
4599
|
+
xMin = x;
|
|
4600
|
+
}
|
|
4601
|
+
}
|
|
4341
4602
|
}
|
|
4342
4603
|
|
|
4343
|
-
|
|
4604
|
+
// shift to better starting point
|
|
4605
|
+
if (idx_top) {
|
|
4606
|
+
pathDataN = shiftSvgStartingPoint(pathDataN, idx_top);
|
|
4344
4607
|
|
|
4345
|
-
|
|
4608
|
+
// update penultimate - reorder might have added new close paths
|
|
4609
|
+
l = pathDataN.length;
|
|
4610
|
+
M = { x: +pathDataN[0].values[0].toFixed(8), y: +pathDataN[0].values[1].toFixed(8) };
|
|
4611
|
+
|
|
4612
|
+
idxPenultimate = isClosed ? l - 2 : l - 1;
|
|
4613
|
+
penultimateCom = pathDataN[idxPenultimate];
|
|
4614
|
+
penultimateType = penultimateCom.type;
|
|
4615
|
+
penultimateComCoords = penultimateCom.values.slice(-2).map(val => +val.toFixed(8));
|
|
4616
|
+
lastIsLine = penultimateType ==='L';
|
|
4346
4617
|
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
penultimateType = penultimateCom.type;
|
|
4350
|
-
penultimateComCoords = penultimateCom.values.slice(-2).map(val=>+val.toFixed(8));
|
|
4618
|
+
// last L command ends at M
|
|
4619
|
+
hasClosingCommand = penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y;
|
|
4351
4620
|
|
|
4352
|
-
|
|
4621
|
+
}
|
|
4353
4622
|
|
|
4354
|
-
|
|
4355
|
-
|
|
4623
|
+
// remove unnecessary closing lineto
|
|
4624
|
+
if (removeFinalLineto && hasClosingCommand && lastIsLine) {
|
|
4625
|
+
pathDataN.splice(l - 2, 1);
|
|
4356
4626
|
}
|
|
4357
4627
|
|
|
4358
|
-
|
|
4628
|
+
// add close path
|
|
4629
|
+
if (autoClose && !isClosed && hasClosingCommand) {
|
|
4630
|
+
pathDataN.push({ type: 'Z', values: [] });
|
|
4631
|
+
}
|
|
4632
|
+
|
|
4633
|
+
return pathDataN
|
|
4359
4634
|
|
|
4360
|
-
return pathDataNew
|
|
4361
4635
|
}
|
|
4362
4636
|
|
|
4363
4637
|
/**
|
|
@@ -4567,8 +4841,130 @@ function refineAdjacentExtremes(pathData, {
|
|
|
4567
4841
|
|
|
4568
4842
|
}
|
|
4569
4843
|
|
|
4844
|
+
function getArcFromPoly(pts, precise = false) {
|
|
4845
|
+
if (pts.length < 3) return false
|
|
4846
|
+
|
|
4847
|
+
// Pick 3 well-spaced points
|
|
4848
|
+
let len = pts.length;
|
|
4849
|
+
let idx1 = Math.floor(len * 0.333);
|
|
4850
|
+
let idx2 = Math.floor(len * 0.666);
|
|
4851
|
+
let idx3 = Math.floor(len * 0.5);
|
|
4852
|
+
|
|
4853
|
+
let p1 = pts[0];
|
|
4854
|
+
let p2 = pts[idx3];
|
|
4855
|
+
let p3 = pts[len - 1];
|
|
4856
|
+
|
|
4857
|
+
// Radius (use start point)
|
|
4858
|
+
let pts1 = [p1, p2, p3];
|
|
4859
|
+
let centroid = getPolyArcCentroid(pts1);
|
|
4860
|
+
|
|
4861
|
+
let r = 0, deltaAngle = 0, startAngle = 0, endAngle = 0, angleData = {};
|
|
4862
|
+
|
|
4863
|
+
// check if radii are consistent
|
|
4864
|
+
if (precise) {
|
|
4865
|
+
|
|
4866
|
+
/**
|
|
4867
|
+
* check multiple centroids
|
|
4868
|
+
* if the polyline can be expressed as
|
|
4869
|
+
* an arc - all centroids should be close
|
|
4870
|
+
*/
|
|
4871
|
+
|
|
4872
|
+
if (len > 3) {
|
|
4873
|
+
let centroid1 = getPolyArcCentroid([p1, pts[idx1], p3]);
|
|
4874
|
+
let centroid2 = getPolyArcCentroid([p1, pts[idx2], p3]);
|
|
4875
|
+
|
|
4876
|
+
if (!centroid1 || !centroid2) return false;
|
|
4877
|
+
|
|
4878
|
+
let dist0 = getDistManhattan(centroid, p2);
|
|
4879
|
+
let dist1 = getDistManhattan(centroid, centroid1);
|
|
4880
|
+
let dist2 = getDistManhattan(centroid, centroid2);
|
|
4881
|
+
let errorCentroid = (dist1 + dist2);
|
|
4882
|
+
|
|
4883
|
+
// centroids diverging too much
|
|
4884
|
+
if (errorCentroid > dist0 * 0.05) {
|
|
4885
|
+
|
|
4886
|
+
return false
|
|
4887
|
+
}
|
|
4888
|
+
|
|
4889
|
+
}
|
|
4890
|
+
|
|
4891
|
+
let rSqMid = getSquareDistance(centroid, p2);
|
|
4892
|
+
|
|
4893
|
+
for (let i = 0; i < len; i++) {
|
|
4894
|
+
let pt = pts[i];
|
|
4895
|
+
let rSq = getSquareDistance(centroid, pt);
|
|
4896
|
+
let error = Math.abs(rSqMid - rSq) / rSqMid;
|
|
4897
|
+
|
|
4898
|
+
if (error > 0.0025) {
|
|
4899
|
+
/*
|
|
4900
|
+
console.log('error', error, len, idx1, idx2, idx3);
|
|
4901
|
+
renderPoint(markers, centroid, 'orange')
|
|
4902
|
+
renderPoint(markers, p1, 'green')
|
|
4903
|
+
renderPoint(markers, p2)
|
|
4904
|
+
renderPoint(markers, p3, 'purple')
|
|
4905
|
+
*/
|
|
4906
|
+
return false;
|
|
4907
|
+
}
|
|
4908
|
+
}
|
|
4909
|
+
|
|
4910
|
+
// calculate proper radius
|
|
4911
|
+
r = Math.sqrt(rSqMid);
|
|
4912
|
+
angleData = getDeltaAngle(centroid, p1, p3);
|
|
4913
|
+
({ deltaAngle, startAngle, endAngle } = angleData);
|
|
4914
|
+
|
|
4915
|
+
} else {
|
|
4916
|
+
r = getDistance(centroid, p1);
|
|
4917
|
+
angleData = getDeltaAngle(centroid, p1, p3);
|
|
4918
|
+
({ deltaAngle, startAngle, endAngle } = angleData);
|
|
4919
|
+
}
|
|
4920
|
+
|
|
4921
|
+
return {
|
|
4922
|
+
centroid,
|
|
4923
|
+
r,
|
|
4924
|
+
startAngle,
|
|
4925
|
+
endAngle,
|
|
4926
|
+
deltaAngle
|
|
4927
|
+
};
|
|
4928
|
+
}
|
|
4929
|
+
|
|
4930
|
+
function getPolyArcCentroid(pts = []) {
|
|
4931
|
+
|
|
4932
|
+
pts = pts.filter(pt => pt !== undefined);
|
|
4933
|
+
if (pts.length < 3) return false
|
|
4934
|
+
|
|
4935
|
+
let p1 = pts[0];
|
|
4936
|
+
let p2 = pts[Math.floor(pts.length / 2)];
|
|
4937
|
+
let p3 = pts[pts.length - 1];
|
|
4938
|
+
|
|
4939
|
+
let x1 = p1.x, y1 = p1.y;
|
|
4940
|
+
let x2 = p2.x, y2 = p2.y;
|
|
4941
|
+
let x3 = p3.x, y3 = p3.y;
|
|
4942
|
+
|
|
4943
|
+
let a = x1 - x2;
|
|
4944
|
+
let b = y1 - y2;
|
|
4945
|
+
let c = x1 - x3;
|
|
4946
|
+
let d = y1 - y3;
|
|
4947
|
+
|
|
4948
|
+
let e = ((x1 * x1 - x2 * x2) + (y1 * y1 - y2 * y2)) / 2;
|
|
4949
|
+
let f = ((x1 * x1 - x3 * x3) + (y1 * y1 - y3 * y3)) / 2;
|
|
4950
|
+
|
|
4951
|
+
let det = a * d - b * c;
|
|
4952
|
+
|
|
4953
|
+
// colinear points
|
|
4954
|
+
if (Math.abs(det) < 1e-10) {
|
|
4955
|
+
return false;
|
|
4956
|
+
}
|
|
4957
|
+
|
|
4958
|
+
// find center of arc
|
|
4959
|
+
let cx = (d * e - b * f) / det;
|
|
4960
|
+
let cy = (-c * e + a * f) / det;
|
|
4961
|
+
let centroid = { x: cx, y: cy };
|
|
4962
|
+
return centroid
|
|
4963
|
+
}
|
|
4964
|
+
|
|
4570
4965
|
function refineRoundedCorners(pathData, {
|
|
4571
4966
|
threshold = 0,
|
|
4967
|
+
simplifyQuadraticCorners = false,
|
|
4572
4968
|
tolerance = 1
|
|
4573
4969
|
} = {}) {
|
|
4574
4970
|
|
|
@@ -4593,6 +4989,9 @@ function refineRoundedCorners(pathData, {
|
|
|
4593
4989
|
let firstIsLine = pathData[1].type === 'L';
|
|
4594
4990
|
let firstIsBez = pathData[1].type === 'C';
|
|
4595
4991
|
|
|
4992
|
+
// in case we have simplified a corner connecting to the start
|
|
4993
|
+
let M_adj = null;
|
|
4994
|
+
|
|
4596
4995
|
let normalizeClose = isClosed && firstIsBez && (lastIsLine || zIsLineto);
|
|
4597
4996
|
|
|
4598
4997
|
// normalize closepath to lineto
|
|
@@ -4632,15 +5031,17 @@ function refineRoundedCorners(pathData, {
|
|
|
4632
5031
|
// closing corner to start
|
|
4633
5032
|
if (isClosed && lastIsBez && firstIsLine && i === l - lastOff - 1) {
|
|
4634
5033
|
comL1 = pathData[1];
|
|
5034
|
+
|
|
4635
5035
|
comBez = [pathData[l - lastOff]];
|
|
4636
5036
|
|
|
4637
5037
|
}
|
|
4638
5038
|
|
|
5039
|
+
// collect enclosed bezier segments
|
|
4639
5040
|
for (let j = i + 1; j < l; j++) {
|
|
4640
5041
|
let comN = pathData[j] ? pathData[j] : null;
|
|
4641
5042
|
let comPrev = pathData[j - 1];
|
|
4642
5043
|
|
|
4643
|
-
if (comPrev.type === 'C') {
|
|
5044
|
+
if (comPrev.type === 'C' && j > 2) {
|
|
4644
5045
|
comBez.push(comPrev);
|
|
4645
5046
|
}
|
|
4646
5047
|
|
|
@@ -4671,39 +5072,67 @@ function refineRoundedCorners(pathData, {
|
|
|
4671
5072
|
let bezThresh = len3 * 0.5 * tolerance;
|
|
4672
5073
|
let isSmall = bezThresh < len1 && bezThresh < len2;
|
|
4673
5074
|
|
|
5075
|
+
/*
|
|
5076
|
+
*/
|
|
5077
|
+
|
|
4674
5078
|
if (comBez.length && !signChange && isSmall) {
|
|
4675
5079
|
|
|
4676
|
-
let
|
|
5080
|
+
let isSquare = false;
|
|
5081
|
+
|
|
5082
|
+
if (comBez.length === 1) {
|
|
5083
|
+
let dx = Math.abs(comBez[0].p.x - comBez[0].p0.x);
|
|
5084
|
+
let dy = Math.abs(comBez[0].p.y - comBez[0].p0.y);
|
|
5085
|
+
let diff = (dx - dy);
|
|
5086
|
+
let rat = Math.abs(diff / dx);
|
|
5087
|
+
isSquare = rat < 0.01;
|
|
5088
|
+
}
|
|
5089
|
+
|
|
5090
|
+
let preferArcs = true;
|
|
5091
|
+
preferArcs = false;
|
|
5092
|
+
|
|
5093
|
+
// if rectangular prefer arcs
|
|
5094
|
+
if (preferArcs && isSquare) {
|
|
5095
|
+
|
|
5096
|
+
let pM = pointAtT([comBez[0].p0, comBez[0].cp1, comBez[0].cp2, comBez[0].p], 0.5);
|
|
5097
|
+
|
|
5098
|
+
let arcProps = getArcFromPoly([comBez[0].p0, pM, comBez[0].p]);
|
|
5099
|
+
let { r, centroid, deltaAngle } = arcProps;
|
|
5100
|
+
|
|
5101
|
+
let sweep = deltaAngle > 0 ? 1 : 0;
|
|
5102
|
+
|
|
5103
|
+
let largeArc = 0;
|
|
5104
|
+
|
|
5105
|
+
let comArc = { type: 'A', values: [r, r, 0, largeArc, sweep, comBez[0].p.x, comBez[0].p.y] };
|
|
5106
|
+
|
|
5107
|
+
pathDataN.push(comL0, comArc);
|
|
5108
|
+
i += offset;
|
|
5109
|
+
continue
|
|
5110
|
+
|
|
5111
|
+
}
|
|
5112
|
+
|
|
5113
|
+
let areaThresh = getSquareDistance(comBez[0].p0, comBez[0].p) * 0.005;
|
|
5114
|
+
let isFlatBezier = Math.abs(area2) < areaThresh;
|
|
5115
|
+
let isFlatBezier2 = Math.abs(area2) < areaThresh * 10;
|
|
5116
|
+
|
|
4677
5117
|
let ptQ = !isFlatBezier ? checkLineIntersection(comL0.p0, comL0.p, comL1.p, comL1.p0, false, true) : null;
|
|
4678
5118
|
|
|
4679
|
-
|
|
5119
|
+
// exit: is rather flat or has no intersection
|
|
5120
|
+
|
|
5121
|
+
if (!ptQ || (isFlatBezier2 && comBez.length === 1)) {
|
|
4680
5122
|
pathDataN.push(com);
|
|
4681
5123
|
continue
|
|
4682
5124
|
}
|
|
4683
5125
|
|
|
4684
|
-
// check sign change
|
|
5126
|
+
// check sign change - exit if present
|
|
4685
5127
|
if (ptQ) {
|
|
4686
5128
|
let area0 = getPolygonArea([comL0.p0, comL0.p, comL1.p0, comL1.p], false);
|
|
4687
5129
|
let area0_abs = Math.abs(area0);
|
|
4688
5130
|
let area1 = getPolygonArea([comL0.p0, comL0.p, ptQ, comL1.p0, comL1.p], false);
|
|
4689
5131
|
let area1_abs = Math.abs(area1);
|
|
4690
5132
|
let areaDiff = Math.abs(area0_abs - area1_abs) / area0_abs;
|
|
4691
|
-
|
|
4692
|
-
/*
|
|
4693
|
-
renderPoint(markers, comL0.p0, 'green', '0.5%', '0.5')
|
|
4694
|
-
renderPoint(markers, comL0.p, 'red', '1.5%', '0.5')
|
|
4695
|
-
renderPoint(markers, comL1.p0, 'blue', '0.5%', '0.5')
|
|
4696
|
-
renderPoint(markers, comL1.p, 'orange', '0.5%', '0.5')
|
|
4697
|
-
if(!area0) {
|
|
4698
|
-
pathDataN.push(com);
|
|
4699
|
-
continue
|
|
4700
|
-
}
|
|
4701
|
-
*/
|
|
4702
|
-
|
|
4703
5133
|
let signChange = area0 < 0 && area1 > 0 || area0 > 0 && area1 < 0;
|
|
4704
5134
|
|
|
4705
5135
|
if (!ptQ || signChange || areaDiff > 0.5) {
|
|
4706
|
-
|
|
4707
5136
|
pathDataN.push(com);
|
|
4708
5137
|
continue
|
|
4709
5138
|
}
|
|
@@ -4718,24 +5147,67 @@ function refineRoundedCorners(pathData, {
|
|
|
4718
5147
|
|
|
4719
5148
|
// not in tolerance – return original command
|
|
4720
5149
|
if (bezThresh && dist1 > bezThresh && dist1 > len3 * 0.3) {
|
|
4721
|
-
|
|
4722
5150
|
pathDataN.push(com);
|
|
4723
5151
|
continue;
|
|
4724
5152
|
|
|
4725
|
-
}
|
|
5153
|
+
}
|
|
4726
5154
|
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
comQ.cp1 = ptQ;
|
|
4730
|
-
comQ.p = comL1.p0;
|
|
5155
|
+
// return simplified quadratic Bézier command
|
|
5156
|
+
let p_Q = comL1.p0;
|
|
4731
5157
|
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
5158
|
+
// adjust previous end point to better fit the cubic curvature
|
|
5159
|
+
let adjustQ = !simplifyQuadraticCorners;
|
|
5160
|
+
|
|
5161
|
+
if (adjustQ) {
|
|
5162
|
+
|
|
5163
|
+
let t = 0.1666;
|
|
5164
|
+
let p0_adj = interpolate(ptQ, comL0.p, (1 + t));
|
|
5165
|
+
p_Q = interpolate(ptQ, comL1.p0, (1 + t));
|
|
5166
|
+
|
|
5167
|
+
// round for large enough segments
|
|
5168
|
+
let isH = ptQ.y===comL0.p.y;
|
|
5169
|
+
let isV = ptQ.x===comL0.p.x;
|
|
5170
|
+
let isH2 = ptQ.y===comL1.p0.y;
|
|
5171
|
+
let isV2 = ptQ.x===comL1.p0.x;
|
|
5172
|
+
|
|
5173
|
+
if(isSquare && com.dimA>3){
|
|
5174
|
+
let dec = 0.5;
|
|
5175
|
+
if(isH) p0_adj.x = roundTo(p0_adj.x, dec);
|
|
5176
|
+
if(isV) p0_adj.y = roundTo(p0_adj.y, dec);
|
|
5177
|
+
if(isH2) p_Q.x = roundTo(p_Q.x, dec);
|
|
5178
|
+
if(isV2) p_Q.y = roundTo(p_Q.y, dec);
|
|
5179
|
+
}
|
|
5180
|
+
|
|
5181
|
+
/*
|
|
5182
|
+
renderPoint(markers, p0_adj, 'orange')
|
|
5183
|
+
renderPoint(markers, p_Q, 'orange')
|
|
5184
|
+
renderPoint(markers, comL0.p, 'green')
|
|
5185
|
+
renderPoint(markers, comL1.p0, 'magenta')
|
|
5186
|
+
*/
|
|
5187
|
+
|
|
5188
|
+
// set new M starting point
|
|
5189
|
+
if (i === l - lastOff - 1) {
|
|
5190
|
+
|
|
5191
|
+
M_adj = p_Q;
|
|
5192
|
+
}
|
|
5193
|
+
|
|
5194
|
+
// adjust previous lineto end point
|
|
5195
|
+
comL0.values = [p0_adj.x, p0_adj.y];
|
|
5196
|
+
comL0.p = p0_adj;
|
|
4735
5197
|
|
|
4736
|
-
continue;
|
|
4737
5198
|
}
|
|
4738
5199
|
|
|
5200
|
+
let comQ = { type: 'Q', values: [ptQ.x, ptQ.y, p_Q.x, p_Q.y] };
|
|
5201
|
+
comQ.cp1 = ptQ;
|
|
5202
|
+
comQ.p0 = comL0.p;
|
|
5203
|
+
comQ.p = p_Q;
|
|
5204
|
+
|
|
5205
|
+
// add quadratic command
|
|
5206
|
+
pathDataN.push(comL0, comQ);
|
|
5207
|
+
|
|
5208
|
+
i += offset;
|
|
5209
|
+
continue;
|
|
5210
|
+
|
|
4739
5211
|
}
|
|
4740
5212
|
}
|
|
4741
5213
|
}
|
|
@@ -4749,6 +5221,12 @@ function refineRoundedCorners(pathData, {
|
|
|
4749
5221
|
|
|
4750
5222
|
}
|
|
4751
5223
|
|
|
5224
|
+
// correct starting point connecting with last corner rounding
|
|
5225
|
+
if (M_adj) {
|
|
5226
|
+
pathDataN[0].values = [M_adj.x, M_adj.y];
|
|
5227
|
+
pathDataN[0].p0 = M_adj;
|
|
5228
|
+
}
|
|
5229
|
+
|
|
4752
5230
|
// revert close path normalization
|
|
4753
5231
|
if (normalizeClose || (isClosed && pathDataN[pathDataN.length - 1].type !== 'Z')) {
|
|
4754
5232
|
pathDataN.push({ type: 'Z', values: [] });
|
|
@@ -4758,51 +5236,101 @@ function refineRoundedCorners(pathData, {
|
|
|
4758
5236
|
|
|
4759
5237
|
}
|
|
4760
5238
|
|
|
4761
|
-
function
|
|
4762
|
-
|
|
5239
|
+
function refineClosingCommand(pathData = [], {
|
|
5240
|
+
threshold = 0,
|
|
5241
|
+
} = {}) {
|
|
4763
5242
|
|
|
4764
|
-
|
|
4765
|
-
let
|
|
4766
|
-
let
|
|
4767
|
-
let
|
|
5243
|
+
let l = pathData.length;
|
|
5244
|
+
let comLast = pathData[l - 1];
|
|
5245
|
+
let isClosed = comLast.type.toLowerCase() === 'z';
|
|
5246
|
+
let idxPenultimate = isClosed ? l - 2 : l - 1;
|
|
5247
|
+
let comPenultimate = isClosed ? pathData[idxPenultimate] : pathData[idxPenultimate];
|
|
5248
|
+
let valsPen = comPenultimate.values.slice(-2);
|
|
4768
5249
|
|
|
4769
|
-
let
|
|
4770
|
-
let
|
|
4771
|
-
let
|
|
5250
|
+
let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
|
|
5251
|
+
let pPen = { x: valsPen[0], y: valsPen[1] };
|
|
5252
|
+
let dist = getDistAv(M, pPen);
|
|
4772
5253
|
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
let c = x1 - x3;
|
|
4776
|
-
let d = y1 - y3;
|
|
5254
|
+
// adjust last coordinates for better reordering
|
|
5255
|
+
if (dist && dist < threshold) {
|
|
4777
5256
|
|
|
4778
|
-
|
|
4779
|
-
|
|
5257
|
+
let valsLast = pathData[idxPenultimate].values;
|
|
5258
|
+
let valsLastLen = valsLast.length;
|
|
5259
|
+
pathData[idxPenultimate].values[valsLastLen - 2] = M.x;
|
|
5260
|
+
pathData[idxPenultimate].values[valsLastLen - 1] = M.y;
|
|
4780
5261
|
|
|
4781
|
-
|
|
5262
|
+
// adjust cpts
|
|
5263
|
+
let comFirst = pathData[1];
|
|
4782
5264
|
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
5265
|
+
if (comFirst.type === 'C' && comPenultimate.type === 'C') {
|
|
5266
|
+
let dx1 = Math.abs(comFirst.values[0] - comPenultimate.values[2]);
|
|
5267
|
+
let dy1 = Math.abs(comFirst.values[1] - comPenultimate.values[3]);
|
|
5268
|
+
|
|
5269
|
+
let dx2 = Math.abs(pathData[1].values[0] - comFirst.values[0]);
|
|
5270
|
+
let dy2 = Math.abs(pathData[1].values[1] - comFirst.values[1]);
|
|
5271
|
+
|
|
5272
|
+
let dx3 = Math.abs(pathData[1].values[0] - comPenultimate.values[2]);
|
|
5273
|
+
let dy3 = Math.abs(pathData[1].values[1] - comPenultimate.values[3]);
|
|
5274
|
+
|
|
5275
|
+
let ver = dx2 < threshold && dx3 < threshold && dy1;
|
|
5276
|
+
let hor = (dy2 < threshold && dy3 < threshold) && dx1;
|
|
5277
|
+
|
|
5278
|
+
if (dx1 && dx1 < threshold && ver) {
|
|
5279
|
+
|
|
5280
|
+
pathData[1].values[0] = M.x;
|
|
5281
|
+
pathData[idxPenultimate].values[2] = M.x;
|
|
5282
|
+
}
|
|
5283
|
+
|
|
5284
|
+
if (dy1 && dy1 < threshold && hor) {
|
|
5285
|
+
|
|
5286
|
+
pathData[1].values[1] = M.y;
|
|
5287
|
+
pathData[idxPenultimate].values[3] = M.y;
|
|
5288
|
+
}
|
|
5289
|
+
|
|
5290
|
+
}
|
|
4786
5291
|
}
|
|
4787
5292
|
|
|
4788
|
-
|
|
4789
|
-
let cx = (d * e - b * f) / det;
|
|
4790
|
-
let cy = (-c * e + a * f) / det;
|
|
4791
|
-
let centroid = { x: cx, y: cy };
|
|
5293
|
+
return pathData;
|
|
4792
5294
|
|
|
4793
|
-
|
|
4794
|
-
let r = getDistance(centroid, p1);
|
|
5295
|
+
}
|
|
4795
5296
|
|
|
4796
|
-
|
|
4797
|
-
let {deltaAngle, startAngle, endAngle} = angleData;
|
|
5297
|
+
function pathDataRevertCubicToQuadratic(pathData, tolerance=1) {
|
|
4798
5298
|
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
5299
|
+
for (let c = 1, l = pathData.length; c < l; c++) {
|
|
5300
|
+
let com = pathData[c];
|
|
5301
|
+
let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
|
|
5302
|
+
if (type === 'C') {
|
|
5303
|
+
let comQ = revertCubicQuadratic(p0, cp1, cp2, p, tolerance);
|
|
5304
|
+
if (comQ.type === 'Q') {
|
|
5305
|
+
comQ.extreme = com.extreme;
|
|
5306
|
+
comQ.corner = com.corner;
|
|
5307
|
+
comQ.dimA = com.dimA;
|
|
5308
|
+
comQ.squareDist = com.squareDist;
|
|
5309
|
+
pathData[c] = comQ;
|
|
5310
|
+
}
|
|
5311
|
+
}
|
|
5312
|
+
}
|
|
5313
|
+
return pathData
|
|
5314
|
+
}
|
|
5315
|
+
|
|
5316
|
+
function pathDataLineToCubic(pathData) {
|
|
5317
|
+
|
|
5318
|
+
for (let c = 1, l = pathData.length; c < l; c++) {
|
|
5319
|
+
let com = pathData[c];
|
|
5320
|
+
let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
|
|
5321
|
+
if (type === 'L') {
|
|
5322
|
+
|
|
5323
|
+
let cp1 = interpolate(p0, p, 0.333);
|
|
5324
|
+
let cp2 = interpolate(p, p0, 0.333);
|
|
5325
|
+
|
|
5326
|
+
pathData[c].type = 'C';
|
|
5327
|
+
pathData[c].values = [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y];
|
|
5328
|
+
pathData[c].cp1 = cp1;
|
|
5329
|
+
pathData[c].cp2 = cp2;
|
|
5330
|
+
|
|
5331
|
+
}
|
|
5332
|
+
}
|
|
5333
|
+
return pathData
|
|
4806
5334
|
}
|
|
4807
5335
|
|
|
4808
5336
|
function refineRoundSegments(pathData, {
|
|
@@ -4821,9 +5349,6 @@ function refineRoundSegments(pathData, {
|
|
|
4821
5349
|
// add fist command
|
|
4822
5350
|
let pathDataN = [pathData[0]];
|
|
4823
5351
|
|
|
4824
|
-
// just for debugging
|
|
4825
|
-
let pathDataTest = [];
|
|
4826
|
-
|
|
4827
5352
|
for (let i = 1; i < l; i++) {
|
|
4828
5353
|
let com = pathData[i];
|
|
4829
5354
|
let { type } = com;
|
|
@@ -4850,11 +5375,12 @@ function refineRoundSegments(pathData, {
|
|
|
4850
5375
|
|
|
4851
5376
|
// 2. line-line-bezier-line-line
|
|
4852
5377
|
if (
|
|
5378
|
+
comN2 && comN3 &&
|
|
4853
5379
|
comP.type === 'L' &&
|
|
4854
5380
|
type === 'L' &&
|
|
4855
5381
|
comBez &&
|
|
4856
5382
|
comN2.type === 'L' &&
|
|
4857
|
-
|
|
5383
|
+
(comN3.type === 'L' || comN3.type === 'Z')
|
|
4858
5384
|
) {
|
|
4859
5385
|
|
|
4860
5386
|
L1 = [com.p0, com.p];
|
|
@@ -4881,10 +5407,10 @@ function refineRoundSegments(pathData, {
|
|
|
4881
5407
|
}
|
|
4882
5408
|
|
|
4883
5409
|
// 1. line-bezier-bezier-line
|
|
4884
|
-
else if ((type === 'C' || type === 'Q') && comP.type === 'L') {
|
|
5410
|
+
else if (comN && (type === 'C' || type === 'Q') && comP.type === 'L') {
|
|
4885
5411
|
|
|
4886
5412
|
// 1.2 next is cubic next is lineto
|
|
4887
|
-
if (
|
|
5413
|
+
if (comN2 && comN2.type === 'L' && (comN.type === 'C' || comN.type === 'Q')) {
|
|
4888
5414
|
|
|
4889
5415
|
combine = true;
|
|
4890
5416
|
|
|
@@ -4943,16 +5469,19 @@ function refineRoundSegments(pathData, {
|
|
|
4943
5469
|
}
|
|
4944
5470
|
);
|
|
4945
5471
|
|
|
4946
|
-
if(bezierCommands.length === 1){
|
|
5472
|
+
if (bezierCommands.length === 1) {
|
|
4947
5473
|
|
|
4948
5474
|
// prefer more compact quadratic - otherwise arcs
|
|
4949
5475
|
let comBezier = revertCubicQuadratic(p0_S, bezierCommands[0].cp1, bezierCommands[0].cp2, p_S);
|
|
4950
5476
|
|
|
4951
5477
|
if (comBezier.type === 'Q') {
|
|
4952
5478
|
toCubic = true;
|
|
5479
|
+
}else {
|
|
5480
|
+
comBezier = bezierCommands[0];
|
|
4953
5481
|
}
|
|
4954
5482
|
|
|
4955
5483
|
com = comBezier;
|
|
5484
|
+
|
|
4956
5485
|
}
|
|
4957
5486
|
|
|
4958
5487
|
// prefer arcs if 2 cubics are required
|
|
@@ -4972,25 +5501,28 @@ function refineRoundSegments(pathData, {
|
|
|
4972
5501
|
|
|
4973
5502
|
// test rendering
|
|
4974
5503
|
|
|
5504
|
+
/*
|
|
4975
5505
|
if (debug) {
|
|
4976
5506
|
// arcs
|
|
4977
5507
|
if (!toCubic) {
|
|
4978
5508
|
pathDataTest = [
|
|
4979
5509
|
{ type: 'M', values: [p0_S.x, p0_S.y] },
|
|
4980
5510
|
{ type: 'A', values: [r, r, xAxisRotation, largeArc, sweep, p_S.x, p_S.y] },
|
|
4981
|
-
]
|
|
5511
|
+
]
|
|
4982
5512
|
}
|
|
4983
5513
|
// cubics
|
|
4984
5514
|
else {
|
|
4985
5515
|
pathDataTest = [
|
|
4986
5516
|
{ type: 'M', values: [p0_S.x, p0_S.y] },
|
|
4987
5517
|
...bezierCommands
|
|
4988
|
-
]
|
|
5518
|
+
]
|
|
5519
|
+
|
|
4989
5520
|
}
|
|
4990
5521
|
|
|
4991
5522
|
let d = pathDataToD(pathDataTest);
|
|
4992
|
-
renderPath(markers, d, 'orange', '0.5%', '0.5')
|
|
5523
|
+
renderPath(markers, d, 'orange', '0.5%', '0.5')
|
|
4993
5524
|
}
|
|
5525
|
+
*/
|
|
4994
5526
|
|
|
4995
5527
|
pathDataN.push(com);
|
|
4996
5528
|
i++;
|
|
@@ -5007,104 +5539,6 @@ function refineRoundSegments(pathData, {
|
|
|
5007
5539
|
return pathDataN;
|
|
5008
5540
|
}
|
|
5009
5541
|
|
|
5010
|
-
function refineClosingCommand(pathData = [], {
|
|
5011
|
-
threshold = 0,
|
|
5012
|
-
} = {}) {
|
|
5013
|
-
|
|
5014
|
-
let l = pathData.length;
|
|
5015
|
-
let comLast = pathData[l - 1];
|
|
5016
|
-
let isClosed = comLast.type.toLowerCase() === 'z';
|
|
5017
|
-
let idxPenultimate = isClosed ? l - 2 : l - 1;
|
|
5018
|
-
let comPenultimate = isClosed ? pathData[idxPenultimate] : pathData[idxPenultimate];
|
|
5019
|
-
let valsPen = comPenultimate.values.slice(-2);
|
|
5020
|
-
|
|
5021
|
-
let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
|
|
5022
|
-
let pPen = { x: valsPen[0], y: valsPen[1] };
|
|
5023
|
-
let dist = getDistAv(M, pPen);
|
|
5024
|
-
|
|
5025
|
-
// adjust last coordinates for better reordering
|
|
5026
|
-
if (dist && dist < threshold) {
|
|
5027
|
-
|
|
5028
|
-
let valsLast = pathData[idxPenultimate].values;
|
|
5029
|
-
let valsLastLen = valsLast.length;
|
|
5030
|
-
pathData[idxPenultimate].values[valsLastLen - 2] = M.x;
|
|
5031
|
-
pathData[idxPenultimate].values[valsLastLen - 1] = M.y;
|
|
5032
|
-
|
|
5033
|
-
// adjust cpts
|
|
5034
|
-
let comFirst = pathData[1];
|
|
5035
|
-
|
|
5036
|
-
if (comFirst.type === 'C' && comPenultimate.type === 'C') {
|
|
5037
|
-
let dx1 = Math.abs(comFirst.values[0] - comPenultimate.values[2]);
|
|
5038
|
-
let dy1 = Math.abs(comFirst.values[1] - comPenultimate.values[3]);
|
|
5039
|
-
|
|
5040
|
-
let dx2 = Math.abs(pathData[1].values[0] - comFirst.values[0]);
|
|
5041
|
-
let dy2 = Math.abs(pathData[1].values[1] - comFirst.values[1]);
|
|
5042
|
-
|
|
5043
|
-
let dx3 = Math.abs(pathData[1].values[0] - comPenultimate.values[2]);
|
|
5044
|
-
let dy3 = Math.abs(pathData[1].values[1] - comPenultimate.values[3]);
|
|
5045
|
-
|
|
5046
|
-
let ver = dx2 < threshold && dx3 < threshold && dy1;
|
|
5047
|
-
let hor = (dy2 < threshold && dy3 < threshold) && dx1;
|
|
5048
|
-
|
|
5049
|
-
if (dx1 && dx1 < threshold && ver) {
|
|
5050
|
-
|
|
5051
|
-
pathData[1].values[0] = M.x;
|
|
5052
|
-
pathData[idxPenultimate].values[2] = M.x;
|
|
5053
|
-
}
|
|
5054
|
-
|
|
5055
|
-
if (dy1 && dy1 < threshold && hor) {
|
|
5056
|
-
|
|
5057
|
-
pathData[1].values[1] = M.y;
|
|
5058
|
-
pathData[idxPenultimate].values[3] = M.y;
|
|
5059
|
-
}
|
|
5060
|
-
|
|
5061
|
-
}
|
|
5062
|
-
}
|
|
5063
|
-
|
|
5064
|
-
return pathData;
|
|
5065
|
-
|
|
5066
|
-
}
|
|
5067
|
-
|
|
5068
|
-
function pathDataRevertCubicToQuadratic(pathData, tolerance=1) {
|
|
5069
|
-
|
|
5070
|
-
for (let c = 1, l = pathData.length; c < l; c++) {
|
|
5071
|
-
let com = pathData[c];
|
|
5072
|
-
let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
|
|
5073
|
-
if (type === 'C') {
|
|
5074
|
-
|
|
5075
|
-
let comQ = revertCubicQuadratic(p0, cp1, cp2, p, tolerance);
|
|
5076
|
-
if (comQ.type === 'Q') {
|
|
5077
|
-
comQ.extreme = com.extreme;
|
|
5078
|
-
comQ.corner = com.corner;
|
|
5079
|
-
comQ.dimA = com.dimA;
|
|
5080
|
-
comQ.squareDist = com.squareDist;
|
|
5081
|
-
pathData[c] = comQ;
|
|
5082
|
-
}
|
|
5083
|
-
}
|
|
5084
|
-
}
|
|
5085
|
-
return pathData
|
|
5086
|
-
}
|
|
5087
|
-
|
|
5088
|
-
function pathDataLineToCubic(pathData) {
|
|
5089
|
-
|
|
5090
|
-
for (let c = 1, l = pathData.length; c < l; c++) {
|
|
5091
|
-
let com = pathData[c];
|
|
5092
|
-
let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
|
|
5093
|
-
if (type === 'L') {
|
|
5094
|
-
|
|
5095
|
-
let cp1 = interpolate(p0, p, 0.333);
|
|
5096
|
-
let cp2 = interpolate(p, p0, 0.333);
|
|
5097
|
-
|
|
5098
|
-
pathData[c].type = 'C';
|
|
5099
|
-
pathData[c].values = [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y];
|
|
5100
|
-
pathData[c].cp1 = cp1;
|
|
5101
|
-
pathData[c].cp2 = cp2;
|
|
5102
|
-
|
|
5103
|
-
}
|
|
5104
|
-
}
|
|
5105
|
-
return pathData
|
|
5106
|
-
}
|
|
5107
|
-
|
|
5108
5542
|
function simplifyPathData(input = '', {
|
|
5109
5543
|
|
|
5110
5544
|
toAbsolute = true,
|
|
@@ -5162,7 +5596,10 @@ function simplifyPathData(input = '', {
|
|
|
5162
5596
|
let yArr = [];
|
|
5163
5597
|
|
|
5164
5598
|
// mode:0 – single path
|
|
5165
|
-
|
|
5599
|
+
|
|
5600
|
+
let inputDetection = detectInputType(input);
|
|
5601
|
+
let {inputType, log} = inputDetection;
|
|
5602
|
+
|
|
5166
5603
|
if (inputType === 'pathDataString') {
|
|
5167
5604
|
d = input;
|
|
5168
5605
|
} else if (inputType === 'polyString') {
|