geometryshapeskonvajskonva

KonvaJS connect squares and correct line placement?


So I am building a UML drawing tool with KonvaJS and KonvaReact, for that I need to connect shapes with lines. I saw the tutorial on the website on connected objects https://konvajs.org/docs/sandbox/Connected_Objects.html.

They use a function get_connecter_points that calculates the possition from the line based on the radians on the circle.

function getConnectorPoints(from, to) {
        const dx = to.x - from.x;
        const dy = to.y - from.y;
        let angle = Math.atan2(-dy, dx);

        const radius = 50;

        return [
          from.x + -radius * Math.cos(angle + Math.PI),
          from.y + radius * Math.sin(angle + Math.PI),
          to.x + -radius * Math.cos(angle),
          to.y + radius * Math.sin(angle)
        ];
      }

I am trying to comeup with a simular function, but can't comeup with a good solution or find a good example. As you can see in the immage I just returned the from x and y and to x and y in the function and so the lines will be placed in the left top corner of every square.

enter image description here

The goal of the function should be to place the lines halfway to the side of the square and on the correct side of the square. So when the to square is placed below it should appear on the bottom side.

So if someone has a solution, any help is appreciated.


Solution

  • For the rectangles, the math is a bit more complex than for circles.

    First, you need to calculate the angle for connection line, between two objects:

    function getCenter(node) {
      return {
        x: node.x() + node.width() / 2,
        y: node.y() + node.height() / 2
      }
    }
    const c1 = getCenter(object1);
    const c2 = getCenter(object2;
    
    const dx = c1.x - c2.x;
    const dy = c1.y - c2.y;
    const angle = Math.atan2(-dy, dx);
    

    Second, when you know the angle, you need a function, that finds a point of the rectangle border that you can use to connect with another object.

    function getRectangleBorderPoint(radians, size, sideOffset = 0) {
      const width = size.width + sideOffset * 2;
    
      const height = size.height + sideOffset * 2;
    
      radians %= 2 * Math.PI;
      if (radians < 0) {
        radians += Math.PI * 2;
      }
    
      const phi = Math.atan(height / width);
    
      let x, y;
      if (
        (radians >= 2 * Math.PI - phi && radians <= 2 * Math.PI) ||
        (radians >= 0 && radians <= phi)
      ) {
        x = width / 2;
        y = Math.tan(radians) * x;
      } else if (radians >= phi && radians <= Math.PI - phi) {
        y = height / 2;
        x = y / Math.tan(radians);
      } else if (radians >= Math.PI - phi && radians <= Math.PI + phi) {
        x = -width / 2;
        y = Math.tan(radians) * x;
      } else if (radians >= Math.PI + phi && radians <= 2 * Math.PI - phi) {
        y = -height / 2;
        x = y / Math.tan(radians);
      }
    
      return {
        x: -Math.round(x),
        y: Math.round(y)
      };
    }
    

    Now, you just need to generate points for line shape:

    function getPoints(r1, r2) {
      const c1 = getCenter(r1);
      const c2 = getCenter(r2);
    
      const dx = c1.x - c2.x;
      const dy = c1.y - c2.y;
      const angle = Math.atan2(-dy, dx);
    
      const startOffset = getRectangleBorderPoint(angle + Math.PI, r1.size());
      const endOffset = getRectangleBorderPoint(angle, r2.size());
    
      const start = {
        x: c1.x - startOffset.x,
        y: c1.y - startOffset.y
      };
    
      const end = {
        x: c2.x - endOffset.x,
        y: c2.y - endOffset.y
      };
    
      return [start.x, start.y, end.x, end.y]
    }
    
    function updateLine() {
      const points = getPoints(rect1, rect2);
      line.points(points);
    }
    

    All of this as a demo:

    function getRectangleBorderPoint(radians, size, sideOffset = 0) {
      const width = size.width + sideOffset * 2;
    
      const height = size.height + sideOffset * 2;
    
      radians %= 2 * Math.PI;
      if (radians < 0) {
        radians += Math.PI * 2;
      }
    
      const phi = Math.atan(height / width);
    
      let x, y;
      if (
        (radians >= 2 * Math.PI - phi && radians <= 2 * Math.PI) ||
        (radians >= 0 && radians <= phi)
      ) {
        x = width / 2;
        y = Math.tan(radians) * x;
      } else if (radians >= phi && radians <= Math.PI - phi) {
        y = height / 2;
        x = y / Math.tan(radians);
      } else if (radians >= Math.PI - phi && radians <= Math.PI + phi) {
        x = -width / 2;
        y = Math.tan(radians) * x;
      } else if (radians >= Math.PI + phi && radians <= 2 * Math.PI - phi) {
        y = -height / 2;
        x = y / Math.tan(radians);
      }
    
      return {
        x: -Math.round(x),
        y: Math.round(y)
      };
    }
    
    const stage = new Konva.Stage({
      container: 'container',
      width: window.innerWidth,
      height: window.innerHeight
    });
    
    const layer = new Konva.Layer();
    stage.add(layer);
    
    const rect1 = new Konva.Rect({
      x: 20,
      y: 20,
      width: 50,
      height: 50,
      fill: 'green',
      draggable: true
    });
    layer.add(rect1);
    
    
    const rect2 = new Konva.Rect({
      x: 220,
      y: 220,
      width: 50,
      height: 50,
      fill: 'red',
      draggable: true
    });
    layer.add(rect2);
    
    const line = new Konva.Line({
      stroke: 'black'
    });
    layer.add(line);
    
    function getCenter(node) {
      return {
        x: node.x() + node.width() / 2,
        y: node.y() + node.height() / 2
      }
    }
    
    function getPoints(r1, r2) {
      const c1 = getCenter(r1);
      const c2 = getCenter(r2);
    
      const dx = c1.x - c2.x;
      const dy = c1.y - c2.y;
      const angle = Math.atan2(-dy, dx);
    
      const startOffset = getRectangleBorderPoint(angle + Math.PI, rect1.size());
      const endOffset = getRectangleBorderPoint(angle, rect2.size());
    
      const start = {
        x: c1.x - startOffset.x,
        y: c1.y - startOffset.y
      };
    
      const end = {
        x: c2.x - endOffset.x,
        y: c2.y - endOffset.y
      };
      
      return [start.x, start.y, end.x, end.y]
    }
    
    function updateLine() {
      const points = getPoints(rect1, rect2);
      line.points(points);
    }
    
    updateLine();
    layer.on('dragmove', updateLine);
    
    layer.draw();
      <script src="https://unpkg.com/konva@^3/konva.min.js"></script>
      <div id="container"></div>