首页 > 解决方案 > 是否可以检查内联 SVG 中各种形状之间的共享边界?

问题描述

我有一个地图的内联 SVG,其中每个区域都被绘制为一个单独的形状。我想做的是获取与该形状共享边界的所有邻居的列表。

这是我目前拥有的一个jsfiddle:

https://jsfiddle.net/kzscxj30/

$( ".map" ).click(function( event ) {
    var $mapTarget = $( event.target );
        if ($mapTarget.is( "path" )) {
            $( ".region" ).attr("class", "region");
        $mapTarget.attr("class", "region active");
    } else {
        return;
    }
})

这个想法是,一旦单击了一个形状,它就会为该形状添加一个“活动”类,并为任何接触的形状添加一个“邻居”类。例如,Region1 是 Region2 和 Region3 的邻居,因此单击 Region1 应该将类“neighbor”添加到 Region2 和 Region3。Region2 和 Region3 彼此不是邻居,因此单击其中一个只会将邻居类添加到 Region1。Region4 不是任何其他形状的邻居,因此它不会将邻居类应用于任何东西。

我考虑过比较每个形状的坐标以找到匹配项,但我意识到可以有两个具有共享边框但没有共享坐标的形状。

这可能更像是一个不知道如何表达我正在寻找的东西的问题!

标签: javascriptjquerysvg

解决方案


有(据我所知),没有内置的方法可以做到这一点......

现在,您可以自己设置一个算法来进行检查,但它可能会有点计算密集型。

作为第一步,您将只检查路径的边界框,消除所有不会以任何方式到达路径的边界框:

function intersectBoxes(bb1, bb2, padding = 0) {
  // half padding (we'll add it in every direction of our rects)
  const pad = padding / 2;
  // helper function to get clean labels
  const getCorners = (bb) => ({
    left: bb.x - pad,
    top: bb.y - pad,
    right: bb.x + bb.width + pad,
    bottom: bb.x + bb.width + pad,  
  });
  const r1 = getCorners(bb1);
  const r2 = getCorners(bb2);
  // check intersection
  return r1.left <= r2.right &&
    r1.right >= r2.left &&
    r1.top <= r2.bottom &&
    r1.bottom >= r2.top;
}
// usage 
intersectBoxes(path1.getBBox(), path2.getBBox(), 2);
// @return Boolean

完成此操作后,您可以开始更重的检查。SVG2 引入了一个新的isPointInStroke方法,但目前只有少数浏览器支持。所以你可能需要 polyfill 它。我没有找到,所以我很快制作了一个猴子补丁,而不是使用 2DContext 等效项。

在这个方法的帮助下,我们只需要沿着它的笔划抓取我们的一条路径的 x, y 坐标并重复调用这个方法。

function slowHitCheck(p1, p2) {
  // we will walk along the smallest of both paths
  const smallest = p1.getTotalLength() < p2.getTotalLength() ? p1 : p2;
  const longest = smallest === p1 ? p2 : p1;
  const length = smallest.getTotalLength();

  let pos = 0;
  while(pos < length) {
    const pt = smallest.getPointAtLength(pos);
    if(longest.isPointInStroke(pt)) return true;
    pos += stroke_width;
  }
  return false;
}

$(".map").click(function(event) {
  var $mapTarget = $(event.target);

  if ($mapTarget.is("path")) {

    $(".region").attr("class", "region");
    const neighbors = getNeighbors($mapTarget[0], $('#Map')[0]);
    neighbors.forEach(node => {
      node.classList.add('neighbour');
    })

    $mapTarget.addClass("active");
  } else {
    return;
  }
})

function getNeighbors(target, root, stroke_width = 1) {
  const targetBB = target.getBBox();
  return [...root.querySelectorAll('path')]
    .filter(path =>
      path !== target && // not the target
      // fast check BBoxes
      intersectBoxes(path.getBBox(), targetBB, stroke_width / 2) &&
      // walk the path
      slowHitCheck(path, target, stroke_width)
    );

}

function intersectBoxes(bb1, bb2, padding) {
  const pad = padding / 2;
  const getCorners = (bb) => ({
    left: bb.x - pad,
    top: bb.y - pad,
    right: bb.x + bb.width + pad,
    bottom: bb.x + bb.width + pad,
  });
  const r1 = getCorners(bb1);
  const r2 = getCorners(bb2);
  return r1.left <= r2.right &&
    r1.right >= r2.left &&
    r1.top <= r2.bottom &&
    r1.bottom >= r2.top;
}

