我想在矩形(由矩形表示的节点)之间绘制有向弧,以使箭头尖始终以优美的方式击中边缘。我已经看到很多关于如何为圈子(以圈子表示的节点)执行此操作的帖子。有趣的是,大多数d3示例都涉及圆形和正方形(尽管正方形程度较小)。

我有一个示例代码here。现在,我最大的尝试只能从中心点画出。我可以移动终点(箭头应该在的位置),但是尝试在周围拖动矩形时,弧线的行为不符合预期。

这就是我所拥有的。
javascript - 如何在d3中不同尺寸的矩形之间绘制有向箭头?-LMLPHP

但是我需要这样的东西。
javascript - 如何在d3中不同尺寸的矩形之间绘制有向箭头?-LMLPHP

关于如何在d3中轻松完成此操作的任何想法?是否有一些内置库/函数可以帮助这类事情(例如拖动功能)?

最佳答案

解决问题的一种简单算法是


拖动节点时,请对其节点的每个传入/传出边缘执行以下操作


a为拖动的节点,b为通过传出/传入边缘到达的节点
lineSegmentab的中心之间的线段
计算alineSegment的交点,方法是迭代4个组成框的线段,并用lineSegment检查它们的交点,令ia是其中一个的交点。 alineSegment的段,以类似的方式找到ib



我考虑过但尚未解决的极端情况


当一个盒子的中心在另一个盒子里面时,不会有2个线段相交
当两个交点相同时! (在编辑中解决了此问题)
当您的图形为multigraph时,边会相互渲染


plunkr demo

编辑:添加了检查ia === ib以避免从左上角创建边缘,您可以在plunkr演示中看到它



