const require_layoutMeasurements = require("./layoutMeasurements.js");
//#region src/components/pisellFloorMapLayout/utils/edgeRouting.ts
const ANCHORS = [
	"top",
	"right",
	"bottom",
	"left"
];
/**
* 将图元逻辑坐标转为内容区像素矩形（与 EditableItemLayer 一致）
*/
function getItemPixelRect(item, options) {
	const { cellSize, itemUnit, itemGap = 0, override } = options;
	const g = require_layoutMeasurements.gapToPx(itemGap);
	let left;
	let top;
	let width;
	let height;
	if (itemUnit === "pixel") {
		var _item$width, _item$height;
		width = (_item$width = item.width) !== null && _item$width !== void 0 ? _item$width : cellSize;
		height = (_item$height = item.height) !== null && _item$height !== void 0 ? _item$height : cellSize;
		left = item.x;
		top = item.y;
	} else {
		var _item$width2, _item$height2;
		const w = (_item$width2 = item.width) !== null && _item$width2 !== void 0 ? _item$width2 : 1;
		const h = (_item$height2 = item.height) !== null && _item$height2 !== void 0 ? _item$height2 : 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
		};
		default: return {
			x: cx,
			y: cy
		};
	}
}
/**
* 编辑态锚点圆点 UI 位置（相对图元局部坐标），沿外法线外移，连线几何仍用 {@link getAnchorPoint}。
*/
function getPortMarkerDisplayPoint(rect, anchor, outset = 16) {
	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) {
	var _options$labelHeight, _options$gap;
	const labelHeight = (_options$labelHeight = options === null || options === void 0 ? void 0 : options.labelHeight) !== null && _options$labelHeight !== void 0 ? _options$labelHeight : 18;
	const gap = (_options$gap = options === null || options === void 0 ? void 0 : options.gap) !== null && _options$gap !== void 0 ? _options$gap : 6;
	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
	};
}
/** 与 EdgeLayer PortLabel 估算宽度一致 */
function estimateEdgeLabelWidth(text) {
	return Math.max(32, text.length * 6.5 + 12);
}
/**
* 在折线指定段上计算标签中心（水平段：标签在线上方；竖直段：标签在线左侧）。
*/
function getEdgeCenterLabelPositionOnSegment(points, segIndex, options) {
	var _options$offsetPx, _options$perpOffsetPx, _options$along;
	if (points.length < 2 || segIndex < 0 || segIndex >= points.length - 1) return null;
	const offset = (_options$offsetPx = options === null || options === void 0 ? void 0 : options.offsetPx) !== null && _options$offsetPx !== void 0 ? _options$offsetPx : 8;
	const perp = (_options$perpOffsetPx = options === null || options === void 0 ? void 0 : options.perpOffsetPx) !== null && _options$perpOffsetPx !== void 0 ? _options$perpOffsetPx : 0;
	const [x1, y1] = points[segIndex];
	const [x2, y2] = points[segIndex + 1];
	const along = (_options$along = options === null || options === void 0 ? void 0 : options.along) !== null && _options$along !== void 0 ? _options$along : .5;
	const x = x1 + (x2 - x1) * along;
	const y = y1 + (y2 - y1) * along;
	if (y1 === y2 && x1 !== x2) return {
		x,
		y: y - offset - perp
	};
	if (x1 === x2 && y1 !== y2) return {
		x: x - offset - perp,
		y
	};
	return {
		x,
		y: y - offset - perp
	};
}
/** 拓扑水平总线段最短可用长度（px） */
const LABEL_HORIZONTAL_MIN_LEN = 20;
function clampAlong(value) {
	return Math.max(.12, Math.min(.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];
	return `h:${Math.round(y1)}:${Math.round(Math.min(x1, x2))}-${Math.round(Math.max(x1, x2))}`;
}
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 _item$label$trim, _item$label;
	const label = (_item$label$trim = (_item$label = item.label) === null || _item$label === void 0 ? void 0 : _item$label.trim()) !== null && _item$label$trim !== void 0 ? _item$label$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: 18
	};
}
/**
* 多边汇入同一目标且仍重叠时：把标签竖排在「目标图元正左侧」（紧贴设备外轮廓），
* pill 中心 x 严格落在设备左边框之外，避免压到设备身上；纵向居中对齐到设备中心。
*/
function placeLabelsBesideTarget(targetRect, group) {
	const sorted = [...group].sort((a, b) => a.edgeId.localeCompare(b.edgeId));
	const out = /* @__PURE__ */ new Map();
	const h = 18;
	const gap = 4;
	const maxW = sorted.reduce((acc, it) => {
		var _it$label$trim, _it$label;
		return Math.max(acc, estimateEdgeLabelWidth((_it$label$trim = (_it$label = it.label) === null || _it$label === void 0 ? void 0 : _it$label.trim()) !== null && _it$label$trim !== void 0 ? _it$label$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;
}
/**
* 为多条边的连接类型标签分配互不重叠的位置。
* - 多边汇入同一目标：优先标在各自「水平总线」段中点（天然错开）
* - 仍重叠时按 pill 宽高做 bbox 分离；必要时在目标图元左侧纵向堆叠
*/
function resolveEdgeCenterLabelPositions(items, options) {
	var _options$placement2, _options$offsetPx3;
	const placement = (_options$placement2 = options === null || options === void 0 ? void 0 : options.placement) !== null && _options$placement2 !== void 0 ? _options$placement2 : "target-tail";
	const offsetPx = (_options$offsetPx3 = options === null || options === void 0 ? void 0 : options.offsetPx) !== null && _options$offsetPx3 !== void 0 ? _options$offsetPx3 : 8;
	const result = /* @__PURE__ */ new Map();
	const states = [];
	const labeled = items.filter((it) => {
		var _it$label2;
		return (_it$label2 = it.label) === null || _it$label2 === void 0 ? void 0 : _it$label2.trim();
	});
	const byTarget = /* @__PURE__ */ new Map();
	for (const it of labeled) {
		var _byTarget$get;
		const list = (_byTarget$get = byTarget.get(it.targetId)) !== null && _byTarget$get !== void 0 ? _byTarget$get : [];
		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));
		/** 同一水平段上多条边：按 x 顺序分配 along */
		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 !== null && hSeg !== void 0 ? hSeg : placement === "source-tail" ? 0 : Math.max(0, item.points.length - 2);
			let along = hSeg != null ? .5 : placement === "source-tail" ? .35 : .68;
			if (hSeg != null) {
				var _alongBySegKey$get;
				const key = horizontalSegmentKey(item.points, hSeg);
				const count = (_alongBySegKey$get = alongBySegKey.get(key)) !== null && _alongBySegKey$get !== void 0 ? _alongBySegKey$get : 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(.2, 24 / Math.max(segLen, 40));
					along = clampAlong(.5 + (count - .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(.18, 26 / Math.max(segLen, 36));
				along = clampAlong((placement === "source-tail" ? .35 : .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);
		}
	}
	/** 全局 bbox 分离（按 pill 宽高，而非中心点距离） */
	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 + .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
	});
	/**
	* 多边汇入同一目标（≥3）：直接改用「目标上方竖排」方案。
	* 经验上 ≥3 条线时即使 channelOffset 错开了线路 y，标签 pill 仍会有视觉重叠，
	* 不如规整堆在设备正上方，与连线对齐到同一根末段竖线，更易读。
	*/
	if (options === null || options === void 0 ? 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";
}
/**
* 距矩形四边锚点最近的一侧
*/
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 = 16) {
	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
		};
		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;
}
/**
* 路径首末两段长度差（压缩冗余后衡量「Z 型两边对称度」），值越小越对称。
*/
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);
}
/** 端口沿外法线走到图元外侧 clearance 处 */
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];
	}
}
/** 校验正交：连续两点必须共享 x 或 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;
}
/**
* 端口出口 stub 不能反向：从源 sourcePt 到下一拐点必须沿外法线方向；目标同理。
*/
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;
}
/**
* 候选正交路径：直 L、Z 型、外侧通道；端点已沿外法线出 clearance 距离。
* 选「不穿过源/目标矩形 + 端口方向正确」且曼哈顿距离最短的一条。
*/
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;
	/** s_out → t_out 之间的拐点序列；不含端点本身 */
	const connectors = [];
	/** 直连：先横后竖 / 先竖后横 */
	connectors.push([[tOut[0], sOut[1]]]);
	connectors.push([[sOut[0], tOut[1]]]);
	/** Z 型中点（共享中段加偏移以错开多条线） */
	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);
		if (len < bestLen || len === bestLen && turns < bestTurns || len === bestLen && turns === bestTurns && asym < bestAsym) {
			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]
	];
}
/**
* 最短正交折线：从端口锚点直接转折，无固定长度 stub。
*/
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;
}
/**
* 单段穿框时插入最小 U 型绕行（多一折：先垂/平到通道，再平/垂接到对端）。
*/
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);
		if (!(i === 0)) 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) {
	var _options$clearance, _options$obstacleRect2;
	const options = typeof stubOrOptions === "object" && stubOrOptions != null ? stubOrOptions : maybeOptions;
	const clearance = (_options$clearance = options === null || options === void 0 ? void 0 : options.clearance) !== null && _options$clearance !== void 0 ? _options$clearance : 10;
	const sourceRect = options === null || options === void 0 ? void 0 : options.sourceRect;
	const targetRect = options === null || options === void 0 ? void 0 : options.targetRect;
	if (sourceRect && targetRect) {
		var _options$channelOffse, _options$obstacleRect;
		let path = buildOrthogonalPathAvoidingRects(sourcePt, sourceSide, targetPt, targetSide, sourceRect, targetRect, clearance, (_options$channelOffse = options === null || options === void 0 ? void 0 : options.channelOffset) !== null && _options$channelOffse !== void 0 ? _options$channelOffse : 0);
		const extra = ((_options$obstacleRect = options === null || options === void 0 ? void 0 : options.obstacleRects) !== null && _options$obstacleRect !== void 0 ? _options$obstacleRect : []).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$obstacleRect2 = options === null || options === void 0 ? void 0 : options.obstacleRects) !== null && _options$obstacleRect2 !== void 0 ? _options$obstacleRect2 : targetRect ? [targetRect] : sourceRect ? [sourceRect] : [];
	if (obstacles.length === 0) return shortest;
	return wrapPathAroundRects(shortest, obstacles, clearance);
}
/**
* 折线转带圆角的 SVG path d
*/
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;
}
/**
* 把一条已存在的 path 快照（snapshot）的两端「平移对齐」到新的源/目标锚点位置，
* 中间路径完全保留。用于「手动编辑过的连线」在图元被拖动后只更新最近两端。
*
* 规则：
* - 首段（snapshot[0] → snapshot[1]）：
*   - 若是水平段（y0 === y1）：新 snapshot[0] = srcPt；新 snapshot[1] = (原 snapshot[1].x, srcPt.y)
*   - 若是竖直段（x0 === x1）：新 snapshot[0] = srcPt；新 snapshot[1] = (srcPt.x, 原 snapshot[1].y)
*   - 若是斜段（理论上 snapshot 都是正交，但兜底）：仅平移 snapshot[0]，snapshot[1] 保持不变
* - 末段（snapshot[n-2] → snapshot[n-1]）：对称处理
* - 同时对 snapshot[2] / snapshot[n-3] 做"必要的轴向跟随"：snapshot[1] → snapshot[2] 段必须保持
*   正交，因此 snapshot[2] 的相邻轴坐标需要跟随 snapshot[1] 更新（同理末端）。
*/
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)
 /** 首段水平 → 用新 y 替换 snapshot[1].y；snapshot[2] 的 x 与 snapshot[1].x 原本相等（正交垂直段），保持不变 */
	result[1] = [x1, srcPt.y];
	else if (x0 === x1 && y0 !== y1) result[1] = [srcPt.x, y1];
	else
 /** 首段退化为零长度：保留原 snapshot[1] 不变 */
	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) {
		/**
		* L 型 3 点情况：result[1] 同时受首段和末段方向约束。
		* - 若首段水平 + 末段竖直 → result[1] = (tgtPt.x, srcPt.y)
		* - 若首段竖直 + 末段水平 → result[1] = (srcPt.x, tgtPt.y)
		* 否则保留原 result[1]。
		*/
		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;
}
/**
* 在路径起点处插入 stub 转角：把首段变成「沿首段实际方向走 stub → 90° 转弯 →
* 走到原 corner」。
*
* 注意：方向取自 *path 首段实际指向*（points[0]→points[1]），而非 sourceAnchor 字段——
* 这样在 path 已经经过手动编辑、首段方向与锚点 outward 不一致时仍能正确插入。
*
* 路径点数 +2；新增 2 个内部段（stub 段 + 「被拖动段」）。被拖动段的初始另一轴
* 坐标与原首段重合（视觉直线），后续由拖动调整。
*/
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;
	if (!isHorizontal && !(x0 === x1 && y0 !== y1)) 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 sx = x0 + (Math.sign(x1 - x0) || 1) * stub;
		const stubEnd = [sx, y0];
		const midCorner = [sx, y1];
		const replacedP1 = [x1, y1];
		return [
			points[0],
			stubEnd,
			midCorner,
			replacedP1,
			...points.slice(2)
		];
	}
	const sy = y0 + (Math.sign(y1 - y0) || 1) * stub;
	const stubEnd = [x0, sy];
	const midCorner = [x1, sy];
	const replacedP1 = [x1, y1];
	return [
		points[0],
		stubEnd,
		midCorner,
		replacedP1,
		...points.slice(2)
	];
}
/**
* 对称地在路径末端插入 stub 转角。方向取自 *path 末段实际指向*。
*
* 路径点数 +2；新增 2 个内部段（「被拖动段」 + stub 段）。
*/
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;
	if (!isHorizontal && !(xA === xT && yA !== yT)) 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 sx = xT - (Math.sign(xT - xA) || 1) * stub;
		const aMod = [xA, yA];
		const midCorner = [sx, yA];
		const stubStart = [sx, yT];
		return [
			...points.slice(0, n - 2),
			aMod,
			midCorner,
			stubStart,
			points[n - 1]
		];
	}
	const sy = yT - (Math.sign(yT - yA) || 1) * 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 buildEdgeRoute(edge, rectsById, options) {
	var _edge$sourceAnchor, _edge$targetAnchor, _options$routingOverr, _options$channelOffse2;
	const srcRect = rectsById.get(edge.sourceId);
	const tgtRect = rectsById.get(edge.targetId);
	if (!srcRect || !tgtRect) return null;
	const sourceAnchor = (_edge$sourceAnchor = edge.sourceAnchor) !== null && _edge$sourceAnchor !== void 0 ? _edge$sourceAnchor : pickAutoAnchor(srcRect, tgtRect);
	const targetAnchor = (_edge$targetAnchor = edge.targetAnchor) !== null && _edge$targetAnchor !== void 0 ? _edge$targetAnchor : pickAutoAnchor(tgtRect, srcRect);
	const sourcePt = getAnchorPoint(srcRect, sourceAnchor);
	const targetPt = getAnchorPoint(tgtRect, targetAnchor);
	const routing = (_options$routingOverr = options === null || options === void 0 ? void 0 : options.routingOverride) !== null && _options$routingOverr !== void 0 ? _options$routingOverr : edge.routing;
	/** 手动编辑过：snapshot 优先，仅对齐两端 */
	if ((routing === null || routing === void 0 ? void 0 : routing.pathSnapshot) && routing.pathSnapshot.length >= 2) {
		const points = simplifyCollinearPath(alignSnapshotToAnchors(routing.pathSnapshot, sourcePt, targetPt));
		return {
			edgeId: edge.id,
			points,
			pathD: pathFromPoints(points)
		};
	}
	const points = simplifyCollinearPath(routeOrthogonal(sourcePt, sourceAnchor, targetPt, targetAnchor, {
		sourceRect: srcRect,
		targetRect: tgtRect,
		channelOffset: (_options$channelOffse2 = options === null || options === void 0 ? void 0 : options.channelOffset) !== null && _options$channelOffse2 !== void 0 ? _options$channelOffse2 : 0
	}));
	return {
		edgeId: edge.id,
		points,
		pathD: pathFromPoints(points)
	};
}
/**
* 去掉路径中"三点共线"的冗余中间点（比如 routeOrthogonal 里 srcPt → sOut →
* 中段折点都在同一 y 上时，sOut 是冗余的）。
*
* 保留零长度段：用户拖动 first/last 段创建 stub 时，stubEnd 与 midCorner 短暂
* 重合（零长度），不能被合并掉，否则会立刻丢失新插入的转角结构。
*/
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];
		/** 零长度段（prev===curr 或 curr===next）一律保留：用户主动创建的结构 */
		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;
}
/**
* 按 edge 顺序分配交替偏移，让落到同一通道的多条线错开。
* 序列：0, +step, -step, +2*step, -2*step, ...
*/
function getEdgeChannelOffset(index, step = 12) {
	if (index <= 0) return 0;
	const k = Math.ceil(index / 2);
	return index % 2 === 1 ? k * step : -k * step;
}
//#endregion
exports.ANCHORS = ANCHORS;
exports.applySourceStub = applySourceStub;
exports.applyTargetStub = applyTargetStub;
exports.buildEdgeRoute = buildEdgeRoute;
exports.getAnchorPoint = getAnchorPoint;
exports.getEdgeChannelOffset = getEdgeChannelOffset;
exports.getItemPixelRect = getItemPixelRect;
exports.getPortLabelPosition = getPortLabelPosition;
exports.getPortMarkerDisplayPoint = getPortMarkerDisplayPoint;
exports.pathFromPoints = pathFromPoints;
exports.pickAutoAnchor = pickAutoAnchor;
exports.resolveDrawingTargetAtPoint = resolveDrawingTargetAtPoint;
exports.resolveEdgeCenterLabelPositions = resolveEdgeCenterLabelPositions;
exports.routeOrthogonal = routeOrthogonal;
exports.simplifyCollinearPath = simplifyCollinearPath;
