svg-path-simplify 0.0.1 → 0.0.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 +28 -1
- package/dist/svg-path-simplify.esm.js +4040 -0
- package/dist/svg-path-simplify.esm.min.js +1 -0
- package/dist/svg-path-simplify.js +4065 -0
- package/dist/svg-path-simplify.min.js +1 -0
- package/dist/svg-path-simplify.node.js +4062 -0
- package/dist/svg-path-simplify.node.min.js +1 -0
- package/index.html +222 -0
- package/package.json +2 -2
- package/src/constants.js +4 -0
- package/src/index.js +18 -3
- package/src/pathData_simplify_cubic.js +324 -0
- package/src/pathData_simplify_cubic_arr.js +50 -0
- package/src/pathData_simplify_cubic_extrapolate.js +220 -0
- package/src/pathSimplify-main.js +294 -0
- package/src/svgii/...parse.js +402 -0
- package/src/svgii/geometry.js +1096 -0
- package/src/svgii/geometry_area.js +265 -0
- package/src/svgii/geometry_bbox.js +223 -0
- package/src/svgii/pathData_analyze.js +896 -0
- package/src/svgii/pathData_convert.js +1180 -0
- package/src/svgii/pathData_parse.js +487 -0
- package/src/svgii/pathData_remove_collinear.js +85 -0
- package/src/svgii/pathData_remove_zerolength.js +28 -0
- package/src/svgii/pathData_reorder.js +204 -0
- package/src/svgii/pathData_reverse.js +124 -0
- package/src/svgii/pathData_scale.js +42 -0
- package/src/svgii/pathData_split.js +449 -0
- package/src/svgii/pathData_stringify.js +146 -0
- package/src/svgii/pathData_toPolygon.js +92 -0
- package/src/svgii/pathdata_cleanup.js +363 -0
- package/src/svgii/poly_analyze.js +172 -0
- package/src/svgii/poly_to_pathdata.js +185 -0
- package/src/svgii/rounding.js +154 -0
- package/src/svgii/simplify.js +248 -0
- package/src/svgii/simplify_bezier.js +470 -0
- package/src/svgii/simplify_linetos.js +93 -0
- package/src/svgii/simplify_polygon.js +135 -0
- package/src/svgii/stringify.js +103 -0
- package/src/svgii/svg_cleanup.js +80 -0
- package/src/svgii/visualize.js +317 -0
- package/LICENSE +0 -21
|
@@ -0,0 +1,1180 @@
|
|
|
1
|
+
|
|
2
|
+
/*
|
|
3
|
+
import { getPathDataVertices, getPointOnEllipse, pointAtT, checkLineIntersection, getDistance, interpolate, getAngle } from './geometry.js';
|
|
4
|
+
|
|
5
|
+
import { splitSubpaths } from "./convert_segments";
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
import { getPolygonArea, getPathArea, getRelativeAreaDiff } from './geometry_area.js';
|
|
9
|
+
import { splitSubpaths } from './pathData_split.js';
|
|
10
|
+
import { getPolyBBox} from './geometry_bbox.js';
|
|
11
|
+
import { renderPoint, renderPath } from "./visualize";
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
import { checkLineIntersection, getAngle, getDistance, getDistAv, getSquareDistance, interpolate, pointAtT, rotatePoint } from './geometry';
|
|
16
|
+
import { getPathArea, getPolygonArea, getRelativeAreaDiff } from './geometry_area';
|
|
17
|
+
import { roundPathData } from './rounding';
|
|
18
|
+
import { renderPoint } from './visualize';
|
|
19
|
+
|
|
20
|
+
export function revertCubicQuadratic(p0 = {}, cp1 = {}, cp2 = {}, p = {}) {
|
|
21
|
+
|
|
22
|
+
// test if cubic can be simplified to quadratic
|
|
23
|
+
let cp1X = interpolate(p0, cp1, 1.5)
|
|
24
|
+
let cp2X = interpolate(p, cp2, 1.5)
|
|
25
|
+
|
|
26
|
+
let dist0 = getDistAv(p0, p)
|
|
27
|
+
let threshold = dist0 * 0.01;
|
|
28
|
+
let dist1 = getDistAv(cp1X, cp2X)
|
|
29
|
+
|
|
30
|
+
let cp1_Q = null;
|
|
31
|
+
let type = 'C'
|
|
32
|
+
let values = [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y];
|
|
33
|
+
|
|
34
|
+
if (dist1 < threshold) {
|
|
35
|
+
cp1_Q = checkLineIntersection(p0, cp1, p, cp2, false);
|
|
36
|
+
if (cp1_Q) {
|
|
37
|
+
//renderPoint(markers, cp1_Q )
|
|
38
|
+
type = 'Q'
|
|
39
|
+
values = [cp1_Q.x, cp1_Q.y, p.x, p.y];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return { type, values }
|
|
44
|
+
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
export function convertPathData(pathData, {
|
|
49
|
+
toShorthands = true,
|
|
50
|
+
toRelative = true,
|
|
51
|
+
decimals = 3
|
|
52
|
+
} = {}) {
|
|
53
|
+
|
|
54
|
+
//if(decimals>-1 && decimals<2) pathData = roundPathData(pathData, decimals);
|
|
55
|
+
if (toShorthands) pathData = pathDataToShorthands(pathData);
|
|
56
|
+
|
|
57
|
+
// pre round - before relative conversion to minimize distortions
|
|
58
|
+
pathData = roundPathData(pathData, decimals);
|
|
59
|
+
if (toRelative) pathData = pathDataToRelative(pathData);
|
|
60
|
+
if (decimals > -1) pathData = roundPathData(pathData, decimals);
|
|
61
|
+
return pathData
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* convert cubic circle approximations
|
|
67
|
+
* to more compact arcs
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
export function pathDataArcsToCubics(pathData, {
|
|
71
|
+
arcAccuracy = 1
|
|
72
|
+
} = {}) {
|
|
73
|
+
|
|
74
|
+
let pathDataCubic = [pathData[0]];
|
|
75
|
+
for (let i = 1, len = pathData.length; i < len; i++) {
|
|
76
|
+
|
|
77
|
+
let com = pathData[i];
|
|
78
|
+
let comPrev = pathData[i - 1];
|
|
79
|
+
let valuesPrev = comPrev.values;
|
|
80
|
+
let valuesPrevL = valuesPrev.length;
|
|
81
|
+
let p0 = { x: valuesPrev[valuesPrevL - 2], y: valuesPrev[valuesPrevL - 1] };
|
|
82
|
+
|
|
83
|
+
//convert arcs to cubics
|
|
84
|
+
if (com.type === 'A') {
|
|
85
|
+
// add all C commands instead of Arc
|
|
86
|
+
let cubicArcs = arcToBezier(p0, com.values, arcAccuracy);
|
|
87
|
+
cubicArcs.forEach((cubicArc) => {
|
|
88
|
+
pathDataCubic.push(cubicArc);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
else {
|
|
93
|
+
// add command
|
|
94
|
+
pathDataCubic.push(com)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return pathDataCubic
|
|
99
|
+
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
export function pathDataQuadraticToCubic(pathData) {
|
|
104
|
+
|
|
105
|
+
let pathDataQuadratic = [pathData[0]];
|
|
106
|
+
for (let i = 1, len = pathData.length; i < len; i++) {
|
|
107
|
+
|
|
108
|
+
let com = pathData[i];
|
|
109
|
+
let comPrev = pathData[i - 1];
|
|
110
|
+
let valuesPrev = comPrev.values;
|
|
111
|
+
let valuesPrevL = valuesPrev.length;
|
|
112
|
+
let p0 = { x: valuesPrev[valuesPrevL - 2], y: valuesPrev[valuesPrevL - 1] };
|
|
113
|
+
|
|
114
|
+
//convert quadratic to cubics
|
|
115
|
+
if (com.type === 'Q') {
|
|
116
|
+
pathDataQuadratic.push(quadratic2Cubic(p0, com.values))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
else {
|
|
120
|
+
// add command
|
|
121
|
+
pathDataQuadratic.push(com)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return pathDataQuadratic
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* convert quadratic commands to cubic
|
|
132
|
+
*/
|
|
133
|
+
export function quadratic2Cubic(p0, values) {
|
|
134
|
+
if (Array.isArray(p0)) {
|
|
135
|
+
p0 = {
|
|
136
|
+
x: p0[0],
|
|
137
|
+
y: p0[1]
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
let cp1 = {
|
|
141
|
+
x: p0.x + 2 / 3 * (values[0] - p0.x),
|
|
142
|
+
y: p0.y + 2 / 3 * (values[1] - p0.y)
|
|
143
|
+
}
|
|
144
|
+
let cp2 = {
|
|
145
|
+
x: values[2] + 2 / 3 * (values[0] - values[2]),
|
|
146
|
+
y: values[3] + 2 / 3 * (values[1] - values[3])
|
|
147
|
+
}
|
|
148
|
+
return ({ type: "C", values: [cp1.x, cp1.y, cp2.x, cp2.y, values[2], values[3]] });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* convert pathData to
|
|
154
|
+
* This is just a port of Dmitry Baranovskiy's
|
|
155
|
+
* pathToRelative/Absolute methods used in snap.svg
|
|
156
|
+
* https://github.com/adobe-webplatform/Snap.svg/
|
|
157
|
+
*/
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
export function pathDataToAbsoluteOrRelative(pathData, toRelative = false, decimals = -1) {
|
|
161
|
+
if (decimals >= 0) {
|
|
162
|
+
pathData[0].values = pathData[0].values.map(val => +val.toFixed(decimals));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let M = pathData[0].values;
|
|
166
|
+
let x = M[0],
|
|
167
|
+
y = M[1],
|
|
168
|
+
mx = x,
|
|
169
|
+
my = y;
|
|
170
|
+
|
|
171
|
+
for (let i = 1, len = pathData.length; i < len; i++) {
|
|
172
|
+
let com = pathData[i];
|
|
173
|
+
let { type, values } = com;
|
|
174
|
+
let newType = toRelative ? type.toLowerCase() : type.toUpperCase();
|
|
175
|
+
|
|
176
|
+
if (type !== newType) {
|
|
177
|
+
type = newType;
|
|
178
|
+
com.type = type;
|
|
179
|
+
|
|
180
|
+
switch (type) {
|
|
181
|
+
case "a":
|
|
182
|
+
case "A":
|
|
183
|
+
values[5] = toRelative ? values[5] - x : values[5] + x;
|
|
184
|
+
values[6] = toRelative ? values[6] - y : values[6] + y;
|
|
185
|
+
break;
|
|
186
|
+
case "v":
|
|
187
|
+
case "V":
|
|
188
|
+
values[0] = toRelative ? values[0] - y : values[0] + y;
|
|
189
|
+
break;
|
|
190
|
+
case "h":
|
|
191
|
+
case "H":
|
|
192
|
+
values[0] = toRelative ? values[0] - x : values[0] + x;
|
|
193
|
+
break;
|
|
194
|
+
case "m":
|
|
195
|
+
case "M":
|
|
196
|
+
if (toRelative) {
|
|
197
|
+
values[0] -= x;
|
|
198
|
+
values[1] -= y;
|
|
199
|
+
} else {
|
|
200
|
+
values[0] += x;
|
|
201
|
+
values[1] += y;
|
|
202
|
+
}
|
|
203
|
+
mx = toRelative ? values[0] + x : values[0];
|
|
204
|
+
my = toRelative ? values[1] + y : values[1];
|
|
205
|
+
break;
|
|
206
|
+
default:
|
|
207
|
+
if (values.length) {
|
|
208
|
+
for (let v = 0; v < values.length; v++) {
|
|
209
|
+
values[v] = toRelative
|
|
210
|
+
? values[v] - (v % 2 ? y : x)
|
|
211
|
+
: values[v] + (v % 2 ? y : x);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let vLen = values.length;
|
|
218
|
+
switch (type) {
|
|
219
|
+
case "z":
|
|
220
|
+
case "Z":
|
|
221
|
+
x = mx;
|
|
222
|
+
y = my;
|
|
223
|
+
break;
|
|
224
|
+
case "h":
|
|
225
|
+
case "H":
|
|
226
|
+
x = toRelative ? x + values[0] : values[0];
|
|
227
|
+
break;
|
|
228
|
+
case "v":
|
|
229
|
+
case "V":
|
|
230
|
+
y = toRelative ? y + values[0] : values[0];
|
|
231
|
+
break;
|
|
232
|
+
case "m":
|
|
233
|
+
case "M":
|
|
234
|
+
mx = values[vLen - 2] + (toRelative ? x : 0);
|
|
235
|
+
my = values[vLen - 1] + (toRelative ? y : 0);
|
|
236
|
+
default:
|
|
237
|
+
x = values[vLen - 2] + (toRelative ? x : 0);
|
|
238
|
+
y = values[vLen - 1] + (toRelative ? y : 0);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (decimals >= 0) {
|
|
242
|
+
com.values = com.values.map(val => +val.toFixed(decimals));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return pathData;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
export function pathDataToRelative(pathData, decimals = -1) {
|
|
250
|
+
return pathDataToAbsoluteOrRelative(pathData, true, decimals)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function pathDataToAbsolute(pathData, decimals = -1) {
|
|
254
|
+
return pathDataToAbsoluteOrRelative(pathData, false, decimals)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* decompose/convert shorthands to "longhand" commands:
|
|
260
|
+
* H, V, S, T => L, L, C, Q
|
|
261
|
+
* reversed method: pathDataToShorthands()
|
|
262
|
+
*/
|
|
263
|
+
|
|
264
|
+
export function pathDataToLonghands(pathData, decimals = -1, test = true) {
|
|
265
|
+
|
|
266
|
+
// analyze pathdata – if you're sure your data is already absolute skip it via test=false
|
|
267
|
+
let hasRel = false;
|
|
268
|
+
|
|
269
|
+
if (test) {
|
|
270
|
+
let commandTokens = pathData.map(com => { return com.type }).join('')
|
|
271
|
+
let hasShorthands = /[hstv]/gi.test(commandTokens);
|
|
272
|
+
hasRel = /[astvqmhlc]/g.test(commandTokens);
|
|
273
|
+
//console.log('test', hasRel, hasShorthands);
|
|
274
|
+
|
|
275
|
+
if (!hasShorthands) {
|
|
276
|
+
return pathData;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
pathData = test && hasRel ? pathDataToAbsolute(pathData, decimals) : pathData;
|
|
281
|
+
|
|
282
|
+
let pathDataLonghand = [];
|
|
283
|
+
let comPrev = {
|
|
284
|
+
type: "M",
|
|
285
|
+
values: pathData[0].values
|
|
286
|
+
};
|
|
287
|
+
pathDataLonghand.push(comPrev);
|
|
288
|
+
|
|
289
|
+
for (let i = 1, len = pathData.length; i < len; i++) {
|
|
290
|
+
let com = pathData[i];
|
|
291
|
+
let { type, values } = com;
|
|
292
|
+
let valuesL = values.length;
|
|
293
|
+
let valuesPrev = comPrev.values;
|
|
294
|
+
let valuesPrevL = valuesPrev.length;
|
|
295
|
+
let [x, y] = [values[valuesL - 2], values[valuesL - 1]];
|
|
296
|
+
let cp1X, cp1Y, cpN1X, cpN1Y, cpN2X, cpN2Y, cp2X, cp2Y;
|
|
297
|
+
let [prevX, prevY] = [
|
|
298
|
+
valuesPrev[valuesPrevL - 2],
|
|
299
|
+
valuesPrev[valuesPrevL - 1]
|
|
300
|
+
];
|
|
301
|
+
switch (type) {
|
|
302
|
+
case "H":
|
|
303
|
+
comPrev = {
|
|
304
|
+
type: "L",
|
|
305
|
+
values: [values[0], prevY]
|
|
306
|
+
};
|
|
307
|
+
break;
|
|
308
|
+
case "V":
|
|
309
|
+
comPrev = {
|
|
310
|
+
type: "L",
|
|
311
|
+
values: [prevX, values[0]]
|
|
312
|
+
};
|
|
313
|
+
break;
|
|
314
|
+
case "T":
|
|
315
|
+
[cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];
|
|
316
|
+
[prevX, prevY] = [
|
|
317
|
+
valuesPrev[valuesPrevL - 2],
|
|
318
|
+
valuesPrev[valuesPrevL - 1]
|
|
319
|
+
];
|
|
320
|
+
// new control point
|
|
321
|
+
cpN1X = prevX + (prevX - cp1X);
|
|
322
|
+
cpN1Y = prevY + (prevY - cp1Y);
|
|
323
|
+
comPrev = {
|
|
324
|
+
type: "Q",
|
|
325
|
+
values: [cpN1X, cpN1Y, x, y]
|
|
326
|
+
};
|
|
327
|
+
break;
|
|
328
|
+
case "S":
|
|
329
|
+
|
|
330
|
+
[cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];
|
|
331
|
+
[prevX, prevY] = [
|
|
332
|
+
valuesPrev[valuesPrevL - 2],
|
|
333
|
+
valuesPrev[valuesPrevL - 1]
|
|
334
|
+
];
|
|
335
|
+
|
|
336
|
+
[cp2X, cp2Y] =
|
|
337
|
+
valuesPrevL > 2 && comPrev.type !== 'A' ?
|
|
338
|
+
[valuesPrev[2], valuesPrev[3]] :
|
|
339
|
+
[prevX, prevY];
|
|
340
|
+
|
|
341
|
+
// new control points
|
|
342
|
+
cpN1X = 2 * prevX - cp2X;
|
|
343
|
+
cpN1Y = 2 * prevY - cp2Y;
|
|
344
|
+
cpN2X = values[0];
|
|
345
|
+
cpN2Y = values[1];
|
|
346
|
+
comPrev = {
|
|
347
|
+
type: "C",
|
|
348
|
+
values: [cpN1X, cpN1Y, cpN2X, cpN2Y, x, y]
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
break;
|
|
352
|
+
default:
|
|
353
|
+
comPrev = {
|
|
354
|
+
type: type,
|
|
355
|
+
values: values
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
// round final longhand values
|
|
359
|
+
if (decimals > -1) {
|
|
360
|
+
comPrev.values = comPrev.values.map(val => { return +val.toFixed(decimals) })
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
pathDataLonghand.push(comPrev);
|
|
364
|
+
}
|
|
365
|
+
return pathDataLonghand;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* apply shorthand commands if possible
|
|
370
|
+
* L, L, C, Q => H, V, S, T
|
|
371
|
+
* reversed method: pathDataToLonghands()
|
|
372
|
+
*/
|
|
373
|
+
export function pathDataToShorthands(pathData, decimals = -1, test = true) {
|
|
374
|
+
|
|
375
|
+
//pathData = JSON.parse(JSON.stringify(pathData))
|
|
376
|
+
//console.log('has dec', pathData);
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* analyze pathdata – if you're sure your data is already absolute skip it via test=false
|
|
380
|
+
*/
|
|
381
|
+
let hasRel
|
|
382
|
+
if (test) {
|
|
383
|
+
let commandTokens = pathData.map(com => { return com.type }).join('')
|
|
384
|
+
hasRel = /[astvqmhlc]/g.test(commandTokens);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
pathData = test && hasRel ? pathDataToAbsolute(pathData, decimals) : pathData;
|
|
388
|
+
|
|
389
|
+
let comShort = {
|
|
390
|
+
type: "M",
|
|
391
|
+
values: pathData[0].values
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
if (pathData[0].decimals) {
|
|
395
|
+
//console.log('has dec');
|
|
396
|
+
comShort.decimals = pathData[0].decimals
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
let pathDataShorts = [comShort];
|
|
400
|
+
|
|
401
|
+
let p0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
|
|
402
|
+
let p;
|
|
403
|
+
let tolerance = 0.01
|
|
404
|
+
|
|
405
|
+
for (let i = 1, len = pathData.length; i < len; i++) {
|
|
406
|
+
|
|
407
|
+
let com = pathData[i];
|
|
408
|
+
let { type, values } = com;
|
|
409
|
+
let valuesLast = values.slice(-2);
|
|
410
|
+
|
|
411
|
+
// previoius command
|
|
412
|
+
let comPrev = pathData[i - 1];
|
|
413
|
+
let typePrev = comPrev.type
|
|
414
|
+
|
|
415
|
+
//last on-path point
|
|
416
|
+
p = { x: valuesLast[0], y: valuesLast[1] };
|
|
417
|
+
|
|
418
|
+
// first bezier control point for S/T shorthand tests
|
|
419
|
+
let cp1 = { x: values[0], y: values[1] };
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
//calculate threshold based on command dimensions
|
|
423
|
+
let w = Math.abs(p.x - p0.x)
|
|
424
|
+
let h = Math.abs(p.y - p0.y)
|
|
425
|
+
let thresh = (w + h) / 2 * tolerance
|
|
426
|
+
|
|
427
|
+
let diffX, diffY, diff, cp1_reflected;
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
switch (type) {
|
|
431
|
+
case "L":
|
|
432
|
+
|
|
433
|
+
if (h === 0 || (h < thresh && w > thresh)) {
|
|
434
|
+
//console.log('is H');
|
|
435
|
+
comShort = {
|
|
436
|
+
type: "H",
|
|
437
|
+
values: [values[0]]
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// V
|
|
442
|
+
else if (w === 0 || (h > thresh && w < thresh)) {
|
|
443
|
+
//console.log('is V', w, h);
|
|
444
|
+
comShort = {
|
|
445
|
+
type: "V",
|
|
446
|
+
values: [values[1]]
|
|
447
|
+
};
|
|
448
|
+
} else {
|
|
449
|
+
//console.log('not', type, h, w, thresh, com);
|
|
450
|
+
comShort = com;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
break;
|
|
454
|
+
|
|
455
|
+
case "Q":
|
|
456
|
+
|
|
457
|
+
// skip test
|
|
458
|
+
if (typePrev !== 'Q') {
|
|
459
|
+
//console.log('skip T:', type, typePrev);
|
|
460
|
+
p0 = { x: valuesLast[0], y: valuesLast[1] };
|
|
461
|
+
pathDataShorts.push(com);
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
let cp1_prev = { x: comPrev.values[0], y: comPrev.values[1] };
|
|
466
|
+
// reflected Q control points
|
|
467
|
+
cp1_reflected = { x: (2 * p0.x - cp1_prev.x), y: (2 * p0.y - cp1_prev.y) };
|
|
468
|
+
|
|
469
|
+
//let thresh = (diffX+diffY)/2
|
|
470
|
+
diffX = Math.abs(cp1.x - cp1_reflected.x)
|
|
471
|
+
diffY = Math.abs(cp1.y - cp1_reflected.y)
|
|
472
|
+
diff = (diffX + diffY) / 2
|
|
473
|
+
|
|
474
|
+
if (diff < thresh) {
|
|
475
|
+
//console.log('is T', diff, thresh);
|
|
476
|
+
comShort = {
|
|
477
|
+
type: "T",
|
|
478
|
+
values: [p.x, p.y]
|
|
479
|
+
};
|
|
480
|
+
} else {
|
|
481
|
+
comShort = com;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
break;
|
|
485
|
+
case "C":
|
|
486
|
+
|
|
487
|
+
let cp2 = { x: values[2], y: values[3] };
|
|
488
|
+
|
|
489
|
+
if (typePrev !== 'C') {
|
|
490
|
+
//console.log('skip S', typePrev);
|
|
491
|
+
pathDataShorts.push(com);
|
|
492
|
+
p0 = { x: valuesLast[0], y: valuesLast[1] };
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
let cp2_prev = { x: comPrev.values[2], y: comPrev.values[3] };
|
|
497
|
+
|
|
498
|
+
// reflected C control points
|
|
499
|
+
cp1_reflected = { x: (2 * p0.x - cp2_prev.x), y: (2 * p0.y - cp2_prev.y) };
|
|
500
|
+
|
|
501
|
+
//let thresh = (diffX+diffY)/2
|
|
502
|
+
diffX = Math.abs(cp1.x - cp1_reflected.x)
|
|
503
|
+
diffY = Math.abs(cp1.y - cp1_reflected.y)
|
|
504
|
+
diff = (diffX + diffY) / 2
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
if (diff < thresh) {
|
|
508
|
+
//console.log('is S');
|
|
509
|
+
comShort = {
|
|
510
|
+
type: "S",
|
|
511
|
+
values: [cp2.x, cp2.y, p.x, p.y]
|
|
512
|
+
};
|
|
513
|
+
} else {
|
|
514
|
+
comShort = com;
|
|
515
|
+
}
|
|
516
|
+
break;
|
|
517
|
+
default:
|
|
518
|
+
comShort = {
|
|
519
|
+
type: type,
|
|
520
|
+
values: values
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
// add decimal info
|
|
526
|
+
if (com.decimals || com.decimals === 0) {
|
|
527
|
+
comShort.decimals = com.decimals
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
// round final values
|
|
532
|
+
if (decimals > -1) {
|
|
533
|
+
comShort.values = comShort.values.map(val => { return +val.toFixed(decimals) })
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
p0 = { x: valuesLast[0], y: valuesLast[1] };
|
|
537
|
+
pathDataShorts.push(comShort);
|
|
538
|
+
}
|
|
539
|
+
return pathDataShorts;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* based on puzrin's
|
|
546
|
+
* fontello/cubic2quad
|
|
547
|
+
* https://github.com/fontello/cubic2quad/blob/master/test/cubic2quad.js
|
|
548
|
+
*/
|
|
549
|
+
|
|
550
|
+
export function pathDataToQuadratic(pathData, precision = 0.1) {
|
|
551
|
+
pathData = pathDataToLonghands(pathData)
|
|
552
|
+
let newPathData = [pathData[0]];
|
|
553
|
+
for (let i = 1, len = pathData.length; i < len; i++) {
|
|
554
|
+
let comPrev = pathData[i - 1];
|
|
555
|
+
let com = pathData[i];
|
|
556
|
+
let [type, values] = [com.type, com.values];
|
|
557
|
+
let [typePrev, valuesPrev] = [comPrev.type, comPrev.values];
|
|
558
|
+
let valuesPrevL = valuesPrev.length;
|
|
559
|
+
let [xPrev, yPrev] = [
|
|
560
|
+
valuesPrev[valuesPrevL - 2],
|
|
561
|
+
valuesPrev[valuesPrevL - 1]
|
|
562
|
+
];
|
|
563
|
+
|
|
564
|
+
// convert C to Q
|
|
565
|
+
if (type == "C") {
|
|
566
|
+
|
|
567
|
+
let quadCommands = cubicToQuad(
|
|
568
|
+
xPrev,
|
|
569
|
+
yPrev,
|
|
570
|
+
values[0],
|
|
571
|
+
values[1],
|
|
572
|
+
values[2],
|
|
573
|
+
values[3],
|
|
574
|
+
values[4],
|
|
575
|
+
values[5],
|
|
576
|
+
precision
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
quadCommands.forEach(comQ => {
|
|
580
|
+
newPathData.push(comQ)
|
|
581
|
+
})
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
} else {
|
|
585
|
+
newPathData.push(com);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return newPathData;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* convert arctocommands to cubic bezier
|
|
595
|
+
* based on puzrin's a2c.js
|
|
596
|
+
* https://github.com/fontello/svgpath/blob/master/lib/a2c.js
|
|
597
|
+
* returns pathData array
|
|
598
|
+
*/
|
|
599
|
+
|
|
600
|
+
export function arcToBezier(p0, values, splitSegments = 1) {
|
|
601
|
+
const TAU = Math.PI * 2;
|
|
602
|
+
let [rx, ry, rotation, largeArcFlag, sweepFlag, x, y] = values;
|
|
603
|
+
|
|
604
|
+
if (rx === 0 || ry === 0) {
|
|
605
|
+
return []
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
let phi = rotation ? rotation * TAU / 360 : 0;
|
|
609
|
+
let sinphi = phi ? Math.sin(phi) : 0
|
|
610
|
+
let cosphi = phi ? Math.cos(phi) : 1
|
|
611
|
+
let pxp = cosphi * (p0.x - x) / 2 + sinphi * (p0.y - y) / 2
|
|
612
|
+
let pyp = -sinphi * (p0.x - x) / 2 + cosphi * (p0.y - y) / 2
|
|
613
|
+
|
|
614
|
+
if (pxp === 0 && pyp === 0) {
|
|
615
|
+
return []
|
|
616
|
+
}
|
|
617
|
+
rx = Math.abs(rx)
|
|
618
|
+
ry = Math.abs(ry)
|
|
619
|
+
let lambda =
|
|
620
|
+
pxp * pxp / (rx * rx) +
|
|
621
|
+
pyp * pyp / (ry * ry)
|
|
622
|
+
if (lambda > 1) {
|
|
623
|
+
let lambdaRt = Math.sqrt(lambda);
|
|
624
|
+
rx *= lambdaRt
|
|
625
|
+
ry *= lambdaRt
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* parametrize arc to
|
|
630
|
+
* get center point start and end angles
|
|
631
|
+
*/
|
|
632
|
+
let rxsq = rx * rx,
|
|
633
|
+
rysq = rx === ry ? rxsq : ry * ry
|
|
634
|
+
|
|
635
|
+
let pxpsq = pxp * pxp,
|
|
636
|
+
pypsq = pyp * pyp
|
|
637
|
+
let radicant = (rxsq * rysq) - (rxsq * pypsq) - (rysq * pxpsq)
|
|
638
|
+
|
|
639
|
+
if (radicant <= 0) {
|
|
640
|
+
radicant = 0
|
|
641
|
+
} else {
|
|
642
|
+
radicant /= (rxsq * pypsq) + (rysq * pxpsq)
|
|
643
|
+
radicant = Math.sqrt(radicant) * (largeArcFlag === sweepFlag ? -1 : 1)
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
let centerxp = radicant ? radicant * rx / ry * pyp : 0
|
|
647
|
+
let centeryp = radicant ? radicant * -ry / rx * pxp : 0
|
|
648
|
+
let centerx = cosphi * centerxp - sinphi * centeryp + (p0.x + x) / 2
|
|
649
|
+
let centery = sinphi * centerxp + cosphi * centeryp + (p0.y + y) / 2
|
|
650
|
+
|
|
651
|
+
let vx1 = (pxp - centerxp) / rx
|
|
652
|
+
let vy1 = (pyp - centeryp) / ry
|
|
653
|
+
let vx2 = (-pxp - centerxp) / rx
|
|
654
|
+
let vy2 = (-pyp - centeryp) / ry
|
|
655
|
+
|
|
656
|
+
// get start and end angle
|
|
657
|
+
const vectorAngle = (ux, uy, vx, vy) => {
|
|
658
|
+
let dot = +(ux * vx + uy * vy).toFixed(9)
|
|
659
|
+
if (dot === 1 || dot === -1) {
|
|
660
|
+
return dot === 1 ? 0 : Math.PI
|
|
661
|
+
}
|
|
662
|
+
dot = dot > 1 ? 1 : (dot < -1 ? -1 : dot)
|
|
663
|
+
let sign = (ux * vy - uy * vx < 0) ? -1 : 1
|
|
664
|
+
return sign * Math.acos(dot);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
let ang1 = vectorAngle(1, 0, vx1, vy1),
|
|
668
|
+
ang2 = vectorAngle(vx1, vy1, vx2, vy2)
|
|
669
|
+
|
|
670
|
+
if (sweepFlag === 0 && ang2 > 0) {
|
|
671
|
+
ang2 -= Math.PI * 2
|
|
672
|
+
}
|
|
673
|
+
else if (sweepFlag === 1 && ang2 < 0) {
|
|
674
|
+
ang2 += Math.PI * 2
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
//ratio must be at least 1
|
|
679
|
+
let ratio = +(Math.abs(ang2) / (TAU / 4)).toFixed(0) || 1
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
// increase segments for more accureate length calculations
|
|
683
|
+
let segments = ratio * splitSegments;
|
|
684
|
+
ang2 /= segments
|
|
685
|
+
let pathDataArc = [];
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
// If 90 degree circular arc, use a constant
|
|
689
|
+
// https://pomax.github.io/bezierinfo/#circles_cubic
|
|
690
|
+
// k=0.551784777779014
|
|
691
|
+
const angle90 = 1.5707963267948966;
|
|
692
|
+
const k = 0.551785
|
|
693
|
+
let a = ang2 === angle90 ? k :
|
|
694
|
+
(
|
|
695
|
+
ang2 === -angle90 ? -k : 4 / 3 * Math.tan(ang2 / 4)
|
|
696
|
+
);
|
|
697
|
+
|
|
698
|
+
let cos2 = ang2 ? Math.cos(ang2) : 1;
|
|
699
|
+
let sin2 = ang2 ? Math.sin(ang2) : 0;
|
|
700
|
+
let type = 'C'
|
|
701
|
+
|
|
702
|
+
const approxUnitArc = (ang1, ang2, a, cos2, sin2) => {
|
|
703
|
+
let x1 = ang1 != ang2 ? Math.cos(ang1) : cos2;
|
|
704
|
+
let y1 = ang1 != ang2 ? Math.sin(ang1) : sin2;
|
|
705
|
+
let x2 = Math.cos(ang1 + ang2);
|
|
706
|
+
let y2 = Math.sin(ang1 + ang2);
|
|
707
|
+
|
|
708
|
+
return [
|
|
709
|
+
{ x: x1 - y1 * a, y: y1 + x1 * a },
|
|
710
|
+
{ x: x2 + y2 * a, y: y2 - x2 * a },
|
|
711
|
+
{ x: x2, y: y2 }
|
|
712
|
+
];
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
for (let i = 0; i < segments; i++) {
|
|
716
|
+
let com = { type: type, values: [] }
|
|
717
|
+
let curve = approxUnitArc(ang1, ang2, a, cos2, sin2);
|
|
718
|
+
|
|
719
|
+
curve.forEach((pt) => {
|
|
720
|
+
let x = pt.x * rx
|
|
721
|
+
let y = pt.y * ry
|
|
722
|
+
com.values.push(cosphi * x - sinphi * y + centerx, sinphi * x + cosphi * y + centery)
|
|
723
|
+
})
|
|
724
|
+
pathDataArc.push(com);
|
|
725
|
+
ang1 += ang2
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
return pathDataArc;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* add readable command point data
|
|
734
|
+
* to pathData command objects
|
|
735
|
+
*/
|
|
736
|
+
export function pathDataToVerbose(pathData) {
|
|
737
|
+
|
|
738
|
+
let pathDataOriginal = JSON.parse(JSON.stringify(pathData))
|
|
739
|
+
|
|
740
|
+
// normalize
|
|
741
|
+
pathData = pathDataToLonghands(pathDataToAbsolute(pathData));
|
|
742
|
+
|
|
743
|
+
let pathDataVerbose = [];
|
|
744
|
+
let pathDataL = pathData.length;
|
|
745
|
+
let closed = pathData[pathDataL - 1].type.toLowerCase() === 'z' ? true : false;
|
|
746
|
+
|
|
747
|
+
pathData.forEach((com, i) => {
|
|
748
|
+
let {
|
|
749
|
+
type,
|
|
750
|
+
values
|
|
751
|
+
} = com;
|
|
752
|
+
|
|
753
|
+
let comO = pathDataOriginal[i];
|
|
754
|
+
let typeO = comO.type;
|
|
755
|
+
let valuesO = comO.values;
|
|
756
|
+
|
|
757
|
+
let typeLc = typeO.toLowerCase();
|
|
758
|
+
let valuesL = values.length;
|
|
759
|
+
let isRel = typeO === typeO.toLowerCase();
|
|
760
|
+
|
|
761
|
+
let comPrev = pathData[i - 1] ? pathData[i - 1] : false;
|
|
762
|
+
let comPrevValues = comPrev ? comPrev.values : [];
|
|
763
|
+
let comPrevValuesL = comPrevValues.length;
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
let p0 = {
|
|
767
|
+
x: comPrevValues[comPrevValuesL - 2],
|
|
768
|
+
y: comPrevValues[comPrevValuesL - 1]
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
let p = valuesL ? {
|
|
772
|
+
x: values[valuesL - 2],
|
|
773
|
+
y: values[valuesL - 1]
|
|
774
|
+
} : (i === pathData.length - 1 && closed ? pathData[0].values : false);
|
|
775
|
+
|
|
776
|
+
let comObj = {
|
|
777
|
+
type: typeO,
|
|
778
|
+
values: valuesO,
|
|
779
|
+
valuesAbsolute: values,
|
|
780
|
+
pFinal: p,
|
|
781
|
+
isRelative: isRel
|
|
782
|
+
}
|
|
783
|
+
if (comPrevValuesL) {
|
|
784
|
+
comObj.pPrev = p0
|
|
785
|
+
}
|
|
786
|
+
switch (typeLc) {
|
|
787
|
+
case 'q':
|
|
788
|
+
comObj.cp1 = {
|
|
789
|
+
x: values[valuesL - 4],
|
|
790
|
+
y: values[valuesL - 3]
|
|
791
|
+
}
|
|
792
|
+
break;
|
|
793
|
+
case 'c':
|
|
794
|
+
comObj.cp1 = {
|
|
795
|
+
x: values[valuesL - 6],
|
|
796
|
+
y: values[valuesL - 5]
|
|
797
|
+
}
|
|
798
|
+
comObj.cp2 = {
|
|
799
|
+
x: values[valuesL - 4],
|
|
800
|
+
y: values[valuesL - 3]
|
|
801
|
+
}
|
|
802
|
+
break;
|
|
803
|
+
case 'a':
|
|
804
|
+
|
|
805
|
+
// parametrized arc rx and ry values
|
|
806
|
+
let arcData = svgArcToCenterParam(p0.x, p0.y, values[0], values[1], values[2], values[3], values[4], values[5], values[6]);
|
|
807
|
+
|
|
808
|
+
comObj.rx = arcData.rx
|
|
809
|
+
comObj.ry = arcData.ry
|
|
810
|
+
comObj.xAxisRotation = values[2]
|
|
811
|
+
comObj.largeArcFlag = values[3]
|
|
812
|
+
comObj.sweepFlag = values[4]
|
|
813
|
+
comObj.startAngle = arcData.startAngle
|
|
814
|
+
comObj.endAngle = arcData.endAngle
|
|
815
|
+
comObj.deltaAngle = arcData.deltaAngle
|
|
816
|
+
break;
|
|
817
|
+
}
|
|
818
|
+
pathDataVerbose.push(comObj);
|
|
819
|
+
});
|
|
820
|
+
return pathDataVerbose;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* convert pathData nested array notation
|
|
825
|
+
* as used in snap and other libraries
|
|
826
|
+
*/
|
|
827
|
+
export function convertArrayPathData(pathDataArray) {
|
|
828
|
+
let pathData = [];
|
|
829
|
+
pathDataArray.forEach(com => {
|
|
830
|
+
let type = com.shift();
|
|
831
|
+
pathData.push({
|
|
832
|
+
type: type,
|
|
833
|
+
values: com
|
|
834
|
+
})
|
|
835
|
+
})
|
|
836
|
+
return pathData;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* cubics to arcs
|
|
846
|
+
*/
|
|
847
|
+
|
|
848
|
+
export function cubicCommandToArc(p0, cp1, cp2, p, tolerance = 7.5) {
|
|
849
|
+
|
|
850
|
+
//console.log(p0, cp1, cp2, p, segArea );
|
|
851
|
+
let com = { type: 'C', values: [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y] };
|
|
852
|
+
//let pathDataChunk = [{ type: 'M', values: [p0.x, p0.y] }, com];
|
|
853
|
+
|
|
854
|
+
let arcSegArea = 0, isArc = false
|
|
855
|
+
|
|
856
|
+
// check angles
|
|
857
|
+
let angle1 = getAngle(p0, cp1, true);
|
|
858
|
+
let angle2 = getAngle(p, cp2, true);
|
|
859
|
+
let deltaAngle = Math.abs(angle1 - angle2) * 180 / Math.PI;
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
let angleDiff = Math.abs((deltaAngle % 180) - 90);
|
|
863
|
+
let isRightAngle = angleDiff < 3;
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
/*
|
|
867
|
+
let cp1_r = rotatePoint(cp1, p0.x, p0.y, (Math.PI * -0.5))
|
|
868
|
+
let cp2_r = rotatePoint(cp2, p.x, p.y, (Math.PI * 0.5))
|
|
869
|
+
//renderPoint(markers, cp1_r )
|
|
870
|
+
|
|
871
|
+
// assumed centroid
|
|
872
|
+
let ptC = checkLineIntersection(p0, cp1_r, p, cp2_r, false)
|
|
873
|
+
|
|
874
|
+
let dist0 = getSquareDistance(p0, p)
|
|
875
|
+
let dist1 = getSquareDistance(p0, ptC)
|
|
876
|
+
let dist2 = getSquareDistance(p, ptC)
|
|
877
|
+
|
|
878
|
+
// let mid point
|
|
879
|
+
let ptM = pointAtT([p0, cp1, cp2, p], 0.5)
|
|
880
|
+
//let dist3 = getSquareDistance(ptM, ptC)
|
|
881
|
+
let diff1 = Math.abs(dist1 - dist2)
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
if (diff1 <= dist0 * 0.01) {
|
|
885
|
+
|
|
886
|
+
let r = Math.sqrt((dist1 + dist2) / 2)
|
|
887
|
+
//r = Math.sqrt((dist1 + dist2 + dist3) / 3)
|
|
888
|
+
//r = Math.sqrt( Math.min(dist1, dist2) )
|
|
889
|
+
//r=0.25
|
|
890
|
+
//console.log('diff1', diff1, r);
|
|
891
|
+
|
|
892
|
+
let arcArea = getPolygonArea([p0, cp1, cp2, p])
|
|
893
|
+
let sweep = arcArea < 0 ? 0 : 1;
|
|
894
|
+
|
|
895
|
+
// new arc command
|
|
896
|
+
let comArc = { type: 'A', values: [r, r, 0, 0, sweep, p.x, p.y] };
|
|
897
|
+
//renderPoint(markers, ptC)
|
|
898
|
+
//renderPoint(markers, ptM)
|
|
899
|
+
isArc = true;
|
|
900
|
+
|
|
901
|
+
return { com: comArc, isArc, area: arcSegArea }
|
|
902
|
+
|
|
903
|
+
}
|
|
904
|
+
*/
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
if (isRightAngle) {
|
|
908
|
+
// point between cps
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
let pI = checkLineIntersection(p0, cp1, p, cp2, false);
|
|
912
|
+
|
|
913
|
+
if (pI) {
|
|
914
|
+
|
|
915
|
+
let r1 = getDistance(p0, pI);
|
|
916
|
+
let r2 = getDistance(p, pI);
|
|
917
|
+
|
|
918
|
+
let rMax = +Math.max(r1, r2).toFixed(8);
|
|
919
|
+
let rMin = +Math.min(r1, r2).toFixed(8);
|
|
920
|
+
|
|
921
|
+
let rx = rMin
|
|
922
|
+
let ry = rMax
|
|
923
|
+
|
|
924
|
+
let arcArea = getPolygonArea([p0, cp1, cp2, p])
|
|
925
|
+
let sweep = arcArea < 0 ? 0 : 1;
|
|
926
|
+
|
|
927
|
+
let w = Math.abs(p.x - p0.x);
|
|
928
|
+
let h = Math.abs(p.y - p0.y);
|
|
929
|
+
let landscape = w > h;
|
|
930
|
+
|
|
931
|
+
let circular = (100 / rx * Math.abs(rx - ry)) < 5;
|
|
932
|
+
|
|
933
|
+
if (circular) {
|
|
934
|
+
//rx = (rx+ry)/2
|
|
935
|
+
rx = rMax
|
|
936
|
+
ry = rx;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
if (landscape) {
|
|
940
|
+
//console.log('landscape', w, h);
|
|
941
|
+
rx = rMax
|
|
942
|
+
ry = rMin
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
// get original cubic area
|
|
947
|
+
let comO = [
|
|
948
|
+
{ type: 'M', values: [p0.x, p0.y] },
|
|
949
|
+
{ type: 'C', values: [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y] }
|
|
950
|
+
];
|
|
951
|
+
|
|
952
|
+
let comArea = getPathArea(comO);
|
|
953
|
+
|
|
954
|
+
// new arc command
|
|
955
|
+
let comArc = { type: 'A', values: [rx, ry, 0, 0, sweep, p.x, p.y] };
|
|
956
|
+
|
|
957
|
+
// calculate arc seg area
|
|
958
|
+
arcSegArea = (Math.PI * (rx * ry)) / 4
|
|
959
|
+
|
|
960
|
+
// subtract polygon between start, end and center point
|
|
961
|
+
arcSegArea -= Math.abs(getPolygonArea([p0, p, pI]))
|
|
962
|
+
|
|
963
|
+
let areaDiff = getRelativeAreaDiff(comArea, arcSegArea);
|
|
964
|
+
|
|
965
|
+
if (areaDiff < tolerance) {
|
|
966
|
+
isArc = true;
|
|
967
|
+
com = comArc;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
return { com: com, isArc, area: arcSegArea }
|
|
974
|
+
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* combine adjacent arcs
|
|
979
|
+
*/
|
|
980
|
+
|
|
981
|
+
export function combineArcs(pathData) {
|
|
982
|
+
|
|
983
|
+
let arcSeq = [[]]
|
|
984
|
+
let ind = 0
|
|
985
|
+
let arcIndices = [[]];
|
|
986
|
+
let p0 = { x: pathData[0].values[0], y: pathData[0].values[1] }, p;
|
|
987
|
+
|
|
988
|
+
for (let i = 0, len = pathData.length; i < len; i++) {
|
|
989
|
+
let com = pathData[i];
|
|
990
|
+
let { type, values } = com;
|
|
991
|
+
|
|
992
|
+
if (type === 'A') {
|
|
993
|
+
|
|
994
|
+
let comPrev = pathData[i - 1];
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* previous p0 values might not be correct
|
|
998
|
+
* anymore due to cubic simplification
|
|
999
|
+
*/
|
|
1000
|
+
let valsL = comPrev.values.slice(-2);
|
|
1001
|
+
p0 = { x: valsL[0], y: valsL[1] };
|
|
1002
|
+
|
|
1003
|
+
let [rx, ry, xAxisRotation, largeArc, sweep, x, y] = values;
|
|
1004
|
+
|
|
1005
|
+
// check if arc is circular
|
|
1006
|
+
let circular = (100 / rx * Math.abs(rx - ry)) < 5;
|
|
1007
|
+
|
|
1008
|
+
|
|
1009
|
+
//add p0
|
|
1010
|
+
p = { x: values[5], y: values[6] }
|
|
1011
|
+
com.p0 = p0;
|
|
1012
|
+
com.p = p;
|
|
1013
|
+
com.circular = circular;
|
|
1014
|
+
|
|
1015
|
+
let comNext = pathData[i + 1];
|
|
1016
|
+
|
|
1017
|
+
//add first
|
|
1018
|
+
if (!arcSeq[ind].length && comNext && comNext.type === 'A') {
|
|
1019
|
+
arcSeq[ind].push(com)
|
|
1020
|
+
arcIndices[ind].push(i)
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
if (comNext && comNext.type === 'A') {
|
|
1024
|
+
let [rx1, ry1, xAxisRotation0, largeArc, sweep, x, y] = comNext.values;
|
|
1025
|
+
let diffRx = rx != rx1 ? 100 / rx * Math.abs(rx - rx1) : 0
|
|
1026
|
+
let diffRy = ry != ry1 ? 100 / ry * Math.abs(ry - ry1) : 0
|
|
1027
|
+
//let diff = (diffRx + diffRy) / 2
|
|
1028
|
+
//let circular2 = (100 / rx1 * Math.abs(rx1 - ry1)) < 5;
|
|
1029
|
+
|
|
1030
|
+
p = { x: comNext.values[5], y: comNext.values[6] }
|
|
1031
|
+
comNext.p0 = p0;
|
|
1032
|
+
comNext.p = p;
|
|
1033
|
+
|
|
1034
|
+
// add if radii are almost same
|
|
1035
|
+
if (diffRx < 5 && diffRy < 5) {
|
|
1036
|
+
//console.log(rx, rx1, ry, ry1, 'diff:',diff, 'circular', circular, circular2);
|
|
1037
|
+
arcSeq[ind].push(comNext)
|
|
1038
|
+
arcIndices[ind].push(i + 1)
|
|
1039
|
+
} else {
|
|
1040
|
+
|
|
1041
|
+
|
|
1042
|
+
// start new segment
|
|
1043
|
+
arcSeq.push([])
|
|
1044
|
+
arcIndices.push([])
|
|
1045
|
+
ind++
|
|
1046
|
+
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
else {
|
|
1051
|
+
//arcSeq[ind].push(com)
|
|
1052
|
+
//arcIndices[ind].push(i - 1)
|
|
1053
|
+
arcSeq.push([])
|
|
1054
|
+
arcIndices.push([])
|
|
1055
|
+
ind++
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
if (!arcIndices.length) return pathData;
|
|
1061
|
+
|
|
1062
|
+
arcSeq = arcSeq.filter(item => item.length)
|
|
1063
|
+
arcIndices = arcIndices.filter(item => item.length)
|
|
1064
|
+
//console.log('combine arcs:', arcSeq, arcIndices);
|
|
1065
|
+
|
|
1066
|
+
|
|
1067
|
+
// Process in reverse to avoid index shifting
|
|
1068
|
+
for (let i = arcSeq.length - 1; i >= 0; i--) {
|
|
1069
|
+
const seq = arcSeq[i];
|
|
1070
|
+
const start = arcIndices[i][0];
|
|
1071
|
+
const len = seq.length;
|
|
1072
|
+
|
|
1073
|
+
// Average radii to prevent distortions
|
|
1074
|
+
let rxA = 0, ryA = 0;
|
|
1075
|
+
seq.forEach(({ values }) => {
|
|
1076
|
+
const [rx, ry] = values;
|
|
1077
|
+
rxA += rx;
|
|
1078
|
+
ryA += ry;
|
|
1079
|
+
});
|
|
1080
|
+
rxA /= len;
|
|
1081
|
+
ryA /= len;
|
|
1082
|
+
|
|
1083
|
+
// Correct near-circular arcs
|
|
1084
|
+
//console.log('seq', seq);
|
|
1085
|
+
|
|
1086
|
+
//let rDiff = 100 / rxA * Math.abs(rxA - ryA);
|
|
1087
|
+
//let circular = rDiff < 5;
|
|
1088
|
+
|
|
1089
|
+
// check if arc is circular
|
|
1090
|
+
let circular = (100 / rxA * Math.abs(rxA - ryA)) < 5;
|
|
1091
|
+
|
|
1092
|
+
|
|
1093
|
+
if (circular) {
|
|
1094
|
+
// average radii
|
|
1095
|
+
rxA = (rxA + ryA) / 2;
|
|
1096
|
+
ryA = rxA;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
let comPrev = pathData[start - 1]
|
|
1100
|
+
let comPrevVals = comPrev.values.slice(-2)
|
|
1101
|
+
let M = { type: 'M', values: [comPrevVals[0], comPrevVals[1]] }
|
|
1102
|
+
|
|
1103
|
+
|
|
1104
|
+
if (len === 4) {
|
|
1105
|
+
//console.log('4 arcs');
|
|
1106
|
+
|
|
1107
|
+
let [rx, ry, xAxisRotation, largeArc, sweep, x1, y1] = seq[1].values;
|
|
1108
|
+
let [, , , , , x2, y2] = seq[3].values;
|
|
1109
|
+
|
|
1110
|
+
let xDiff = Math.abs(x2 - x1);
|
|
1111
|
+
let yDiff = Math.abs(y2 - y1);
|
|
1112
|
+
let horizontal = xDiff > yDiff;
|
|
1113
|
+
|
|
1114
|
+
if (circular) {
|
|
1115
|
+
let adjustY = !horizontal ? rxA * 2 : 0;
|
|
1116
|
+
//x1 = M.values[0];
|
|
1117
|
+
//y1 = M.values[1] + adjustY;
|
|
1118
|
+
//x2 = M.values[0];
|
|
1119
|
+
//y2 = M.values[1];
|
|
1120
|
+
|
|
1121
|
+
// simplify radii
|
|
1122
|
+
rxA = 1;
|
|
1123
|
+
ryA = 1;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
let com1 = { type: 'A', values: [rxA, ryA, xAxisRotation, largeArc, sweep, x1, y1] };
|
|
1127
|
+
let com2 = { type: 'A', values: [rxA, ryA, xAxisRotation, largeArc, sweep, x2, y2] };
|
|
1128
|
+
|
|
1129
|
+
// This now correctly replaces the original 4 arc commands with 2
|
|
1130
|
+
pathData.splice(start, len, com1, com2);
|
|
1131
|
+
//console.log(com1, com2);
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
else if (len === 3) {
|
|
1135
|
+
//console.log('3 arcs');
|
|
1136
|
+
let [rx, ry, xAxisRotation, largeArc, sweep, x1, y1] = seq[0].values;
|
|
1137
|
+
let [rx2, ry2, , , , x2, y2] = seq[2].values;
|
|
1138
|
+
|
|
1139
|
+
// must be large arc
|
|
1140
|
+
largeArc = 1;
|
|
1141
|
+
let com1 = { type: 'A', values: [rxA, ryA, xAxisRotation, largeArc, sweep, x2, y2] };
|
|
1142
|
+
|
|
1143
|
+
// replace
|
|
1144
|
+
pathData.splice(start, len, com1);
|
|
1145
|
+
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
|
|
1149
|
+
else if (len === 2) {
|
|
1150
|
+
//console.log('2 arcs');
|
|
1151
|
+
let [rx, ry, xAxisRotation, largeArc, sweep, x1, y1] = seq[0].values;
|
|
1152
|
+
let [rx2, ry2, , , , x2, y2] = seq[1].values;
|
|
1153
|
+
|
|
1154
|
+
// if circular or non-elliptic xAxisRotation has no effect
|
|
1155
|
+
if (circular) {
|
|
1156
|
+
rxA = 1;
|
|
1157
|
+
ryA = 1;
|
|
1158
|
+
xAxisRotation = 0;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// check if arc is already ideal
|
|
1162
|
+
let { p0, p } = seq[0];
|
|
1163
|
+
let [p0_1, p_1] = [seq[1].p0, seq[1].p];
|
|
1164
|
+
|
|
1165
|
+
if (p0.x !== p_1.x || p0.y !== p_1.y) {
|
|
1166
|
+
|
|
1167
|
+
let com1 = { type: 'A', values: [rxA, ryA, xAxisRotation, largeArc, sweep, x2, y2] };
|
|
1168
|
+
|
|
1169
|
+
// replace
|
|
1170
|
+
pathData.splice(start, len, com1);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
else {
|
|
1175
|
+
//console.log('single arc');
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
return pathData
|
|
1180
|
+
}
|