$(document).ready(function() {
  var graph = {
    nodes: [
      { id: 'n1', x: 10, y: 10, width: 200, height: 200 },
      { id: 'n2', x: 10, y: 270, width: 200, height: 250 },
      { id: 'n3', x: 400, y: 270, width: 200, height: 300 }
    ],
    edges: [
      { start: 'n1', stop: 'n2' },
      { start: 'n2', stop: 'n3' }
    ],
    node: function(id) {
      if(!this.nmap) {
        this.nmap = { };
        for(var i=0; i < this.nodes.length; i++) {
          var node = this.nodes[i];
          this.nmap[node.id] = node;
        }
      }
      return this.nmap[id];
    },
    mid: function(id) {
      var node = this.node(id);
      var x = node.width / 2.0 + node.x,
          y = node.height / 2.0 + node.y;
      return { x: x, y: y };
    }
  };

  var arcs = d3.select('#mysvg')
  .selectAll('line')
  .data(graph.edges)
  .enter()
    .append('line')
    .attr({
      'data-start': function(d) { return d.start; },
      'data-stop': function(d) { return d.stop; },
      x1: function(d) { return graph.mid(d.start).x; },
      y1: function(d) { return graph.mid(d.start).y; },
      x2: function(d) { return graph.mid(d.stop).x; },
      y2: function(d) { return graph.mid(d.stop).y },
      style: 'stroke:rgb(255,0,0);stroke-width:2',
      'marker-end': 'url(#arrow)'
    });

  var g = d3.select('#mysvg')
    .selectAll('g')
    .data(graph.nodes)
    .enter()
      .append('g')
      .attr({
        id: function(d) { return d.id; },
        transform: function(d) {
          return 'translate(' + d.x + ',' + d.y + ')';
        }
      });
  g.append('rect')
    .attr({
      id: function(d) { return d.id; },
      x: 0,
      y: 0,
      style: 'stroke:#000000; fill:none;',
      width: function(d) { return d.width; },
      height: function(d) { return d.height; },
      'pointer-events': 'visible'
    });

  function Point(x, y) {
    if (!(this instanceof Point)) {
      return new Point(x, y)
    }
    this.x = x
    this.y = y
  }
  Point.add = function (a, b) {
    return Point(a.x + b.x, a.y + b.y)
  }
  Point.sub = function (a, b) {
    return Point(a.x - b.x, a.y - b.y)
  }
  Point.cross = function (a, b) {
    return a.x * b.y - a.y * b.x;
  }
  Point.scale = function (a, k) {
    return Point(a.x * k, a.y * k)
  }
  Point.unit = function (a) {
    return Point.scale(a, 1 / Point.norm(a))
  }
  Point.norm = function (a) {
    return Math.sqrt(a.x * a.x + a.y * a.y)
  }
  Point.neg = function (a) {
    return Point(-a.x, -a.y)
  }

  function pointInSegment(s, p) {
    var a = s[0]
    var b = s[1]
    return Math.abs(Point.cross(Point.sub(p, a), Point.sub(b, a))) < 1e-6 &&
      Math.min(a.x, b.x) <= p.x && p.x <= Math.max(a.x, b.x) &&
      Math.min(a.y, b.y) <= p.y && p.y <= Math.max(a.y, b.y)
  }

  function lineLineIntersection(s1, s2) {
    var a = s1[0]
    var b = s1[1]
    var c = s2[0]
    var d = s2[1]
    var v1 = Point.sub(b, a)
    var v2 = Point.sub(d, c)
    //if (Math.abs(Point.cross(v1, v2)) < 1e-6) {
    //  // collinear
    //  return null
    //}
    var kNum = Point.cross(
      Point.sub(c, a),
      Point.sub(d, c)
    )
    var kDen = Point.cross(
      Point.sub(b, a),
      Point.sub(d, c)
    )
    var ip = Point.add(
      a,
      Point.scale(
        Point.sub(b, a),
        Math.abs(kNum / kDen)
      )
    )
    return ip
  }

  function segmentSegmentIntersection(s1, s2) {
    var ip = lineLineIntersection(s1, s2)
    if (ip && pointInSegment(s1, ip) && pointInSegment(s2, ip)) {
      return ip
    }
  }

  function boxSegmentIntersection(box, lineSegment) {
    var data = box.data()[0]
    var topLeft = Point(data.x, data.y)
    var topRight = Point(data.x + data.width, data.y)
    var botLeft = Point(data.x, data.y + data.height)
    var botRight = Point(data.x + data.width, data.y + data.height)
    var boxSegments = [
      // top
      [topLeft, topRight],
      // bot
      [botLeft, botRight],
      // left
      [topLeft, botLeft],
      // right
      [topRight, botRight]
    ]
    var ip
    for (var i = 0; !ip && i < 4; i += 1) {
      ip = segmentSegmentIntersection(boxSegments[i], lineSegment)
    }
    return ip
  }

  function boxCenter(a) {
    var data = a.data()[0]
    return Point(
      data.x + data.width / 2,
      data.y + data.height / 2
    )
  }

  function buildSegmentThroughCenters(a, b) {
    return [boxCenter(a), boxCenter(b)]
  }

  // should return {x1, y1, x2, y2}
  function getIntersection(a, b) {
    var segment = buildSegmentThroughCenters(a, b)
    console.log(segment[0], segment[1])
    var ia = boxSegmentIntersection(a, segment)
    var ib = boxSegmentIntersection(b, segment)
    if (ia && ib) {

      // problem: the arrows are drawn after the intersection with the box
      // solution: move the arrow toward the other end

      var unitV = Point.unit(Point.sub(ib, ia))
      // k = the width of the marker
      var k = 18
      ib = Point.sub(ib, Point.scale(unitV, k))

      return {
        x1: ia.x,
        y1: ia.y,
        x2: ib.x,
        y2: ib.y
      }
    }
  }

  var drag = d3.behavior.drag()
  .origin(function(d) {
    return d;
  })
  .on('dragstart', function(e) {
    d3.event.sourceEvent.stopPropagation();
  })
  .on('drag', function(e) {
    e.x = d3.event.x;
    e.y = d3.event.y;

    var id = 'g#' + e.id
    var target = d3.select(id)
    target.data().x = e.x
    target.data().y = e.y
    target.attr({
      transform: 'translate(' + e.x + ',' + e.y + ')'
    });

    d3.selectAll('line[data-start=' + e.id + ']')
      .each(function (d) {
        var line = d3.select(this)
        var other = d3.select('g#' + line.attr('data-stop'))
        var intersection = getIntersection(target, other)
        intersection && line.attr(intersection)
      })

    d3.selectAll('line[data-stop=' + e.id + ']')
      .each(function (d) {
        var line = d3.select(this)
        var other = d3.select('g#' + line.attr('data-start'))
        var intersection = getIntersection(other, target)
        intersection && line.attr(intersection)
      })

  })
  .on('dragend', function(e) {

  });
  g.call(drag);
})

      svg#mysvg { border: 1px solid black;}

<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

<svg id="mysvg" width="800" height="800">
  <defs>
    <marker id="arrow" markerWidth="10" markerHeight="10" refx="0" refy="3" orient="auto" markerUnits="strokeWidth">
      <path d="M0,0 L0,6 L9,3 z" fill="#f00" />
    </marker>
  </defs>
</svg>

09-11 07:19