svg-path-simplify 0.4.4 → 0.4.6
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 +19 -0
- package/README.md +9 -1
- package/dist/svg-path-simplify.esm.js +1649 -1205
- package/dist/svg-path-simplify.esm.min.js +2 -2
- package/dist/svg-path-simplify.js +1649 -1204
- package/dist/svg-path-simplify.min.js +2 -2
- package/dist/svg-path-simplify.pathdata.esm.js +301 -637
- package/dist/svg-path-simplify.pathdata.esm.min.js +2 -2
- package/dist/svg-path-simplify.poly.cjs +8 -9
- package/drawing.svg +62 -0
- package/index.html +13 -8
- package/package.json +1 -1
- package/src/index.js +3 -1
- package/src/pathData_get_intersections.js +777 -0
- package/src/pathData_offset.js +201 -0
- package/src/pathData_simplify_cubic.js +19 -21
- package/src/pathData_simplify_cubic_extrapolate.js +147 -8
- package/src/pathData_simplify_cubicsToArcs.js +65 -21
- package/src/pathSimplify-main.js +267 -64
- package/src/pathSimplify-presets.js +1 -1
- package/src/poly-fit-curve-schneider.js +171 -132
- package/src/poly-fit-curve-schneider_check_bulge.js +131 -0
- package/src/poly-offset.js +133 -0
- package/src/simplify_poly_RC.js +29 -7
- package/src/svg-getAttributes.js +1 -1
- package/src/svgii/geometry.js +130 -27
- package/src/svgii/geometry_bbox.js +1 -1
- package/src/svgii/geometry_length.js +1 -1
- package/src/svgii/pathData_analyze.js +6 -28
- package/src/svgii/pathData_convert.js +63 -15
- package/src/svgii/pathData_remove_collinear.js +32 -36
- package/src/svgii/pathData_reorder.js +3 -2
- package/src/svgii/pathData_simplify_refineExtremes.js +1 -3
- package/src/svgii/pathData_stringify.js +132 -6
- package/src/svgii/pathData_toPolygon.js +33 -23
- package/src/svgii/poly_analyze.js +272 -125
- package/src/svgii/poly_analyze_cleanup.js +311 -0
- package/src/svgii/poly_analyze_getTangents.js +125 -0
- package/src/svgii/poly_analyze_get_chunks.js +3 -1
- package/src/svgii/poly_normalize.js +21 -8
- package/src/svgii/poly_to_pathdata.js +477 -8
- package/src/svgii/rounding.js +35 -14
- package/src/svgii/svg_cleanup.js +44 -51
- package/src/svgii/svg_cleanup_convertPathLength.js +5 -0
- package/src/svgii/svg_cleanup_normalize_transforms.js +2 -2
- package/src/svgii/visualize.js +33 -2
- package/v/0.4.5/index.html +1040 -0
- package/v/0.4.5/svg-path-simplify.js +13227 -0
|
@@ -0,0 +1,777 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Find all intersections between two SVG paths.
|
|
3
|
+
* Based on snap.svg intersection function
|
|
4
|
+
* Inspired by https://github.com/bpmn-io/path-intersection
|
|
5
|
+
* lower sample distance = higher accuracy
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { pointAtT } from "./svgii/geometry";
|
|
9
|
+
import { parsePathDataNormalized } from "./svgii/pathData_convert";
|
|
10
|
+
|
|
11
|
+
// intersection from stringified path data
|
|
12
|
+
export function findPathIntersections(d1, d2, stopAtFirst = false, quality = 'medium') {
|
|
13
|
+
|
|
14
|
+
// parse and normalize
|
|
15
|
+
let options = {
|
|
16
|
+
toAbsolute: true,
|
|
17
|
+
arcsToCubic: true,
|
|
18
|
+
arcAccuracy: 1
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// parse path data
|
|
22
|
+
let pathData1 = parsePathDataNormalized(d1, options);
|
|
23
|
+
let pathData2 = parsePathDataNormalized(d2, options);
|
|
24
|
+
|
|
25
|
+
//is congruent: return starting point
|
|
26
|
+
if (JSON.stringify(pathData1) === JSON.stringify(pathData2)) {
|
|
27
|
+
return [{ x: pathData1[0].values[0], y: pathData1[0].values[1] }]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return findPathDataIntersections(pathData1, pathData2, stopAtFirst, quality)
|
|
31
|
+
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
// intersection from parsed path data
|
|
38
|
+
export function findPathDataIntersections(pathData1, pathData2, stopAtFirst = false, quality = "medium") {
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* helpers
|
|
42
|
+
*/
|
|
43
|
+
const findCommandIntersections = (data1, data2, xy, stopAtFirst = false, maxInter = 3) => {
|
|
44
|
+
|
|
45
|
+
let intersections = [];
|
|
46
|
+
let quit = false;
|
|
47
|
+
let scan = 0
|
|
48
|
+
for (let i = 0; i < data1.splits && !quit; i++) {
|
|
49
|
+
|
|
50
|
+
for (let j = 0; j < data2.splits && !quit; j++) {
|
|
51
|
+
let l1 = data1.dots[i],
|
|
52
|
+
l1_1 = data1.dots[i + 1],
|
|
53
|
+
|
|
54
|
+
l2 = data2.dots[j],
|
|
55
|
+
l2_1 = data2.dots[j + 1],
|
|
56
|
+
ci = Math.abs(l1_1.x - l1.x) < .01 ? 'y' : 'x',
|
|
57
|
+
cj = Math.abs(l2_1.x - l2.x) < .01 ? 'y' : 'x';
|
|
58
|
+
|
|
59
|
+
let intersection = intersectLines(l1, l1_1, l2, l2_1)
|
|
60
|
+
scan++
|
|
61
|
+
|
|
62
|
+
if (intersection) {
|
|
63
|
+
if (stopAtFirst && intersections) {
|
|
64
|
+
quit = true
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let intersection_key = intersection.x.toFixed(1) + '_' + intersection.y.toFixed(1);
|
|
68
|
+
|
|
69
|
+
//if coorl1nates already found: skip
|
|
70
|
+
if (xy[intersection_key]) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
// save found intersection
|
|
74
|
+
xy[intersection_key] = true;
|
|
75
|
+
|
|
76
|
+
let t1 = l1.t + Math.abs((intersection[ci] - l1[ci]) / (l1_1[ci] - l1[ci])) * (l1_1.t - l1.t),
|
|
77
|
+
t2 = l2.t + Math.abs((intersection[cj] - l2[cj]) / (l2_1[cj] - l2[cj])) * (l2_1.t - l2.t);
|
|
78
|
+
|
|
79
|
+
if (t1 >= 0 && t1 <= 1 && t2 >= 0 && t2 <= 1) {
|
|
80
|
+
|
|
81
|
+
intersections.push({
|
|
82
|
+
x: intersection.x,
|
|
83
|
+
y: intersection.y,
|
|
84
|
+
t1: t1,
|
|
85
|
+
t2: t2
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// quit if max intersections were reached
|
|
89
|
+
if (intersections.length >= maxInter) {
|
|
90
|
+
//console.log('max intersections found', scan, intersections.length);
|
|
91
|
+
quit = true
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return intersections;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
const lineLength = (p0, p1) => {
|
|
103
|
+
return Math.sqrt(
|
|
104
|
+
(p1.x - p0.x) * (p1.x - p0.x) + (p1.y - p0.y) * (p1.y - p0.y)
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const getPathDataBBox = (pathData) => {
|
|
109
|
+
// get segment bboxes
|
|
110
|
+
let allX = [];
|
|
111
|
+
let allY = [];
|
|
112
|
+
for (let i = 1; i < pathData.length; i++) {
|
|
113
|
+
let comPrev = pathData[i - 1];
|
|
114
|
+
let valuesPrev = comPrev.values;
|
|
115
|
+
let valuesPrevL = valuesPrev.length;
|
|
116
|
+
let p0 = { x: valuesPrev[valuesPrevL - 2], y: valuesPrev[valuesPrevL - 1] };
|
|
117
|
+
|
|
118
|
+
let com = pathData[i];
|
|
119
|
+
let { type, values } = com;
|
|
120
|
+
let valuesL = values.length;
|
|
121
|
+
let p = { x: values[valuesL - 2], y: values[valuesL - 1] };
|
|
122
|
+
let cp1, cp2, length, commandBBox;
|
|
123
|
+
|
|
124
|
+
let M = pathData[0].values;
|
|
125
|
+
cp1 = valuesL ? { x: values[0], y: values[1] } : M;
|
|
126
|
+
cp2 = type === 'C' ? { x: values[valuesL - 4], y: values[valuesL - 3] } : cp1;
|
|
127
|
+
|
|
128
|
+
// get approximated path bbox
|
|
129
|
+
if (valuesL) {
|
|
130
|
+
allX.push(p0.x, cp1.x, cp2.x, p.x);
|
|
131
|
+
allY.push(p0.y, cp1.y, cp2.y, p.y);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* total bounding box
|
|
137
|
+
* (coarse approximation)
|
|
138
|
+
* are two paths remotely intersecting at all
|
|
139
|
+
*/
|
|
140
|
+
let minX = Math.min(...allX);
|
|
141
|
+
let minY = Math.min(...allY);
|
|
142
|
+
let maxX = Math.max(...allX);
|
|
143
|
+
let maxY = Math.max(...allY);
|
|
144
|
+
let bb = { x: minX, y: minY, right: maxX, bottom: maxY };
|
|
145
|
+
|
|
146
|
+
return bb;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
const isLine = (bez) => {
|
|
152
|
+
return (
|
|
153
|
+
bez[0] === bez[2] &&
|
|
154
|
+
bez[1] === bez[3] &&
|
|
155
|
+
bez[4] === bez[6] &&
|
|
156
|
+
bez[5] === bez[7]
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
const getBezierLineIntersection = (points, line) => {
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* based on:
|
|
166
|
+
* https://www.particleincell.com/2013/cubic-line-intersection/
|
|
167
|
+
*/
|
|
168
|
+
const getIntersectionsCubic = (points, line) => {
|
|
169
|
+
|
|
170
|
+
let [p0, cp1, cp2, p] = points;
|
|
171
|
+
|
|
172
|
+
// adjust horizonzal or vertical start and end points
|
|
173
|
+
let epsilon = 0.0001;
|
|
174
|
+
p0.y = p0.y === p.y ? p0.y - epsilon : p0.y;
|
|
175
|
+
p0.x = p0.x === p.x ? p0.x - epsilon : p0.x;
|
|
176
|
+
|
|
177
|
+
const solveCubicRoots = (P) => {
|
|
178
|
+
let [a, b, c, d] = P;
|
|
179
|
+
var A = b / a,
|
|
180
|
+
B = c / a,
|
|
181
|
+
C = d / a,
|
|
182
|
+
Q = (3 * B - Math.pow(A, 2)) / 9,
|
|
183
|
+
R = (9 * A * B - 27 * C - 2 * Math.pow(A, 3)) / 54,
|
|
184
|
+
D = Math.pow(Q, 3) + Math.pow(R, 2),
|
|
185
|
+
Im;
|
|
186
|
+
let t = [];
|
|
187
|
+
if (D >= 0) { // complex or duplicate roots
|
|
188
|
+
const S = Math.sign(R + Math.sqrt(D)) * Math.pow(Math.abs(R + Math.sqrt(D)), (1 / 3));
|
|
189
|
+
const T = Math.sign(R - Math.sqrt(D)) * Math.pow(Math.abs(R - Math.sqrt(D)), (1 / 3));
|
|
190
|
+
t = [
|
|
191
|
+
-A / 3 + (S + T), // real root
|
|
192
|
+
-A / 3 - (S + T) / 2, // real part of complex root
|
|
193
|
+
-A / 3 - (S + T) / 2 // real part of complex root
|
|
194
|
+
];
|
|
195
|
+
// complex part of root pair
|
|
196
|
+
Im = Math.abs(Math.sqrt(3) * (S - T) / 2);
|
|
197
|
+
// discard complex roots
|
|
198
|
+
if (Im !== 0) {
|
|
199
|
+
t[1] = -1;
|
|
200
|
+
t[2] = -1;
|
|
201
|
+
}
|
|
202
|
+
} else { // distinct real roots
|
|
203
|
+
let th = Math.acos(R / Math.sqrt(-Math.pow(Q, 3)));
|
|
204
|
+
t = [
|
|
205
|
+
2 * Math.sqrt(-Q) * Math.cos(th / 3) - A / 3,
|
|
206
|
+
2 * Math.sqrt(-Q) * Math.cos((th + 2 * Math.PI) / 3) - A / 3,
|
|
207
|
+
2 * Math.sqrt(-Q) * Math.cos((th + 4 * Math.PI) / 3) - A / 3
|
|
208
|
+
];
|
|
209
|
+
Im = 0.0;
|
|
210
|
+
}
|
|
211
|
+
// discard out of spec roots
|
|
212
|
+
return t.filter(val => { return val >= 0 && val <= 1 })
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const bezierCoefficients = (p0, p1, p2, p3) => {
|
|
216
|
+
return [
|
|
217
|
+
-p0 + 3 * p1 + -3 * p2 + p3,
|
|
218
|
+
3 * p0 - 6 * p1 + 3 * p2,
|
|
219
|
+
-3 * p0 + 3 * p1,
|
|
220
|
+
p0
|
|
221
|
+
];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
let A = line[1].y - line[0].y;
|
|
226
|
+
let B = line[0].x - line[1].x;
|
|
227
|
+
let C = line[0].x * (line[0].y - line[1].y) + line[0].y * (line[1].x - line[0].x);
|
|
228
|
+
|
|
229
|
+
const xBezierCoefficients = bezierCoefficients(p0.x, cp1.x, cp2.x, p.x);
|
|
230
|
+
const yBezierCoefficients = bezierCoefficients(p0.y, cp1.y, cp2.y, p.y);
|
|
231
|
+
const P = [
|
|
232
|
+
A * xBezierCoefficients[0] + B * yBezierCoefficients[0],
|
|
233
|
+
A * xBezierCoefficients[1] + B * yBezierCoefficients[1],
|
|
234
|
+
A * xBezierCoefficients[2] + B * yBezierCoefficients[2],
|
|
235
|
+
A * xBezierCoefficients[3] + B * yBezierCoefficients[3] + C
|
|
236
|
+
];
|
|
237
|
+
let cubicRoots = solveCubicRoots(P)
|
|
238
|
+
|
|
239
|
+
let pts = [];
|
|
240
|
+
cubicRoots.forEach(t => {
|
|
241
|
+
let pt = {
|
|
242
|
+
x: xBezierCoefficients[0] * t ** 3 + xBezierCoefficients[1] * t ** 2 + xBezierCoefficients[2] * t + xBezierCoefficients[3],
|
|
243
|
+
y: yBezierCoefficients[0] * t ** 3 + yBezierCoefficients[1] * t ** 2 + yBezierCoefficients[2] * t + yBezierCoefficients[3]
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// calc t for lineto
|
|
247
|
+
let pt1 = { x: pt.x - 10, y: pt.y - 10 }
|
|
248
|
+
let pt2 = { x: pt.x + 10, y: pt.y + 10 }
|
|
249
|
+
let inter = intersectLines(pt1, pt2, line[0], line[1])
|
|
250
|
+
|
|
251
|
+
pts.push(
|
|
252
|
+
{
|
|
253
|
+
x: pt.x,
|
|
254
|
+
y: pt.y,
|
|
255
|
+
t1: inter.t,
|
|
256
|
+
t2: t
|
|
257
|
+
})
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
return pts;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Based on
|
|
267
|
+
* https://stackoverflow.com/questions/77003429/intersection-of-quadratic-bezier-path-and-line/77010181?noredirect=1#comment135763117_77010181
|
|
268
|
+
*/
|
|
269
|
+
const getIntersectionsQuadratic = (points, line) => {
|
|
270
|
+
const { atan2, cos, sin, sqrt } = Math;
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
const getRoots = (pts, [[x1, y1], [x2, y2]]) => {
|
|
274
|
+
// Transform and rotate our coordinates as per above,
|
|
275
|
+
// noting that we only care about the y coordinate:
|
|
276
|
+
const angle = atan2(y2 - y1, x2 - x1);
|
|
277
|
+
const v = pts.map(
|
|
278
|
+
([x, y]) => (x - x1) * sin(-angle) + (y - y1) * cos(-angle)
|
|
279
|
+
);
|
|
280
|
+
// And now we're essentially done, we can trivially find our roots:
|
|
281
|
+
return (
|
|
282
|
+
solveQuadratic(v[0], v[1], v[2])
|
|
283
|
+
// ...as long as those roots are in the Bezier interval [0,1] of course.
|
|
284
|
+
.filter((t) => 0 <= t && t <= 1)
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const solveQuadratic = (v1, v2, v3) => {
|
|
289
|
+
const a = v1 - 2 * v2 + v3,
|
|
290
|
+
b = 2 * (v2 - v1),
|
|
291
|
+
c = v1;
|
|
292
|
+
// quick check, is "a" zero? if so, the solution is linear.
|
|
293
|
+
if (a === 0) return b === 0 ? [] : [-c / b];
|
|
294
|
+
const u = -b / (2 * a),
|
|
295
|
+
v = b ** 2 - 4 * a * c;
|
|
296
|
+
if (v < 0) return []; // If there are no roots, there are no roots. Done.
|
|
297
|
+
if (v === 0) return [u]; // If there's only one root, return that.
|
|
298
|
+
// And if there are two roots we compute the "full" formula
|
|
299
|
+
const w = sqrt(v) / (2 * a);
|
|
300
|
+
return [u + w, u - w];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// flatten to coordinate array
|
|
304
|
+
points = points.map(pt => { return Object.values(pt) });
|
|
305
|
+
let lineArr = line.map(pt => { return Object.values(pt) });
|
|
306
|
+
const [[x1, y1], [x2, y2], [x3, y3]] = points;
|
|
307
|
+
const roots = getRoots(points, lineArr);
|
|
308
|
+
|
|
309
|
+
const coordForRoot = (t, x2, x3, y2, y3) => {
|
|
310
|
+
const mt = 1 - t;
|
|
311
|
+
let pt = {
|
|
312
|
+
x: x1 * mt ** 2 + 2 * x2 * t * mt + x3 * t ** 2,
|
|
313
|
+
y: y1 * mt ** 2 + 2 * y2 * t * mt + y3 * t ** 2,
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// calc t for lineto
|
|
317
|
+
let pt1 = { x: pt.x - 10, y: pt.y - 10 }
|
|
318
|
+
let pt2 = { x: pt.x + 10, y: pt.y + 10 }
|
|
319
|
+
let inter = intersectLines(pt1, pt2, line[0], line[1])
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
x: pt.x,
|
|
323
|
+
y: pt.y,
|
|
324
|
+
t1: inter.t,
|
|
325
|
+
t2: t
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const intersections = roots.map((t) => coordForRoot(t, x2, x3, y2, y3));
|
|
330
|
+
return intersections;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
let inter = points.length === 4 ? getIntersectionsCubic(points, line) : getIntersectionsQuadratic(points, line)
|
|
334
|
+
return inter;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
// all results
|
|
344
|
+
let res = [];
|
|
345
|
+
|
|
346
|
+
//no path intersection at all - exit
|
|
347
|
+
let bb1 = getPathDataBBox(pathData1)
|
|
348
|
+
let bb2 = getPathDataBBox(pathData2)
|
|
349
|
+
|
|
350
|
+
if (!isBBoxIntersect(bb1, bb2)) {
|
|
351
|
+
return res
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// collect found intersections to exclude duplicates close points
|
|
355
|
+
let xy = {}
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
function addSegmentDots(obj, quality = "medium") {
|
|
360
|
+
let { cpts } = obj;
|
|
361
|
+
let l = 240, sampleDist = 20, div = 16, minDiv = 16, maxDiv = 16;
|
|
362
|
+
|
|
363
|
+
if (quality === "medium" || quality === "high") {
|
|
364
|
+
// approximate length by polygon length
|
|
365
|
+
let pM = pointAtT(cpts, 0.5)
|
|
366
|
+
l = lineLength(cpts[0], pM) + lineLength(cpts[cpts.length - 1], pM);
|
|
367
|
+
sampleDist = quality === "medium" ? 10 : 5
|
|
368
|
+
div = Math.ceil(l / sampleDist)
|
|
369
|
+
minDiv = quality === "high" ? 48 : 24;
|
|
370
|
+
maxDiv = quality === "high" ? 1000 : 500;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// set minimum and maximum precision for short or long segments
|
|
374
|
+
let splits = cpts.length === 2 || isLine(cpts) ? 1 : (div > minDiv ? div : (div > maxDiv ? maxDiv : minDiv)) || 1
|
|
375
|
+
|
|
376
|
+
//console.log(sampleDist, maxDiv, splits);
|
|
377
|
+
|
|
378
|
+
obj.len = l
|
|
379
|
+
obj.splits = splits
|
|
380
|
+
obj.bb = commandBBox(cpts)
|
|
381
|
+
|
|
382
|
+
for (let i = 0; i < splits + 1; i++) {
|
|
383
|
+
let t = i / splits
|
|
384
|
+
let pt = pointAtT(cpts, t);
|
|
385
|
+
obj.dots.push({ x: pt.x, y: pt.y, t: t });
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
let pathInfo1 = getPathInfo(pathData1)
|
|
390
|
+
let pathInfo2 = getPathInfo(pathData2)
|
|
391
|
+
let quit = false;
|
|
392
|
+
|
|
393
|
+
// check segment intersections
|
|
394
|
+
for (let i = 0; i < pathData1.length && !quit; i++) {
|
|
395
|
+
|
|
396
|
+
//measure paths
|
|
397
|
+
let data1 = pathInfo1[i];
|
|
398
|
+
|
|
399
|
+
if (!data1.cpts.length) {
|
|
400
|
+
continue
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
for (var j = 0; j < pathData2.length && !quit; j++) {
|
|
404
|
+
let data2 = pathInfo2[j]
|
|
405
|
+
if (!data2.cpts.length) {
|
|
406
|
+
continue
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (isBBoxIntersect(data1.bb, data2.bb)) {
|
|
410
|
+
|
|
411
|
+
let type1 = data1.type, type2 = data2.type;
|
|
412
|
+
let maxInter = type1 === 'C' && type2 === 'C' ? 4 : (type1 === 'L' && type2 === 'L' ? 1 : 2)
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* 1. line vs bezier
|
|
416
|
+
*/
|
|
417
|
+
let useBezCalc = true;
|
|
418
|
+
if ((type1 === 'L' || type2 === 'L') && (data1.cpts.length > 2 || data2.cpts.length > 2) && useBezCalc) {
|
|
419
|
+
let line = type1 === 'L' ? data1 : data2;
|
|
420
|
+
let bez = type1 === 'L' ? data2 : data1;
|
|
421
|
+
|
|
422
|
+
let inter = getBezierLineIntersection(bez.cpts, line.cpts);
|
|
423
|
+
if (inter.length) {
|
|
424
|
+
inter.forEach(item => {
|
|
425
|
+
let it = {
|
|
426
|
+
cpts1: line.cpts,
|
|
427
|
+
cpts2: bez.cpts,
|
|
428
|
+
segment1: i,
|
|
429
|
+
segment2: i,
|
|
430
|
+
t1: item.t1,
|
|
431
|
+
t2: item.t2,
|
|
432
|
+
x: item.x,
|
|
433
|
+
y: item.y,
|
|
434
|
+
}
|
|
435
|
+
res.push(it)
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
//self intersecting cubic
|
|
445
|
+
if (maxInter === 4) {
|
|
446
|
+
if (!data1.selfintersects) {
|
|
447
|
+
maxInter--
|
|
448
|
+
}
|
|
449
|
+
if (!data2.selfintersects) {
|
|
450
|
+
maxInter--
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// segment intersection - calculate sample points
|
|
455
|
+
if (!data1.dots.length) {
|
|
456
|
+
addSegmentDots(data1, quality)
|
|
457
|
+
}
|
|
458
|
+
if (!data2.dots.length) {
|
|
459
|
+
addSegmentDots(data2, quality)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
let intersections = findCommandIntersections(data1, data2, xy, stopAtFirst, maxInter);
|
|
463
|
+
|
|
464
|
+
if (stopAtFirst && intersections.length) {
|
|
465
|
+
quit = true
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
for (let k = 0; k < intersections.length; k++) {
|
|
469
|
+
intersections[k].segment1 = i;
|
|
470
|
+
intersections[k].segment2 = j;
|
|
471
|
+
intersections[k].cpts1 = data1.cpts;
|
|
472
|
+
intersections[k].cpts2 = data2.cpts;
|
|
473
|
+
}
|
|
474
|
+
res = res.concat(intersections);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return res;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* based on: https://stackoverflow.com/questions/12219802/a-javascript-function-that-returns-the-x-y-points-of-intersection-between-two-ci
|
|
487
|
+
*/
|
|
488
|
+
|
|
489
|
+
function findCircleIntersection(c1, c2, decimals = 8) {
|
|
490
|
+
|
|
491
|
+
// Start constructing the response object.
|
|
492
|
+
let result = {
|
|
493
|
+
intersect_count: 0,
|
|
494
|
+
intersect_occurs: false,
|
|
495
|
+
one_is_in_other: false,
|
|
496
|
+
are_equal: false,
|
|
497
|
+
points: []
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
// Get vertical and horizontal distances between circles.
|
|
501
|
+
const dx = c2.cx - c1.cx;
|
|
502
|
+
const dy = c2.cy - c1.cy;
|
|
503
|
+
|
|
504
|
+
// Calculate the distance between the circle centers as a straight line.
|
|
505
|
+
const dist = Math.hypot(dy, dx);
|
|
506
|
+
|
|
507
|
+
// Check if circles are the same.
|
|
508
|
+
if (c1.cx === c2.cx && c1.cy === c2.cy && c1.r === c2.r) {
|
|
509
|
+
result.are_equal = true;
|
|
510
|
+
return [];
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Check one circle isn't inside the other.
|
|
514
|
+
let rDiff = Math.abs(c1.r - c2.r);
|
|
515
|
+
if (dist <= rDiff) {
|
|
516
|
+
result.one_is_in_other = true;
|
|
517
|
+
if (dist < rDiff) return [];
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Check if circles intersect.
|
|
521
|
+
if (dist <= c1.r + c2.r) {
|
|
522
|
+
result.intersect_occurs = true;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Find the intersection points
|
|
526
|
+
if (result.intersect_occurs) {
|
|
527
|
+
// Centroid is the pt where two lines cross. A line between the circle centers
|
|
528
|
+
// and a line between the intersection points.
|
|
529
|
+
const centroid = (c1.r * c1.r - c2.r * c2.r + dist * dist) / (2 * dist);
|
|
530
|
+
|
|
531
|
+
// Get the coordinates of centroid.
|
|
532
|
+
const x2 = c1.cx + (dx * centroid) / dist;
|
|
533
|
+
const y2 = c1.cy + (dy * centroid) / dist;
|
|
534
|
+
|
|
535
|
+
// Get the distance from centroid to the intersection points.
|
|
536
|
+
const h = Math.sqrt(c1.r * c1.r - centroid * centroid);
|
|
537
|
+
|
|
538
|
+
// Get the x and y dist of the intersection points from centroid.
|
|
539
|
+
const rx = -dy * (h / dist);
|
|
540
|
+
const ry = dx * (h / dist);
|
|
541
|
+
|
|
542
|
+
let pt1 = {
|
|
543
|
+
x: +(x2 + rx).toFixed(decimals),
|
|
544
|
+
y: +(y2 + ry).toFixed(decimals)
|
|
545
|
+
};
|
|
546
|
+
let pt2 = {
|
|
547
|
+
x: +(x2 - rx).toFixed(decimals),
|
|
548
|
+
y: +(y2 - ry).toFixed(decimals)
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
// Add intersection count to results
|
|
552
|
+
if (pt1.x === pt2.x && pt1.y === pt2.y) {
|
|
553
|
+
result.intersect_count = 1;
|
|
554
|
+
result.points.push(pt1);
|
|
555
|
+
} else {
|
|
556
|
+
result.intersect_count = 2;
|
|
557
|
+
result.points.push(pt1, pt2);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
//return result;
|
|
561
|
+
return result.points;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
function intersectLines(p1, p2, p3, p4) {
|
|
567
|
+
|
|
568
|
+
const isOnLine = (x1, y1, x2, y2, px, py, tolerance = 0.001) => {
|
|
569
|
+
var f = function (somex) { return (y2 - y1) / (x2 - x1) * (somex - x1) + y1; };
|
|
570
|
+
return Math.abs(f(px) - py) < tolerance
|
|
571
|
+
&& px >= x1 && px <= x2;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (
|
|
575
|
+
Math.max(p1.x, p2.x) < Math.min(p3.x, p4.x) ||
|
|
576
|
+
Math.min(p1.x, p2.x) > Math.max(p3.x, p4.x) ||
|
|
577
|
+
Math.max(p1.y, p2.y) < Math.min(p3.y, p4.y) ||
|
|
578
|
+
Math.min(p1.y, p2.y) > Math.max(p3.y, p4.y)
|
|
579
|
+
) {
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
let denominator = (p1.x - p2.x) * (p3.y - p4.y) - (p1.y - p2.y) * (p3.x - p4.x);
|
|
584
|
+
if (denominator == 0) {
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
let a = p1.y - p3.y;
|
|
589
|
+
let b = p1.x - p3.x;
|
|
590
|
+
let numerator1 = ((p4.x - p3.x) * a) - ((p4.y - p3.y) * b);
|
|
591
|
+
let numerator2 = ((p2.x - p1.x) * a) - ((p2.y - p1.y) * b);
|
|
592
|
+
a = numerator1 / denominator;
|
|
593
|
+
b = numerator2 / denominator;
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
let px = p1.x + (a * (p2.x - p1.x)),
|
|
597
|
+
py = p1.y + (a * (p2.y - p1.y));
|
|
598
|
+
|
|
599
|
+
let px2 = +px.toFixed(2),
|
|
600
|
+
py2 = +py.toFixed(2);
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
// is point in boundaries/actually on line?
|
|
604
|
+
if (
|
|
605
|
+
px2 < +Math.min(p1.x, p2.x).toFixed(2) ||
|
|
606
|
+
px2 > +Math.max(p1.x, p2.x).toFixed(2) ||
|
|
607
|
+
px2 < +Math.min(p3.x, p4.x).toFixed(2) ||
|
|
608
|
+
px2 > +Math.max(p3.x, p4.x).toFixed(2) ||
|
|
609
|
+
py2 < +Math.min(p1.y, p2.y).toFixed(2) ||
|
|
610
|
+
py2 > +Math.max(p1.y, p2.y).toFixed(2) ||
|
|
611
|
+
py2 < +Math.min(p3.y, p4.y).toFixed(2) ||
|
|
612
|
+
py2 > +Math.max(p3.y, p4.y).toFixed(2)
|
|
613
|
+
) {
|
|
614
|
+
|
|
615
|
+
// if final point is on line
|
|
616
|
+
if (isOnLine(p3.x, p3.y, p4.x, p4.y, p2.x, p2.y, 0.1)) {
|
|
617
|
+
return { x: p2.x, y: p2.y };
|
|
618
|
+
}
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
return { x: px, y: py, t: b };
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
function isBBoxIntersect(bbox1, bbox2) {
|
|
627
|
+
let { x, y, right, bottom } = bbox1;
|
|
628
|
+
let [x2, y2, right2, bottom2] = [bbox2.x, bbox2.y, bbox2.right, bbox2.bottom];
|
|
629
|
+
|
|
630
|
+
let bboxIntersection =
|
|
631
|
+
x <= right2 &&
|
|
632
|
+
y <= bottom2 &&
|
|
633
|
+
bottom >= y2 &&
|
|
634
|
+
right >= x2 ?
|
|
635
|
+
true :
|
|
636
|
+
false;
|
|
637
|
+
|
|
638
|
+
return bboxIntersection;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
function commandBBox(points) {
|
|
643
|
+
let allX = points.map(pt => { return pt.x })
|
|
644
|
+
let allY = points.map(pt => { return pt.y })
|
|
645
|
+
|
|
646
|
+
let minX = Math.min(...allX);
|
|
647
|
+
let maxX = Math.max(...allX);
|
|
648
|
+
let minY = Math.min(...allY);
|
|
649
|
+
let maxY = Math.max(...allY);
|
|
650
|
+
|
|
651
|
+
let bb = {
|
|
652
|
+
x: minX,
|
|
653
|
+
y: minY,
|
|
654
|
+
width: maxX - minX,
|
|
655
|
+
height: maxY - minY,
|
|
656
|
+
right: maxX,
|
|
657
|
+
bottom: maxY
|
|
658
|
+
}
|
|
659
|
+
//console.log(bb);
|
|
660
|
+
return bb;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
function getPolyIntersections(el1, el2) {
|
|
665
|
+
function arrayToPoints(pts) {
|
|
666
|
+
let ptsN = []
|
|
667
|
+
for (let i = 1; i < pts.length; i += 2) {
|
|
668
|
+
ptsN.push({ x: pts[i - 1], y: pts[i] })
|
|
669
|
+
}
|
|
670
|
+
return ptsN
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
let pts1 = el1.type === 'line' ? [el1.x1, el1.y1, el1.x2, el1.y2] : el1.points
|
|
674
|
+
pts1 = arrayToPoints(pts1)
|
|
675
|
+
|
|
676
|
+
// close to start
|
|
677
|
+
if (el1.type === 'polygon') pts1.push(pts1[0])
|
|
678
|
+
|
|
679
|
+
let pts2 = el2.type === 'line' ? [el2.x1, el2.y1, el2.x2, el2.y2] : el2.points
|
|
680
|
+
pts2 = arrayToPoints(pts2);
|
|
681
|
+
if (el2.type === 'polygon') pts2.push(pts2[0])
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
let intersections = []
|
|
685
|
+
for (let i = 0; i < pts1.length - 1; i++) {
|
|
686
|
+
|
|
687
|
+
let l1 = pts1[i]
|
|
688
|
+
let l1_1 = pts1[i + 1] ? pts1[i + 1] : l1
|
|
689
|
+
|
|
690
|
+
for (let j = 0; j < pts2.length - 1; j++) {
|
|
691
|
+
|
|
692
|
+
let l2 = pts2[j]
|
|
693
|
+
let l2_1 = pts2[j + 1] ? pts2[j + 1] : l2
|
|
694
|
+
|
|
695
|
+
let intersection = intersectLines(l1, l1_1, l2, l2_1)
|
|
696
|
+
if (intersection) {
|
|
697
|
+
intersections.push(intersection)
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return intersections
|
|
703
|
+
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
function getPathInfo(pathData) {
|
|
711
|
+
let pathArr = [];
|
|
712
|
+
let M = { x: pathData[0].values[0], y: pathData[0].values[1] }
|
|
713
|
+
|
|
714
|
+
pathData.forEach((com, i) => {
|
|
715
|
+
let cpts = [];
|
|
716
|
+
let { type, values } = com;
|
|
717
|
+
let obj = {
|
|
718
|
+
type: type,
|
|
719
|
+
cpts: [],
|
|
720
|
+
selfintersects: false,
|
|
721
|
+
len: 0,
|
|
722
|
+
splits: 0,
|
|
723
|
+
dots: [],
|
|
724
|
+
bb: {}
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
let valuesL = values.length;
|
|
728
|
+
let comPrev = i > 0 ? pathData[i - 1] : pathData[i]
|
|
729
|
+
let valuesPrev = comPrev.values
|
|
730
|
+
let valuesPrevL = valuesPrev.length
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
let p0 = { x: valuesPrev[valuesPrevL - 2], y: valuesPrev[valuesPrevL - 1] }
|
|
734
|
+
let p = valuesL ? { x: values[valuesL - 2], y: values[valuesL - 1] } : M;
|
|
735
|
+
let cp1 = valuesL ? { x: values[0], y: values[1] } : p0;
|
|
736
|
+
let cp2 = valuesL > 2 ? { x: values[2], y: values[3] } : p;
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
switch (type) {
|
|
740
|
+
// new M
|
|
741
|
+
case 'M':
|
|
742
|
+
M = { x: p.x, y: p.y };
|
|
743
|
+
cpts = [];
|
|
744
|
+
break;
|
|
745
|
+
case 'C':
|
|
746
|
+
cpts = [p0, cp1, cp2, p];
|
|
747
|
+
obj.selfintersects = intersectLines(p0, cp1, cp2, p) !== false ? true : false;
|
|
748
|
+
break;
|
|
749
|
+
|
|
750
|
+
case 'Q':
|
|
751
|
+
cpts = [p0, cp1, p];
|
|
752
|
+
break;
|
|
753
|
+
|
|
754
|
+
// treat Z as lineto
|
|
755
|
+
case 'Z':
|
|
756
|
+
case 'z':
|
|
757
|
+
p = M
|
|
758
|
+
cpts = [p0, p]
|
|
759
|
+
break;
|
|
760
|
+
default:
|
|
761
|
+
cpts = [p0, p];
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
if (cpts.length) {
|
|
765
|
+
|
|
766
|
+
obj.cpts = cpts
|
|
767
|
+
obj.bb = commandBBox(cpts)
|
|
768
|
+
|
|
769
|
+
}
|
|
770
|
+
pathArr.push(obj)
|
|
771
|
+
})
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
return pathArr
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
|