svg-path-simplify 0.1.3 → 0.2.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 +10 -0
- package/dist/svg-path-simplify.esm.js +3905 -1533
- package/dist/svg-path-simplify.esm.min.js +13 -1
- package/dist/svg-path-simplify.js +3923 -1551
- package/dist/svg-path-simplify.min.js +13 -1
- package/dist/svg-path-simplify.min.js.gz +0 -0
- package/index.html +61 -31
- package/package.json +3 -5
- package/src/constants.js +3 -0
- package/src/index-node.js +0 -1
- package/src/index.js +26 -0
- package/src/pathData_simplify_cubic.js +74 -31
- package/src/pathData_simplify_cubicsToArcs.js +566 -0
- package/src/pathData_simplify_harmonize_cpts.js +170 -0
- package/src/pathData_simplify_revertToquadratics.js +21 -0
- package/src/pathSimplify-main.js +253 -86
- package/src/poly-fit-curve-schneider.js +570 -0
- package/src/simplify_poly_RDP.js +146 -0
- package/src/simplify_poly_radial_distance.js +100 -0
- package/src/svg_getViewbox.js +1 -1
- package/src/svgii/geometry.js +389 -63
- package/src/svgii/geometry_area.js +2 -1
- package/src/svgii/pathData_analyze.js +259 -212
- package/src/svgii/pathData_convert.js +91 -663
- package/src/svgii/pathData_fromPoly.js +12 -0
- package/src/svgii/pathData_parse.js +90 -89
- package/src/svgii/pathData_parse_els.js +3 -0
- package/src/svgii/pathData_parse_fontello.js +449 -0
- package/src/svgii/pathData_remove_collinear.js +44 -37
- package/src/svgii/pathData_reorder.js +2 -1
- package/src/svgii/pathData_simplify_redraw.js +343 -0
- package/src/svgii/pathData_simplify_refineCorners.js +18 -9
- package/src/svgii/pathData_simplify_refineExtremes.js +19 -78
- package/src/svgii/pathData_split.js +42 -45
- package/src/svgii/pathData_toPolygon.js +130 -4
- package/src/svgii/poly_analyze.js +470 -14
- package/src/svgii/poly_to_pathdata.js +224 -19
- package/src/svgii/rounding.js +55 -112
- package/src/svgii/svg_cleanup.js +13 -1
- package/src/svgii/visualize.js +8 -3
- package/{debug.cjs → tests/debug.cjs} +3 -0
- /package/{test.js → tests/test.js} +0 -0
- /package/{testSVG.js → tests/testSVG.js} +0 -0
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* Algorithm for Automatically Fitting Digitized Curves
|
|
4
|
+
* by Philip J. Schneider
|
|
5
|
+
* "Graphics Gems", Academic Press, 1990
|
|
6
|
+
* The MIT License (MIT)
|
|
7
|
+
* https://github.com/soswow/fit-curves
|
|
8
|
+
*
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { harmonizeCubicCpts, harmonizeCubicCptsThird } from "./pathData_simplify_harmonize_cpts";
|
|
12
|
+
import { getAngle, getDistance, pointAtT, rotatePoint } from "./svgii/geometry";
|
|
13
|
+
import { renderPoint } from "./svgii/visualize";
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
let polyPtsToArray = (pts) => {
|
|
17
|
+
return Array.from(pts).map(pt => [pt.x, pt.y])
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// convert to pathdata
|
|
21
|
+
let bezierPtsToPathData = (beziers) => {
|
|
22
|
+
//let pathData = [{ type: 'M', values: [beziers[0][0][0], beziers[0][0][1]] }];
|
|
23
|
+
let pathData = [];
|
|
24
|
+
|
|
25
|
+
beziers.forEach(bez => {
|
|
26
|
+
let cp1 = bez[1]
|
|
27
|
+
let cp2 = bez[2]
|
|
28
|
+
let p = bez[3]
|
|
29
|
+
let com = { type: 'C', values: [cp1[0], cp1[1], cp2[0], cp2[1], p[0], p[1]] }
|
|
30
|
+
pathData.push(com)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
return pathData
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Fit one or more Bezier curves to a set of pts.
|
|
40
|
+
*
|
|
41
|
+
*/
|
|
42
|
+
export function fitCurveN(pts, maxError, adjustCpts = true, harmonize= true) {
|
|
43
|
+
|
|
44
|
+
if (!Array.isArray(pts) || (pts[0].x !== undefined)) {
|
|
45
|
+
|
|
46
|
+
if (pts[0].x !== undefined) {
|
|
47
|
+
pts = polyPtsToArray(pts)
|
|
48
|
+
} else {
|
|
49
|
+
throw Error("Not a valid point array");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
//console.log(pts);
|
|
54
|
+
|
|
55
|
+
// Remove duplicate pts
|
|
56
|
+
pts = pts.filter(function (point, i) {
|
|
57
|
+
return i === 0 || !point.every((val, j) => {
|
|
58
|
+
return val === pts[i - 1][j];
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (pts.length === 1) {
|
|
63
|
+
//return [{ type: 'L', values: [pts[0][0], pts[0][1]] }];
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
// single lineto
|
|
69
|
+
if (pts.length === 2) {
|
|
70
|
+
return [
|
|
71
|
+
{ type: 'L', values: [pts[0][0], pts[0][1]] },
|
|
72
|
+
{ type: 'L', values: [pts[1][0], pts[1][1]] }
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
let len = pts.length;
|
|
78
|
+
|
|
79
|
+
let leftTangent = createTangent(pts[1], pts[0]);
|
|
80
|
+
let rightTangent = createTangent(pts[len - 2], pts[len - 1]);
|
|
81
|
+
|
|
82
|
+
let beziers = fitCubic(pts, leftTangent, rightTangent, maxError);
|
|
83
|
+
|
|
84
|
+
// create pathdata
|
|
85
|
+
let pathData = bezierPtsToPathData(beziers)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
// adjustCpts -post
|
|
90
|
+
//adjustCpts = false
|
|
91
|
+
//harmonize= false;
|
|
92
|
+
|
|
93
|
+
let cp1, cp2;
|
|
94
|
+
if (adjustCpts) {
|
|
95
|
+
|
|
96
|
+
let len2 = pathData.length;
|
|
97
|
+
let com1 = pathData[0]
|
|
98
|
+
|
|
99
|
+
// last cubic segment
|
|
100
|
+
let com2 = pathData[len2 - 1]
|
|
101
|
+
|
|
102
|
+
//adjust 1st and last angle
|
|
103
|
+
let p0 = { x: pts[0][0], y: pts[0][1] }
|
|
104
|
+
let p1 = { x: pts[1][0], y: pts[1][1] }
|
|
105
|
+
let p2 = pts[2] ? { x: pts[2][0], y: pts[2][1] } : null
|
|
106
|
+
|
|
107
|
+
if (p2) {
|
|
108
|
+
cp1 = { x: com1.values[0], y: com1.values[1] }
|
|
109
|
+
cp1 = adjustTangentAngle(cp1, p0, p1, p2)
|
|
110
|
+
com1.values[0] = cp1.x
|
|
111
|
+
com1.values[1] = cp1.y
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let pL = { x: pts[len - 1][0], y: pts[len - 1][1] }
|
|
115
|
+
let pL1 = { x: pts[len - 2][0], y: pts[len - 2][1] }
|
|
116
|
+
let pL2 = pts[len - 3] ? { x: pts[len - 3][0], y: pts[len - 3][1] } : null
|
|
117
|
+
|
|
118
|
+
if (pL2) {
|
|
119
|
+
cp2 = { x: com2.values[2], y: com2.values[3] }
|
|
120
|
+
cp2 = adjustTangentAngle(cp2, pL, pL1, pL2)
|
|
121
|
+
com2.values[2] = cp2.x
|
|
122
|
+
com2.values[3] = cp2.y
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// harmonize too tight tangents
|
|
126
|
+
//let harmonize= true;
|
|
127
|
+
if(harmonize){
|
|
128
|
+
pathData = harmonizeCubicCptsThird([{ type: 'M', values: [pts[0][0], pts[0][1]] },
|
|
129
|
+
...pathData])
|
|
130
|
+
pathData.shift()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
//console.log('pathData schneider', pathData);
|
|
137
|
+
return pathData
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
function adjustTangentAngle(cp, p0, p1, p2) {
|
|
142
|
+
let ang1 = getAngle(p0, p1)
|
|
143
|
+
let ang2 = getAngle(p0, p2)
|
|
144
|
+
let angDiff = (ang2 - ang1)
|
|
145
|
+
cp = rotatePoint(cp, p0.x, p0.y, -angDiff)
|
|
146
|
+
return cp
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Use least-squares method to find Bezier control pts for region.
|
|
152
|
+
*/
|
|
153
|
+
let generateBezier = (pts, parameters, leftTangent, rightTangent) => {
|
|
154
|
+
|
|
155
|
+
//Bezier curve ctl pts
|
|
156
|
+
let a, tmp, u, ux, firstPoint = pts[0], lastPoint = pts[pts.length - 1];
|
|
157
|
+
|
|
158
|
+
let bezCurve = [firstPoint, null, null, lastPoint];
|
|
159
|
+
let A = zeros_Xx2x2(parameters.length);
|
|
160
|
+
let len = parameters.length;
|
|
161
|
+
|
|
162
|
+
for (let i = 0; i < len; i++) {
|
|
163
|
+
u = parameters[i];
|
|
164
|
+
ux = 1 - u;
|
|
165
|
+
a = A[i];
|
|
166
|
+
|
|
167
|
+
a[0] = mulItems(leftTangent, 3 * u * (ux * ux));
|
|
168
|
+
a[1] = mulItems(rightTangent, 3 * ux * (u * u));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
//Create the C and X matrices
|
|
172
|
+
let C = [[0, 0], [0, 0]];
|
|
173
|
+
let X = [0, 0];
|
|
174
|
+
let l = pts.length;
|
|
175
|
+
|
|
176
|
+
for (let i = 0; i < l; i++) {
|
|
177
|
+
u = parameters[i];
|
|
178
|
+
a = A[i];
|
|
179
|
+
|
|
180
|
+
C[0][0] += dot(a[0], a[0]);
|
|
181
|
+
C[0][1] += dot(a[0], a[1]);
|
|
182
|
+
C[1][0] += dot(a[0], a[1]);
|
|
183
|
+
C[1][1] += dot(a[1], a[1]);
|
|
184
|
+
|
|
185
|
+
tmp = subtract(pts[i], pointAtT([firstPoint, firstPoint, lastPoint, lastPoint], u));
|
|
186
|
+
|
|
187
|
+
X[0] += dot(a[0], tmp);
|
|
188
|
+
X[1] += dot(a[1], tmp);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
//Compute the determinants of C and X
|
|
192
|
+
let det_C0_C1 = C[0][0] * C[1][1] - C[1][0] * C[0][1];
|
|
193
|
+
let det_C0_X = C[0][0] * X[1] - C[1][0] * X[0];
|
|
194
|
+
let det_X_C1 = X[0] * C[1][1] - X[1] * C[0][1];
|
|
195
|
+
|
|
196
|
+
//Finally, derive alpha values
|
|
197
|
+
let alpha_l = det_C0_C1 === 0 ? 0 : det_X_C1 / det_C0_C1;
|
|
198
|
+
let alpha_r = det_C0_C1 === 0 ? 0 : det_C0_X / det_C0_C1;
|
|
199
|
+
let segLength = getDistance(firstPoint, lastPoint, true);
|
|
200
|
+
let epsilon = 1.0e-6 * segLength;
|
|
201
|
+
|
|
202
|
+
if (alpha_l < epsilon || alpha_r < epsilon) {
|
|
203
|
+
//Fall back on standard (probably inaccurate) formula, and subdivide further if needed.
|
|
204
|
+
bezCurve[1] = addArrays(firstPoint, mulItems(leftTangent, segLength * 0.333));
|
|
205
|
+
bezCurve[2] = addArrays(lastPoint, mulItems(rightTangent, segLength * 0.333));
|
|
206
|
+
} else {
|
|
207
|
+
// First and last control pts of the Bezier curve
|
|
208
|
+
bezCurve[1] = addArrays(firstPoint, mulItems(leftTangent, alpha_l));
|
|
209
|
+
bezCurve[2] = addArrays(lastPoint, mulItems(rightTangent, alpha_r));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return bezCurve;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Fit a Bezier curve to a (sub)set of digitized pts.
|
|
218
|
+
* Your code should not call this function directly. Use {@link fitCurve} instead.
|
|
219
|
+
*control-point-1, control-point-2, second-point] and pts are [x, y]
|
|
220
|
+
*/
|
|
221
|
+
let fitCubic = (pts, leftTangent, rightTangent, error) => {
|
|
222
|
+
//Max times to try iterating (to find an acceptable curve)
|
|
223
|
+
let MaxIterations = 20;
|
|
224
|
+
let bezCurve;
|
|
225
|
+
|
|
226
|
+
//Use heuristic if region only has two pts in it
|
|
227
|
+
if (pts.length === 2) {
|
|
228
|
+
let dist = getDistance(pts[0], pts[1], true) * 0.333;
|
|
229
|
+
bezCurve = [pts[0], addArrays(pts[0], mulItems(leftTangent, dist)), addArrays(pts[1], mulItems(rightTangent, dist)), pts[1]];
|
|
230
|
+
return [bezCurve];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
//Parameterize pts, and attempt to fit curve
|
|
234
|
+
let u = chordLengthParameterize(pts);
|
|
235
|
+
let _generateAndReport = generateAndReport(pts, u, u, leftTangent, rightTangent);
|
|
236
|
+
|
|
237
|
+
bezCurve = _generateAndReport[0];
|
|
238
|
+
let maxError = _generateAndReport[1];
|
|
239
|
+
let splitPoint = _generateAndReport[2];
|
|
240
|
+
|
|
241
|
+
if (maxError === 0 || maxError < error) {
|
|
242
|
+
return [bezCurve];
|
|
243
|
+
}
|
|
244
|
+
//If error not too large, try some reparameterization and iteration
|
|
245
|
+
if (maxError < error * error) {
|
|
246
|
+
|
|
247
|
+
let uPrime = u;
|
|
248
|
+
let prevErr = maxError;
|
|
249
|
+
let prevSplit = splitPoint;
|
|
250
|
+
|
|
251
|
+
for (let i = 0; i < MaxIterations; i++) {
|
|
252
|
+
|
|
253
|
+
uPrime = reparameterize(bezCurve, pts, uPrime);
|
|
254
|
+
|
|
255
|
+
let _generateAndReport2 = generateAndReport(pts, u, uPrime, leftTangent, rightTangent);
|
|
256
|
+
|
|
257
|
+
bezCurve = _generateAndReport2[0];
|
|
258
|
+
maxError = _generateAndReport2[1];
|
|
259
|
+
splitPoint = _generateAndReport2[2];
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
if (maxError < error) {
|
|
263
|
+
return [bezCurve];
|
|
264
|
+
}
|
|
265
|
+
//If the development of the fitted curve grinds to a halt,
|
|
266
|
+
//we abort this attempt (and try a shorter curve):
|
|
267
|
+
else if (splitPoint === prevSplit) {
|
|
268
|
+
let errChange = maxError / prevErr;
|
|
269
|
+
if (errChange > .9999 && errChange < 1.0001) {
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
prevErr = maxError;
|
|
275
|
+
prevSplit = splitPoint;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
//Fitting failed -- split at max error point and fit recursively
|
|
280
|
+
let beziers = [];
|
|
281
|
+
let centerVector = subtract(pts[splitPoint - 1], pts[splitPoint + 1]);
|
|
282
|
+
|
|
283
|
+
if (centerVector.every(function (val) {
|
|
284
|
+
return val === 0;
|
|
285
|
+
})) {
|
|
286
|
+
//[x,y] -> [-y,x]: http://stackoverflow.com/a/4780141/1869660
|
|
287
|
+
centerVector = subtract(pts[splitPoint - 1], pts[splitPoint]);
|
|
288
|
+
let _ref = [-centerVector[1], centerVector[0]];
|
|
289
|
+
centerVector[0] = _ref[0];
|
|
290
|
+
centerVector[1] = _ref[1];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
let toCenterTangent = normalize(centerVector);
|
|
294
|
+
//To and from need to point in opposite directions:
|
|
295
|
+
let fromCenterTangent = mulItems(toCenterTangent, -1);
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
beziers = beziers.concat(fitCubic(pts.slice(0, splitPoint + 1), leftTangent, toCenterTangent, error));
|
|
299
|
+
beziers = beziers.concat(fitCubic(pts.slice(splitPoint), fromCenterTangent, rightTangent, error));
|
|
300
|
+
return beziers;
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
const generateAndReport = (pts, paramsOrig, paramsPrime, leftTangent, rightTangent) => {
|
|
305
|
+
let bezCurve, maxError, splitPoint;
|
|
306
|
+
|
|
307
|
+
bezCurve = generateBezier(pts, paramsPrime, leftTangent, rightTangent);
|
|
308
|
+
let _computeMaxError = computeMaxError(pts, bezCurve, paramsOrig);
|
|
309
|
+
|
|
310
|
+
maxError = _computeMaxError[0];
|
|
311
|
+
splitPoint = _computeMaxError[1];
|
|
312
|
+
|
|
313
|
+
return [bezCurve, maxError, splitPoint];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Given set of pts and their parameterization, try to find a better parameterization.
|
|
320
|
+
*/
|
|
321
|
+
function reparameterize(bezier, pts, parameters) {
|
|
322
|
+
return parameters.map((p, i) => {
|
|
323
|
+
return newtonRaphsonRootFind(bezier, pts[i], p);
|
|
324
|
+
});
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Use Newton-Raphson iteration to find better root.
|
|
329
|
+
*/
|
|
330
|
+
|
|
331
|
+
function newtonRaphsonRootFind(bez, point, u) {
|
|
332
|
+
// bez is [p0, cp1, cp2, p1] where each is [x, y]
|
|
333
|
+
// point is the target point [x, y] we're trying to get close to
|
|
334
|
+
// u is our current parameter value (0-1)
|
|
335
|
+
|
|
336
|
+
// Calculate q(u) - point (the vector from target to curve point)
|
|
337
|
+
//let q = bezierQ(bez, u);
|
|
338
|
+
let q = pointAtT(bez, u)
|
|
339
|
+
|
|
340
|
+
let dx = q[0] - point[0];
|
|
341
|
+
let dy = q[1] - point[1];
|
|
342
|
+
|
|
343
|
+
// First derivative (tangent vector at u)
|
|
344
|
+
let qp = bezierQprime(bez, u);
|
|
345
|
+
|
|
346
|
+
// Numerator: dot product of (q(u) - point) and q'(u)
|
|
347
|
+
// This represents how much the error aligns with the tangent
|
|
348
|
+
let numerator = dx * qp[0] + dy * qp[1];
|
|
349
|
+
|
|
350
|
+
// Denominator: |q'(u)|² + 2 * (q(u)-point) · q''(u)
|
|
351
|
+
// First part: squared length of tangent vector
|
|
352
|
+
let qpLenSq = qp[0] * qp[0] + qp[1] * qp[1];
|
|
353
|
+
|
|
354
|
+
// Second derivative
|
|
355
|
+
let qpp = bezierQprime(bez, u, true);
|
|
356
|
+
|
|
357
|
+
// Second part: 2 * (q(u)-point) · q''(u)
|
|
358
|
+
let secondPart = 2 * (dx * qpp[0] + dy * qpp[1]);
|
|
359
|
+
|
|
360
|
+
let denominator = qpLenSq + secondPart;
|
|
361
|
+
|
|
362
|
+
if (Math.abs(denominator) < 1e-10) { // Avoid division by zero
|
|
363
|
+
return u;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Newton-Raphson step: u_new = u - f(u)/f'(u)
|
|
367
|
+
return u - numerator / denominator;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Assign parameter values to digitized pts using relative distances between pts.
|
|
375
|
+
*/
|
|
376
|
+
function chordLengthParameterize(pts) {
|
|
377
|
+
let u = [];
|
|
378
|
+
let l = pts.length;
|
|
379
|
+
let p0 = pts[0];
|
|
380
|
+
let p = pts[1];
|
|
381
|
+
let currU = 0
|
|
382
|
+
let prevU = 0
|
|
383
|
+
|
|
384
|
+
//prevU = 0
|
|
385
|
+
//console.log('prevU', prevU);
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
for (let i = 0; i < l; i++) {
|
|
389
|
+
p = pts[i];
|
|
390
|
+
//currU = i ? prevU + length(subtract(p, p0)) : 0;
|
|
391
|
+
currU = prevU + getDistance(p, p0, true);
|
|
392
|
+
u.push(currU);
|
|
393
|
+
prevU = currU;
|
|
394
|
+
|
|
395
|
+
p0 = p;
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
u = u.map(function (x) {
|
|
400
|
+
return x / prevU;
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
return u;
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Find the maximum squared distance of digitized pts to fitted curve.
|
|
408
|
+
*/
|
|
409
|
+
function computeMaxError(pts, bez, parameters) {
|
|
410
|
+
let dist,
|
|
411
|
+
maxDist,
|
|
412
|
+
splitPoint,
|
|
413
|
+
v,
|
|
414
|
+
i, point, t;
|
|
415
|
+
|
|
416
|
+
maxDist = 0;
|
|
417
|
+
splitPoint = Math.floor(pts.length * 0.5);
|
|
418
|
+
|
|
419
|
+
//console.log('computeMaxError', pts, bez, parameters);
|
|
420
|
+
|
|
421
|
+
let t_distMap = mapTtoRelativeDistances(bez, 10);
|
|
422
|
+
let l = pts.length
|
|
423
|
+
|
|
424
|
+
for (i = 0; i < l; i++) {
|
|
425
|
+
point = pts[i];
|
|
426
|
+
//Find 't' for a point on the bez curve that's as close to 'point' as possible:
|
|
427
|
+
t = find_t(parameters[i], t_distMap, 10);
|
|
428
|
+
|
|
429
|
+
v = subtract(pointAtT(bez, t), point);
|
|
430
|
+
dist = v[0] * v[0] + v[1] * v[1];
|
|
431
|
+
|
|
432
|
+
if (dist > maxDist) {
|
|
433
|
+
maxDist = dist;
|
|
434
|
+
splitPoint = i;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return [maxDist, splitPoint];
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
//Sample 't's and map them to relative distances along the curve:
|
|
442
|
+
function mapTtoRelativeDistances(bez, B_parts) {
|
|
443
|
+
let B_t_curr;
|
|
444
|
+
let B_t_dist = [0];
|
|
445
|
+
let B_t_prev = bez[0];
|
|
446
|
+
let sumLen = 0;
|
|
447
|
+
|
|
448
|
+
for (let i = 1; i <= B_parts; i++) {
|
|
449
|
+
B_t_curr = pointAtT(bez, i / B_parts);
|
|
450
|
+
sumLen += getDistance(B_t_curr, B_t_prev, true);
|
|
451
|
+
B_t_dist.push(sumLen);
|
|
452
|
+
B_t_prev = B_t_curr;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
//Normalize B_length to the same interval as the parameter distances; 0 to 1:
|
|
456
|
+
B_t_dist = B_t_dist.map(function (x) {
|
|
457
|
+
return x / sumLen;
|
|
458
|
+
});
|
|
459
|
+
return B_t_dist;
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
function find_t(param, t_distMap, B_parts) {
|
|
463
|
+
|
|
464
|
+
if (param < 0) {
|
|
465
|
+
return 0;
|
|
466
|
+
}
|
|
467
|
+
if (param > 1) {
|
|
468
|
+
return 1;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
let lenMax, lenMin, tMax, tMin, t;
|
|
472
|
+
|
|
473
|
+
//Find the two t-s that the current param distance lies between,
|
|
474
|
+
//and then interpolate a somewhat accurate value for the exact t:
|
|
475
|
+
for (let i = 1; i <= B_parts; i++) {
|
|
476
|
+
|
|
477
|
+
if (param <= t_distMap[i]) {
|
|
478
|
+
tMin = (i - 1) / B_parts;
|
|
479
|
+
tMax = i / B_parts;
|
|
480
|
+
lenMin = t_distMap[i - 1];
|
|
481
|
+
lenMax = t_distMap[i];
|
|
482
|
+
t = (param - lenMin) / (lenMax - lenMin) * (tMax - tMin) + tMin;
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return t;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Creates a vector of length 1 which shows the direction from B to A
|
|
491
|
+
*/
|
|
492
|
+
function createTangent(p1, p2) {
|
|
493
|
+
// Returns unit vector pointing from B to A
|
|
494
|
+
let dx = p1[0] - p2[0];
|
|
495
|
+
let dy = p1[1] - p2[1];
|
|
496
|
+
let length = Math.sqrt(dx * dx + dy * dy);
|
|
497
|
+
|
|
498
|
+
if (length === 0) return [0, 0];
|
|
499
|
+
return [dx / length, dy / length];
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Math helpers
|
|
505
|
+
*/
|
|
506
|
+
|
|
507
|
+
// Basic vector utilities (only what's absolutely necessary)
|
|
508
|
+
function subtract(a, b) {
|
|
509
|
+
return [a[0] - b[0], a[1] - b[1]];
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function addArrays(a, b) {
|
|
513
|
+
return [a[0] + b[0], a[1] + b[1]];
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
function mulItems(v, s) {
|
|
518
|
+
return [v[0] * s, v[1] * s];
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
function normalize(v) {
|
|
523
|
+
let len = Math.sqrt(v[0] * v[0] + v[1] * v[1]);
|
|
524
|
+
return len === 0 ? [0, 0] : [v[0] / len, v[1] / len];
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function dot(a, b) {
|
|
528
|
+
return a[0] * b[0] + a[1] * b[1];
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function zeros_Xx2x2(x) {
|
|
532
|
+
let zs = [];
|
|
533
|
+
while (x--) {
|
|
534
|
+
zs.push([0, 0]);
|
|
535
|
+
}
|
|
536
|
+
return zs;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
// First derivative (tangent vector)
|
|
541
|
+
function bezierQprime(bez, u, second = false) {
|
|
542
|
+
let p0 = bez[0], cp1 = bez[1], cp2 = bez[2], p1 = bez[3];
|
|
543
|
+
let t = u;
|
|
544
|
+
let mt = 1 - t;
|
|
545
|
+
let mt2 = mt * mt;
|
|
546
|
+
let t2 = t * t;
|
|
547
|
+
let dx, dy;
|
|
548
|
+
|
|
549
|
+
if (second) {
|
|
550
|
+
dx = 6 * mt * (cp2[0] - 2 * cp1[0] + p0[0]) +
|
|
551
|
+
6 * t * (p1[0] - 2 * cp2[0] + cp1[0]);
|
|
552
|
+
|
|
553
|
+
dy = 6 * mt * (cp2[1] - 2 * cp1[1] + p0[1]) +
|
|
554
|
+
6 * t * (p1[1] - 2 * cp2[1] + cp1[1]);
|
|
555
|
+
|
|
556
|
+
} else {
|
|
557
|
+
dx = 3 * mt2 * (cp1[0] - p0[0]) +
|
|
558
|
+
6 * mt * t * (cp2[0] - cp1[0]) +
|
|
559
|
+
3 * t2 * (p1[0] - cp2[0]);
|
|
560
|
+
|
|
561
|
+
dy = 3 * mt2 * (cp1[1] - p0[1]) +
|
|
562
|
+
6 * mt * t * (cp2[1] - cp1[1]) +
|
|
563
|
+
3 * t2 * (p1[1] - cp2[1]);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return [dx, dy];
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { reducePoints } from "./svgii/geometry";
|
|
2
|
+
import { getPolyBBox } from "./svgii/geometry_bbox";
|
|
3
|
+
import { renderPoint } from "./svgii/visualize";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export function simplifyRDP(pts, {
|
|
7
|
+
quality = 0.9,
|
|
8
|
+
width = 0,
|
|
9
|
+
height = 0,
|
|
10
|
+
absolute = false,
|
|
11
|
+
// use square or manhattan distances
|
|
12
|
+
manhattan = false,
|
|
13
|
+
exclude = []
|
|
14
|
+
} = {}) {
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
//console.log(exclude);
|
|
18
|
+
|
|
19
|
+
let excludeSet = new Set(exclude);
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* switch between absolute or
|
|
24
|
+
* quality based relative thresholds
|
|
25
|
+
*/
|
|
26
|
+
if (typeof quality === 'string') {
|
|
27
|
+
absolute = true;
|
|
28
|
+
quality = parseFloat(quality);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (pts.length < 4 || (!absolute && quality) >= 1) return pts;
|
|
32
|
+
|
|
33
|
+
// convert quality to squaredistance or manhattan tolerance
|
|
34
|
+
let tolerance = quality;
|
|
35
|
+
|
|
36
|
+
if (!absolute) {
|
|
37
|
+
|
|
38
|
+
tolerance = 1 - quality;
|
|
39
|
+
|
|
40
|
+
// adjust for higher qualities
|
|
41
|
+
if (quality > 0.5) tolerance /= 2;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* approximate dimensions
|
|
45
|
+
* adjust tolerance for
|
|
46
|
+
* very small polygons e.g geodata
|
|
47
|
+
*/
|
|
48
|
+
if (!width && !height) {
|
|
49
|
+
let polyS = reducePoints(pts, 12);
|
|
50
|
+
({ width, height } = getPolyBBox(polyS));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!manhattan) {
|
|
54
|
+
// average side lengths
|
|
55
|
+
let dimAvg = (width + height) / 2;
|
|
56
|
+
let scale = dimAvg / 100;
|
|
57
|
+
tolerance = (tolerance * (scale)) ** 2
|
|
58
|
+
} else {
|
|
59
|
+
// use manhattan
|
|
60
|
+
tolerance = (width + height) * 0.003 * (1 - quality)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
const segmentDistance = (p, p1, p2, manhattan = false) => {
|
|
67
|
+
let x = p1.x, y = p1.y;
|
|
68
|
+
let dx = p2.x - x, dy = p2.y - y;
|
|
69
|
+
|
|
70
|
+
if (dx !== 0 || dy !== 0) {
|
|
71
|
+
let t = ((p.x - x) * dx + (p.y - y) * dy) / (dx * dx + dy * dy);
|
|
72
|
+
|
|
73
|
+
if (t > 1) {
|
|
74
|
+
x = p2.x;
|
|
75
|
+
y = p2.y;
|
|
76
|
+
} else if (t > 0) {
|
|
77
|
+
x += dx * t;
|
|
78
|
+
y += dy * t;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// use manhattan or square distance
|
|
83
|
+
return !manhattan ? (p.x - x) ** 2 + (p.y - y) ** 2 : Math.abs(p.x - x) + Math.abs(p.y - y);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
// start collecting ptsSmp polyline
|
|
88
|
+
let ptsSmp = [pts[0]];
|
|
89
|
+
|
|
90
|
+
// create processing stack
|
|
91
|
+
let stack = [];
|
|
92
|
+
stack.push([0, pts.length - 1]);
|
|
93
|
+
|
|
94
|
+
let maxDist = tolerance;
|
|
95
|
+
let currentDist = 0;
|
|
96
|
+
let index = -1;
|
|
97
|
+
let lenExclude = exclude.length;
|
|
98
|
+
|
|
99
|
+
while (stack.length > 0) {
|
|
100
|
+
let [first, last] = stack.pop();
|
|
101
|
+
maxDist = tolerance;
|
|
102
|
+
index = -1;
|
|
103
|
+
|
|
104
|
+
// Check if there is an excluded point inside this segment
|
|
105
|
+
let forcedIndex = -1;
|
|
106
|
+
for (let i = first + 1; i < last; i++) {
|
|
107
|
+
if (excludeSet.has(i)) {
|
|
108
|
+
forcedIndex = i;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (forcedIndex !== -1) {
|
|
114
|
+
// Force split at excluded point
|
|
115
|
+
stack.push([forcedIndex, last], [first, forcedIndex]);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Normal RDP distance check
|
|
120
|
+
for (let i = first + 1; i < last; i++) {
|
|
121
|
+
currentDist = segmentDistance(
|
|
122
|
+
pts[i],
|
|
123
|
+
pts[first],
|
|
124
|
+
pts[last],
|
|
125
|
+
manhattan
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
if (currentDist > maxDist) {
|
|
129
|
+
index = i;
|
|
130
|
+
maxDist = currentDist;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (maxDist > tolerance) {
|
|
135
|
+
stack.push([index, last], [first, index]);
|
|
136
|
+
} else {
|
|
137
|
+
ptsSmp.push(pts[last]);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
return ptsSmp;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|