@@ -469,11 +469,178 @@ function normalize(vector) {
469
469
return norm === 0 ? vector : { x : vector . x / norm , y : vector . y / norm } ;
470
470
}
471
471
472
- const SYMBOLS_WITH_OPTIMIZED_POSITIONING = new Set ( [ CONST . SYMBOLS . CIRCLE ] ) ;
472
+ /**
473
+ * Calculate the length of a vector, from the center of a rectangle,
474
+ * to one of it's edges.
475
+ * This calculation is taken from https://stackoverflow.com/a/3197924.
476
+ *
477
+ * @param {Object.<string, number> } RectangleCoords, The coords of the left-top vertex, and the right-bottom vertex.
478
+ * @param {Object.<string, number> } VectorOriginCoords The center of the rectangle coords.
479
+ * @param {Object.<string, number> } directionVector a 2D vector with x and y components
480
+ */
481
+ function calcRectangleVectorLengthFromCoords ( { x1, y1, x2, y2 } , { px, py } , directionVector ) {
482
+ const angle = Math . atan ( directionVector . y / directionVector . x ) ;
483
+
484
+ const vx = Math . cos ( angle ) ;
485
+ const vy = Math . sin ( angle ) ;
486
+
487
+ if ( vx === 0 ) {
488
+ return x2 - x1 ;
489
+ } else if ( vy === 0 ) {
490
+ return y2 - y1 ;
491
+ }
492
+
493
+ const leftEdge = ( x1 - px ) / vx ;
494
+ const rightEdge = ( x2 - px ) / vx ;
495
+
496
+ const topEdge = ( y1 - py ) / vy ;
497
+ const bottomEdge = ( y2 - py ) / vy ;
498
+
499
+ return Math . min ( ...[ leftEdge , rightEdge , topEdge , bottomEdge ] . filter ( edge => edge > 0 ) ) ;
500
+ }
501
+
502
+ /**
503
+ * Calculate a the vector length from the center of a circle to it's perimeter.
504
+ *
505
+ * @param {number } nodeSize The size of the circle, when no viewGenerator is specified, else the size of an edge of the viewGenerator square.
506
+ * @param {boolean } isCustomNode is viewGenerator specified.
507
+ * @returns {number }
508
+ */
509
+ function calcCircleVectorLength ( nodeSize , isCustomNode ) {
510
+ let radiusLength ;
511
+ if ( isCustomNode ) {
512
+ // nodeSize equals the Diameter in the case of custome-node.
513
+ radiusLength = nodeSize / 10 / 2 ;
514
+ } else {
515
+ // because this is a circle and A = pi * r^2
516
+ // we multiply by 0.95, because if we don't the link is not melting properly
517
+ radiusLength = Math . sqrt ( nodeSize / Math . PI ) ;
518
+ }
519
+ return radiusLength ;
520
+ }
521
+
522
+ /**
523
+ * Calculate a the vector length from the center of a square to it's perimeter.
524
+ *
525
+ * @param {number } nodeSize The size of the square, when no viewGenerator is specified, else the size of an edge of the viewGenerator square.
526
+ * @param {Object.<string, number> } nodeCoords The coords of a the square node.
527
+ * @param {Object.<string, number> } directionVector a 2D vector with x and y components
528
+ * @param {boolean } isCustomNode is viewGenerator specified.
529
+ * @returns {number }
530
+ */
531
+ function calcSquareVectorLength ( nodeSize , { x, y } , directionVector , isCustomNode ) {
532
+ let edgeSize ;
533
+ if ( isCustomNode ) {
534
+ // nodeSize equals the edgeSize in the case of custome-node.
535
+ edgeSize = nodeSize / 10 ;
536
+ } else {
537
+ // All the edges of a square are equal, inorder to calc it's size we multplie two edges.
538
+ edgeSize = Math . sqrt ( nodeSize ) ;
539
+ }
540
+
541
+ // The x and y coords in this library, represent the top center of the component.
542
+ const leftSquareX = x - edgeSize / 2 ;
543
+ const topSquareY = y - edgeSize / 2 ;
544
+
545
+ return calcRectangleVectorLengthFromCoords (
546
+ { x1 : leftSquareX , y1 : topSquareY , x2 : leftSquareX + edgeSize , y2 : topSquareY + edgeSize } ,
547
+ { px : x , py : y } ,
548
+ directionVector
549
+ ) ;
550
+ }
551
+
552
+ /**
553
+ * Calculate a the vector length from the center of a rectangle to it's perimeter.
554
+ *
555
+ * @param {number } nodeSize The size of the square, when no viewGenerator is specified, else the size of an edge of the viewGenerator square.
556
+ * @param {Object.<string, number> } nodeCoords The coords of a the square node.
557
+ * @param {Object.<string, number> } directionVector a 2D vector with x and y components.
558
+ * @returns {number }
559
+ */
560
+ function calcRectangleVectorLength ( nodeSize , { x, y } , directionVector ) {
561
+ const horizEdgeSize = nodeSize . width / 10 ;
562
+ const vertEdgeSize = nodeSize . height / 10 ;
563
+
564
+ // The x and y coords in this library, represent the top center of the component.
565
+ const leftSquareX = x - horizEdgeSize / 2 ;
566
+ const topSquareY = y - vertEdgeSize / 2 ;
567
+
568
+ // The size between the square center, to the appropriate square edges
569
+ return calcRectangleVectorLengthFromCoords (
570
+ { x1 : leftSquareX , y1 : topSquareY , x2 : leftSquareX + horizEdgeSize , y2 : topSquareY + vertEdgeSize } ,
571
+ { px : x , py : y } ,
572
+ directionVector
573
+ ) ;
574
+ }
575
+
576
+ /**
577
+ * Calculate a the vector length of symbol that included in symbols with optimized positioning.
578
+ *
579
+ * @param {string } symbolType the string that specifies the symbol type (should be one of {@link #node-symbol-type|node.symbolType})
580
+ * @param {(number | Object.<string, number>) } nodeSize The size of the square, when no viewGenerator is specified, else the size of an edge of the viewGenerator square.
581
+ * @param {Object.<string, number> } nodeCoords The coords of a the square node.
582
+ * @param {Object.<string, number> } directionVector a 2D vector with x and y components.
583
+ * @param {boolean } isCustomNode is viewGenerator specified.
584
+ * @returns {number }
585
+ */
586
+ function calcVectorLength ( symbolType , nodeSize , { x, y } , directionVector , isCustomNode ) {
587
+ switch ( symbolType ) {
588
+ case CONST . SYMBOLS . CIRCLE :
589
+ if ( typeof nodeSize === "number" ) {
590
+ return calcCircleVectorLength ( nodeSize , isCustomNode ) ;
591
+ }
592
+ console . warn ( "When specifiying 'circle' as node symbol, the size of the node must be a number." ) ;
593
+
594
+ case CONST . SYMBOLS . SQUARE :
595
+ if ( typeof nodeSize === "number" ) {
596
+ return calcSquareVectorLength ( nodeSize , { x, y } , directionVector , isCustomNode ) ;
597
+ }
598
+ console . warn ( "When specifiying 'square' as node symbol, the size of the node must be a number." ) ;
599
+
600
+ case CONST . SYMBOLS . RECTANGLE :
601
+ if ( typeof nodeSize === "object" && nodeSize ?. width && nodeSize ?. height ) {
602
+ return calcRectangleVectorLength ( nodeSize , { x, y } , directionVector ) ;
603
+ }
604
+ console . warn ( "When specifiying 'rectangle' as node symbol, width and height must be specified in the node size." ) ;
605
+
606
+ default :
607
+ return 1 ;
608
+ }
609
+ }
610
+
611
+ /**
612
+ * When directed graph is specified, we add arrow head to the link.
613
+ * In order to add the arrow head we subtract it's size from the last point of the link.
614
+ *
615
+ * @param {number } p1 x or y, of the link starting point.
616
+ * @param {number } p2 x or y, of the link ending point.
617
+ * @param {number } pDirectionVector The link direction vector in the x or y axis.
618
+ * @param {number } arrowSize The size of the arrow head.
619
+ * @returns The amount we should add to the x or y coords, in order to free up space for the arrow head.
620
+ */
621
+ function directedGraphOptimization ( p1 , p2 , pDirectionVector , arrowSize ) {
622
+ const pDiff = Math . abs ( p2 - p1 ) ;
623
+ const invertedDirectionVector = pDirectionVector * - 1 ;
624
+ const pVectorArrowSize = Math . abs ( arrowSize * invertedDirectionVector ) ;
625
+
626
+ let p1Opti = 0 ;
627
+ let p2Opti = 0 ;
628
+ if ( pDiff > pVectorArrowSize ) {
629
+ p2Opti = pVectorArrowSize * invertedDirectionVector ;
630
+ } else {
631
+ p2Opti = ( pDiff - 1 ) * invertedDirectionVector ;
632
+ }
633
+
634
+ return {
635
+ p1 : p1Opti ,
636
+ p2 : p2Opti ,
637
+ } ;
638
+ }
473
639
474
640
/**
475
641
* Computes new node coordinates to make arrowheads point at nodes.
476
- * Arrow configuration is only available for circles.
642
+ * Arrow configuration is only available for circles, squares and rectangles.
643
+ *
477
644
* @param {Object } info - the couple of nodes we need to compute new coordinates
478
645
* @param {string } info.sourceId - node source id
479
646
* @param {string } info.targetId - node target id
@@ -498,16 +665,12 @@ function getNormalizedNodeCoordinates(
498
665
return { sourceCoords, targetCoords } ;
499
666
}
500
667
501
- if ( config . node ?. viewGenerator || sourceNode ?. viewGenerator || targetNode ?. viewGenerator ) {
502
- return { sourceCoords, targetCoords } ;
503
- }
504
-
505
668
const sourceSymbolType = sourceNode . symbolType || config . node ?. symbolType ;
506
669
const targetSymbolType = targetNode . symbolType || config . node ?. symbolType ;
507
670
508
671
if (
509
- ! SYMBOLS_WITH_OPTIMIZED_POSITIONING . has ( sourceSymbolType ) &&
510
- ! SYMBOLS_WITH_OPTIMIZED_POSITIONING . has ( targetSymbolType )
672
+ ! CONST . SYMBOLS_WITH_OPTIMIZED_POSITIONING . has ( sourceSymbolType ) &&
673
+ ! CONST . SYMBOLS_WITH_OPTIMIZED_POSITIONING . has ( targetSymbolType )
511
674
) {
512
675
// if symbols don't have optimized positioning implementations fallback to input coords
513
676
return { sourceCoords, targetCoords } ;
@@ -517,38 +680,35 @@ function getNormalizedNodeCoordinates(
517
680
let { x : x2 , y : y2 } = targetCoords ;
518
681
const directionVector = normalize ( { x : x2 - x1 , y : y2 - y1 } ) ;
519
682
520
- switch ( sourceSymbolType ) {
521
- case CONST . SYMBOLS . CIRCLE : {
522
- let sourceNodeSize = sourceNode ?. size || config . node . size ;
683
+ const isSourceCustomNode = sourceNode . viewGenerator || config . node . viewGenerator ;
684
+ const sourceNodeSize = sourceNode ?. size || config . node . size ;
523
685
524
- // because this is a circle and A = pi * r^2
525
- // we multiply by 0.95, because if we don't the link is not melting properly
526
- sourceNodeSize = Math . sqrt ( sourceNodeSize / Math . PI ) * 0.95 ;
686
+ const sourceVectorLength =
687
+ calcVectorLength ( sourceSymbolType , sourceNodeSize , { x : x1 , y : y1 } , directionVector , isSourceCustomNode ) * 0.95 ;
527
688
528
- // points from the sourceCoords, we move them not to begin in the circle but outside
529
- x1 += sourceNodeSize * directionVector . x ;
530
- y1 += sourceNodeSize * directionVector . y ;
531
- break ;
532
- }
533
- }
689
+ x1 += sourceVectorLength * directionVector . x ;
690
+ y1 += sourceVectorLength * directionVector . y ;
534
691
535
- switch ( targetSymbolType ) {
536
- case CONST . SYMBOLS . CIRCLE : {
537
- // it's fine `markerWidth` or `markerHeight` we just want to fallback to a number
538
- // to avoid NaN on `Math.min(undefined, undefined) > NaN
539
- let strokeSize = strokeWidth * Math . min ( config . link ?. markerWidth || 0 , config . link ?. markerHeight || 0 ) ;
540
- let targetNodeSize = targetNode ?. size || config . node . size ;
541
-
542
- // because this is a circle and A = pi * r^2
543
- // we multiply by 0.95, because if we don't the link is not melting properly
544
- targetNodeSize = Math . sqrt ( targetNodeSize / Math . PI ) * 0.95 ;
545
-
546
- // points from the targetCoords, we move the by the size of the radius of the circle + the size of the arrow
547
- x2 -= ( targetNodeSize + ( config . directed ? strokeSize : 0 ) ) * directionVector . x ;
548
- y2 -= ( targetNodeSize + ( config . directed ? strokeSize : 0 ) ) * directionVector . y ;
549
- break ;
550
- }
551
- }
692
+ // it's fine `markerWidth` or `markerHeight` we just want to fallback to a number
693
+ // to avoid NaN on `Math.min(undefined, undefined) > NaN
694
+ const strokeSize = strokeWidth * Math . min ( config . link ?. markerWidth || 5 , config . link ?. markerHeight || 5 ) ;
695
+ const isTargetCustomNode = targetNode . viewGenerator || config . node . viewGenerator ;
696
+ const targetNodeSize = targetNode ?. size || config . node . size ;
697
+
698
+ const targetVectorLength =
699
+ calcVectorLength ( targetSymbolType , targetNodeSize , { x : x2 , y : y2 } , directionVector , isTargetCustomNode ) * 0.95 ;
700
+
701
+ const arrowSize = config . directed ? strokeSize : 0 ;
702
+ x2 -= targetVectorLength * directionVector . x ;
703
+ y2 -= targetVectorLength * directionVector . y ;
704
+
705
+ const xOptimization = directedGraphOptimization ( x1 , x2 , directionVector . x , arrowSize ) ;
706
+ x2 += xOptimization . p2 ;
707
+ x1 += xOptimization . p1 ;
708
+
709
+ const yOptimization = directedGraphOptimization ( y1 , y2 , directionVector . y , arrowSize ) ;
710
+ y2 += yOptimization . p2 ;
711
+ y1 += yOptimization . p1 ;
552
712
553
713
return { sourceCoords : { x : x1 , y : y1 } , targetCoords : { x : x2 , y : y2 } } ;
554
714
}
0 commit comments