@joint/core 4.0.0
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/LICENSE +376 -0
- package/README.md +49 -0
- package/dist/geometry.js +6486 -0
- package/dist/geometry.min.js +8 -0
- package/dist/joint.d.ts +5536 -0
- package/dist/joint.js +39629 -0
- package/dist/joint.min.js +8 -0
- package/dist/joint.nowrap.js +39626 -0
- package/dist/joint.nowrap.min.js +8 -0
- package/dist/vectorizer.js +9135 -0
- package/dist/vectorizer.min.js +8 -0
- package/dist/version.mjs +3 -0
- package/index.js +3 -0
- package/joint.mjs +27 -0
- package/package.json +192 -0
- package/src/V/annotation.mjs +0 -0
- package/src/V/index.mjs +2642 -0
- package/src/anchors/index.mjs +123 -0
- package/src/config/index.mjs +12 -0
- package/src/connectionPoints/index.mjs +202 -0
- package/src/connectionStrategies/index.mjs +73 -0
- package/src/connectors/curve.mjs +553 -0
- package/src/connectors/index.mjs +6 -0
- package/src/connectors/jumpover.mjs +452 -0
- package/src/connectors/normal.mjs +12 -0
- package/src/connectors/rounded.mjs +17 -0
- package/src/connectors/smooth.mjs +44 -0
- package/src/connectors/straight.mjs +110 -0
- package/src/dia/Cell.mjs +945 -0
- package/src/dia/CellView.mjs +1316 -0
- package/src/dia/Element.mjs +519 -0
- package/src/dia/ElementView.mjs +859 -0
- package/src/dia/Graph.mjs +1112 -0
- package/src/dia/HighlighterView.mjs +319 -0
- package/src/dia/Link.mjs +565 -0
- package/src/dia/LinkView.mjs +2207 -0
- package/src/dia/Paper.mjs +3171 -0
- package/src/dia/PaperLayer.mjs +75 -0
- package/src/dia/ToolView.mjs +69 -0
- package/src/dia/ToolsView.mjs +128 -0
- package/src/dia/attributes/calc.mjs +128 -0
- package/src/dia/attributes/connection.mjs +75 -0
- package/src/dia/attributes/defs.mjs +76 -0
- package/src/dia/attributes/eval.mjs +64 -0
- package/src/dia/attributes/index.mjs +69 -0
- package/src/dia/attributes/legacy.mjs +148 -0
- package/src/dia/attributes/offset.mjs +53 -0
- package/src/dia/attributes/props.mjs +30 -0
- package/src/dia/attributes/shape.mjs +92 -0
- package/src/dia/attributes/text.mjs +180 -0
- package/src/dia/index.mjs +13 -0
- package/src/dia/layers/GridLayer.mjs +176 -0
- package/src/dia/ports.mjs +874 -0
- package/src/elementTools/Control.mjs +153 -0
- package/src/elementTools/HoverConnect.mjs +37 -0
- package/src/elementTools/index.mjs +5 -0
- package/src/env/index.mjs +43 -0
- package/src/g/bezier.mjs +175 -0
- package/src/g/curve.mjs +956 -0
- package/src/g/ellipse.mjs +245 -0
- package/src/g/extend.mjs +64 -0
- package/src/g/geometry.helpers.mjs +58 -0
- package/src/g/index.mjs +17 -0
- package/src/g/intersection.mjs +511 -0
- package/src/g/line.bearing.mjs +30 -0
- package/src/g/line.length.mjs +5 -0
- package/src/g/line.mjs +356 -0
- package/src/g/line.squaredLength.mjs +10 -0
- package/src/g/path.mjs +2260 -0
- package/src/g/point.mjs +375 -0
- package/src/g/points.mjs +247 -0
- package/src/g/polygon.mjs +51 -0
- package/src/g/polyline.mjs +523 -0
- package/src/g/rect.mjs +556 -0
- package/src/g/types.mjs +10 -0
- package/src/highlighters/addClass.mjs +27 -0
- package/src/highlighters/index.mjs +5 -0
- package/src/highlighters/list.mjs +111 -0
- package/src/highlighters/mask.mjs +220 -0
- package/src/highlighters/opacity.mjs +17 -0
- package/src/highlighters/stroke.mjs +100 -0
- package/src/layout/index.mjs +4 -0
- package/src/layout/ports/port.mjs +188 -0
- package/src/layout/ports/portLabel.mjs +224 -0
- package/src/linkAnchors/index.mjs +76 -0
- package/src/linkTools/Anchor.mjs +235 -0
- package/src/linkTools/Arrowhead.mjs +103 -0
- package/src/linkTools/Boundary.mjs +48 -0
- package/src/linkTools/Button.mjs +121 -0
- package/src/linkTools/Connect.mjs +85 -0
- package/src/linkTools/HoverConnect.mjs +161 -0
- package/src/linkTools/Segments.mjs +393 -0
- package/src/linkTools/Vertices.mjs +253 -0
- package/src/linkTools/helpers.mjs +33 -0
- package/src/linkTools/index.mjs +8 -0
- package/src/mvc/Collection.mjs +560 -0
- package/src/mvc/Data.mjs +46 -0
- package/src/mvc/Dom/Dom.mjs +587 -0
- package/src/mvc/Dom/Event.mjs +130 -0
- package/src/mvc/Dom/animations.mjs +122 -0
- package/src/mvc/Dom/events.mjs +69 -0
- package/src/mvc/Dom/index.mjs +13 -0
- package/src/mvc/Dom/methods.mjs +392 -0
- package/src/mvc/Dom/props.mjs +77 -0
- package/src/mvc/Dom/vars.mjs +5 -0
- package/src/mvc/Events.mjs +337 -0
- package/src/mvc/Listener.mjs +33 -0
- package/src/mvc/Model.mjs +239 -0
- package/src/mvc/View.mjs +323 -0
- package/src/mvc/ViewBase.mjs +182 -0
- package/src/mvc/index.mjs +9 -0
- package/src/mvc/mvcUtils.mjs +90 -0
- package/src/polyfills/array.js +4 -0
- package/src/polyfills/base64.js +68 -0
- package/src/polyfills/index.mjs +5 -0
- package/src/polyfills/number.js +3 -0
- package/src/polyfills/string.js +3 -0
- package/src/polyfills/typedArray.js +47 -0
- package/src/routers/index.mjs +6 -0
- package/src/routers/manhattan.mjs +856 -0
- package/src/routers/metro.mjs +91 -0
- package/src/routers/normal.mjs +6 -0
- package/src/routers/oneSide.mjs +60 -0
- package/src/routers/orthogonal.mjs +323 -0
- package/src/routers/rightAngle.mjs +1056 -0
- package/src/shapes/index.mjs +3 -0
- package/src/shapes/standard.mjs +755 -0
- package/src/util/cloneCells.mjs +67 -0
- package/src/util/getRectPoint.mjs +65 -0
- package/src/util/index.mjs +5 -0
- package/src/util/svgTagTemplate.mjs +110 -0
- package/src/util/util.mjs +1754 -0
- package/src/util/utilHelpers.mjs +2402 -0
- package/src/util/wrappers.mjs +56 -0
- package/types/geometry.d.ts +815 -0
- package/types/index.d.ts +53 -0
- package/types/joint.d.ts +4391 -0
- package/types/joint.head.d.ts +12 -0
- package/types/vectorizer.d.ts +327 -0
package/src/g/point.mjs
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Point is the most basic object consisting of x/y coordinate.
|
|
3
|
+
|
|
4
|
+
Possible instantiations are:
|
|
5
|
+
* `Point(10, 20)`
|
|
6
|
+
* `new Point(10, 20)`
|
|
7
|
+
* `Point('10 20')`
|
|
8
|
+
* `Point(Point(10, 20))`
|
|
9
|
+
*/
|
|
10
|
+
import { normalizeAngle, random, snapToGrid, toDeg, toRad } from './geometry.helpers.mjs';
|
|
11
|
+
import { bearing } from './line.bearing.mjs';
|
|
12
|
+
import { squaredLength } from './line.squaredLength.mjs';
|
|
13
|
+
import { length } from './line.length.mjs';
|
|
14
|
+
import { types } from './types.mjs';
|
|
15
|
+
|
|
16
|
+
const {
|
|
17
|
+
abs,
|
|
18
|
+
cos,
|
|
19
|
+
sin,
|
|
20
|
+
sqrt,
|
|
21
|
+
min,
|
|
22
|
+
max,
|
|
23
|
+
atan2,
|
|
24
|
+
round,
|
|
25
|
+
pow,
|
|
26
|
+
PI
|
|
27
|
+
} = Math;
|
|
28
|
+
|
|
29
|
+
export const Point = function(x, y) {
|
|
30
|
+
|
|
31
|
+
if (!(this instanceof Point)) {
|
|
32
|
+
return new Point(x, y);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (typeof x === 'string') {
|
|
36
|
+
var xy = x.split(x.indexOf('@') === -1 ? ' ' : '@');
|
|
37
|
+
x = parseFloat(xy[0]);
|
|
38
|
+
y = parseFloat(xy[1]);
|
|
39
|
+
|
|
40
|
+
} else if (Object(x) === x) {
|
|
41
|
+
y = x.y;
|
|
42
|
+
x = x.x;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this.x = x === undefined ? 0 : x;
|
|
46
|
+
this.y = y === undefined ? 0 : y;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Alternative constructor, from polar coordinates.
|
|
50
|
+
// @param {number} Distance.
|
|
51
|
+
// @param {number} Angle in radians.
|
|
52
|
+
// @param {point} [optional] Origin.
|
|
53
|
+
Point.fromPolar = function(distance, angle, origin) {
|
|
54
|
+
|
|
55
|
+
origin = new Point(origin);
|
|
56
|
+
var x = abs(distance * cos(angle));
|
|
57
|
+
var y = abs(distance * sin(angle));
|
|
58
|
+
var deg = normalizeAngle(toDeg(angle));
|
|
59
|
+
|
|
60
|
+
if (deg < 90) {
|
|
61
|
+
y = -y;
|
|
62
|
+
|
|
63
|
+
} else if (deg < 180) {
|
|
64
|
+
x = -x;
|
|
65
|
+
y = -y;
|
|
66
|
+
|
|
67
|
+
} else if (deg < 270) {
|
|
68
|
+
x = -x;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return new Point(origin.x + x, origin.y + y);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Create a point with random coordinates that fall into the range `[x1, x2]` and `[y1, y2]`.
|
|
75
|
+
Point.random = function(x1, x2, y1, y2) {
|
|
76
|
+
|
|
77
|
+
return new Point(random(x1, x2), random(y1, y2));
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
Point.prototype = {
|
|
81
|
+
|
|
82
|
+
type: types.Point,
|
|
83
|
+
|
|
84
|
+
chooseClosest: function(points) {
|
|
85
|
+
|
|
86
|
+
var n = points.length;
|
|
87
|
+
if (n === 1) return new Point(points[0]);
|
|
88
|
+
var closest = null;
|
|
89
|
+
var minSqrDistance = Infinity;
|
|
90
|
+
for (var i = 0; i < n; i++) {
|
|
91
|
+
var p = new Point(points[i]);
|
|
92
|
+
var sqrDistance = this.squaredDistance(p);
|
|
93
|
+
if (sqrDistance < minSqrDistance) {
|
|
94
|
+
closest = p;
|
|
95
|
+
minSqrDistance = sqrDistance;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return closest;
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
// If point lies outside rectangle `r`, return the nearest point on the boundary of rect `r`,
|
|
102
|
+
// otherwise return point itself.
|
|
103
|
+
// (see Squeak Smalltalk, Point>>adhereTo:)
|
|
104
|
+
adhereToRect: function(r) {
|
|
105
|
+
|
|
106
|
+
if (r.containsPoint(this)) {
|
|
107
|
+
return this;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
this.x = min(max(this.x, r.x), r.x + r.width);
|
|
111
|
+
this.y = min(max(this.y, r.y), r.y + r.height);
|
|
112
|
+
return this;
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
// Compute the angle between vector from me to p1 and the vector from me to p2.
|
|
116
|
+
// ordering of points p1 and p2 is important!
|
|
117
|
+
// theta function's angle convention:
|
|
118
|
+
// returns angles between 0 and 180 when the angle is counterclockwise
|
|
119
|
+
// returns angles between 180 and 360 to convert clockwise angles into counterclockwise ones
|
|
120
|
+
// returns NaN if any of the points p1, p2 is coincident with this point
|
|
121
|
+
angleBetween: function(p1, p2) {
|
|
122
|
+
|
|
123
|
+
var angleBetween = (this.equals(p1) || this.equals(p2)) ? NaN : (this.theta(p2) - this.theta(p1));
|
|
124
|
+
|
|
125
|
+
if (angleBetween < 0) {
|
|
126
|
+
angleBetween += 360; // correction to keep angleBetween between 0 and 360
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return angleBetween;
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
// Return the bearing between me and the given point.
|
|
133
|
+
bearing: function(point) {
|
|
134
|
+
return bearing(this, point);
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
// Returns change in angle from my previous position (-dx, -dy) to my new position
|
|
138
|
+
// relative to ref point.
|
|
139
|
+
changeInAngle: function(dx, dy, ref) {
|
|
140
|
+
|
|
141
|
+
// Revert the translation and measure the change in angle around x-axis.
|
|
142
|
+
return this.clone().offset(-dx, -dy).theta(ref) - this.theta(ref);
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
clone: function() {
|
|
146
|
+
|
|
147
|
+
return new Point(this);
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
// Returns the cross product of this point relative to two other points
|
|
151
|
+
// this point is the common point
|
|
152
|
+
// point p1 lies on the first vector, point p2 lies on the second vector
|
|
153
|
+
// watch out for the ordering of points p1 and p2!
|
|
154
|
+
// positive result indicates a clockwise ("right") turn from first to second vector
|
|
155
|
+
// negative result indicates a counterclockwise ("left") turn from first to second vector
|
|
156
|
+
// zero indicates that the first and second vector are collinear
|
|
157
|
+
// note that the above directions are reversed from the usual answer on the Internet
|
|
158
|
+
// that is because we are in a left-handed coord system (because the y-axis points downward)
|
|
159
|
+
cross: function(p1, p2) {
|
|
160
|
+
|
|
161
|
+
return (p1 && p2) ? (((p2.x - this.x) * (p1.y - this.y)) - ((p2.y - this.y) * (p1.x - this.x))) : NaN;
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
difference: function(dx, dy) {
|
|
165
|
+
|
|
166
|
+
if ((Object(dx) === dx)) {
|
|
167
|
+
dy = dx.y;
|
|
168
|
+
dx = dx.x;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return new Point(this.x - (dx || 0), this.y - (dy || 0));
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
// Returns distance between me and point `p`.
|
|
175
|
+
distance: function(p) {
|
|
176
|
+
return length(this, p);
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
// Returns the dot product of this point with given other point
|
|
180
|
+
dot: function(p) {
|
|
181
|
+
|
|
182
|
+
return p ? (this.x * p.x + this.y * p.y) : NaN;
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
equals: function(p) {
|
|
186
|
+
|
|
187
|
+
return !!p &&
|
|
188
|
+
this.x === p.x &&
|
|
189
|
+
this.y === p.y;
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
// Linear interpolation
|
|
193
|
+
lerp: function(p, t) {
|
|
194
|
+
|
|
195
|
+
var x = this.x;
|
|
196
|
+
var y = this.y;
|
|
197
|
+
return new Point((1 - t) * x + t * p.x, (1 - t) * y + t * p.y);
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
magnitude: function() {
|
|
201
|
+
|
|
202
|
+
return sqrt((this.x * this.x) + (this.y * this.y)) || 0.01;
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
// Returns a manhattan (taxi-cab) distance between me and point `p`.
|
|
206
|
+
manhattanDistance: function(p) {
|
|
207
|
+
|
|
208
|
+
return abs(p.x - this.x) + abs(p.y - this.y);
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
// Move point on line starting from ref ending at me by
|
|
212
|
+
// distance distance.
|
|
213
|
+
move: function(ref, distance) {
|
|
214
|
+
|
|
215
|
+
var theta = toRad((new Point(ref)).theta(this));
|
|
216
|
+
var offset = this.offset(cos(theta) * distance, -sin(theta) * distance);
|
|
217
|
+
return offset;
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
// Scales x and y such that the distance between the point and the origin (0,0) is equal to the given length.
|
|
221
|
+
normalize: function(length) {
|
|
222
|
+
|
|
223
|
+
var scale = (length || 1) / this.magnitude();
|
|
224
|
+
return this.scale(scale, scale);
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
// Offset me by the specified amount.
|
|
228
|
+
offset: function(dx, dy) {
|
|
229
|
+
|
|
230
|
+
if ((Object(dx) === dx)) {
|
|
231
|
+
dy = dx.y;
|
|
232
|
+
dx = dx.x;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
this.x += dx || 0;
|
|
236
|
+
this.y += dy || 0;
|
|
237
|
+
return this;
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
// Returns a point that is the reflection of me with
|
|
241
|
+
// the center of inversion in ref point.
|
|
242
|
+
reflection: function(ref) {
|
|
243
|
+
|
|
244
|
+
return (new Point(ref)).move(this, this.distance(ref));
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
// Rotate point by angle around origin.
|
|
248
|
+
// Angle is flipped because this is a left-handed coord system (y-axis points downward).
|
|
249
|
+
rotate: function(origin, angle) {
|
|
250
|
+
|
|
251
|
+
if (angle === 0) return this;
|
|
252
|
+
|
|
253
|
+
origin = origin || new Point(0, 0);
|
|
254
|
+
|
|
255
|
+
angle = toRad(normalizeAngle(-angle));
|
|
256
|
+
var cosAngle = cos(angle);
|
|
257
|
+
var sinAngle = sin(angle);
|
|
258
|
+
|
|
259
|
+
var x = (cosAngle * (this.x - origin.x)) - (sinAngle * (this.y - origin.y)) + origin.x;
|
|
260
|
+
var y = (sinAngle * (this.x - origin.x)) + (cosAngle * (this.y - origin.y)) + origin.y;
|
|
261
|
+
|
|
262
|
+
this.x = x;
|
|
263
|
+
this.y = y;
|
|
264
|
+
return this;
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
round: function(precision) {
|
|
268
|
+
|
|
269
|
+
let f = 1; // case 0
|
|
270
|
+
if (precision) {
|
|
271
|
+
switch (precision) {
|
|
272
|
+
case 1: f = 10; break;
|
|
273
|
+
case 2: f = 100; break;
|
|
274
|
+
case 3: f = 1000; break;
|
|
275
|
+
default: f = pow(10, precision); break;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
this.x = round(this.x * f) / f;
|
|
280
|
+
this.y = round(this.y * f) / f;
|
|
281
|
+
return this;
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
// Scale point with origin.
|
|
285
|
+
scale: function(sx, sy, origin) {
|
|
286
|
+
|
|
287
|
+
origin = (origin && new Point(origin)) || new Point(0, 0);
|
|
288
|
+
this.x = origin.x + sx * (this.x - origin.x);
|
|
289
|
+
this.y = origin.y + sy * (this.y - origin.y);
|
|
290
|
+
return this;
|
|
291
|
+
},
|
|
292
|
+
|
|
293
|
+
snapToGrid: function(gx, gy) {
|
|
294
|
+
|
|
295
|
+
this.x = snapToGrid(this.x, gx);
|
|
296
|
+
this.y = snapToGrid(this.y, gy || gx);
|
|
297
|
+
return this;
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
squaredDistance: function(p) {
|
|
301
|
+
return squaredLength(this, p);
|
|
302
|
+
},
|
|
303
|
+
|
|
304
|
+
// Compute the angle between me and `p` and the x axis.
|
|
305
|
+
// (cartesian-to-polar coordinates conversion)
|
|
306
|
+
// Return theta angle in degrees.
|
|
307
|
+
theta: function(p) {
|
|
308
|
+
|
|
309
|
+
p = new Point(p);
|
|
310
|
+
|
|
311
|
+
// Invert the y-axis.
|
|
312
|
+
var y = -(p.y - this.y);
|
|
313
|
+
var x = p.x - this.x;
|
|
314
|
+
var rad = atan2(y, x); // defined for all 0 corner cases
|
|
315
|
+
|
|
316
|
+
// Correction for III. and IV. quadrant.
|
|
317
|
+
if (rad < 0) {
|
|
318
|
+
rad = 2 * PI + rad;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return 180 * rad / PI;
|
|
322
|
+
},
|
|
323
|
+
|
|
324
|
+
toJSON: function() {
|
|
325
|
+
|
|
326
|
+
return { x: this.x, y: this.y };
|
|
327
|
+
},
|
|
328
|
+
|
|
329
|
+
// Converts rectangular to polar coordinates.
|
|
330
|
+
// An origin can be specified, otherwise it's 0@0.
|
|
331
|
+
toPolar: function(o) {
|
|
332
|
+
|
|
333
|
+
o = (o && new Point(o)) || new Point(0, 0);
|
|
334
|
+
var x = this.x;
|
|
335
|
+
var y = this.y;
|
|
336
|
+
this.x = sqrt((x - o.x) * (x - o.x) + (y - o.y) * (y - o.y)); // r
|
|
337
|
+
this.y = toRad(o.theta(new Point(x, y)));
|
|
338
|
+
return this;
|
|
339
|
+
},
|
|
340
|
+
|
|
341
|
+
toString: function() {
|
|
342
|
+
|
|
343
|
+
return this.x + '@' + this.y;
|
|
344
|
+
},
|
|
345
|
+
|
|
346
|
+
serialize: function() {
|
|
347
|
+
|
|
348
|
+
return this.x + ',' + this.y;
|
|
349
|
+
},
|
|
350
|
+
|
|
351
|
+
update: function(x, y) {
|
|
352
|
+
|
|
353
|
+
if ((Object(x) === x)) {
|
|
354
|
+
y = x.y;
|
|
355
|
+
x = x.x;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
this.x = x || 0;
|
|
359
|
+
this.y = y || 0;
|
|
360
|
+
return this;
|
|
361
|
+
},
|
|
362
|
+
|
|
363
|
+
// Compute the angle between the vector from 0,0 to me and the vector from 0,0 to p.
|
|
364
|
+
// Returns NaN if p is at 0,0.
|
|
365
|
+
vectorAngle: function(p) {
|
|
366
|
+
|
|
367
|
+
var zero = new Point(0, 0);
|
|
368
|
+
return zero.angleBetween(this, p);
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
Point.prototype.translate = Point.prototype.offset;
|
|
373
|
+
|
|
374
|
+
// For backwards compatibility:
|
|
375
|
+
export const point = Point;
|
package/src/g/points.mjs
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
export function parsePoints(svgString) {
|
|
2
|
+
|
|
3
|
+
// Step 1: Discard surrounding spaces
|
|
4
|
+
const trimmedString = svgString.trim();
|
|
5
|
+
if (trimmedString === '') return [];
|
|
6
|
+
|
|
7
|
+
const points = [];
|
|
8
|
+
|
|
9
|
+
// Step 2: Split at commas (+ their surrounding spaces) or at multiple spaces
|
|
10
|
+
// ReDoS mitigation: Have an anchor at the beginning of each alternation
|
|
11
|
+
// Note: This doesn't simplify double (or more) commas - causes empty coords
|
|
12
|
+
// This regex is used by `split()`, so it doesn't need to use /g
|
|
13
|
+
const coords = trimmedString.split(/\b\s*,\s*|,\s*|\s+/);
|
|
14
|
+
|
|
15
|
+
const numCoords = coords.length;
|
|
16
|
+
for (let i = 0; i < numCoords; i += 2) {
|
|
17
|
+
// Step 3: Convert each coord to number
|
|
18
|
+
// Note: If the coord cannot be converted to a number, it will be `NaN`
|
|
19
|
+
// Note: If the coord is empty ("", e.g. from ",," input), it will be `0`
|
|
20
|
+
// Note: If we end up with an odd number of coords, the last point's second coord will be `NaN`
|
|
21
|
+
points.push({ x: +coords[i], y: +coords[i + 1] });
|
|
22
|
+
}
|
|
23
|
+
return points;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function clonePoints(points) {
|
|
27
|
+
const numPoints = points.length;
|
|
28
|
+
if (numPoints === 0) return [];
|
|
29
|
+
const newPoints = [];
|
|
30
|
+
for (let i = 0; i < numPoints; i++) {
|
|
31
|
+
const point = points[i].clone();
|
|
32
|
+
newPoints.push(point);
|
|
33
|
+
}
|
|
34
|
+
return newPoints;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Returns a convex-hull polyline from this polyline.
|
|
38
|
+
// Implements the Graham scan (https://en.wikipedia.org/wiki/Graham_scan).
|
|
39
|
+
// Output polyline starts at the first element of the original polyline that is on the hull, then continues clockwise.
|
|
40
|
+
// Minimal polyline is found (only vertices of the hull are reported, no collinear points).
|
|
41
|
+
export function convexHull(points) {
|
|
42
|
+
|
|
43
|
+
const { abs } = Math;
|
|
44
|
+
|
|
45
|
+
var i;
|
|
46
|
+
var n;
|
|
47
|
+
|
|
48
|
+
var numPoints = points.length;
|
|
49
|
+
if (numPoints === 0) return []; // if points array is empty
|
|
50
|
+
|
|
51
|
+
// step 1: find the starting point - point with the lowest y (if equality, highest x)
|
|
52
|
+
var startPoint;
|
|
53
|
+
for (i = 0; i < numPoints; i++) {
|
|
54
|
+
if (startPoint === undefined) {
|
|
55
|
+
// if this is the first point we see, set it as start point
|
|
56
|
+
startPoint = points[i];
|
|
57
|
+
|
|
58
|
+
} else if (points[i].y < startPoint.y) {
|
|
59
|
+
// start point should have lowest y from all points
|
|
60
|
+
startPoint = points[i];
|
|
61
|
+
|
|
62
|
+
} else if ((points[i].y === startPoint.y) && (points[i].x > startPoint.x)) {
|
|
63
|
+
// if two points have the lowest y, choose the one that has highest x
|
|
64
|
+
// there are no points to the right of startPoint - no ambiguity about theta 0
|
|
65
|
+
// if there are several coincident start point candidates, first one is reported
|
|
66
|
+
startPoint = points[i];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// step 2: sort the list of points
|
|
71
|
+
// sorting by angle between line from startPoint to point and the x-axis (theta)
|
|
72
|
+
|
|
73
|
+
// step 2a: create the point records = [point, originalIndex, angle]
|
|
74
|
+
var sortedPointRecords = [];
|
|
75
|
+
for (i = 0; i < numPoints; i++) {
|
|
76
|
+
|
|
77
|
+
var angle = startPoint.theta(points[i]);
|
|
78
|
+
if (angle === 0) {
|
|
79
|
+
angle = 360; // give highest angle to start point
|
|
80
|
+
// the start point will end up at end of sorted list
|
|
81
|
+
// the start point will end up at beginning of hull points list
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
var entry = [points[i], i, angle];
|
|
85
|
+
sortedPointRecords.push(entry);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// step 2b: sort the list in place
|
|
89
|
+
sortedPointRecords.sort(function(record1, record2) {
|
|
90
|
+
// returning a negative number here sorts record1 before record2
|
|
91
|
+
// if first angle is smaller than second, first angle should come before second
|
|
92
|
+
|
|
93
|
+
var sortOutput = record1[2] - record2[2]; // negative if first angle smaller
|
|
94
|
+
if (sortOutput === 0) {
|
|
95
|
+
// if the two angles are equal, sort by originalIndex
|
|
96
|
+
sortOutput = record2[1] - record1[1]; // negative if first index larger
|
|
97
|
+
// coincident points will be sorted in reverse-numerical order
|
|
98
|
+
// so the coincident points with lower original index will be considered first
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return sortOutput;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// step 2c: duplicate start record from the top of the stack to the bottom of the stack
|
|
105
|
+
if (sortedPointRecords.length > 2) {
|
|
106
|
+
var startPointRecord = sortedPointRecords[sortedPointRecords.length - 1];
|
|
107
|
+
sortedPointRecords.unshift(startPointRecord);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// step 3a: go through sorted points in order and find those with right turns
|
|
111
|
+
// we want to get our results in clockwise order
|
|
112
|
+
var insidePoints = {}; // dictionary of points with left turns - cannot be on the hull
|
|
113
|
+
var hullPointRecords = []; // stack of records with right turns - hull point candidates
|
|
114
|
+
|
|
115
|
+
var currentPointRecord;
|
|
116
|
+
var currentPoint;
|
|
117
|
+
var lastHullPointRecord;
|
|
118
|
+
var lastHullPoint;
|
|
119
|
+
var secondLastHullPointRecord;
|
|
120
|
+
var secondLastHullPoint;
|
|
121
|
+
while (sortedPointRecords.length !== 0) {
|
|
122
|
+
|
|
123
|
+
currentPointRecord = sortedPointRecords.pop();
|
|
124
|
+
currentPoint = currentPointRecord[0];
|
|
125
|
+
|
|
126
|
+
// check if point has already been discarded
|
|
127
|
+
// keys for insidePoints are stored in the form 'point.x@point.y@@originalIndex'
|
|
128
|
+
if (insidePoints.hasOwnProperty(currentPointRecord[0] + '@@' + currentPointRecord[1])) {
|
|
129
|
+
// this point had an incorrect turn at some previous iteration of this loop
|
|
130
|
+
// this disqualifies it from possibly being on the hull
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
var correctTurnFound = false;
|
|
135
|
+
while (!correctTurnFound) {
|
|
136
|
+
|
|
137
|
+
if (hullPointRecords.length < 2) {
|
|
138
|
+
// not enough points for comparison, just add current point
|
|
139
|
+
hullPointRecords.push(currentPointRecord);
|
|
140
|
+
correctTurnFound = true;
|
|
141
|
+
|
|
142
|
+
} else {
|
|
143
|
+
lastHullPointRecord = hullPointRecords.pop();
|
|
144
|
+
lastHullPoint = lastHullPointRecord[0];
|
|
145
|
+
secondLastHullPointRecord = hullPointRecords.pop();
|
|
146
|
+
secondLastHullPoint = secondLastHullPointRecord[0];
|
|
147
|
+
|
|
148
|
+
var crossProduct = secondLastHullPoint.cross(lastHullPoint, currentPoint);
|
|
149
|
+
|
|
150
|
+
if (crossProduct < 0) {
|
|
151
|
+
// found a right turn
|
|
152
|
+
hullPointRecords.push(secondLastHullPointRecord);
|
|
153
|
+
hullPointRecords.push(lastHullPointRecord);
|
|
154
|
+
hullPointRecords.push(currentPointRecord);
|
|
155
|
+
correctTurnFound = true;
|
|
156
|
+
|
|
157
|
+
} else if (crossProduct === 0) {
|
|
158
|
+
// the three points are collinear
|
|
159
|
+
// three options:
|
|
160
|
+
// there may be a 180 or 0 degree angle at lastHullPoint
|
|
161
|
+
// or two of the three points are coincident
|
|
162
|
+
var THRESHOLD = 1e-10; // we have to take rounding errors into account
|
|
163
|
+
var angleBetween = lastHullPoint.angleBetween(secondLastHullPoint, currentPoint);
|
|
164
|
+
if (abs(angleBetween - 180) < THRESHOLD) { // rounding around 180 to 180
|
|
165
|
+
// if the cross product is 0 because the angle is 180 degrees
|
|
166
|
+
// discard last hull point (add to insidePoints)
|
|
167
|
+
//insidePoints.unshift(lastHullPoint);
|
|
168
|
+
insidePoints[lastHullPointRecord[0] + '@@' + lastHullPointRecord[1]] = lastHullPoint;
|
|
169
|
+
// reenter second-to-last hull point (will be last at next iter)
|
|
170
|
+
hullPointRecords.push(secondLastHullPointRecord);
|
|
171
|
+
// do not do anything with current point
|
|
172
|
+
// correct turn not found
|
|
173
|
+
|
|
174
|
+
} else if (lastHullPoint.equals(currentPoint) || secondLastHullPoint.equals(lastHullPoint)) {
|
|
175
|
+
// if the cross product is 0 because two points are the same
|
|
176
|
+
// discard last hull point (add to insidePoints)
|
|
177
|
+
//insidePoints.unshift(lastHullPoint);
|
|
178
|
+
insidePoints[lastHullPointRecord[0] + '@@' + lastHullPointRecord[1]] = lastHullPoint;
|
|
179
|
+
// reenter second-to-last hull point (will be last at next iter)
|
|
180
|
+
hullPointRecords.push(secondLastHullPointRecord);
|
|
181
|
+
// do not do anything with current point
|
|
182
|
+
// correct turn not found
|
|
183
|
+
|
|
184
|
+
} else if (abs(((angleBetween + 1) % 360) - 1) < THRESHOLD) { // rounding around 0 and 360 to 0
|
|
185
|
+
// if the cross product is 0 because the angle is 0 degrees
|
|
186
|
+
// remove last hull point from hull BUT do not discard it
|
|
187
|
+
// reenter second-to-last hull point (will be last at next iter)
|
|
188
|
+
hullPointRecords.push(secondLastHullPointRecord);
|
|
189
|
+
// put last hull point back into the sorted point records list
|
|
190
|
+
sortedPointRecords.push(lastHullPointRecord);
|
|
191
|
+
// we are switching the order of the 0deg and 180deg points
|
|
192
|
+
// correct turn not found
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
} else {
|
|
196
|
+
// found a left turn
|
|
197
|
+
// discard last hull point (add to insidePoints)
|
|
198
|
+
//insidePoints.unshift(lastHullPoint);
|
|
199
|
+
insidePoints[lastHullPointRecord[0] + '@@' + lastHullPointRecord[1]] = lastHullPoint;
|
|
200
|
+
// reenter second-to-last hull point (will be last at next iter of loop)
|
|
201
|
+
hullPointRecords.push(secondLastHullPointRecord);
|
|
202
|
+
// do not do anything with current point
|
|
203
|
+
// correct turn not found
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// at this point, hullPointRecords contains the output points in clockwise order
|
|
209
|
+
// the points start with lowest-y,highest-x startPoint, and end at the same point
|
|
210
|
+
|
|
211
|
+
// step 3b: remove duplicated startPointRecord from the end of the array
|
|
212
|
+
if (hullPointRecords.length > 2) {
|
|
213
|
+
hullPointRecords.pop();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// step 4: find the lowest originalIndex record and put it at the beginning of hull
|
|
217
|
+
var lowestHullIndex; // the lowest originalIndex on the hull
|
|
218
|
+
var indexOfLowestHullIndexRecord = -1; // the index of the record with lowestHullIndex
|
|
219
|
+
n = hullPointRecords.length;
|
|
220
|
+
for (i = 0; i < n; i++) {
|
|
221
|
+
|
|
222
|
+
var currentHullIndex = hullPointRecords[i][1];
|
|
223
|
+
|
|
224
|
+
if (lowestHullIndex === undefined || currentHullIndex < lowestHullIndex) {
|
|
225
|
+
lowestHullIndex = currentHullIndex;
|
|
226
|
+
indexOfLowestHullIndexRecord = i;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
var hullPointRecordsReordered = [];
|
|
231
|
+
if (indexOfLowestHullIndexRecord > 0) {
|
|
232
|
+
var newFirstChunk = hullPointRecords.slice(indexOfLowestHullIndexRecord);
|
|
233
|
+
var newSecondChunk = hullPointRecords.slice(0, indexOfLowestHullIndexRecord);
|
|
234
|
+
hullPointRecordsReordered = newFirstChunk.concat(newSecondChunk);
|
|
235
|
+
|
|
236
|
+
} else {
|
|
237
|
+
hullPointRecordsReordered = hullPointRecords;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
var hullPoints = [];
|
|
241
|
+
n = hullPointRecordsReordered.length;
|
|
242
|
+
for (i = 0; i < n; i++) {
|
|
243
|
+
hullPoints.push(hullPointRecordsReordered[i][0]);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return hullPoints;
|
|
247
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Point } from './point.mjs';
|
|
2
|
+
import { Polyline } from './polyline.mjs';
|
|
3
|
+
import { extend } from './extend.mjs';
|
|
4
|
+
import { types } from './types.mjs';
|
|
5
|
+
import { clonePoints, parsePoints, convexHull } from './points.mjs';
|
|
6
|
+
|
|
7
|
+
export const Polygon = function(points) {
|
|
8
|
+
|
|
9
|
+
if (!(this instanceof Polygon)) {
|
|
10
|
+
return new Polygon(points);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (typeof points === 'string') {
|
|
14
|
+
return new Polygon.parse(points);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
this.points = (Array.isArray(points) ? points.map(Point) : []);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
Polygon.parse = function(svgString) {
|
|
21
|
+
return new Polygon(parsePoints(svgString));
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
Polygon.fromRect = function(rect) {
|
|
25
|
+
return new Polygon([
|
|
26
|
+
rect.topLeft(),
|
|
27
|
+
rect.topRight(),
|
|
28
|
+
rect.bottomRight(),
|
|
29
|
+
rect.bottomLeft()
|
|
30
|
+
]);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
Polygon.prototype = extend(Polyline.prototype, {
|
|
34
|
+
|
|
35
|
+
type: types.Polygon,
|
|
36
|
+
|
|
37
|
+
clone: function() {
|
|
38
|
+
return new Polygon(clonePoints(this.points));
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
convexHull: function() {
|
|
42
|
+
return new Polygon(convexHull(this.points));
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
lengthPoints: function() {
|
|
46
|
+
const { start, end, points } = this;
|
|
47
|
+
if (points.length <= 1 || start.equals(end)) return points;
|
|
48
|
+
return [...points, start.clone()];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
});
|