var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);

// src/components/pisellFloorMapLayout/utils/edgeRouting.ts
var edgeRouting_exports = {};
__export(edgeRouting_exports, {
  DEFAULT_EDGE_STUB_LENGTH: () => DEFAULT_EDGE_STUB_LENGTH,
  EDGE_CENTER_LABEL_MIN_GAP: () => EDGE_CENTER_LABEL_MIN_GAP,
  EDGE_CENTER_LABEL_OFFSET: () => EDGE_CENTER_LABEL_OFFSET,
  EDGE_PORT_LABEL_HEIGHT: () => EDGE_PORT_LABEL_HEIGHT,
  EDGE_PORT_LABEL_OFFSET_GAP: () => EDGE_PORT_LABEL_OFFSET_GAP,
  EDGE_PORT_MARKER_OUTSET: () => EDGE_PORT_MARKER_OUTSET,
  EDGE_PORT_SNAP_THRESHOLD: () => EDGE_PORT_SNAP_THRESHOLD,
  EDGE_ROUTE_RECT_CLEARANCE: () => EDGE_ROUTE_RECT_CLEARANCE,
  FLOOR_MAP_EDGE_ANCHORS: () => ANCHORS,
  alignSnapshotToAnchors: () => alignSnapshotToAnchors,
  applyMidSegmentOverrides: () => applyMidSegmentOverrides,
  applySourceStub: () => applySourceStub,
  applyTargetStub: () => applyTargetStub,
  buildEdgeRoute: () => buildEdgeRoute,
  distanceToPolyline: () => distanceToPolyline,
  estimateEdgeLabelWidth: () => estimateEdgeLabelWidth,
  getAnchorPoint: () => getAnchorPoint,
  getEdgeCenterLabelPosition: () => getEdgeCenterLabelPosition,
  getEdgeCenterLabelPositionOnSegment: () => getEdgeCenterLabelPositionOnSegment,
  getEdgeChannelOffset: () => getEdgeChannelOffset,
  getEdgeInnerSegments: () => getEdgeInnerSegments,
  getItemPixelRect: () => getItemPixelRect,
  getPortLabelPosition: () => getPortLabelPosition,
  getPortMarkerDisplayPoint: () => getPortMarkerDisplayPoint,
  hitTestEdgeAtPoint: () => hitTestEdgeAtPoint,
  pathCrossesRectInterior: () => pathCrossesRectInterior,
  pathFromPoints: () => pathFromPoints,
  pickAutoAnchor: () => pickAutoAnchor,
  pickNearestAnchorOnRect: () => pickNearestAnchorOnRect,
  rectCenter: () => rectCenter,
  resolveDrawingTargetAtPoint: () => resolveDrawingTargetAtPoint,
  resolveEdgeCenterLabelPositions: () => resolveEdgeCenterLabelPositions,
  routeOrthogonal: () => routeOrthogonal,
  simplifyCollinearPath: () => simplifyCollinearPath
});
module.exports = __toCommonJS(edgeRouting_exports);
var import_layoutMeasurements = require("./layoutMeasurements");
var ANCHORS = [
  "top",
  "right",
  "bottom",
  "left"
];
function getItemPixelRect(item, options) {
  const { cellSize, itemUnit, itemGap = 0, override } = options;
  const g = (0, import_layoutMeasurements.gapToPx)(itemGap);
  let left;
  let top;
  let width;
  let height;
  if (itemUnit === "pixel") {
    width = item.width ?? cellSize;
    height = item.height ?? cellSize;
    left = item.x;
    top = item.y;
  } else {
    const w = item.width ?? 1;
    const h = item.height ?? 1;
    left = g > 0 ? item.x * (cellSize + g) + g : item.x * cellSize;
    top = g > 0 ? item.y * (cellSize + g) + g : item.y * cellSize;
    width = g > 0 ? w * cellSize + (w - 1) * g : w * cellSize;
    height = g > 0 ? h * cellSize + (h - 1) * g : h * cellSize;
  }
  if (override) {
    if (override.left != null) left = override.left;
    if (override.top != null) top = override.top;
    if (override.width != null) width = override.width;
    if (override.height != null) height = override.height;
  }
  return { left, top, width, height };
}
function rectCenter(rect) {
  return {
    x: rect.left + rect.width / 2,
    y: rect.top + rect.height / 2
  };
}
function getAnchorPoint(rect, anchor) {
  const cx = rect.left + rect.width / 2;
  const cy = rect.top + rect.height / 2;
  switch (anchor) {
    case "top":
      return { x: cx, y: rect.top };
    case "right":
      return { x: rect.left + rect.width, y: cy };
    case "bottom":
      return { x: cx, y: rect.top + rect.height };
    case "left":
      return { x: rect.left, y: cy };
    case "center":
    default:
      return { x: cx, y: cy };
  }
}
var EDGE_PORT_LABEL_HEIGHT = 18;
var EDGE_PORT_LABEL_OFFSET_GAP = 6;
var EDGE_PORT_MARKER_OUTSET = 16;
var EDGE_ROUTE_RECT_CLEARANCE = 10;
function getPortMarkerDisplayPoint(rect, anchor, outset = EDGE_PORT_MARKER_OUTSET) {
  const base = getAnchorPoint(rect, anchor);
  const v = sideVector(anchor);
  return {
    x: base.x + v.dx * outset,
    y: base.y + v.dy * outset
  };
}
function getPortLabelPosition(rect, anchor, options) {
  const labelHeight = (options == null ? void 0 : options.labelHeight) ?? EDGE_PORT_LABEL_HEIGHT;
  const gap = (options == null ? void 0 : options.gap) ?? EDGE_PORT_LABEL_OFFSET_GAP;
  const base = getAnchorPoint(rect, anchor);
  const v = sideVector(anchor);
  const offset = labelHeight / 2 + gap;
  return {
    x: base.x + v.dx * offset,
    y: base.y + v.dy * offset
  };
}
var EDGE_CENTER_LABEL_OFFSET = 8;
function estimateEdgeLabelWidth(text) {
  const padX = 6;
  return Math.max(32, text.length * 6.5 + padX * 2);
}
function getEdgeCenterLabelPositionOnSegment(points, segIndex, options) {
  if (points.length < 2 || segIndex < 0 || segIndex >= points.length - 1) {
    return null;
  }
  const offset = (options == null ? void 0 : options.offsetPx) ?? EDGE_CENTER_LABEL_OFFSET;
  const perp = (options == null ? void 0 : options.perpOffsetPx) ?? 0;
  const [x1, y1] = points[segIndex];
  const [x2, y2] = points[segIndex + 1];
  const along = (options == null ? void 0 : options.along) ?? 0.5;
  const x = x1 + (x2 - x1) * along;
  const y = y1 + (y2 - y1) * along;
  const isHorizontal = y1 === y2 && x1 !== x2;
  if (isHorizontal) {
    return { x, y: y - offset - perp };
  }
  const isVertical = x1 === x2 && y1 !== y2;
  if (isVertical) {
    return { x: x - offset - perp, y };
  }
  return { x, y: y - offset - perp };
}
function getEdgeCenterLabelPosition(points, options) {
  if (points.length < 2) return null;
  const offset = (options == null ? void 0 : options.offsetPx) ?? EDGE_CENTER_LABEL_OFFSET;
  const perp = (options == null ? void 0 : options.perpOffsetPx) ?? 0;
  const placement = (options == null ? void 0 : options.placement) ?? "target-tail";
  if (placement === "middle") {
    const i = Math.floor(points.length / 2);
    const [x, y] = points[i];
    return { x, y: y - offset - perp };
  }
  const segIndex = (options == null ? void 0 : options.segIndex) ?? (placement === "source-tail" ? 0 : Math.max(0, points.length - 2));
  const along = (options == null ? void 0 : options.along) ?? (placement === "source-tail" ? 0.35 : 0.65);
  return getEdgeCenterLabelPositionOnSegment(points, segIndex, {
    along,
    offsetPx: offset,
    perpOffsetPx: perp
  });
}
var EDGE_CENTER_LABEL_MIN_GAP = 22;
var LABEL_HORIZONTAL_MIN_LEN = 20;
function clampAlong(value) {
  return Math.max(0.12, Math.min(0.9, value));
}
function pickLongestHorizontalSegmentIndex(points, minLen = LABEL_HORIZONTAL_MIN_LEN) {
  let bestIndex = null;
  let bestLen = 0;
  for (let i = 0; i < points.length - 1; i += 1) {
    const [x1, y1] = points[i];
    const [x2, y2] = points[i + 1];
    if (y1 !== y2 || x1 === x2) continue;
    const len = Math.abs(x2 - x1);
    if (len >= minLen && len > bestLen) {
      bestLen = len;
      bestIndex = i;
    }
  }
  return bestIndex;
}
function horizontalSegmentKey(points, segIndex) {
  const [x1, y1] = points[segIndex];
  const [x2, y2] = points[segIndex + 1];
  const ya = Math.round(y1);
  const xa = Math.round(Math.min(x1, x2));
  const xb = Math.round(Math.max(x1, x2));
  return `h:${ya}:${xa}-${xb}`;
}
function labelBoxesOverlap(a, b) {
  const margin = 4;
  return Math.abs(a.x - b.x) < (a.w + b.w) / 2 + margin && Math.abs(a.y - b.y) < (a.h + b.h) / 2 + margin;
}
function buildPlacementState(item, segIndex, along, perpOffsetPx, offsetPx) {
  var _a;
  const label = ((_a = item.label) == null ? void 0 : _a.trim()) ?? "";
  const pos = getEdgeCenterLabelPositionOnSegment(item.points, segIndex, {
    along,
    offsetPx,
    perpOffsetPx
  });
  if (!pos) return null;
  return {
    edgeId: item.edgeId,
    targetId: item.targetId,
    label,
    points: item.points,
    segIndex,
    along,
    perpOffsetPx,
    x: pos.x,
    y: pos.y,
    w: estimateEdgeLabelWidth(label),
    h: EDGE_PORT_LABEL_HEIGHT
  };
}
function placeLabelsBesideTarget(targetRect, group) {
  const sorted = [...group].sort((a, b) => a.edgeId.localeCompare(b.edgeId));
  const out = /* @__PURE__ */ new Map();
  const h = EDGE_PORT_LABEL_HEIGHT;
  const gap = 4;
  const maxW = sorted.reduce(
    (acc, it) => {
      var _a;
      return Math.max(acc, estimateEdgeLabelWidth(((_a = it.label) == null ? void 0 : _a.trim()) ?? ""));
    },
    32
  );
  const cx = targetRect.left - 6 - maxW / 2;
  const stackH = sorted.length * (h + gap) - gap;
  const startY = targetRect.top + targetRect.height / 2 - stackH / 2 + h / 2;
  for (let i = 0; i < sorted.length; i += 1) {
    out.set(sorted[i].edgeId, { x: cx, y: startY + i * (h + gap) });
  }
  return out;
}
function resolveEdgeCenterLabelPositions(items, options) {
  const placement = (options == null ? void 0 : options.placement) ?? "target-tail";
  const offsetPx = (options == null ? void 0 : options.offsetPx) ?? EDGE_CENTER_LABEL_OFFSET;
  const result = /* @__PURE__ */ new Map();
  const states = [];
  const labeled = items.filter((it) => {
    var _a;
    return (_a = it.label) == null ? void 0 : _a.trim();
  });
  const byTarget = /* @__PURE__ */ new Map();
  for (const it of labeled) {
    const list = byTarget.get(it.targetId) ?? [];
    list.push(it);
    byTarget.set(it.targetId, list);
  }
  for (const group of byTarget.values()) {
    const sorted = [...group].sort((a, b) => a.edgeId.localeCompare(b.edgeId));
    const alongBySegKey = /* @__PURE__ */ new Map();
    for (let i = 0; i < sorted.length; i += 1) {
      const item = sorted[i];
      const hSeg = pickLongestHorizontalSegmentIndex(item.points);
      const segIndex = hSeg ?? (placement === "source-tail" ? 0 : Math.max(0, item.points.length - 2));
      let along = hSeg != null ? 0.5 : placement === "source-tail" ? 0.35 : 0.68;
      if (hSeg != null) {
        const key = horizontalSegmentKey(item.points, hSeg);
        const count = alongBySegKey.get(key) ?? 0;
        alongBySegKey.set(key, count + 1);
        if (count > 0) {
          const [x1] = item.points[hSeg];
          const [x2] = item.points[hSeg + 1];
          const segLen = Math.abs(x2 - x1);
          const step = Math.min(
            0.2,
            (EDGE_PORT_LABEL_HEIGHT + 6) / Math.max(segLen, 40)
          );
          along = clampAlong(0.5 + (count - 0.5) * step);
        }
      } else if (sorted.length > 1) {
        const segLen = Math.hypot(
          item.points[segIndex + 1][0] - item.points[segIndex][0],
          item.points[segIndex + 1][1] - item.points[segIndex][1]
        );
        const step = Math.min(
          0.18,
          (EDGE_PORT_LABEL_HEIGHT + 8) / Math.max(segLen, 36)
        );
        along = clampAlong(
          (placement === "source-tail" ? 0.35 : 0.68) + (i - (sorted.length - 1) / 2) * step
        );
      }
      const perpLane = Math.floor((i + 1) / 2) * 16;
      const perpOffsetPx = sorted.length > 1 ? (i % 2 === 0 ? -1 : 1) * perpLane : 0;
      const state = buildPlacementState(
        item,
        segIndex,
        along,
        perpOffsetPx,
        offsetPx
      );
      if (state) states.push(state);
    }
  }
  for (let iter = 0; iter < 12; iter += 1) {
    let adjusted = false;
    for (let i = 0; i < states.length; i += 1) {
      for (let j = i + 1; j < states.length; j += 1) {
        const a = states[i];
        const b = states[j];
        if (!labelBoxesOverlap(a, b)) continue;
        const mover = states[j];
        mover.along = clampAlong(mover.along + 0.08);
        mover.perpOffsetPx += (j % 2 === 0 ? -1 : 1) * 12;
        const pos = getEdgeCenterLabelPositionOnSegment(
          mover.points,
          mover.segIndex,
          {
            along: mover.along,
            offsetPx,
            perpOffsetPx: mover.perpOffsetPx
          }
        );
        if (!pos) continue;
        mover.x = pos.x;
        mover.y = pos.y;
        adjusted = true;
      }
    }
    if (!adjusted) break;
  }
  for (const state of states) {
    result.set(state.edgeId, { x: state.x, y: state.y });
  }
  if (options == null ? void 0 : options.rectsById) {
    for (const group of byTarget.values()) {
      if (group.length < 3) continue;
      const rect = options.rectsById.get(group[0].targetId);
      if (!rect) continue;
      const stacked = placeLabelsBesideTarget(rect, group);
      for (const [id, pos] of stacked) {
        result.set(id, pos);
      }
    }
  }
  return result;
}
function pickAutoAnchor(from, to) {
  const a = rectCenter(from);
  const b = rectCenter(to);
  const dx = b.x - a.x;
  const dy = b.y - a.y;
  if (Math.abs(dx) >= Math.abs(dy)) {
    return dx >= 0 ? "right" : "left";
  }
  return dy >= 0 ? "bottom" : "top";
}
var EDGE_PORT_SNAP_THRESHOLD = 16;
function pickNearestAnchorOnRect(pt, rect) {
  let best = "top";
  let minDist = Infinity;
  for (const anchor of ANCHORS) {
    const ap = getAnchorPoint(rect, anchor);
    const d = Math.hypot(pt.x - ap.x, pt.y - ap.y);
    if (d < minDist) {
      minDist = d;
      best = anchor;
    }
  }
  return best;
}
function resolveDrawingTargetAtPoint(pt, instanceIds, rectsById, excludeInstanceId, snapThreshold = EDGE_PORT_SNAP_THRESHOLD) {
  let bestSnap = null;
  let bestSnapDist = snapThreshold;
  for (const id of instanceIds) {
    if (id === excludeInstanceId) continue;
    const rect = rectsById.get(id);
    if (!rect) continue;
    for (const anchor of ANCHORS) {
      const ap = getAnchorPoint(rect, anchor);
      const d = Math.hypot(pt.x - ap.x, pt.y - ap.y);
      if (d <= bestSnapDist) {
        bestSnapDist = d;
        bestSnap = { instanceId: id, anchor };
      }
    }
  }
  if (bestSnap) return bestSnap;
  for (const id of instanceIds) {
    if (id === excludeInstanceId) continue;
    const rect = rectsById.get(id);
    if (!rect) continue;
    const right = rect.left + rect.width;
    const bottom = rect.top + rect.height;
    if (pt.x >= rect.left && pt.x <= right && pt.y >= rect.top && pt.y <= bottom) {
      return {
        instanceId: id,
        anchor: pickNearestAnchorOnRect(pt, rect)
      };
    }
  }
  return null;
}
function sideVector(anchor) {
  switch (anchor) {
    case "top":
      return { dx: 0, dy: -1 };
    case "bottom":
      return { dx: 0, dy: 1 };
    case "left":
      return { dx: -1, dy: 0 };
    case "right":
    default:
      return { dx: 1, dy: 0 };
  }
}
function horizontalSegmentCrossesRect(y, x1, x2, rect) {
  const minX = Math.min(x1, x2);
  const maxX = Math.max(x1, x2);
  const bottom = rect.top + rect.height;
  const right = rect.left + rect.width;
  if (y <= rect.top || y >= bottom) return false;
  return maxX > rect.left && minX < right;
}
function verticalSegmentCrossesRect(x, y1, y2, rect) {
  const minY = Math.min(y1, y2);
  const maxY = Math.max(y1, y2);
  const bottom = rect.top + rect.height;
  const right = rect.left + rect.width;
  if (x <= rect.left || x >= right) return false;
  return maxY > rect.top && minY < bottom;
}
function channelYOutsideRect(rect, preferY, clearance) {
  const above = rect.top - clearance;
  const below = rect.top + rect.height + clearance;
  return Math.abs(preferY - above) <= Math.abs(preferY - below) ? above : below;
}
function channelXOutsideRect(rect, preferX, clearance) {
  const left = rect.left - clearance;
  const right = rect.left + rect.width + clearance;
  return Math.abs(preferX - left) <= Math.abs(preferX - right) ? left : right;
}
function pushRoutePoint(out, point) {
  const last = out[out.length - 1];
  if (!last || last[0] !== point[0] || last[1] !== point[1]) {
    out.push(point);
  }
}
function pathCrossesRectInterior(points, rect) {
  const bottom = rect.top + rect.height;
  const right = rect.left + rect.width;
  for (let i = 0; i < points.length - 1; i++) {
    const [x1, y1] = points[i];
    const [x2, y2] = points[i + 1];
    if (y1 === y2) {
      const minX = Math.min(x1, x2);
      const maxX = Math.max(x1, x2);
      if (y1 <= rect.top || y1 >= bottom) continue;
      if (maxX > rect.left && minX < right) return true;
    }
    if (x1 === x2) {
      const minY = Math.min(y1, y2);
      const maxY = Math.max(y1, y2);
      if (x1 <= rect.left || x1 >= right) continue;
      if (maxY > rect.top && minY < bottom) return true;
    }
  }
  return false;
}
function pathCrossesAnyRectInterior(points, rects) {
  return rects.some((rect) => pathCrossesRectInterior(points, rect));
}
function manhattanPathLength(points) {
  let sum = 0;
  for (let i = 0; i < points.length - 1; i++) {
    sum += Math.abs(points[i + 1][0] - points[i][0]) + Math.abs(points[i + 1][1] - points[i][1]);
  }
  return sum;
}
function compressCollinearPoints(points) {
  if (points.length < 3) return points.map((p) => [...p]);
  const out = [points[0]];
  for (let i = 1; i < points.length - 1; i++) {
    const a = out[out.length - 1];
    const b = points[i];
    const c = points[i + 1];
    const collinearX = a[0] === b[0] && b[0] === c[0];
    const collinearY = a[1] === b[1] && b[1] === c[1];
    if (collinearX || collinearY) continue;
    out.push(b);
  }
  out.push(points[points.length - 1]);
  return out;
}
function pathAsymmetry(points) {
  const compressed = compressCollinearPoints(points);
  if (compressed.length < 4) return 0;
  const segLen = (a, b) => Math.abs(b[0] - a[0]) + Math.abs(b[1] - a[1]);
  const first = segLen(compressed[0], compressed[1]);
  const last = segLen(
    compressed[compressed.length - 2],
    compressed[compressed.length - 1]
  );
  return Math.abs(first - last);
}
function pathTurnCount(points) {
  const compressed = compressCollinearPoints(points);
  return Math.max(0, compressed.length - 2);
}
function outwardClearancePoint(pt, side, rect, clearance) {
  const bottom = rect.top + rect.height;
  const right = rect.left + rect.width;
  switch (side) {
    case "right":
      return [right + clearance, pt.y];
    case "left":
      return [rect.left - clearance, pt.y];
    case "top":
      return [pt.x, rect.top - clearance];
    case "bottom":
      return [pt.x, bottom + clearance];
    default:
      return [pt.x, pt.y];
  }
}
function isOrthogonalPath(points) {
  for (let i = 0; i < points.length - 1; i++) {
    if (points[i][0] !== points[i + 1][0] && points[i][1] !== points[i + 1][1]) {
      return false;
    }
  }
  return true;
}
function exitsAlignWithSides(sourcePt, sourceSide, targetPt, targetSide, pts) {
  if (pts.length < 2) return true;
  const sv = sideVector(sourceSide);
  const sNext = pts[1];
  const sDx = sNext[0] - sourcePt.x;
  const sDy = sNext[1] - sourcePt.y;
  if (sv.dx !== 0) {
    if (Math.sign(sDx) !== sv.dx) return false;
    if (sDy !== 0) return false;
  } else {
    if (Math.sign(sDy) !== sv.dy) return false;
    if (sDx !== 0) return false;
  }
  const tv = sideVector(targetSide);
  const tPrev = pts[pts.length - 2];
  const tDx = targetPt.x - tPrev[0];
  const tDy = targetPt.y - tPrev[1];
  if (tv.dx !== 0) {
    if (Math.sign(tDx) !== -tv.dx) return false;
    if (tDy !== 0) return false;
  } else {
    if (Math.sign(tDy) !== -tv.dy) return false;
    if (tDx !== 0) return false;
  }
  return true;
}
function buildOrthogonalPathAvoidingRects(sourcePt, sourceSide, targetPt, targetSide, sourceRect, targetRect, clearance, channelOffset = 0) {
  const rects = [sourceRect, targetRect];
  const sOut = outwardClearancePoint(sourcePt, sourceSide, sourceRect, clearance);
  const tOut = outwardClearancePoint(targetPt, targetSide, targetRect, clearance);
  const channelGap = channelOffset;
  const outerGap = Math.abs(channelOffset);
  const aboveY = Math.min(...rects.map((r) => r.top)) - clearance - outerGap;
  const belowY = Math.max(...rects.map((r) => r.top + r.height)) + clearance + outerGap;
  const leftX = Math.min(...rects.map((r) => r.left)) - clearance - outerGap;
  const rightX = Math.max(...rects.map((r) => r.left + r.width)) + clearance + outerGap;
  const connectors = [];
  connectors.push([[tOut[0], sOut[1]]]);
  connectors.push([[sOut[0], tOut[1]]]);
  const midX = (sOut[0] + tOut[0]) / 2 + channelGap;
  const midY = (sOut[1] + tOut[1]) / 2 + channelGap;
  connectors.push([
    [midX, sOut[1]],
    [midX, tOut[1]]
  ]);
  connectors.push([
    [sOut[0], midY],
    [tOut[0], midY]
  ]);
  for (const channelY of [aboveY, belowY]) {
    connectors.push([
      [sOut[0], channelY],
      [tOut[0], channelY]
    ]);
  }
  for (const channelX of [leftX, rightX]) {
    connectors.push([
      [channelX, sOut[1]],
      [channelX, tOut[1]]
    ]);
  }
  let bestPath = null;
  let bestLen = Infinity;
  let bestTurns = Infinity;
  let bestAsym = Infinity;
  for (const inner of connectors) {
    const trial = [];
    pushRoutePoint(trial, [sourcePt.x, sourcePt.y]);
    pushRoutePoint(trial, sOut);
    for (const p of inner) pushRoutePoint(trial, p);
    pushRoutePoint(trial, tOut);
    pushRoutePoint(trial, [targetPt.x, targetPt.y]);
    if (!isOrthogonalPath(trial)) continue;
    if (!exitsAlignWithSides(sourcePt, sourceSide, targetPt, targetSide, trial)) {
      continue;
    }
    if (pathCrossesAnyRectInterior(trial, rects)) continue;
    const len = manhattanPathLength(trial);
    const turns = pathTurnCount(trial);
    const asym = pathAsymmetry(trial);
    const better = len < bestLen || len === bestLen && turns < bestTurns || len === bestLen && turns === bestTurns && asym < bestAsym;
    if (better) {
      bestLen = len;
      bestTurns = turns;
      bestAsym = asym;
      bestPath = trial;
    }
  }
  if (bestPath) return bestPath;
  return [
    [sourcePt.x, sourcePt.y],
    sOut,
    [sOut[0], aboveY],
    [tOut[0], aboveY],
    tOut,
    [targetPt.x, targetPt.y]
  ];
}
function buildShortestOrthogonalPath(sourcePt, sourceSide, targetPt, targetSide) {
  const sourceExitsVertical = sideVector(sourceSide).dx === 0;
  const sameHoriz = (sourceSide === "left" || sourceSide === "right") && (targetSide === "left" || targetSide === "right");
  const sameVert = (sourceSide === "top" || sourceSide === "bottom") && (targetSide === "top" || targetSide === "bottom");
  const out = [];
  if (sameHoriz) {
    const midX = (sourcePt.x + targetPt.x) / 2;
    pushRoutePoint(out, [sourcePt.x, sourcePt.y]);
    pushRoutePoint(out, [midX, sourcePt.y]);
    pushRoutePoint(out, [midX, targetPt.y]);
    pushRoutePoint(out, [targetPt.x, targetPt.y]);
    return out;
  }
  if (sameVert) {
    const midY = (sourcePt.y + targetPt.y) / 2;
    pushRoutePoint(out, [sourcePt.x, sourcePt.y]);
    pushRoutePoint(out, [sourcePt.x, midY]);
    pushRoutePoint(out, [targetPt.x, midY]);
    pushRoutePoint(out, [targetPt.x, targetPt.y]);
    return out;
  }
  const pathA = sourceExitsVertical ? [
    [sourcePt.x, sourcePt.y],
    [targetPt.x, sourcePt.y],
    [targetPt.x, targetPt.y]
  ] : [
    [sourcePt.x, sourcePt.y],
    [targetPt.x, sourcePt.y],
    [targetPt.x, targetPt.y]
  ];
  const pathB = sourceExitsVertical ? [
    [sourcePt.x, sourcePt.y],
    [sourcePt.x, targetPt.y],
    [targetPt.x, targetPt.y]
  ] : [
    [sourcePt.x, sourcePt.y],
    [sourcePt.x, targetPt.y],
    [targetPt.x, targetPt.y]
  ];
  const len = (pts) => {
    let sum = 0;
    for (let i = 0; i < pts.length - 1; i++) {
      sum += Math.abs(pts[i + 1][0] - pts[i][0]) + Math.abs(pts[i + 1][1] - pts[i][1]);
    }
    return sum;
  };
  const chosen = len(pathA) <= len(pathB) ? pathA : pathB;
  for (const p of chosen) {
    pushRoutePoint(out, p);
  }
  return out;
}
function buildWrapAroundPoints(a, b, rect, clearance) {
  if (a[1] === b[1]) {
    if (!horizontalSegmentCrossesRect(a[1], a[0], b[0], rect)) {
      return [];
    }
    const bypassY = channelYOutsideRect(rect, a[1], clearance);
    return [
      [a[0], bypassY],
      [b[0], bypassY]
    ];
  }
  if (a[0] === b[0]) {
    if (!verticalSegmentCrossesRect(a[0], a[1], b[1], rect)) {
      return [];
    }
    const bypassX = channelXOutsideRect(rect, a[0], clearance);
    return [
      [bypassX, a[1]],
      [bypassX, b[1]]
    ];
  }
  return [];
}
function wrapPathAroundRects(points, obstacleRects, clearance) {
  if (points.length < 3 || obstacleRects.length === 0) {
    return points;
  }
  const wrapped = [];
  for (let i = 0; i < points.length - 1; i++) {
    const a = points[i];
    const b = points[i + 1];
    pushRoutePoint(wrapped, a);
    const isLeavingSourceAnchor = i === 0;
    if (!isLeavingSourceAnchor) {
      for (const rect of obstacleRects) {
        const detour = buildWrapAroundPoints(a, b, rect, clearance);
        if (detour.length === 0) {
          continue;
        }
        for (const p of detour) {
          pushRoutePoint(wrapped, p);
        }
        break;
      }
    }
    pushRoutePoint(wrapped, b);
  }
  return wrapped;
}
function routeOrthogonal(sourcePt, sourceSide, targetPt, targetSide, stubOrOptions, maybeOptions) {
  const options = typeof stubOrOptions === "object" && stubOrOptions != null ? stubOrOptions : maybeOptions;
  const clearance = (options == null ? void 0 : options.clearance) ?? EDGE_ROUTE_RECT_CLEARANCE;
  const sourceRect = options == null ? void 0 : options.sourceRect;
  const targetRect = options == null ? void 0 : options.targetRect;
  if (sourceRect && targetRect) {
    let path = buildOrthogonalPathAvoidingRects(
      sourcePt,
      sourceSide,
      targetPt,
      targetSide,
      sourceRect,
      targetRect,
      clearance,
      (options == null ? void 0 : options.channelOffset) ?? 0
    );
    const extra = ((options == null ? void 0 : options.obstacleRects) ?? []).filter(
      (r) => r !== sourceRect && r !== targetRect
    );
    if (extra.length > 0) {
      path = wrapPathAroundRects(path, extra, clearance);
    }
    return path;
  }
  const shortest = buildShortestOrthogonalPath(
    sourcePt,
    sourceSide,
    targetPt,
    targetSide
  );
  const obstacles = (options == null ? void 0 : options.obstacleRects) ?? (targetRect ? [targetRect] : sourceRect ? [sourceRect] : []);
  if (obstacles.length === 0) {
    return shortest;
  }
  return wrapPathAroundRects(shortest, obstacles, clearance);
}
function pathFromPoints(points, radius = 4) {
  if (points.length < 2) return "";
  if (points.length === 2) {
    return `M ${points[0][0]} ${points[0][1]} L ${points[1][0]} ${points[1][1]}`;
  }
  let d = `M ${points[0][0]} ${points[0][1]}`;
  for (let i = 1; i < points.length - 1; i++) {
    const prev = points[i - 1];
    const curr = points[i];
    const next = points[i + 1];
    const r = Math.min(
      radius,
      Math.hypot(curr[0] - prev[0], curr[1] - prev[1]) / 2,
      Math.hypot(next[0] - curr[0], next[1] - curr[1]) / 2
    );
    const dx1 = curr[0] - prev[0];
    const dy1 = curr[1] - prev[1];
    const len1 = Math.hypot(dx1, dy1) || 1;
    const dx2 = next[0] - curr[0];
    const dy2 = next[1] - curr[1];
    const len2 = Math.hypot(dx2, dy2) || 1;
    const x1 = curr[0] - dx1 / len1 * r;
    const y1 = curr[1] - dy1 / len1 * r;
    const x2 = curr[0] + dx2 / len2 * r;
    const y2 = curr[1] + dy2 / len2 * r;
    d += ` L ${x1} ${y1} Q ${curr[0]} ${curr[1]} ${x2} ${y2}`;
  }
  const last = points[points.length - 1];
  d += ` L ${last[0]} ${last[1]}`;
  return d;
}
function alignSnapshotToAnchors(snapshot, srcPt, tgtPt) {
  if (snapshot.length < 2) return snapshot;
  const result = snapshot.map((p) => [p[0], p[1]]);
  const [x0, y0] = result[0];
  const [x1, y1] = result[1];
  result[0] = [srcPt.x, srcPt.y];
  if (y0 === y1 && x0 !== x1) {
    result[1] = [x1, srcPt.y];
  } else if (x0 === x1 && y0 !== y1) {
    result[1] = [srcPt.x, y1];
  } else {
    result[1] = [x1, y1];
  }
  const n = result.length;
  if (n >= 4) {
    const [xnm2, ynm2] = result[n - 2];
    const [xnm1, ynm1] = result[n - 1];
    result[n - 1] = [tgtPt.x, tgtPt.y];
    if (ynm2 === ynm1 && xnm2 !== xnm1) {
      result[n - 2] = [xnm2, tgtPt.y];
    } else if (xnm2 === xnm1 && ynm2 !== ynm1) {
      result[n - 2] = [tgtPt.x, ynm2];
    } else {
      result[n - 2] = [xnm2, ynm2];
    }
  } else if (n === 3) {
    const [xA, yA] = snapshot[0];
    const [xB, yB] = snapshot[1];
    const [xC, yC] = snapshot[2];
    const firstHorizontal = yA === yB && xA !== xB;
    const firstVertical = xA === xB && yA !== yB;
    const lastHorizontal = yB === yC && xB !== xC;
    const lastVertical = xB === xC && yB !== yC;
    let rx = xB;
    let ry = yB;
    if (firstHorizontal) ry = srcPt.y;
    if (firstVertical) rx = srcPt.x;
    if (lastHorizontal) ry = tgtPt.y;
    if (lastVertical) rx = tgtPt.x;
    result[1] = [rx, ry];
    result[2] = [tgtPt.x, tgtPt.y];
  } else if (n === 2) {
    result[1] = [tgtPt.x, tgtPt.y];
  }
  return result;
}
var DEFAULT_EDGE_STUB_LENGTH = 24;
function applySourceStub(points, stubLength, sourceAnchor) {
  if (!stubLength || stubLength <= 0 || points.length < 2) return points;
  const [x0, y0] = points[0];
  const [x1, y1] = points[1];
  const isHorizontal = y0 === y1 && x0 !== x1;
  const isVertical = x0 === x1 && y0 !== y1;
  if (!isHorizontal && !isVertical) return points;
  const firstLen = isHorizontal ? Math.abs(x1 - x0) : Math.abs(y1 - y0);
  if (firstLen <= 1) return points;
  const stub = Math.min(stubLength, firstLen / 2);
  if (stub <= 0) return points;
  if (isHorizontal) {
    const sgn2 = Math.sign(x1 - x0) || 1;
    const sx = x0 + sgn2 * stub;
    const stubEnd2 = [sx, y0];
    const midCorner2 = [sx, y1];
    const replacedP12 = [x1, y1];
    return [points[0], stubEnd2, midCorner2, replacedP12, ...points.slice(2)];
  }
  const sgn = Math.sign(y1 - y0) || 1;
  const sy = y0 + sgn * stub;
  const stubEnd = [x0, sy];
  const midCorner = [x1, sy];
  const replacedP1 = [x1, y1];
  return [points[0], stubEnd, midCorner, replacedP1, ...points.slice(2)];
}
function applyTargetStub(points, stubLength, targetAnchor) {
  if (!stubLength || stubLength <= 0 || points.length < 2) return points;
  const n = points.length;
  const [xA, yA] = points[n - 2];
  const [xT, yT] = points[n - 1];
  const isHorizontal = yA === yT && xA !== xT;
  const isVertical = xA === xT && yA !== yT;
  if (!isHorizontal && !isVertical) return points;
  const lastLen = isHorizontal ? Math.abs(xT - xA) : Math.abs(yT - yA);
  if (lastLen <= 1) return points;
  const stub = Math.min(stubLength, lastLen / 2);
  if (stub <= 0) return points;
  if (isHorizontal) {
    const sgn2 = Math.sign(xT - xA) || 1;
    const sx = xT - sgn2 * stub;
    const aMod2 = [xA, yA];
    const midCorner2 = [sx, yA];
    const stubStart2 = [sx, yT];
    return [
      ...points.slice(0, n - 2),
      aMod2,
      midCorner2,
      stubStart2,
      points[n - 1]
    ];
  }
  const sgn = Math.sign(yT - yA) || 1;
  const sy = yT - sgn * stub;
  const aMod = [xA, yA];
  const midCorner = [xA, sy];
  const stubStart = [xT, sy];
  return [
    ...points.slice(0, n - 2),
    aMod,
    midCorner,
    stubStart,
    points[n - 1]
  ];
}
function applyMidSegmentOverrides(points, overrides) {
  if (!overrides || overrides.length === 0 || points.length < 4) {
    return points;
  }
  const result = points.map((p) => [p[0], p[1]]);
  const innerCount = result.length - 3;
  const max = Math.min(innerCount, overrides.length);
  for (let k = 0; k < max; k++) {
    const v = overrides[k];
    if (v == null) continue;
    const i = k + 1;
    const [x1, y1] = result[i];
    const [x2, y2] = result[i + 1];
    if (y1 === y2 && x1 !== x2) {
      result[i] = [x1, v];
      result[i + 1] = [x2, v];
    } else if (x1 === x2 && y1 !== y2) {
      result[i] = [v, y1];
      result[i + 1] = [v, y2];
    }
  }
  return result;
}
function getEdgeInnerSegments(points) {
  const result = [];
  if (points.length < 4) return result;
  for (let i = 1; i <= points.length - 3; i++) {
    const [x1, y1] = points[i];
    const [x2, y2] = points[i + 1];
    if (y1 === y2 && x1 !== x2) {
      result.push({
        index: i - 1,
        pointIndex: i,
        axis: "h",
        position: y1,
        midX: (x1 + x2) / 2,
        midY: y1,
        length: Math.abs(x2 - x1)
      });
    } else if (x1 === x2 && y1 !== y2) {
      result.push({
        index: i - 1,
        pointIndex: i,
        axis: "v",
        position: x1,
        midX: x1,
        midY: (y1 + y2) / 2,
        length: Math.abs(y2 - y1)
      });
    }
  }
  return result;
}
function buildEdgeRoute(edge, rectsById, options) {
  const srcRect = rectsById.get(edge.sourceId);
  const tgtRect = rectsById.get(edge.targetId);
  if (!srcRect || !tgtRect) return null;
  const sourceAnchor = edge.sourceAnchor ?? pickAutoAnchor(srcRect, tgtRect);
  const targetAnchor = edge.targetAnchor ?? pickAutoAnchor(tgtRect, srcRect);
  const sourcePt = getAnchorPoint(srcRect, sourceAnchor);
  const targetPt = getAnchorPoint(tgtRect, targetAnchor);
  const routing = (options == null ? void 0 : options.routingOverride) ?? edge.routing;
  if ((routing == null ? void 0 : routing.pathSnapshot) && routing.pathSnapshot.length >= 2) {
    const aligned = alignSnapshotToAnchors(
      routing.pathSnapshot,
      sourcePt,
      targetPt
    );
    const points2 = simplifyCollinearPath(aligned);
    return {
      edgeId: edge.id,
      points: points2,
      pathD: pathFromPoints(points2)
    };
  }
  const rawPoints = routeOrthogonal(
    sourcePt,
    sourceAnchor,
    targetPt,
    targetAnchor,
    {
      sourceRect: srcRect,
      targetRect: tgtRect,
      channelOffset: (options == null ? void 0 : options.channelOffset) ?? 0
    }
  );
  const points = simplifyCollinearPath(rawPoints);
  return {
    edgeId: edge.id,
    points,
    pathD: pathFromPoints(points)
  };
}
function simplifyCollinearPath(points) {
  if (points.length < 3) return points.map((p) => [p[0], p[1]]);
  const out = [[points[0][0], points[0][1]]];
  for (let i = 1; i < points.length - 1; i++) {
    const prev = out[out.length - 1];
    const curr = points[i];
    const next = points[i + 1];
    const zeroBefore = prev[0] === curr[0] && prev[1] === curr[1];
    const zeroAfter = curr[0] === next[0] && curr[1] === next[1];
    if (zeroBefore || zeroAfter) {
      out.push([curr[0], curr[1]]);
      continue;
    }
    const collinearH = prev[1] === curr[1] && curr[1] === next[1];
    const collinearV = prev[0] === curr[0] && curr[0] === next[0];
    if (collinearH || collinearV) continue;
    out.push([curr[0], curr[1]]);
  }
  const last = points[points.length - 1];
  out.push([last[0], last[1]]);
  return out;
}
function getEdgeChannelOffset(index, step = 12) {
  if (index <= 0) return 0;
  const k = Math.ceil(index / 2);
  return index % 2 === 1 ? k * step : -k * step;
}
function distanceToPolyline(px, py, points) {
  let min = Infinity;
  for (let i = 0; i < points.length - 1; i++) {
    const [x1, y1] = points[i];
    const [x2, y2] = points[i + 1];
    const dx = x2 - x1;
    const dy = y2 - y1;
    const lenSq = dx * dx + dy * dy;
    let t = 0;
    if (lenSq > 0) t = Math.max(0, Math.min(1, ((px - x1) * dx + (py - y1) * dy) / lenSq));
    const nx = x1 + t * dx;
    const ny = y1 + t * dy;
    min = Math.min(min, Math.hypot(px - nx, py - ny));
  }
  return min;
}
function hitTestEdgeAtPoint(px, py, routes, threshold = 8) {
  for (const route of routes) {
    if (distanceToPolyline(px, py, route.points) <= threshold) {
      return route.edgeId;
    }
  }
  return null;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
  DEFAULT_EDGE_STUB_LENGTH,
  EDGE_CENTER_LABEL_MIN_GAP,
  EDGE_CENTER_LABEL_OFFSET,
  EDGE_PORT_LABEL_HEIGHT,
  EDGE_PORT_LABEL_OFFSET_GAP,
  EDGE_PORT_MARKER_OUTSET,
  EDGE_PORT_SNAP_THRESHOLD,
  EDGE_ROUTE_RECT_CLEARANCE,
  FLOOR_MAP_EDGE_ANCHORS,
  alignSnapshotToAnchors,
  applyMidSegmentOverrides,
  applySourceStub,
  applyTargetStub,
  buildEdgeRoute,
  distanceToPolyline,
  estimateEdgeLabelWidth,
  getAnchorPoint,
  getEdgeCenterLabelPosition,
  getEdgeCenterLabelPositionOnSegment,
  getEdgeChannelOffset,
  getEdgeInnerSegments,
  getItemPixelRect,
  getPortLabelPosition,
  getPortMarkerDisplayPoint,
  hitTestEdgeAtPoint,
  pathCrossesRectInterior,
  pathFromPoints,
  pickAutoAnchor,
  pickNearestAnchorOnRect,
  rectCenter,
  resolveDrawingTargetAtPoint,
  resolveEdgeCenterLabelPositions,
  routeOrthogonal,
  simplifyCollinearPath
});