function slowHitCheck(p1, p2, stroke_width) {
  const smallest = p1.getTotalLength() < p2.getTotalLength() ? p1 : p2;
  const longest = smallest === p1 ? p2 : p1;
  const length = smallest.getTotalLength();

  let pos = 0;
  while (pos < length) {
    const pt = smallest.getPointAtLength(pos);
    if (longest.isPointInStroke(pt)) return true;
    pos += stroke_width;
  }
  return false;
}



/* Half related code below: 
 * Monkey Patches SVGGeometryElement's isPointInStroke
 *  and is isPointInFill.
 * You can check the revision history
 *  for a simpler version that only worked for SVGPathElements
 */
// Beware untested code below
// There may very well be a lot of cases where it will not work at all
if (window.SVGGeometryElement && !window.SVGGeometryElement.prototype.isPointInStroke) {
  monkeyPatchSVGIsPointIn();
}
function monkeyPatchSVGIsPointIn() {
  const ctx = get2DContext(0, 0);
  const default_ctx = get2DContext(0, 0);
  Object.defineProperty(SVGGeometryElement.prototype, 'isPointInStroke', {
    value: function isPointInStroke(point) {
      returnIfAbrupt(point);
      const path = generatePath2DFromSVGElement(this);
      setUpContextToSVGElement(ctx, this);
      ctx.stroke(path);
      return ctx.isPointInStroke(path, point.x, point.y);
    }
  });
  Object.defineProperty(SVGGeometryElement.prototype, 'isPointInFill', {
    value: function isPointInFill(point) {
      returnIfAbrupt(point);
      const path = generatePath2DFromSVGElement(this);
      setUpContextToSVGElement(ctx, this);
      ctx.fill(path, this.getAttribute('fill-rule') || "nonzero")
      return ctx.isPointInPath(path, point.x, point.y, this.getAttribute('fill-rule') || 'nonzero');
    }
  });

  function returnIfAbrupt(svgPoint) {
    if (svgPoint instanceof SVGPoint === false) {
      throw new TypeError("Failed to execute 'isPointInStroke' on 'SVGGeometryElement':" +
        "parameter 1 is not of type 'SVGPoint'.")
    }
  }

  function generatePath2DFromSVGElement(el) {
    const def = el instanceof SVGPathElement ?
      el.getAttribute('d') :
      (el instanceof SVGPolygonElement ||
        el instanceof SVGPolylineElement) ?
      ("M" + el.getAttribute('points').split(' ').filter(Boolean).join('L')) :
      "";
    const path = new Path2D(def);
    if (!def) {
      if (el instanceof SVGLineElement) {
        path.lineTo(el.getAttribute('x1'), el.getAttribute('y1'))
        path.lineTo(el.getAttribute('x2'), el.getAttribute('y2'))
      }
      if (el instanceof SVGRectElement) {
        path.rect(el.getAttribute('x'), el.getAttribute('y'), el.getAttribute('width'), el.getAttribute('height'));
      } else if (el instanceof SVGCircleElement) {
        path.arc(el.getAttribute('cx'), el.getAttribute('cy'), el.getAttribute('r'), Math.PI * 2, 0);
      } else if (el instanceof SVGEllipseElement) {
        path.ellipse(el.getAttribute('cx'), el.getAttribute('cy'), el.getAttribute('rx'), el.getAttribute('ry'), 0, Math.PI * 2, 0);
      }
    }
    return path;
  }

  function setUpContextToSVGElement(ctx, svgEl) {
    const default_ctx = get2DContext();
    const dict = {
      "stroke-width": "lineWidth",
      "stroke-linecap": "lineCap",
      "stroke-linejoin": "lineJoin",
      "stroke-miterlimit": "miterLimit",
      "stroke-dashoffset": "lineDashOffset"
    };

    for (const [key, value] of Object.entries(dict)) {
      ctx[value] = svgEl.getAttribute(key) || default_ctx[value];
    }
    ctx.setLineDash((svgEl.getAttribute("stroke-dasharray") || "").split(' '));
  }

  function get2DContext(width = 0, height = 0) {
    return Object.assign(
      document.createElement("canvas"),
      { width, height }
     ).getContext('2d');
  }
}
body {
  background: lightblue;
}

.region {
  fill: green;
  stroke: white;
  stroke-width: 0.5;
}

.region:hover {
  fill: lightgreen;
}

.active {
  fill: red;
}

.neighbour {
  fill: blue;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<svg id="Map" class="map" viewBox="0 0 125 75">
  <g>
    <path id="Region01" class="region" d="M0,0 L 0,50 L 50,50Z"/>
    <path id="Region02" class="region" d="M25,25 L 75,25 L 75,75Z"/>
    <path id="Region03" class="region" d="M0,50 L 25,50 L 25,75 L 0,75Z"/>
    <path id="Region04" class="region" d="M100,0 L 125,0 L 125,25 L 100,25Z"/>
  </g>
</svg>


推荐阅读