UNPKG

36.6 kBJavaScriptView Raw
1export const arePositionsSame = ( p1, p2 ) =>
2 p1.x === p2.x && p1.y === p2.y;
3
4export const copyPosition = p =>
5 ({ x: p.x, y: p.y });
6
7export const modelToRenderedPosition = ( p, zoom, pan ) => ({
8 x: p.x * zoom + pan.x,
9 y: p.y * zoom + pan.y
10});
11
12export const renderedToModelPosition = ( p, zoom, pan ) => ({
13 x: ( p.x - pan.x ) / zoom,
14 y: ( p.y - pan.y ) / zoom
15});
16
17export const array2point = arr => ({
18 x: arr[0],
19 y: arr[1]
20});
21
22export const min = ( arr, begin = 0, end = arr.length ) => {
23 let min = Infinity;
24
25 for( let i = begin; i < end; i++ ){
26 let val = arr[i];
27
28 if( isFinite(val) ){
29 min = Math.min( val, min );
30 }
31 }
32
33 return min;
34};
35
36export const max = ( arr, begin = 0, end = arr.length ) => {
37 let max = -Infinity;
38
39 for( let i = begin; i < end; i++ ){
40 let val = arr[i];
41
42 if( isFinite(val) ){
43 max = Math.max( val, max );
44 }
45 }
46
47 return max;
48};
49
50export const mean = ( arr, begin = 0, end = arr.length ) => {
51 let total = 0;
52 let n = 0;
53
54 for( let i = begin; i < end; i++ ){
55 let val = arr[i];
56
57 if( isFinite(val) ){
58 total += val;
59 n++;
60 }
61 }
62
63 return total / n;
64};
65
66export const median = ( arr, begin = 0, end = arr.length, copy = true, sort = true, includeHoles = true ) => {
67 if( copy ){
68 arr = arr.slice( begin, end );
69 } else {
70 if( end < arr.length ){
71 arr.splice( end, arr.length - end );
72 }
73
74 if( begin > 0 ){
75 arr.splice( 0, begin );
76 }
77 }
78
79 // all non finite (e.g. Infinity, NaN) elements must be -Infinity so they go to the start
80 let off = 0; // offset from non-finite values
81 for( let i = arr.length - 1; i >= 0; i-- ){
82 let v = arr[i];
83
84 if( includeHoles ){
85 if( !isFinite(v) ){
86 arr[i] = -Infinity;
87 off++;
88 }
89 } else { // just remove it if we don't want to consider holes
90 arr.splice(i, 1);
91 }
92 }
93
94 if( sort ){
95 arr.sort( (a, b) => a - b ); // requires copy = true if you don't want to change the orig
96 }
97
98 let len = arr.length;
99 let mid = Math.floor( len / 2 );
100
101 if( len % 2 !== 0 ){
102 return arr[mid + 1 + off];
103 } else {
104 return ( arr[mid - 1 + off] + arr[mid + off] )/2;
105 }
106};
107
108export const deg2rad = deg =>
109 Math.PI * deg / 180;
110
111export const getAngleFromDisp = ( dispX, dispY ) =>
112 Math.atan2( dispY, dispX ) - Math.PI / 2;
113
114export const log2 = Math.log2 || (n => Math.log( n ) / Math.log( 2 ));
115
116export const signum = x => {
117 if( x > 0 ){
118 return 1;
119 } else if( x < 0 ){
120 return -1;
121 } else {
122 return 0;
123 }
124};
125
126export const dist = ( p1, p2 ) =>
127 Math.sqrt( sqdist( p1, p2 ) );
128
129export const sqdist = ( p1, p2 ) => {
130 let dx = p2.x - p1.x;
131 let dy = p2.y - p1.y;
132
133 return dx * dx + dy * dy;
134};
135
136export const inPlaceSumNormalize = v => {
137 let length = v.length;
138
139 // First, get sum of all elements
140 let total = 0;
141 for( let i = 0; i < length; i++ ){
142 total += v[i];
143 }
144
145 // Now, divide each by the sum of all elements
146 for( let i = 0; i < length; i++ ){
147 v[i] = v[i] / total;
148 }
149
150 return v;
151};
152
153export const normalize = v => inPlaceSumNormalize( v.slice() );
154
155// from http://en.wikipedia.org/wiki/Bézier_curve#Quadratic_curves
156export const qbezierAt = ( p0, p1, p2, t ) =>
157 (1 - t) * (1 - t) * p0 + 2 * (1 - t) * t * p1 + t * t * p2;
158
159export const qbezierPtAt = ( p0, p1, p2, t ) => ({
160 x: qbezierAt( p0.x, p1.x, p2.x, t ),
161 y: qbezierAt( p0.y, p1.y, p2.y, t )
162});
163
164export const lineAt = ( p0, p1, t, d ) => {
165 let vec = {
166 x: p1.x - p0.x,
167 y: p1.y - p0.y
168 };
169
170 let vecDist = dist( p0, p1 );
171
172 let normVec = {
173 x: vec.x / vecDist,
174 y: vec.y / vecDist
175 };
176
177 t = t == null ? 0 : t;
178
179 d = d != null ? d : t * vecDist;
180
181 return {
182 x: p0.x + normVec.x * d,
183 y: p0.y + normVec.y * d
184 };
185};
186
187export const lineAtDist = ( p0, p1, d ) =>
188 lineAt( p0, p1, undefined, d );
189
190// get angle at A via cosine law
191export const triangleAngle = ( A, B, C ) => {
192 let a = dist( B, C );
193 let b = dist( A, C );
194 let c = dist( A, B );
195
196 return Math.acos( (a*a + b*b - c*c)/(2*a*b) );
197};
198
199export const bound = ( min, val, max ) =>
200 Math.max( min, Math.min( max, val ) );
201
202// makes a full bb (x1, y1, x2, y2, w, h) from implicit params
203export const makeBoundingBox = bb => {
204 if( bb == null ){
205 return {
206 x1: Infinity,
207 y1: Infinity,
208 x2: -Infinity,
209 y2: -Infinity,
210 w: 0,
211 h: 0
212 };
213 } else if( bb.x1 != null && bb.y1 != null ){
214 if( bb.x2 != null && bb.y2 != null && bb.x2 >= bb.x1 && bb.y2 >= bb.y1 ){
215 return {
216 x1: bb.x1,
217 y1: bb.y1,
218 x2: bb.x2,
219 y2: bb.y2,
220 w: bb.x2 - bb.x1,
221 h: bb.y2 - bb.y1
222 };
223 } else if( bb.w != null && bb.h != null && bb.w >= 0 && bb.h >= 0 ){
224 return {
225 x1: bb.x1,
226 y1: bb.y1,
227 x2: bb.x1 + bb.w,
228 y2: bb.y1 + bb.h,
229 w: bb.w,
230 h: bb.h
231 };
232 }
233 }
234};
235
236export const copyBoundingBox = bb => {
237 return { x1: bb.x1, x2: bb.x2, w: bb.w, y1: bb.y1, y2: bb.y2, h: bb.h };
238};
239
240export const clearBoundingBox = bb => {
241 bb.x1 = Infinity;
242 bb.y1 = Infinity;
243 bb.x2 = -Infinity;
244 bb.y2 = -Infinity;
245 bb.w = 0;
246 bb.h = 0;
247};
248
249export const shiftBoundingBox = function( bb, dx, dy ){
250 return {
251 x1: bb.x1 + dx,
252 x2: bb.x2 + dx,
253 y1: bb.y1 + dy,
254 y2: bb.y2 + dy,
255 w: bb.w,
256 h: bb.h
257 };
258};
259
260export const updateBoundingBox = function( bb1, bb2 ){
261 // update bb1 with bb2 bounds
262
263 bb1.x1 = Math.min( bb1.x1, bb2.x1 );
264 bb1.x2 = Math.max( bb1.x2, bb2.x2 );
265 bb1.w = bb1.x2 - bb1.x1;
266
267 bb1.y1 = Math.min( bb1.y1, bb2.y1 );
268 bb1.y2 = Math.max( bb1.y2, bb2.y2 );
269 bb1.h = bb1.y2 - bb1.y1;
270};
271
272export const expandBoundingBoxByPoint = ( bb, x, y ) => {
273 bb.x1 = Math.min( bb.x1, x );
274 bb.x2 = Math.max( bb.x2, x );
275 bb.w = bb.x2 - bb.x1;
276
277 bb.y1 = Math.min( bb.y1, y );
278 bb.y2 = Math.max( bb.y2, y );
279 bb.h = bb.y2 - bb.y1;
280};
281
282export const expandBoundingBox = ( bb, padding = 0 ) => {
283 bb.x1 -= padding;
284 bb.x2 += padding;
285 bb.y1 -= padding;
286 bb.y2 += padding;
287 bb.w = bb.x2 - bb.x1;
288 bb.h = bb.y2 - bb.y1;
289
290 return bb;
291};
292
293export const expandBoundingBoxSides = (bb, padding = [0] ) => {
294 let top, right, bottom, left;
295 if (padding.length === 1) {
296 top = right = bottom = left = padding[0];
297 } else if (padding.length === 2) {
298 top = bottom = padding[0];
299 left = right = padding[1];
300 } else if (padding.length === 4) {
301 [top, right, bottom, left] = padding;
302 }
303
304 bb.x1 -= left;
305 bb.x2 += right;
306 bb.y1 -= top;
307 bb.y2 += bottom;
308 bb.w = bb.x2 - bb.x1;
309 bb.h = bb.y2 - bb.y1;
310
311 return bb;
312};
313
314const expandToInt = x => x > 0 ? Math.ceil(x) : Math.floor(x);
315
316export const expandBoundingBoxToInts = ( bb, padding = 0 ) => {
317 bb.x1 = expandToInt(bb.x1 - padding);
318 bb.y1 = expandToInt(bb.y1 - padding);
319 bb.x2 = expandToInt(bb.x2 + padding);
320 bb.y2 = expandToInt(bb.y2 + padding);
321 bb.w = bb.x2 - bb.x1;
322 bb.h = bb.y2 - bb.y1;
323};
324
325// assign the values of bb2 into bb1
326export const assignBoundingBox = ( bb1, bb2 ) => {
327 bb1.x1 = bb2.x1;
328 bb1.y1 = bb2.y1;
329 bb1.x2 = bb2.x2;
330 bb1.y2 = bb2.y2;
331 bb1.w = bb1.x2 - bb1.x1;
332 bb1.h = bb1.y2 - bb1.y1;
333};
334
335export const assignShiftToBoundingBox = ( bb, delta ) => {
336 bb.x1 += delta.x;
337 bb.x2 += delta.x;
338 bb.y1 += delta.y;
339 bb.y2 += delta.y;
340};
341
342export const boundingBoxesIntersect = ( bb1, bb2 ) => {
343 // case: one bb to right of other
344 if( bb1.x1 > bb2.x2 ){ return false; }
345 if( bb2.x1 > bb1.x2 ){ return false; }
346
347 // case: one bb to left of other
348 if( bb1.x2 < bb2.x1 ){ return false; }
349 if( bb2.x2 < bb1.x1 ){ return false; }
350
351 // case: one bb above other
352 if( bb1.y2 < bb2.y1 ){ return false; }
353 if( bb2.y2 < bb1.y1 ){ return false; }
354
355 // case: one bb below other
356 if( bb1.y1 > bb2.y2 ){ return false; }
357 if( bb2.y1 > bb1.y2 ){ return false; }
358
359 // otherwise, must have some overlap
360 return true;
361};
362
363export const inBoundingBox = ( bb, x, y ) =>
364 bb.x1 <= x && x <= bb.x2 && bb.y1 <= y && y <= bb.y2;
365
366export const pointInBoundingBox = ( bb, pt ) =>
367 inBoundingBox( bb, pt.x, pt.y );
368
369export const boundingBoxInBoundingBox = ( bb1, bb2 ) => (
370 inBoundingBox( bb1, bb2.x1, bb2.y1 )
371 && inBoundingBox( bb1, bb2.x2, bb2.y2 )
372);
373
374export const roundRectangleIntersectLine = ( x, y, nodeX, nodeY, width, height, padding ) => {
375
376 let cornerRadius = getRoundRectangleRadius( width, height );
377
378 let halfWidth = width / 2;
379 let halfHeight = height / 2;
380
381 // Check intersections with straight line segments
382 let straightLineIntersections;
383
384 // Top segment, left to right
385 {
386 let topStartX = nodeX - halfWidth + cornerRadius - padding;
387 let topStartY = nodeY - halfHeight - padding;
388 let topEndX = nodeX + halfWidth - cornerRadius + padding;
389 let topEndY = topStartY;
390
391 straightLineIntersections = finiteLinesIntersect(
392 x, y, nodeX, nodeY, topStartX, topStartY, topEndX, topEndY, false );
393
394 if( straightLineIntersections.length > 0 ){
395 return straightLineIntersections;
396 }
397 }
398
399 // Right segment, top to bottom
400 {
401 let rightStartX = nodeX + halfWidth + padding;
402 let rightStartY = nodeY - halfHeight + cornerRadius - padding;
403 let rightEndX = rightStartX;
404 let rightEndY = nodeY + halfHeight - cornerRadius + padding;
405
406 straightLineIntersections = finiteLinesIntersect(
407 x, y, nodeX, nodeY, rightStartX, rightStartY, rightEndX, rightEndY, false );
408
409 if( straightLineIntersections.length > 0 ){
410 return straightLineIntersections;
411 }
412 }
413
414 // Bottom segment, left to right
415 {
416 let bottomStartX = nodeX - halfWidth + cornerRadius - padding;
417 let bottomStartY = nodeY + halfHeight + padding;
418 let bottomEndX = nodeX + halfWidth - cornerRadius + padding;
419 let bottomEndY = bottomStartY;
420
421 straightLineIntersections = finiteLinesIntersect(
422 x, y, nodeX, nodeY, bottomStartX, bottomStartY, bottomEndX, bottomEndY, false );
423
424 if( straightLineIntersections.length > 0 ){
425 return straightLineIntersections;
426 }
427 }
428
429 // Left segment, top to bottom
430 {
431 let leftStartX = nodeX - halfWidth - padding;
432 let leftStartY = nodeY - halfHeight + cornerRadius - padding;
433 let leftEndX = leftStartX;
434 let leftEndY = nodeY + halfHeight - cornerRadius + padding;
435
436 straightLineIntersections = finiteLinesIntersect(
437 x, y, nodeX, nodeY, leftStartX, leftStartY, leftEndX, leftEndY, false );
438
439 if( straightLineIntersections.length > 0 ){
440 return straightLineIntersections;
441 }
442 }
443
444 // Check intersections with arc segments
445 let arcIntersections;
446
447 // Top Left
448 {
449 let topLeftCenterX = nodeX - halfWidth + cornerRadius;
450 let topLeftCenterY = nodeY - halfHeight + cornerRadius;
451 arcIntersections = intersectLineCircle(
452 x, y, nodeX, nodeY,
453 topLeftCenterX, topLeftCenterY, cornerRadius + padding );
454
455 // Ensure the intersection is on the desired quarter of the circle
456 if( arcIntersections.length > 0
457 && arcIntersections[0] <= topLeftCenterX
458 && arcIntersections[1] <= topLeftCenterY ){
459 return [ arcIntersections[0], arcIntersections[1] ];
460 }
461 }
462
463 // Top Right
464 {
465 let topRightCenterX = nodeX + halfWidth - cornerRadius;
466 let topRightCenterY = nodeY - halfHeight + cornerRadius;
467 arcIntersections = intersectLineCircle(
468 x, y, nodeX, nodeY,
469 topRightCenterX, topRightCenterY, cornerRadius + padding );
470
471 // Ensure the intersection is on the desired quarter of the circle
472 if( arcIntersections.length > 0
473 && arcIntersections[0] >= topRightCenterX
474 && arcIntersections[1] <= topRightCenterY ){
475 return [ arcIntersections[0], arcIntersections[1] ];
476 }
477 }
478
479 // Bottom Right
480 {
481 let bottomRightCenterX = nodeX + halfWidth - cornerRadius;
482 let bottomRightCenterY = nodeY + halfHeight - cornerRadius;
483 arcIntersections = intersectLineCircle(
484 x, y, nodeX, nodeY,
485 bottomRightCenterX, bottomRightCenterY, cornerRadius + padding );
486
487 // Ensure the intersection is on the desired quarter of the circle
488 if( arcIntersections.length > 0
489 && arcIntersections[0] >= bottomRightCenterX
490 && arcIntersections[1] >= bottomRightCenterY ){
491 return [ arcIntersections[0], arcIntersections[1] ];
492 }
493 }
494
495 // Bottom Left
496 {
497 let bottomLeftCenterX = nodeX - halfWidth + cornerRadius;
498 let bottomLeftCenterY = nodeY + halfHeight - cornerRadius;
499 arcIntersections = intersectLineCircle(
500 x, y, nodeX, nodeY,
501 bottomLeftCenterX, bottomLeftCenterY, cornerRadius + padding );
502
503 // Ensure the intersection is on the desired quarter of the circle
504 if( arcIntersections.length > 0
505 && arcIntersections[0] <= bottomLeftCenterX
506 && arcIntersections[1] >= bottomLeftCenterY ){
507 return [ arcIntersections[0], arcIntersections[1] ];
508 }
509 }
510
511 return []; // if nothing
512};
513
514export const inLineVicinity = ( x, y, lx1, ly1, lx2, ly2, tolerance ) => {
515 let t = tolerance;
516
517 let x1 = Math.min( lx1, lx2 );
518 let x2 = Math.max( lx1, lx2 );
519 let y1 = Math.min( ly1, ly2 );
520 let y2 = Math.max( ly1, ly2 );
521
522 return x1 - t <= x && x <= x2 + t
523 && y1 - t <= y && y <= y2 + t;
524};
525
526export const inBezierVicinity = ( x, y, x1, y1, x2, y2, x3, y3, tolerance ) => {
527
528 let bb = {
529 x1: Math.min( x1, x3, x2 ) - tolerance,
530 x2: Math.max( x1, x3, x2 ) + tolerance,
531 y1: Math.min( y1, y3, y2 ) - tolerance,
532 y2: Math.max( y1, y3, y2 ) + tolerance
533 };
534
535 // if outside the rough bounding box for the bezier, then it can't be a hit
536 if( x < bb.x1 || x > bb.x2 || y < bb.y1 || y > bb.y2 ){
537 // console.log('bezier out of rough bb')
538 return false;
539 } else {
540 // console.log('do more expensive check');
541 return true;
542 }
543
544};
545
546export const solveQuadratic = ( a, b, c, val ) => {
547 c -= val;
548
549 var r = b * b - 4 * a * c;
550
551 if( r < 0 ){ return []; }
552
553 var sqrtR = Math.sqrt( r );
554 var denom = 2 * a;
555 var root1 = ( -b + sqrtR ) / denom;
556 var root2 = ( -b - sqrtR ) / denom;
557
558 return [ root1, root2 ];
559};
560
561export const solveCubic = ( a, b, c, d, result ) => {
562
563 // Solves a cubic function, returns root in form [r1, i1, r2, i2, r3, i3], where
564 // r is the real component, i is the imaginary component
565
566 // An implementation of the Cardano method from the year 1545
567 // http://en.wikipedia.org/wiki/Cubic_function#The_nature_of_the_roots
568
569 var epsilon = 0.00001;
570
571 // avoid division by zero while keeping the overall expression close in value
572 if( a === 0 ){
573 a = epsilon;
574 }
575
576 b /= a;
577 c /= a;
578 d /= a;
579
580 let discriminant, q, r, dum1, s, t, term1, r13;
581
582 q = (3.0 * c - (b * b)) / 9.0;
583 r = -(27.0 * d) + b * (9.0 * c - 2.0 * (b * b));
584 r /= 54.0;
585
586 discriminant = q * q * q + r * r;
587 result[1] = 0;
588 term1 = (b / 3.0);
589
590 if( discriminant > 0 ){
591 s = r + Math.sqrt( discriminant );
592 s = ((s < 0) ? -Math.pow( -s, (1.0 / 3.0) ) : Math.pow( s, (1.0 / 3.0) ));
593 t = r - Math.sqrt( discriminant );
594 t = ((t < 0) ? -Math.pow( -t, (1.0 / 3.0) ) : Math.pow( t, (1.0 / 3.0) ));
595 result[0] = -term1 + s + t;
596 term1 += (s + t) / 2.0;
597 result[4] = result[2] = -term1;
598 term1 = Math.sqrt( 3.0 ) * (-t + s) / 2;
599 result[3] = term1;
600 result[5] = -term1;
601 return;
602 }
603
604 result[5] = result[3] = 0;
605
606 if( discriminant === 0 ){
607 r13 = ((r < 0) ? -Math.pow( -r, (1.0 / 3.0) ) : Math.pow( r, (1.0 / 3.0) ));
608 result[0] = -term1 + 2.0 * r13;
609 result[4] = result[2] = -(r13 + term1);
610 return;
611 }
612
613 q = -q;
614 dum1 = q * q * q;
615 dum1 = Math.acos( r / Math.sqrt( dum1 ) );
616 r13 = 2.0 * Math.sqrt( q );
617 result[0] = -term1 + r13 * Math.cos( dum1 / 3.0 );
618 result[2] = -term1 + r13 * Math.cos( (dum1 + 2.0 * Math.PI) / 3.0 );
619 result[4] = -term1 + r13 * Math.cos( (dum1 + 4.0 * Math.PI) / 3.0 );
620
621 return;
622};
623
624export const sqdistToQuadraticBezier = ( x, y, x1, y1, x2, y2, x3, y3 ) => {
625
626 // Find minimum distance by using the minimum of the distance
627 // function between the given point and the curve
628
629 // This gives the coefficients of the resulting cubic equation
630 // whose roots tell us where a possible minimum is
631 // (Coefficients are divided by 4)
632
633 let a = 1.0 * x1 * x1 - 4 * x1 * x2 + 2 * x1 * x3 + 4 * x2 * x2 - 4 * x2 * x3 + x3 * x3
634 + y1 * y1 - 4 * y1 * y2 + 2 * y1 * y3 + 4 * y2 * y2 - 4 * y2 * y3 + y3 * y3;
635
636 let b = 1.0 * 9 * x1 * x2 - 3 * x1 * x1 - 3 * x1 * x3 - 6 * x2 * x2 + 3 * x2 * x3
637 + 9 * y1 * y2 - 3 * y1 * y1 - 3 * y1 * y3 - 6 * y2 * y2 + 3 * y2 * y3;
638
639 let c = 1.0 * 3 * x1 * x1 - 6 * x1 * x2 + x1 * x3 - x1 * x + 2 * x2 * x2 + 2 * x2 * x - x3 * x
640 + 3 * y1 * y1 - 6 * y1 * y2 + y1 * y3 - y1 * y + 2 * y2 * y2 + 2 * y2 * y - y3 * y;
641
642 let d = 1.0 * x1 * x2 - x1 * x1 + x1 * x - x2 * x
643 + y1 * y2 - y1 * y1 + y1 * y - y2 * y;
644
645 // debug("coefficients: " + a / a + ", " + b / a + ", " + c / a + ", " + d / a);
646
647 let roots = [];
648
649 // Use the cubic solving algorithm
650 solveCubic( a, b, c, d, roots );
651
652 let zeroThreshold = 0.0000001;
653
654 let params = [];
655
656 for( let index = 0; index < 6; index += 2 ){
657 if( Math.abs( roots[ index + 1] ) < zeroThreshold
658 && roots[ index ] >= 0
659 && roots[ index ] <= 1.0 ){
660 params.push( roots[ index ] );
661 }
662 }
663
664 params.push( 1.0 );
665 params.push( 0.0 );
666
667 let minDistanceSquared = -1;
668
669 let curX, curY, distSquared;
670 for( let i = 0; i < params.length; i++ ){
671 curX = Math.pow( 1.0 - params[ i ], 2.0 ) * x1
672 + 2.0 * (1 - params[ i ]) * params[ i ] * x2
673 + params[ i ] * params[ i ] * x3;
674
675 curY = Math.pow( 1 - params[ i ], 2.0 ) * y1
676 + 2 * (1.0 - params[ i ]) * params[ i ] * y2
677 + params[ i ] * params[ i ] * y3;
678
679 distSquared = Math.pow( curX - x, 2 ) + Math.pow( curY - y, 2 );
680 // debug('distance for param ' + params[i] + ": " + Math.sqrt(distSquared));
681 if( minDistanceSquared >= 0 ){
682 if( distSquared < minDistanceSquared ){
683 minDistanceSquared = distSquared;
684 }
685 } else {
686 minDistanceSquared = distSquared;
687 }
688 }
689
690 return minDistanceSquared;
691};
692
693export const sqdistToFiniteLine = ( x, y, x1, y1, x2, y2 ) => {
694 let offset = [ x - x1, y - y1 ];
695 let line = [ x2 - x1, y2 - y1 ];
696
697 let lineSq = line[0] * line[0] + line[1] * line[1];
698 let hypSq = offset[0] * offset[0] + offset[1] * offset[1];
699
700 let dotProduct = offset[0] * line[0] + offset[1] * line[1];
701 let adjSq = dotProduct * dotProduct / lineSq;
702
703 if( dotProduct < 0 ){
704 return hypSq;
705 }
706
707 if( adjSq > lineSq ){
708 return (x - x2) * (x - x2) + (y - y2) * (y - y2);
709 }
710
711 return hypSq - adjSq;
712};
713
714export const pointInsidePolygonPoints = ( x, y, points ) => {
715 let x1, y1, x2, y2;
716 let y3;
717
718 // Intersect with vertical line through (x, y)
719 let up = 0;
720 // let down = 0;
721 for( let i = 0; i < points.length / 2; i++ ){
722 x1 = points[ i * 2];
723 y1 = points[ i * 2 + 1];
724
725 if( i + 1 < points.length / 2 ){
726 x2 = points[ (i + 1) * 2];
727 y2 = points[ (i + 1) * 2 + 1];
728 } else {
729 x2 = points[ (i + 1 - points.length / 2) * 2];
730 y2 = points[ (i + 1 - points.length / 2) * 2 + 1];
731 }
732
733 if( x1 == x && x2 == x ){
734 // then ignore
735 } else if( (x1 >= x && x >= x2)
736 || (x1 <= x && x <= x2) ){
737
738 y3 = (x - x1) / (x2 - x1) * (y2 - y1) + y1;
739
740 if( y3 > y ){
741 up++;
742 }
743
744 // if( y3 < y ){
745 // down++;
746 // }
747
748 } else {
749 continue;
750 }
751
752 }
753
754 if( up % 2 === 0 ){
755 return false;
756 } else {
757 return true;
758 }
759};
760
761export const pointInsidePolygon = ( x, y, basePoints, centerX, centerY, width, height, direction, padding ) => {
762 let transformedPoints = new Array( basePoints.length );
763
764 // Gives negative angle
765 let angle;
766
767 if( direction[0] != null ){
768 angle = Math.atan( direction[1] / direction[0] );
769
770 if( direction[0] < 0 ){
771 angle = angle + Math.PI / 2;
772 } else {
773 angle = -angle - Math.PI / 2;
774 }
775 } else {
776 angle = direction;
777 }
778
779 let cos = Math.cos( -angle );
780 let sin = Math.sin( -angle );
781
782 // console.log("base: " + basePoints);
783 for( let i = 0; i < transformedPoints.length / 2; i++ ){
784 transformedPoints[ i * 2] =
785 width / 2 * (basePoints[ i * 2] * cos
786 - basePoints[ i * 2 + 1] * sin);
787
788 transformedPoints[ i * 2 + 1] =
789 height / 2 * (basePoints[ i * 2 + 1] * cos
790 + basePoints[ i * 2] * sin);
791
792 transformedPoints[ i * 2] += centerX;
793 transformedPoints[ i * 2 + 1] += centerY;
794 }
795
796 let points;
797
798 if( padding > 0 ){
799 let expandedLineSet = expandPolygon(
800 transformedPoints,
801 -padding );
802
803 points = joinLines( expandedLineSet );
804 } else {
805 points = transformedPoints;
806 }
807
808 return pointInsidePolygonPoints( x, y, points );
809};
810
811export const pointInsideRoundPolygon = ( x, y, basePoints, centerX, centerY, width, height ) => {
812 const cutPolygonPoints = new Array( basePoints.length);
813 const halfW = width / 2;
814 const halfH = height / 2;
815 const cornerRadius = getRoundPolygonRadius(width, height);
816 const squaredCornerRadius = cornerRadius * cornerRadius;
817
818 for ( let i = 0; i < basePoints.length / 4; i++ ){
819 let sourceUv, destUv;
820 if ( i === 0 ) {
821 sourceUv = basePoints.length - 2;
822 } else {
823 sourceUv = i * 4 - 2;
824 }
825 destUv = i * 4 + 2;
826
827 const px = centerX + halfW * basePoints[ i * 4 ];
828 const py = centerY + halfH * basePoints[ i * 4 + 1 ];
829 const cosTheta = (-basePoints[ sourceUv ] * basePoints[ destUv ] - basePoints[ sourceUv + 1 ] * basePoints[ destUv + 1]);
830 const offset = cornerRadius / Math.tan(Math.acos(cosTheta) / 2);
831
832 const cp0x = px - offset * basePoints[ sourceUv ];
833 const cp0y = py - offset * basePoints[ sourceUv + 1 ];
834 const cp1x = px + offset * basePoints[ destUv ];
835 const cp1y = py + offset * basePoints[ destUv + 1 ];
836 cutPolygonPoints[ i * 4] = cp0x;
837 cutPolygonPoints[ i * 4 + 1] = cp0y;
838 cutPolygonPoints[ i * 4 + 2] = cp1x;
839 cutPolygonPoints[ i * 4 + 3] = cp1y;
840
841 let orthx = basePoints[ sourceUv + 1 ];
842 let orthy = -basePoints[ sourceUv ];
843 const cosAlpha = orthx * basePoints[ destUv ] + orthy * basePoints [ destUv + 1 ];
844 if (cosAlpha < 0) {
845 orthx *= -1;
846 orthy *= -1;
847 }
848
849 const cx = cp0x + orthx * cornerRadius;
850 const cy = cp0y + orthy * cornerRadius;
851
852 const squaredDistance = Math.pow(cx - x, 2) + Math.pow(cy - y, 2);
853 if (squaredDistance <= squaredCornerRadius) {
854 return true;
855 }
856 }
857
858 return pointInsidePolygonPoints(x, y, cutPolygonPoints);
859};
860
861export const joinLines = ( lineSet ) => {
862
863 let vertices = new Array( lineSet.length / 2 );
864
865 let currentLineStartX, currentLineStartY, currentLineEndX, currentLineEndY;
866 let nextLineStartX, nextLineStartY, nextLineEndX, nextLineEndY;
867
868 for( let i = 0; i < lineSet.length / 4; i++ ){
869 currentLineStartX = lineSet[ i * 4];
870 currentLineStartY = lineSet[ i * 4 + 1];
871 currentLineEndX = lineSet[ i * 4 + 2];
872 currentLineEndY = lineSet[ i * 4 + 3];
873
874 if( i < lineSet.length / 4 - 1 ){
875 nextLineStartX = lineSet[ (i + 1) * 4];
876 nextLineStartY = lineSet[ (i + 1) * 4 + 1];
877 nextLineEndX = lineSet[ (i + 1) * 4 + 2];
878 nextLineEndY = lineSet[ (i + 1) * 4 + 3];
879 } else {
880 nextLineStartX = lineSet[0];
881 nextLineStartY = lineSet[1];
882 nextLineEndX = lineSet[2];
883 nextLineEndY = lineSet[3];
884 }
885
886 let intersection = finiteLinesIntersect(
887 currentLineStartX, currentLineStartY,
888 currentLineEndX, currentLineEndY,
889 nextLineStartX, nextLineStartY,
890 nextLineEndX, nextLineEndY,
891 true );
892
893 vertices[ i * 2] = intersection[0];
894 vertices[ i * 2 + 1] = intersection[1];
895 }
896
897 return vertices;
898};
899
900export const expandPolygon = ( points, pad ) => {
901
902 let expandedLineSet = new Array( points.length * 2 );
903
904 let currentPointX, currentPointY, nextPointX, nextPointY;
905
906 for( let i = 0; i < points.length / 2; i++ ){
907 currentPointX = points[ i * 2];
908 currentPointY = points[ i * 2 + 1];
909
910 if( i < points.length / 2 - 1 ){
911 nextPointX = points[ (i + 1) * 2];
912 nextPointY = points[ (i + 1) * 2 + 1];
913 } else {
914 nextPointX = points[0];
915 nextPointY = points[1];
916 }
917
918 // Current line: [currentPointX, currentPointY] to [nextPointX, nextPointY]
919
920 // Assume CCW polygon winding
921
922 let offsetX = (nextPointY - currentPointY);
923 let offsetY = -(nextPointX - currentPointX);
924
925 // Normalize
926 let offsetLength = Math.sqrt( offsetX * offsetX + offsetY * offsetY );
927 let normalizedOffsetX = offsetX / offsetLength;
928 let normalizedOffsetY = offsetY / offsetLength;
929
930 expandedLineSet[ i * 4] = currentPointX + normalizedOffsetX * pad;
931 expandedLineSet[ i * 4 + 1] = currentPointY + normalizedOffsetY * pad;
932 expandedLineSet[ i * 4 + 2] = nextPointX + normalizedOffsetX * pad;
933 expandedLineSet[ i * 4 + 3] = nextPointY + normalizedOffsetY * pad;
934 }
935
936 return expandedLineSet;
937};
938
939export const intersectLineEllipse = ( x, y, centerX, centerY, ellipseWradius, ellipseHradius ) => {
940
941 let dispX = centerX - x;
942 let dispY = centerY - y;
943
944 dispX /= ellipseWradius;
945 dispY /= ellipseHradius;
946
947 let len = Math.sqrt( dispX * dispX + dispY * dispY );
948
949 let newLength = len - 1;
950
951 if( newLength < 0 ){
952 return [];
953 }
954
955 let lenProportion = newLength / len;
956
957 return [ (centerX - x) * lenProportion + x, (centerY - y) * lenProportion + y ];
958};
959
960export const checkInEllipse = ( x, y, width, height, centerX, centerY, padding ) => {
961 x -= centerX;
962 y -= centerY;
963
964 x /= (width / 2 + padding);
965 y /= (height / 2 + padding);
966
967 return x * x + y * y <= 1;
968};
969
970// Returns intersections of increasing distance from line's start point
971export const intersectLineCircle = ( x1, y1, x2, y2, centerX, centerY, radius ) => {
972
973 // Calculate d, direction vector of line
974 let d = [ x2 - x1, y2 - y1 ]; // Direction vector of line
975 let f = [ x1 - centerX, y1 - centerY ];
976
977 let a = d[0] * d[0] + d[1] * d[1];
978 let b = 2 * (f[0] * d[0] + f[1] * d[1]);
979 let c = (f[0] * f[0] + f[1] * f[1]) - radius * radius ;
980
981 let discriminant = b * b - 4 * a * c;
982
983 if( discriminant < 0 ){
984 return [];
985 }
986
987 let t1 = (-b + Math.sqrt( discriminant )) / (2 * a);
988 let t2 = (-b - Math.sqrt( discriminant )) / (2 * a);
989
990 let tMin = Math.min( t1, t2 );
991 let tMax = Math.max( t1, t2 );
992 let inRangeParams = [];
993
994 if( tMin >= 0 && tMin <= 1 ){
995 inRangeParams.push( tMin );
996 }
997
998 if( tMax >= 0 && tMax <= 1 ){
999 inRangeParams.push( tMax );
1000 }
1001
1002 if( inRangeParams.length === 0 ){
1003 return [];
1004 }
1005
1006 let nearIntersectionX = inRangeParams[0] * d[0] + x1;
1007 let nearIntersectionY = inRangeParams[0] * d[1] + y1;
1008
1009 if( inRangeParams.length > 1 ){
1010
1011 if( inRangeParams[0] == inRangeParams[1] ){
1012 return [ nearIntersectionX, nearIntersectionY ];
1013 } else {
1014
1015 let farIntersectionX = inRangeParams[1] * d[0] + x1;
1016 let farIntersectionY = inRangeParams[1] * d[1] + y1;
1017
1018 return [ nearIntersectionX, nearIntersectionY, farIntersectionX, farIntersectionY ];
1019 }
1020
1021 } else {
1022 return [ nearIntersectionX, nearIntersectionY ];
1023 }
1024
1025};
1026
1027export const findCircleNearPoint = ( centerX, centerY, radius, farX, farY ) => {
1028
1029 let displacementX = farX - centerX;
1030 let displacementY = farY - centerY;
1031 let distance = Math.sqrt( displacementX * displacementX
1032 + displacementY * displacementY );
1033
1034 let unitDisplacementX = displacementX / distance;
1035 let unitDisplacementY = displacementY / distance;
1036
1037 return [ centerX + unitDisplacementX * radius,
1038 centerY + unitDisplacementY * radius ];
1039};
1040
1041export const findMaxSqDistanceToOrigin = ( points ) => {
1042 let maxSqDistance = 0.000001;
1043 let sqDistance;
1044
1045 for( let i = 0; i < points.length / 2; i++ ){
1046
1047 sqDistance = points[ i * 2] * points[ i * 2]
1048 + points[ i * 2 + 1] * points[ i * 2 + 1];
1049
1050 if( sqDistance > maxSqDistance ){
1051 maxSqDistance = sqDistance;
1052 }
1053 }
1054
1055 return maxSqDistance;
1056};
1057
1058export const midOfThree = ( a, b, c ) => {
1059 if( (b <= a && a <= c) || (c <= a && a <= b) ){
1060 return a;
1061 } else if( (a <= b && b <= c) || (c <= b && b <= a) ){
1062 return b;
1063 } else {
1064 return c;
1065 }
1066};
1067
1068// (x1,y1)=>(x2,y2) intersect with (x3,y3)=>(x4,y4)
1069export const finiteLinesIntersect = (
1070 x1, y1, x2, y2,
1071 x3, y3, x4, y4,
1072 infiniteLines
1073) => {
1074
1075 let dx13 = x1 - x3;
1076 let dx21 = x2 - x1;
1077 let dx43 = x4 - x3;
1078
1079 let dy13 = y1 - y3;
1080 let dy21 = y2 - y1;
1081 let dy43 = y4 - y3;
1082
1083 let ua_t = dx43 * dy13 - dy43 * dx13;
1084 let ub_t = dx21 * dy13 - dy21 * dx13;
1085 let u_b = dy43 * dx21 - dx43 * dy21;
1086
1087 if( u_b !== 0 ){
1088 let ua = ua_t / u_b;
1089 let ub = ub_t / u_b;
1090
1091 let flptThreshold = 0.001;
1092 let min = 0 - flptThreshold;
1093 let max = 1 + flptThreshold;
1094
1095 if( min <= ua && ua <= max && min <= ub && ub <= max ){
1096 return [ x1 + ua * dx21, y1 + ua * dy21 ];
1097
1098 } else {
1099 if( !infiniteLines ){
1100 return [];
1101 } else {
1102 return [ x1 + ua * dx21, y1 + ua * dy21 ];
1103 }
1104 }
1105 } else {
1106 if( ua_t === 0 || ub_t === 0 ){
1107
1108 // Parallel, coincident lines. Check if overlap
1109
1110 // Check endpoint of second line
1111 if( midOfThree( x1, x2, x4 ) === x4 ){
1112 return [ x4, y4 ];
1113 }
1114
1115 // Check start point of second line
1116 if( midOfThree( x1, x2, x3 ) === x3 ){
1117 return [ x3, y3 ];
1118 }
1119
1120 // Endpoint of first line
1121 if( midOfThree( x3, x4, x2 ) === x2 ){
1122 return [ x2, y2 ];
1123 }
1124
1125 return [];
1126 } else {
1127
1128 // Parallel, non-coincident
1129 return [];
1130 }
1131 }
1132};
1133
1134// math.polygonIntersectLine( x, y, basePoints, centerX, centerY, width, height, padding )
1135// intersect a node polygon (pts transformed)
1136//
1137// math.polygonIntersectLine( x, y, basePoints, centerX, centerY )
1138// intersect the points (no transform)
1139export const polygonIntersectLine = ( x, y, basePoints, centerX, centerY, width, height, padding ) => {
1140
1141 let intersections = [];
1142 let intersection;
1143
1144 let transformedPoints = new Array( basePoints.length );
1145
1146 let doTransform = true;
1147 if( width == null ){
1148 doTransform = false;
1149 }
1150
1151 let points;
1152
1153 if( doTransform ){
1154 for( let i = 0; i < transformedPoints.length / 2; i++ ){
1155 transformedPoints[ i * 2] = basePoints[ i * 2] * width + centerX;
1156 transformedPoints[ i * 2 + 1] = basePoints[ i * 2 + 1] * height + centerY;
1157 }
1158
1159 if( padding > 0 ){
1160 let expandedLineSet = expandPolygon(
1161 transformedPoints,
1162 -padding );
1163
1164 points = joinLines( expandedLineSet );
1165 } else {
1166 points = transformedPoints;
1167 }
1168 } else {
1169 points = basePoints;
1170 }
1171
1172 let currentX, currentY, nextX, nextY;
1173
1174 for( let i = 0; i < points.length / 2; i++ ){
1175
1176 currentX = points[ i * 2];
1177 currentY = points[ i * 2 + 1];
1178
1179 if( i < points.length / 2 - 1 ){
1180 nextX = points[ (i + 1) * 2];
1181 nextY = points[ (i + 1) * 2 + 1];
1182 } else {
1183 nextX = points[0];
1184 nextY = points[1];
1185 }
1186
1187 intersection = finiteLinesIntersect(
1188 x, y, centerX, centerY,
1189 currentX, currentY,
1190 nextX, nextY );
1191
1192 if( intersection.length !== 0 ){
1193 intersections.push( intersection[0], intersection[1] );
1194 }
1195 }
1196
1197 return intersections;
1198};
1199
1200export const roundPolygonIntersectLine = ( x, y, basePoints, centerX, centerY, width, height, padding ) => {
1201 let intersections = [];
1202 let intersection;
1203 let lines = new Array(basePoints.length);
1204 const halfW = width / 2;
1205 const halfH = height / 2;
1206 const cornerRadius = getRoundPolygonRadius(width, height);
1207
1208 for ( let i = 0; i < basePoints.length / 4; i++ ){
1209 let sourceUv, destUv;
1210 if ( i === 0 ) {
1211 sourceUv = basePoints.length - 2;
1212 } else {
1213 sourceUv = i * 4 - 2;
1214 }
1215 destUv = i * 4 + 2;
1216
1217 const px = centerX + halfW * basePoints[ i * 4 ];
1218 const py = centerY + halfH * basePoints[ i * 4 + 1 ];
1219
1220
1221 const cosTheta = (-basePoints[ sourceUv ] * basePoints[ destUv ] - basePoints[ sourceUv + 1 ] * basePoints[ destUv + 1]);
1222 const offset = cornerRadius / Math.tan(Math.acos(cosTheta) / 2);
1223
1224 const cp0x = px - offset * basePoints[ sourceUv ];
1225 const cp0y = py - offset * basePoints[ sourceUv + 1 ];
1226 const cp1x = px + offset * basePoints[ destUv ];
1227 const cp1y = py + offset * basePoints[ destUv + 1 ];
1228
1229 if (i === 0) {
1230 lines[basePoints.length - 2] = cp0x;
1231 lines[basePoints.length - 1] = cp0y;
1232 } else {
1233 lines[i * 4 - 2] = cp0x;
1234 lines[i * 4 - 1] = cp0y;
1235 }
1236
1237 lines[i * 4] = cp1x;
1238 lines[i * 4 + 1] = cp1y;
1239
1240 let orthx = basePoints[ sourceUv + 1 ];
1241 let orthy = -basePoints[ sourceUv ];
1242 const cosAlpha = orthx * basePoints[ destUv ] + orthy * basePoints [ destUv + 1 ];
1243 if (cosAlpha < 0) {
1244 orthx *= -1;
1245 orthy *= -1;
1246 }
1247
1248 const cx = cp0x + orthx * cornerRadius;
1249 const cy = cp0y + orthy * cornerRadius;
1250
1251 intersection = intersectLineCircle(x, y, centerX, centerY, cx, cy, cornerRadius);
1252
1253 if( intersection.length !== 0 ){
1254 intersections.push( intersection[0], intersection[1] );
1255 }
1256 }
1257
1258 for( let i = 0; i < lines.length / 4; i++ ) {
1259 intersection = finiteLinesIntersect(
1260 x, y, centerX, centerY,
1261 lines[i * 4], lines[i * 4 + 1],
1262 lines[i * 4 + 2], lines[i * 4 + 3], false );
1263
1264 if( intersection.length !== 0 ){
1265 intersections.push( intersection[0], intersection[1] );
1266 }
1267 }
1268
1269 if (intersections.length > 2) {
1270 let lowestIntersection = [ intersections[0], intersections[1] ];
1271 let lowestSquaredDistance = Math.pow(lowestIntersection[0] - x, 2) + Math.pow(lowestIntersection[1] - y, 2);
1272 for ( let i = 1; i < intersections.length / 2; i++){
1273 const squaredDistance = Math.pow(intersections[ i * 2 ] - x, 2) + Math.pow(intersections[ i * 2 + 1 ] - y, 2);
1274 if ( squaredDistance <= lowestSquaredDistance ){
1275 lowestIntersection[0] = intersections[ i * 2 ];
1276 lowestIntersection[1] = intersections[ i * 2 + 1 ];
1277 lowestSquaredDistance = squaredDistance;
1278 }
1279 }
1280 return lowestIntersection;
1281 }
1282
1283 return intersections;
1284};
1285
1286export const shortenIntersection = ( intersection, offset, amount ) => {
1287
1288 let disp = [ intersection[0] - offset[0], intersection[1] - offset[1] ];
1289
1290 let length = Math.sqrt( disp[0] * disp[0] + disp[1] * disp[1] );
1291
1292 let lenRatio = (length - amount) / length;
1293
1294 if( lenRatio < 0 ){
1295 lenRatio = 0.00001;
1296 }
1297
1298 return [ offset[0] + lenRatio * disp[0], offset[1] + lenRatio * disp[1] ];
1299};
1300
1301export const generateUnitNgonPointsFitToSquare = ( sides, rotationRadians ) => {
1302 let points = generateUnitNgonPoints( sides, rotationRadians );
1303 points = fitPolygonToSquare( points );
1304
1305 return points;
1306};
1307
1308export const fitPolygonToSquare = ( points ) => {
1309 let x, y;
1310 let sides = points.length / 2;
1311 let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1312
1313 for( let i = 0; i < sides; i++ ){
1314 x = points[2 * i ];
1315 y = points[2 * i + 1];
1316
1317 minX = Math.min( minX, x );
1318 maxX = Math.max( maxX, x );
1319 minY = Math.min( minY, y );
1320 maxY = Math.max( maxY, y );
1321 }
1322
1323 // stretch factors
1324 let sx = 2 / (maxX - minX);
1325 let sy = 2 / (maxY - minY);
1326
1327 for( let i = 0; i < sides; i++ ){
1328 x = points[2 * i ] = points[2 * i ] * sx;
1329 y = points[2 * i + 1] = points[2 * i + 1] * sy;
1330
1331 minX = Math.min( minX, x );
1332 maxX = Math.max( maxX, x );
1333 minY = Math.min( minY, y );
1334 maxY = Math.max( maxY, y );
1335 }
1336
1337 if( minY < -1 ){
1338 for( let i = 0; i < sides; i++ ){
1339 y = points[2 * i + 1] = points[2 * i + 1] + (-1 - minY);
1340 }
1341 }
1342
1343 return points;
1344};
1345
1346export const generateUnitNgonPoints = ( sides, rotationRadians ) => {
1347
1348 let increment = 1.0 / sides * 2 * Math.PI;
1349 let startAngle = sides % 2 === 0 ?
1350 Math.PI / 2.0 + increment / 2.0 : Math.PI / 2.0;
1351
1352 startAngle += rotationRadians;
1353
1354 let points = new Array( sides * 2 );
1355
1356 let currentAngle;
1357 for( let i = 0; i < sides; i++ ){
1358 currentAngle = i * increment + startAngle;
1359
1360 points[2 * i ] = Math.cos( currentAngle ); // x
1361 points[2 * i + 1] = Math.sin( -currentAngle ); // y
1362 }
1363
1364 return points;
1365};
1366
1367// Set the default radius, unless half of width or height is smaller than default
1368export const getRoundRectangleRadius = ( width, height ) =>
1369 Math.min( width / 4, height / 4, 8 );
1370
1371// Set the default radius
1372export const getRoundPolygonRadius = (width, height ) =>
1373 Math.min( width / 10, height / 10, 8 );
1374
1375export const getCutRectangleCornerLength = () => 8;
1376
1377export const bezierPtsToQuadCoeff = ( p0, p1, p2 ) => [
1378 p0 - 2 * p1 + p2,
1379 2 * ( p1 - p0 ),
1380 p0
1381];
1382
1383// get curve width, height, and control point position offsets as a percentage of node height / width
1384export const getBarrelCurveConstants = ( width, height ) => ({
1385 heightOffset: Math.min(15, 0.05 * height),
1386 widthOffset: Math.min(100, 0.25 * width),
1387 ctrlPtOffsetPct: 0.05
1388});