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,896 @@
|
|
|
1
|
+
import { splitSubpaths } from './pathData_split.js';
|
|
2
|
+
import { getAngle, bezierhasExtreme, getPathDataVertices, svgArcToCenterParam, getSquareDistance, commandIsFlat, getDistAv } from "./geometry.js";
|
|
3
|
+
import { getPolygonArea, getPathArea } from './geometry_area.js';
|
|
4
|
+
import { getPolyBBox } from './geometry_bbox.js';
|
|
5
|
+
import { renderPoint, renderPath } from "./visualize.js";
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* analyze path data for
|
|
11
|
+
* decimal detection
|
|
12
|
+
* sub paths
|
|
13
|
+
* directions
|
|
14
|
+
* crucial geometry properties
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
export function addDimensionData(pathData) {
|
|
19
|
+
|
|
20
|
+
let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
|
|
21
|
+
let p0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
|
|
22
|
+
let p;
|
|
23
|
+
|
|
24
|
+
pathData[0].dimA = 0;
|
|
25
|
+
let len = pathData.length
|
|
26
|
+
|
|
27
|
+
for (let c = 2; len && c <= len; c++) {
|
|
28
|
+
|
|
29
|
+
let com = pathData[c - 1];
|
|
30
|
+
let { type, values } = com;
|
|
31
|
+
let valsL = values.slice(-2);
|
|
32
|
+
|
|
33
|
+
p = valsL.length ? { x: valsL[0], y: valsL[1] } : M;
|
|
34
|
+
|
|
35
|
+
// update M for Z starting points
|
|
36
|
+
if (type === 'M') {
|
|
37
|
+
M = p;
|
|
38
|
+
}
|
|
39
|
+
else if (type.toLowerCase() === 'z') {
|
|
40
|
+
p = M;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let dimA = getDistAv(p0, p);
|
|
44
|
+
com.dimA = dimA;
|
|
45
|
+
com.p0 = p0
|
|
46
|
+
com.p = p
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if(type==='C' || type==='Q') com.cp1 = {x:values[0], y:values[1]}
|
|
50
|
+
if(type==='C' ) com.cp2 = {x:values[2], y:values[3]}
|
|
51
|
+
|
|
52
|
+
p0=p
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
console.log('!!!pathData', pathData);
|
|
57
|
+
return pathData
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
export function analyzePathData(pathData = []) {
|
|
62
|
+
|
|
63
|
+
let pathDataPlus = [];
|
|
64
|
+
|
|
65
|
+
let pathPoly = getPathDataVertices(pathData);
|
|
66
|
+
let bb = getPolyBBox(pathPoly)
|
|
67
|
+
let { left, right, top, bottom, width, height } = bb;
|
|
68
|
+
|
|
69
|
+
// initial starting point coordinates
|
|
70
|
+
let M0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
|
|
71
|
+
let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
|
|
72
|
+
let p0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
|
|
73
|
+
let p;
|
|
74
|
+
|
|
75
|
+
// init starting point data
|
|
76
|
+
pathData[0].idx = 0;
|
|
77
|
+
pathData[0].p0 = M;
|
|
78
|
+
pathData[0].p = M;
|
|
79
|
+
pathData[0].lineto = false;
|
|
80
|
+
pathData[0].corner = false;
|
|
81
|
+
pathData[0].extreme = false;
|
|
82
|
+
pathData[0].directionChange = false;
|
|
83
|
+
pathData[0].closePath = false;
|
|
84
|
+
pathData[0].dimA = 0;
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
// add first M command
|
|
88
|
+
let pathDataProps = [pathData[0]];
|
|
89
|
+
let area0 = 0;
|
|
90
|
+
let len = pathData.length;
|
|
91
|
+
|
|
92
|
+
for (let c = 2; len && c <= len; c++) {
|
|
93
|
+
|
|
94
|
+
let com = pathData[c - 1];
|
|
95
|
+
let { type, values } = com;
|
|
96
|
+
let valsL = values.slice(-2);
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* get command points for
|
|
100
|
+
* flatness checks:
|
|
101
|
+
* this way we can skip certain tests
|
|
102
|
+
*/
|
|
103
|
+
let commandPts = [p0];
|
|
104
|
+
let isFlat = false;
|
|
105
|
+
|
|
106
|
+
// init properties
|
|
107
|
+
com.idx = c - 1;
|
|
108
|
+
com.lineto = false;
|
|
109
|
+
com.corner = false;
|
|
110
|
+
com.extreme = false;
|
|
111
|
+
com.directionChange = false;
|
|
112
|
+
com.closePath = false;
|
|
113
|
+
com.dimA = 0;
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* define angle threshold for
|
|
118
|
+
* corner detection
|
|
119
|
+
*/
|
|
120
|
+
let angleThreshold = 0.05
|
|
121
|
+
p = valsL.length ? { x: valsL[0], y: valsL[1] } : M;
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
// update M for Z starting points
|
|
125
|
+
if (type === 'M') {
|
|
126
|
+
M = p;
|
|
127
|
+
p0 = p
|
|
128
|
+
}
|
|
129
|
+
else if (type.toLowerCase() === 'z') {
|
|
130
|
+
p = M;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// add on-path points
|
|
134
|
+
com.p0 = p0;
|
|
135
|
+
com.p = p;
|
|
136
|
+
|
|
137
|
+
let cp1, cp2, cp1N, cp2N, pN, typeN, area1;
|
|
138
|
+
|
|
139
|
+
//let dimA = (width + height) / 2;
|
|
140
|
+
let dimA = getDistAv(p0, p);
|
|
141
|
+
com.dimA = dimA;
|
|
142
|
+
//com.a = dimA;
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* explicit and implicit linetos
|
|
148
|
+
* - introduced by Z
|
|
149
|
+
*/
|
|
150
|
+
if (type === 'L') com.lineto = true;
|
|
151
|
+
|
|
152
|
+
if (type === 'Z') {
|
|
153
|
+
com.closePath = true;
|
|
154
|
+
// if Z introduces an implicit lineto with a length
|
|
155
|
+
if (M.x !== M0.x && M.y !== M0.y) {
|
|
156
|
+
com.lineto = true;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// if bezier
|
|
161
|
+
if (type === 'Q' || type === 'C') {
|
|
162
|
+
cp1 = { x: values[0], y: values[1] }
|
|
163
|
+
cp2 = type === 'C' ? { x: values[2], y: values[3] } : null;
|
|
164
|
+
com.cp1 = cp1;
|
|
165
|
+
if (cp2) com.cp2 = cp2;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* check command flatness
|
|
171
|
+
* we leave it to the bezier simplifier
|
|
172
|
+
* to convert flat beziers to linetos
|
|
173
|
+
* otherwise we may strip rather flat starting segments
|
|
174
|
+
* preventing a better simplification
|
|
175
|
+
*/
|
|
176
|
+
|
|
177
|
+
if (values.length > 2) {
|
|
178
|
+
if (type === 'Q' || type === 'C') commandPts.push(cp1);
|
|
179
|
+
if (type === 'C') commandPts.push(cp2);
|
|
180
|
+
commandPts.push(p);
|
|
181
|
+
|
|
182
|
+
let commandFlatness = commandIsFlat(commandPts);
|
|
183
|
+
isFlat = commandFlatness.flat;
|
|
184
|
+
com.flat = isFlat;
|
|
185
|
+
|
|
186
|
+
if (isFlat) {
|
|
187
|
+
com.extreme = false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* is extreme relative to bounding box
|
|
193
|
+
* in case elements are rotated we can't rely on 90degree angles
|
|
194
|
+
* so we interpret maximum x/y on-path points as well as extremes
|
|
195
|
+
* but we ignore linetos to allow chunk compilation
|
|
196
|
+
*/
|
|
197
|
+
if (!isFlat && type !== 'L' && (p.x === left || p.y === top || p.x === right || p.y === bottom)) {
|
|
198
|
+
com.extreme = true;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
//next command
|
|
203
|
+
let comN = pathData[c] ? pathData[c] : null;
|
|
204
|
+
let comNValsL = comN ? comN.values.slice(-2) : null;
|
|
205
|
+
typeN = comN ? comN.type : null;
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
// get bezier control points
|
|
209
|
+
if (comN && (comN.type === 'Q' || comN.type === 'C')) {
|
|
210
|
+
pN = comN ? { x: comNValsL[0], y: comNValsL[1] } : null;
|
|
211
|
+
|
|
212
|
+
cp1N = { x: comN.values[0], y: comN.values[1] }
|
|
213
|
+
cp2N = comN.type === 'C' ? { x: comN.values[2], y: comN.values[3] } : null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Detect direction change points
|
|
219
|
+
* this will prevent distortions when simplifying
|
|
220
|
+
* e.g in the "spine" of an "S" glyph
|
|
221
|
+
*/
|
|
222
|
+
area1 = getPolygonArea(commandPts)
|
|
223
|
+
let signChange = (area0 < 0 && area1 > 0) || (area0 > 0 && area1 < 0) ? true : false;
|
|
224
|
+
// update area
|
|
225
|
+
area0 = area1
|
|
226
|
+
|
|
227
|
+
if (signChange) {
|
|
228
|
+
//renderPoint(svg1, p0, 'orange', '1%', '0.75')
|
|
229
|
+
com.directionChange = true;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* check extremes or corners
|
|
235
|
+
* for adjacent curves by
|
|
236
|
+
* control point angles
|
|
237
|
+
*/
|
|
238
|
+
if ((type === 'Q' || type === 'C')) {
|
|
239
|
+
|
|
240
|
+
if ((type === 'Q' && typeN === 'Q') || (type === 'C' && typeN === 'C')) {
|
|
241
|
+
|
|
242
|
+
// check extremes
|
|
243
|
+
let cpts = commandPts.slice(1);
|
|
244
|
+
|
|
245
|
+
let w = pN ? Math.abs(pN.x - p0.x) : 0
|
|
246
|
+
let h = pN ? Math.abs(pN.y - p0.y) : 0
|
|
247
|
+
let thresh = (w + h) / 2 * 0.1;
|
|
248
|
+
let pts1 = type === 'C' ? [p, cp1N, cp2N, pN] : [p, cp1N, pN];
|
|
249
|
+
|
|
250
|
+
let flatness2 = commandIsFlat(pts1, thresh)
|
|
251
|
+
let isFlat2 = flatness2.flat;
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* if current and next cubic are flat
|
|
255
|
+
* we don't flag them as extremes to allow simplification
|
|
256
|
+
*/
|
|
257
|
+
let hasExtremes = (isFlat && isFlat2) ? false : (!com.extreme ? bezierhasExtreme(p0, cpts, angleThreshold) : true);
|
|
258
|
+
|
|
259
|
+
//let bezierExtreme = bezierhasExtreme(p0, cpts, angleThreshold);
|
|
260
|
+
|
|
261
|
+
//console.log(isFlat, isFlat2, cpts, hasExtremes, 'com.extreme', com.extreme, 'commandPts', commandPts);
|
|
262
|
+
|
|
263
|
+
if (hasExtremes) {
|
|
264
|
+
com.extreme = true
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// check corners
|
|
268
|
+
else {
|
|
269
|
+
|
|
270
|
+
let cpts1 = cp2 ? [cp2, p] : [cp1, p];
|
|
271
|
+
let cpts2 = cp2 ? [p, cp1N] : [p, cp1N];
|
|
272
|
+
|
|
273
|
+
let angCom1 = getAngle(...cpts1, true)
|
|
274
|
+
let angCom2 = getAngle(...cpts2, true)
|
|
275
|
+
let angDiff = Math.abs(angCom1 - angCom2) * 180 / Math.PI
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
let cpDist1 = getSquareDistance(...cpts1)
|
|
279
|
+
let cpDist2 = getSquareDistance(...cpts2)
|
|
280
|
+
|
|
281
|
+
let cornerThreshold = 10
|
|
282
|
+
let isCorner = angDiff > cornerThreshold && cpDist1 && cpDist2
|
|
283
|
+
|
|
284
|
+
if (isCorner) {
|
|
285
|
+
com.corner = true;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
pathDataProps.push(com)
|
|
293
|
+
p0 = p;
|
|
294
|
+
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
let dimA = (width + height) / 2
|
|
299
|
+
//pathDataPlus.push({ pathData: pathDataProps, bb: bb, dimA: dimA })
|
|
300
|
+
pathDataPlus = { pathData: pathDataProps, bb: bb, dimA: dimA }
|
|
301
|
+
|
|
302
|
+
//console.log('pathDataPlus', pathDataPlus);
|
|
303
|
+
return pathDataPlus
|
|
304
|
+
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
export function analyzePathData2(pathData = [], debug = true) {
|
|
311
|
+
|
|
312
|
+
// clone
|
|
313
|
+
pathData = JSON.parse(JSON.stringify(pathData));
|
|
314
|
+
|
|
315
|
+
// split to sub paths
|
|
316
|
+
let pathDataSubArr = splitSubpaths(pathData)
|
|
317
|
+
|
|
318
|
+
// collect more verbose data
|
|
319
|
+
let pathDataPlus = [];
|
|
320
|
+
|
|
321
|
+
// log
|
|
322
|
+
let simplyfy_debug_log = [];
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* analyze sub paths
|
|
326
|
+
* add simplified bbox (based on on-path-points)
|
|
327
|
+
* get area
|
|
328
|
+
*/
|
|
329
|
+
pathDataSubArr.forEach(pathData => {
|
|
330
|
+
|
|
331
|
+
let pathPoly = getPathDataVertices(pathData);
|
|
332
|
+
//let pathDataArea = getPathArea(pathData);
|
|
333
|
+
let bb = getPolyBBox(pathPoly)
|
|
334
|
+
let { left, right, top, bottom, width, height } = bb;
|
|
335
|
+
|
|
336
|
+
// initial starting point coordinates
|
|
337
|
+
let M0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
|
|
338
|
+
let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
|
|
339
|
+
let p0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
|
|
340
|
+
let p;
|
|
341
|
+
|
|
342
|
+
// init starting point data
|
|
343
|
+
pathData[0].p0 = M;
|
|
344
|
+
pathData[0].p = M;
|
|
345
|
+
pathData[0].lineto = false;
|
|
346
|
+
pathData[0].corner = false;
|
|
347
|
+
pathData[0].extreme = false;
|
|
348
|
+
pathData[0].directionChange = false;
|
|
349
|
+
pathData[0].closePath = false;
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
// add first M command
|
|
353
|
+
let pathDataProps = [pathData[0]];
|
|
354
|
+
let area0 = 0;
|
|
355
|
+
let len = pathData.length;
|
|
356
|
+
|
|
357
|
+
for (let c = 2; len && c <= len; c++) {
|
|
358
|
+
|
|
359
|
+
let com = pathData[c - 1];
|
|
360
|
+
let { type, values } = com;
|
|
361
|
+
let valsL = values.slice(-2);
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* get command points for
|
|
365
|
+
* flatness checks:
|
|
366
|
+
* this way we can skip certain tests
|
|
367
|
+
*/
|
|
368
|
+
let commandPts = [p0];
|
|
369
|
+
let isFlat = false;
|
|
370
|
+
|
|
371
|
+
// init properties
|
|
372
|
+
com.lineto = false;
|
|
373
|
+
com.corner = false;
|
|
374
|
+
com.extreme = false;
|
|
375
|
+
com.directionChange = false;
|
|
376
|
+
com.closePath = false;
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* define angle threshold for
|
|
380
|
+
* corner detection
|
|
381
|
+
*/
|
|
382
|
+
let angleThreshold = 0.05
|
|
383
|
+
p = valsL.length ? { x: valsL[0], y: valsL[1] } : M;
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
// update M for Z starting points
|
|
387
|
+
if (type === 'M') {
|
|
388
|
+
M = p;
|
|
389
|
+
p0 = p
|
|
390
|
+
//p0 = p
|
|
391
|
+
}
|
|
392
|
+
else if (type.toLowerCase() === 'z') {
|
|
393
|
+
//p0 = M;
|
|
394
|
+
p = M;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// add on-path points
|
|
398
|
+
com.p0 = p0;
|
|
399
|
+
com.p = p;
|
|
400
|
+
|
|
401
|
+
let cp1, cp2, cp1N, cp2N, pN, typeN, area1;
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* explicit and implicit linetos
|
|
405
|
+
* - introduced by Z
|
|
406
|
+
*/
|
|
407
|
+
if (type === 'L') com.lineto = true;
|
|
408
|
+
|
|
409
|
+
if (type === 'Z') {
|
|
410
|
+
com.closePath = true;
|
|
411
|
+
// if Z introduces an implicit lineto with a length
|
|
412
|
+
if (M.x !== M0.x && M.y !== M0.y) {
|
|
413
|
+
com.lineto = true;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// if bezier
|
|
418
|
+
if (type === 'Q' || type === 'C') {
|
|
419
|
+
cp1 = { x: values[0], y: values[1] }
|
|
420
|
+
cp2 = type === 'C' ? { x: values[2], y: values[3] } : null;
|
|
421
|
+
com.cp1 = cp1;
|
|
422
|
+
if (cp2) com.cp2 = cp2;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* check command flatness
|
|
427
|
+
* we leave it to the bezier simplifier
|
|
428
|
+
* to convert flat beziers to linetos
|
|
429
|
+
* otherwise we may strip rather flat starting segments
|
|
430
|
+
* preventing a better simplification
|
|
431
|
+
*/
|
|
432
|
+
|
|
433
|
+
if (values.length > 2) {
|
|
434
|
+
if (type === 'Q' || type === 'C') commandPts.push(cp1);
|
|
435
|
+
if (type === 'C') commandPts.push(cp2);
|
|
436
|
+
commandPts.push(p);
|
|
437
|
+
|
|
438
|
+
let commandFlatness = commandIsFlat(commandPts);
|
|
439
|
+
isFlat = commandFlatness.flat;
|
|
440
|
+
com.flat = isFlat;
|
|
441
|
+
|
|
442
|
+
if (isFlat) {
|
|
443
|
+
com.extreme = false;
|
|
444
|
+
/*
|
|
445
|
+
pathDataProps.push(com)
|
|
446
|
+
p0 = p;
|
|
447
|
+
continue;
|
|
448
|
+
*/
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* is extreme relative to bounding box
|
|
455
|
+
* in case elements are rotated we can't rely on 90degree angles
|
|
456
|
+
* so we interpret maximum x/y on-path points as well as extremes
|
|
457
|
+
* but we ignore linetos to allow chunk compilation
|
|
458
|
+
*/
|
|
459
|
+
if (!isFlat && type !== 'L' && (p.x === left || p.y === top || p.x === right || p.y === bottom)) {
|
|
460
|
+
com.extreme = true;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
// add to average
|
|
465
|
+
//let squareDist = getSquareDistance(p0, p)
|
|
466
|
+
//com.size = squareDist;
|
|
467
|
+
|
|
468
|
+
let dimA = (width + height) / 2;
|
|
469
|
+
com.dimA = dimA;
|
|
470
|
+
//console.log('decimals', decimals, size);
|
|
471
|
+
|
|
472
|
+
//next command
|
|
473
|
+
let comN = pathData[c] ? pathData[c] : null;
|
|
474
|
+
let comNValsL = comN ? comN.values.slice(-2) : null;
|
|
475
|
+
typeN = comN ? comN.type : null;
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
// get bezier control points
|
|
479
|
+
if (comN) {
|
|
480
|
+
pN = comN ? { x: comNValsL[0], y: comNValsL[1] } : null;
|
|
481
|
+
|
|
482
|
+
if (comN.type === 'Q' || comN.type === 'C') {
|
|
483
|
+
cp1N = { x: comN.values[0], y: comN.values[1] }
|
|
484
|
+
cp2N = comN.type === 'C' ? { x: comN.values[2], y: comN.values[3] } : null;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Detect direction change points
|
|
491
|
+
* this will prevent distortions when simplifying
|
|
492
|
+
* e.g in the "spine" of an "S" glyph
|
|
493
|
+
*/
|
|
494
|
+
area1 = getPolygonArea(commandPts)
|
|
495
|
+
let signChange = (area0 < 0 && area1 > 0) || (area0 > 0 && area1 < 0) ? true : false;
|
|
496
|
+
// update area
|
|
497
|
+
area0 = area1
|
|
498
|
+
|
|
499
|
+
if (signChange) {
|
|
500
|
+
//renderPoint(svg1, p0, 'orange', '1%', '0.75')
|
|
501
|
+
com.directionChange = true;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* check extremes or corners for adjacent curves by control point angles
|
|
507
|
+
*/
|
|
508
|
+
if ((type === 'Q' || type === 'C')) {
|
|
509
|
+
|
|
510
|
+
if ((type === 'Q' && typeN === 'Q') || (type === 'C' && typeN === 'C')) {
|
|
511
|
+
|
|
512
|
+
// check extremes
|
|
513
|
+
let cpts = commandPts.slice(1);
|
|
514
|
+
|
|
515
|
+
let w = Math.abs(pN.x - p0.x)
|
|
516
|
+
let h = Math.abs(pN.y - p0.y)
|
|
517
|
+
let thresh = (w + h) / 2 * 0.1;
|
|
518
|
+
let pts1 = type === 'C' ? [p, cp1N, cp2N, pN] : [p, cp1N, pN];
|
|
519
|
+
|
|
520
|
+
let flatness2 = commandIsFlat(pts1, thresh)
|
|
521
|
+
let isFlat2 = flatness2.flat;
|
|
522
|
+
|
|
523
|
+
//console.log('isFlat2', isFlat2, isFlat);
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* if current and next cubic are flat
|
|
527
|
+
* we don't flag them as extremes to allow simplification
|
|
528
|
+
*/
|
|
529
|
+
let hasExtremes = (isFlat && isFlat2) ? false : (!com.extreme ? bezierhasExtreme(p0, cpts, angleThreshold) : true);
|
|
530
|
+
|
|
531
|
+
if (hasExtremes) {
|
|
532
|
+
com.extreme = true
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// check corners
|
|
536
|
+
else {
|
|
537
|
+
|
|
538
|
+
let cpts1 = cp2 ? [cp2, p] : [cp1, p];
|
|
539
|
+
let cpts2 = cp2 ? [p, cp1N] : [p, cp1N];
|
|
540
|
+
|
|
541
|
+
let angCom1 = getAngle(...cpts1, true)
|
|
542
|
+
let angCom2 = getAngle(...cpts2, true)
|
|
543
|
+
let angDiff = Math.abs(angCom1 - angCom2) * 180 / Math.PI
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
let cpDist1 = getSquareDistance(...cpts1)
|
|
547
|
+
let cpDist2 = getSquareDistance(...cpts2)
|
|
548
|
+
|
|
549
|
+
let cornerThreshold = 10
|
|
550
|
+
let isCorner = angDiff > cornerThreshold && cpDist1 && cpDist2
|
|
551
|
+
|
|
552
|
+
if (isCorner) {
|
|
553
|
+
com.corner = true;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
//let debug = false
|
|
562
|
+
//debug = true
|
|
563
|
+
if (debug) {
|
|
564
|
+
if (com.signChange) {
|
|
565
|
+
renderPoint(markers, p0, 'orange', '1.5%', '0.75')
|
|
566
|
+
}
|
|
567
|
+
if (com.extreme) {
|
|
568
|
+
renderPoint(markers, p, 'cyan', '1%', '0.75')
|
|
569
|
+
}
|
|
570
|
+
if (com.corner) {
|
|
571
|
+
renderPoint(markers, p, 'magenta')
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
pathDataProps.push(com)
|
|
576
|
+
p0 = p;
|
|
577
|
+
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
//decimalsAV = Array.from(decimalsAV)
|
|
582
|
+
//decimalsAV = Math.ceil(decimalsAV.reduce((a, b) => a + b) / decimalsAV.length);
|
|
583
|
+
//console.log('decimalsAV', decimalsAV);
|
|
584
|
+
//pathDataProps[0].decimals = decimalsAV
|
|
585
|
+
|
|
586
|
+
//decimalsAV = Math.floor(decimalsAV/decimalsAV.length);
|
|
587
|
+
let dimA = (width + height) / 2
|
|
588
|
+
pathDataPlus.push({ pathData: pathDataProps, bb: bb, dimA: dimA })
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
if (simplyfy_debug_log.length) {
|
|
592
|
+
console.log(simplyfy_debug_log);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
})
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
return pathDataPlus
|
|
600
|
+
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
export function analyzePathData__(pathData = [], debug = true) {
|
|
606
|
+
|
|
607
|
+
// clone
|
|
608
|
+
pathData = JSON.parse(JSON.stringify(pathData));
|
|
609
|
+
|
|
610
|
+
// split to sub paths
|
|
611
|
+
let pathDataSubArr = splitSubpaths(pathData)
|
|
612
|
+
|
|
613
|
+
// collect more verbose data
|
|
614
|
+
let pathDataPlus = [];
|
|
615
|
+
|
|
616
|
+
// log
|
|
617
|
+
let simplyfy_debug_log = [];
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* analyze sub paths
|
|
621
|
+
* add simplified bbox (based on on-path-points)
|
|
622
|
+
* get area
|
|
623
|
+
*/
|
|
624
|
+
pathDataSubArr.forEach(pathData => {
|
|
625
|
+
|
|
626
|
+
let pathDataArea = getPathArea(pathData);
|
|
627
|
+
let pathPoly = getPathDataVertices(pathData);
|
|
628
|
+
let bb = getPolyBBox(pathPoly)
|
|
629
|
+
let { left, right, top, bottom, width, height } = bb;
|
|
630
|
+
|
|
631
|
+
// initial starting point coordinates
|
|
632
|
+
let M0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
|
|
633
|
+
let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
|
|
634
|
+
let p0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
|
|
635
|
+
let p;
|
|
636
|
+
|
|
637
|
+
// init starting point data
|
|
638
|
+
pathData[0].p0 = M;
|
|
639
|
+
pathData[0].p = M;
|
|
640
|
+
pathData[0].lineto = false;
|
|
641
|
+
pathData[0].corner = false;
|
|
642
|
+
pathData[0].extreme = false;
|
|
643
|
+
pathData[0].directionChange = false;
|
|
644
|
+
pathData[0].closePath = false;
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
// add first M command
|
|
648
|
+
let pathDataProps = [pathData[0]];
|
|
649
|
+
let area0 = 0;
|
|
650
|
+
let len = pathData.length;
|
|
651
|
+
|
|
652
|
+
for (let c = 2; len && c <= len; c++) {
|
|
653
|
+
|
|
654
|
+
let com = pathData[c - 1];
|
|
655
|
+
let { type, values } = com;
|
|
656
|
+
let valsL = values.slice(-2);
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* get command points for
|
|
660
|
+
* flatness checks:
|
|
661
|
+
* this way we can skip certain tests
|
|
662
|
+
*/
|
|
663
|
+
let commandPts = [p0];
|
|
664
|
+
let isFlat = false;
|
|
665
|
+
|
|
666
|
+
// init properties
|
|
667
|
+
com.lineto = false;
|
|
668
|
+
com.corner = false;
|
|
669
|
+
com.extreme = false;
|
|
670
|
+
com.directionChange = false;
|
|
671
|
+
com.closePath = false;
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* define angle threshold for
|
|
675
|
+
* corner detection
|
|
676
|
+
*/
|
|
677
|
+
let angleThreshold = 0.05
|
|
678
|
+
p = valsL.length ? { x: valsL[0], y: valsL[1] } : M;
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
// update M for Z starting points
|
|
682
|
+
if (type === 'M') {
|
|
683
|
+
M = p;
|
|
684
|
+
p0 = p
|
|
685
|
+
//p0 = p
|
|
686
|
+
}
|
|
687
|
+
else if (type.toLowerCase() === 'z') {
|
|
688
|
+
//p0 = M;
|
|
689
|
+
p = M;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// add on-path points
|
|
693
|
+
com.p0 = p0;
|
|
694
|
+
com.p = p;
|
|
695
|
+
|
|
696
|
+
let cp1, cp2, cp1N, cp2N, pN, typeN, area1;
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* explicit and implicit linetos
|
|
700
|
+
* - introduced by Z
|
|
701
|
+
*/
|
|
702
|
+
if (type === 'L') com.lineto = true;
|
|
703
|
+
|
|
704
|
+
if (type === 'Z') {
|
|
705
|
+
com.closePath = true;
|
|
706
|
+
// if Z introduces an implicit lineto with a length
|
|
707
|
+
if (M.x !== M0.x && M.y !== M0.y) {
|
|
708
|
+
com.lineto = true;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// if bezier
|
|
713
|
+
if (type === 'Q' || type === 'C') {
|
|
714
|
+
cp1 = { x: values[0], y: values[1] }
|
|
715
|
+
cp2 = type === 'C' ? { x: values[2], y: values[3] } : null;
|
|
716
|
+
com.cp1 = cp1;
|
|
717
|
+
if (cp2) com.cp2 = cp2;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* check command flatness
|
|
722
|
+
* we leave it to the bezier simplifier
|
|
723
|
+
* to convert flat beziers to linetos
|
|
724
|
+
* otherwise we may strip rather flat starting segments
|
|
725
|
+
* preventing a better simplification
|
|
726
|
+
*/
|
|
727
|
+
|
|
728
|
+
if (values.length > 2) {
|
|
729
|
+
if (type === 'Q' || type === 'C') commandPts.push(cp1);
|
|
730
|
+
if (type === 'C') commandPts.push(cp2);
|
|
731
|
+
commandPts.push(p);
|
|
732
|
+
|
|
733
|
+
let commandFlatness = commandIsFlat(commandPts);
|
|
734
|
+
isFlat = commandFlatness.flat;
|
|
735
|
+
com.flat = isFlat;
|
|
736
|
+
|
|
737
|
+
if (isFlat) {
|
|
738
|
+
com.extreme = false;
|
|
739
|
+
/*
|
|
740
|
+
pathDataProps.push(com)
|
|
741
|
+
p0 = p;
|
|
742
|
+
continue;
|
|
743
|
+
*/
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* is extreme relative to bounding box
|
|
750
|
+
* in case elements are rotated we can't rely on 90degree angles
|
|
751
|
+
* so we interpret maximum x/y on-path points as well as extremes
|
|
752
|
+
* but we ignore linetos to allow chunk compilation
|
|
753
|
+
*/
|
|
754
|
+
if (!isFlat && type !== 'L' && (p.x === left || p.y === top || p.x === right || p.y === bottom)) {
|
|
755
|
+
com.extreme = true;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
// add to average
|
|
760
|
+
//let squareDist = getSquareDistance(p0, p)
|
|
761
|
+
//com.size = squareDist;
|
|
762
|
+
|
|
763
|
+
let dimA = (width + height) / 2;
|
|
764
|
+
com.dimA = dimA;
|
|
765
|
+
//console.log('decimals', decimals, size);
|
|
766
|
+
|
|
767
|
+
//next command
|
|
768
|
+
let comN = pathData[c] ? pathData[c] : null;
|
|
769
|
+
let comNValsL = comN ? comN.values.slice(-2) : null;
|
|
770
|
+
typeN = comN ? comN.type : null;
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
// get bezier control points
|
|
774
|
+
if (comN) {
|
|
775
|
+
pN = comN ? { x: comNValsL[0], y: comNValsL[1] } : null;
|
|
776
|
+
|
|
777
|
+
if (comN.type === 'Q' || comN.type === 'C') {
|
|
778
|
+
cp1N = { x: comN.values[0], y: comN.values[1] }
|
|
779
|
+
cp2N = comN.type === 'C' ? { x: comN.values[2], y: comN.values[3] } : null;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Detect direction change points
|
|
786
|
+
* this will prevent distortions when simplifying
|
|
787
|
+
* e.g in the "spine" of an "S" glyph
|
|
788
|
+
*/
|
|
789
|
+
area1 = getPolygonArea(commandPts)
|
|
790
|
+
let signChange = (area0 < 0 && area1 > 0) || (area0 > 0 && area1 < 0) ? true : false;
|
|
791
|
+
// update area
|
|
792
|
+
area0 = area1
|
|
793
|
+
|
|
794
|
+
if (signChange) {
|
|
795
|
+
//renderPoint(svg1, p0, 'orange', '1%', '0.75')
|
|
796
|
+
com.directionChange = true;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* check extremes or corners for adjacent curves by control point angles
|
|
802
|
+
*/
|
|
803
|
+
if ((type === 'Q' || type === 'C')) {
|
|
804
|
+
|
|
805
|
+
if ((type === 'Q' && typeN === 'Q') || (type === 'C' && typeN === 'C')) {
|
|
806
|
+
|
|
807
|
+
// check extremes
|
|
808
|
+
let cpts = commandPts.slice(1);
|
|
809
|
+
|
|
810
|
+
let w = Math.abs(pN.x - p0.x)
|
|
811
|
+
let h = Math.abs(pN.y - p0.y)
|
|
812
|
+
let thresh = (w + h) / 2 * 0.1;
|
|
813
|
+
let pts1 = type === 'C' ? [p, cp1N, cp2N, pN] : [p, cp1N, pN];
|
|
814
|
+
|
|
815
|
+
let flatness2 = commandIsFlat(pts1, thresh)
|
|
816
|
+
let isFlat2 = flatness2.flat;
|
|
817
|
+
|
|
818
|
+
//console.log('isFlat2', isFlat2, isFlat);
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* if current and next cubic are flat
|
|
822
|
+
* we don't flag them as extremes to allow simplification
|
|
823
|
+
*/
|
|
824
|
+
let hasExtremes = (isFlat && isFlat2) ? false : (!com.extreme ? bezierhasExtreme(p0, cpts, angleThreshold) : true);
|
|
825
|
+
|
|
826
|
+
if (hasExtremes) {
|
|
827
|
+
com.extreme = true
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// check corners
|
|
831
|
+
else {
|
|
832
|
+
|
|
833
|
+
let cpts1 = cp2 ? [cp2, p] : [cp1, p];
|
|
834
|
+
let cpts2 = cp2 ? [p, cp1N] : [p, cp1N];
|
|
835
|
+
|
|
836
|
+
let angCom1 = getAngle(...cpts1, true)
|
|
837
|
+
let angCom2 = getAngle(...cpts2, true)
|
|
838
|
+
let angDiff = Math.abs(angCom1 - angCom2) * 180 / Math.PI
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
let cpDist1 = getSquareDistance(...cpts1)
|
|
842
|
+
let cpDist2 = getSquareDistance(...cpts2)
|
|
843
|
+
|
|
844
|
+
let cornerThreshold = 10
|
|
845
|
+
let isCorner = angDiff > cornerThreshold && cpDist1 && cpDist2
|
|
846
|
+
|
|
847
|
+
if (isCorner) {
|
|
848
|
+
com.corner = true;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
let debug = false
|
|
857
|
+
//debug = true
|
|
858
|
+
if (debug) {
|
|
859
|
+
if (com.signChange) {
|
|
860
|
+
renderPoint(svg1, p0, 'orange', '1.5%', '0.75')
|
|
861
|
+
}
|
|
862
|
+
if (com.extreme) {
|
|
863
|
+
renderPoint(svg1, p, 'cyan', '1%', '0.75')
|
|
864
|
+
}
|
|
865
|
+
if (com.corner) {
|
|
866
|
+
renderPoint(svg1, p, 'magenta')
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
pathDataProps.push(com)
|
|
871
|
+
p0 = p;
|
|
872
|
+
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
//decimalsAV = Array.from(decimalsAV)
|
|
877
|
+
//decimalsAV = Math.ceil(decimalsAV.reduce((a, b) => a + b) / decimalsAV.length);
|
|
878
|
+
//console.log('decimalsAV', decimalsAV);
|
|
879
|
+
//pathDataProps[0].decimals = decimalsAV
|
|
880
|
+
|
|
881
|
+
//decimalsAV = Math.floor(decimalsAV/decimalsAV.length);
|
|
882
|
+
let dimA = (width + height) / 2
|
|
883
|
+
pathDataPlus.push({ pathData: pathDataProps, bb: bb, area: pathDataArea, dimA: dimA })
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
if (simplyfy_debug_log.length) {
|
|
887
|
+
console.log(simplyfy_debug_log);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
})
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
return pathDataPlus
|
|
895
|
+
|
|
896
|
+
}
|