@pisell/materials 1.8.54 → 1.8.56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/lowcode/assets-daily.json +11 -11
- package/build/lowcode/assets-dev.json +2 -2
- package/build/lowcode/assets-prod.json +11 -11
- package/build/lowcode/async/view.js +1 -1
- package/build/lowcode/index.js +1 -1
- package/build/lowcode/meta.js +3 -3
- package/build/lowcode/preview.js +1 -1
- package/build/lowcode/render/default/async/view.js +1 -1
- package/build/lowcode/render/default/view.css +1 -1
- package/build/lowcode/render/default/view.js +37 -37
- package/build/lowcode/view.css +1 -1
- package/build/lowcode/view.js +37 -37
- package/es/components/buttonGroupPreview/index.js +8 -4
- package/es/components/pisellFloorMapLayout/PisellFloorMapLayout.js +207 -49
- package/es/components/pisellFloorMapLayout/PisellFloorMapLayout.less +33 -2
- package/es/components/pisellFloorMapLayout/appearance/floorMapAppearance.js +50 -2
- package/es/components/pisellFloorMapLayout/components/EdgeLayer.js +252 -0
- package/es/components/pisellFloorMapLayout/components/EdgeLayer.less +11 -0
- package/es/components/pisellFloorMapLayout/components/EditableItemLayer.js +179 -29
- package/es/components/pisellFloorMapLayout/components/EditableItemLayer.less +0 -28
- package/es/components/pisellFloorMapLayout/components/FloorMapEdgeEditPanel.js +131 -0
- package/es/components/pisellFloorMapLayout/components/FloorMapEdgeEditPanel.less +65 -0
- package/es/components/pisellFloorMapLayout/components/FloorMapEditPanel.js +5 -4
- package/es/components/pisellFloorMapLayout/components/FloorMapEditPanel.less +12 -1
- package/es/components/pisellFloorMapLayout/components/FloorMapEditQuickActions.js +8 -1
- package/es/components/pisellFloorMapLayout/components/FloorMapOverview.js +16 -8
- package/es/components/pisellFloorMapLayout/components/FloorMapOverview.less +69 -17
- package/es/components/pisellFloorMapLayout/components/FloorMapToolbar.js +290 -147
- package/es/components/pisellFloorMapLayout/components/FloorMapToolbar.less +210 -11
- package/es/components/pisellFloorMapLayout/components/FloorMapToolbarViewAllModal.js +81 -0
- package/es/components/pisellFloorMapLayout/components/FloorMapZoneElement.d.ts +8 -0
- package/es/components/pisellFloorMapLayout/components/FloorMapZoneElement.js +21 -0
- package/es/components/pisellFloorMapLayout/components/FloorMapZoneElement.less +74 -0
- package/es/components/pisellFloorMapLayout/components/ItemLayer.less +3 -1
- package/es/components/pisellFloorMapLayout/components/NodePortMarkers.js +70 -0
- package/es/components/pisellFloorMapLayout/components/NodePortMarkers.less +33 -0
- package/es/components/pisellFloorMapLayout/components/ViewControls.js +13 -5
- package/es/components/pisellFloorMapLayout/components/ViewControlsWithZoom.js +2 -0
- package/es/components/pisellFloorMapLayout/context/mergeFloorMapLayoutContext.js +2 -1
- package/es/components/pisellFloorMapLayout/floorMapLayoutConstants.js +1 -1
- package/es/components/pisellFloorMapLayout/hooks/useFloorMapEdgeEditing.js +340 -0
- package/es/components/pisellFloorMapLayout/hooks/useFloorMapEdgeViewModel.js +102 -0
- package/es/components/pisellFloorMapLayout/hooks/useFloorMapEditState.js +244 -84
- package/es/components/pisellFloorMapLayout/hooks/useFloorMapTransformMetrics.js +54 -0
- package/es/components/pisellFloorMapLayout/hooks/useShopFloorPlanSubscription.js +7 -4
- package/es/components/pisellFloorMapLayout/index.d.ts +5 -1
- package/es/components/pisellFloorMapLayout/locales-ja.js +14 -2
- package/es/components/pisellFloorMapLayout/locales-pt.js +14 -2
- package/es/components/pisellFloorMapLayout/locales.js +161 -8
- package/es/components/pisellFloorMapLayout/types.d.ts +86 -4
- package/es/components/pisellFloorMapLayout/types.js +5 -1
- package/es/components/pisellFloorMapLayout/utils/alignSnap.js +170 -0
- package/es/components/pisellFloorMapLayout/utils/batchScenePlacementLayout.js +86 -0
- package/es/components/pisellFloorMapLayout/utils/edgeRouting.js +942 -0
- package/es/components/pisellFloorMapLayout/utils/floorMapCanvasDisplay.d.ts +7 -0
- package/es/components/pisellFloorMapLayout/utils/floorMapCanvasStage.js +23 -1
- package/es/components/pisellFloorMapLayout/utils/floorMapConfigMerge.js +11 -2
- package/es/components/pisellFloorMapLayout/utils/floorMapElementKindUtils.js +20 -3
- package/es/components/pisellFloorMapLayout/utils/floorMapFallbackRender.js +11 -5
- package/es/components/pisellFloorMapLayout/utils/floorMapOverviewLayout.js +5 -3
- package/es/components/pisellFloorMapLayout/utils/floorMapResourcePickerDisplay.d.ts +20 -0
- package/es/components/pisellFloorMapLayout/utils/floorMapResourcePickerDisplay.js +62 -0
- package/es/components/pisellFloorMapLayout/utils/floorMapResourcePickerTypes.d.ts +83 -0
- package/es/components/pisellFloorMapLayout/utils/floorMapResourcePickerTypes.js +29 -0
- package/es/components/pisellFloorMapLayout/utils/floorMapToolbarPalette.js +51 -0
- package/es/components/productCard/cartSkuCard/components/BookingItem/index.js +14 -0
- package/es/components/productCard/cartSkuCard/components/BookingItem/index.less +32 -0
- package/es/components/productCard/cartSkuCard/components/resources/index.less +4 -0
- package/es/components/productCard/cartSkuCard/components/timeRange/index.js +1 -2
- package/es/components/productCard/cartSkuCard/index.js +17 -11
- package/es/components/productCard/lineItem/BookingLineItem.js +17 -6
- package/es/components/productCard/lineItem/components/Holders/index.js +51 -0
- package/es/components/productCard/lineItem/components/Holders/index.less +39 -0
- package/es/components/productCard/lineItem/components/Resources/index.js +45 -0
- package/es/components/productCard/lineItem/components/TimeRange/index.js +64 -0
- package/es/components/productCard/lineItem/index.js +5 -2
- package/es/components/productCard/lineItem/index.less +21 -10
- package/es/components/productCard/types.d.ts +1 -0
- package/es/index.d.ts +6 -2
- package/es/index.js +6 -2
- package/es/pisell-materials.tw.css +3 -0
- package/lib/components/buttonGroupPreview/index.js +8 -4
- package/lib/components/pisellFloorMapLayout/PisellFloorMapLayout.js +206 -48
- package/lib/components/pisellFloorMapLayout/PisellFloorMapLayout.less +33 -2
- package/lib/components/pisellFloorMapLayout/appearance/floorMapAppearance.js +50 -2
- package/lib/components/pisellFloorMapLayout/components/EdgeLayer.js +255 -0
- package/lib/components/pisellFloorMapLayout/components/EdgeLayer.less +11 -0
- package/lib/components/pisellFloorMapLayout/components/EditableItemLayer.js +179 -29
- package/lib/components/pisellFloorMapLayout/components/EditableItemLayer.less +0 -28
- package/lib/components/pisellFloorMapLayout/components/FloorMapEdgeEditPanel.js +134 -0
- package/lib/components/pisellFloorMapLayout/components/FloorMapEdgeEditPanel.less +65 -0
- package/lib/components/pisellFloorMapLayout/components/FloorMapEditPanel.js +4 -3
- package/lib/components/pisellFloorMapLayout/components/FloorMapEditPanel.less +12 -1
- package/lib/components/pisellFloorMapLayout/components/FloorMapEditQuickActions.js +8 -1
- package/lib/components/pisellFloorMapLayout/components/FloorMapOverview.js +16 -8
- package/lib/components/pisellFloorMapLayout/components/FloorMapOverview.less +69 -17
- package/lib/components/pisellFloorMapLayout/components/FloorMapToolbar.js +287 -144
- package/lib/components/pisellFloorMapLayout/components/FloorMapToolbar.less +210 -11
- package/lib/components/pisellFloorMapLayout/components/FloorMapToolbarViewAllModal.js +84 -0
- package/lib/components/pisellFloorMapLayout/components/FloorMapZoneElement.d.ts +8 -0
- package/lib/components/pisellFloorMapLayout/components/FloorMapZoneElement.js +23 -0
- package/lib/components/pisellFloorMapLayout/components/FloorMapZoneElement.less +74 -0
- package/lib/components/pisellFloorMapLayout/components/ItemLayer.less +3 -1
- package/lib/components/pisellFloorMapLayout/components/NodePortMarkers.js +73 -0
- package/lib/components/pisellFloorMapLayout/components/NodePortMarkers.less +33 -0
- package/lib/components/pisellFloorMapLayout/components/ViewControls.js +12 -4
- package/lib/components/pisellFloorMapLayout/components/ViewControlsWithZoom.js +2 -0
- package/lib/components/pisellFloorMapLayout/context/mergeFloorMapLayoutContext.js +2 -1
- package/lib/components/pisellFloorMapLayout/floorMapLayoutConstants.js +1 -1
- package/lib/components/pisellFloorMapLayout/hooks/useFloorMapEdgeEditing.js +341 -0
- package/lib/components/pisellFloorMapLayout/hooks/useFloorMapEdgeViewModel.js +103 -0
- package/lib/components/pisellFloorMapLayout/hooks/useFloorMapEditState.js +242 -82
- package/lib/components/pisellFloorMapLayout/hooks/useFloorMapTransformMetrics.js +54 -0
- package/lib/components/pisellFloorMapLayout/hooks/useShopFloorPlanSubscription.js +7 -4
- package/lib/components/pisellFloorMapLayout/index.d.ts +5 -1
- package/lib/components/pisellFloorMapLayout/locales-ja.js +14 -2
- package/lib/components/pisellFloorMapLayout/locales-pt.js +14 -2
- package/lib/components/pisellFloorMapLayout/locales.js +161 -8
- package/lib/components/pisellFloorMapLayout/types.d.ts +86 -4
- package/lib/components/pisellFloorMapLayout/types.js +5 -0
- package/lib/components/pisellFloorMapLayout/utils/alignSnap.js +170 -0
- package/lib/components/pisellFloorMapLayout/utils/batchScenePlacementLayout.js +87 -0
- package/lib/components/pisellFloorMapLayout/utils/edgeRouting.js +956 -0
- package/lib/components/pisellFloorMapLayout/utils/floorMapCanvasDisplay.d.ts +7 -0
- package/lib/components/pisellFloorMapLayout/utils/floorMapCanvasStage.js +23 -0
- package/lib/components/pisellFloorMapLayout/utils/floorMapConfigMerge.js +11 -2
- package/lib/components/pisellFloorMapLayout/utils/floorMapElementKindUtils.js +21 -2
- package/lib/components/pisellFloorMapLayout/utils/floorMapFallbackRender.js +11 -5
- package/lib/components/pisellFloorMapLayout/utils/floorMapOverviewLayout.js +5 -3
- package/lib/components/pisellFloorMapLayout/utils/floorMapResourcePickerDisplay.d.ts +20 -0
- package/lib/components/pisellFloorMapLayout/utils/floorMapResourcePickerDisplay.js +64 -0
- package/lib/components/pisellFloorMapLayout/utils/floorMapResourcePickerTypes.d.ts +83 -0
- package/lib/components/pisellFloorMapLayout/utils/floorMapResourcePickerTypes.js +29 -0
- package/lib/components/pisellFloorMapLayout/utils/floorMapToolbarPalette.js +54 -0
- package/lib/components/productCard/cartSkuCard/components/BookingItem/index.js +17 -0
- package/lib/components/productCard/cartSkuCard/components/BookingItem/index.less +32 -0
- package/lib/components/productCard/cartSkuCard/components/resources/index.less +4 -0
- package/lib/components/productCard/cartSkuCard/components/timeRange/index.js +1 -2
- package/lib/components/productCard/cartSkuCard/index.js +17 -11
- package/lib/components/productCard/lineItem/BookingLineItem.js +17 -6
- package/lib/components/productCard/lineItem/components/Holders/index.js +53 -0
- package/lib/components/productCard/lineItem/components/Holders/index.less +39 -0
- package/lib/components/productCard/lineItem/components/Resources/index.js +47 -0
- package/lib/components/productCard/lineItem/components/TimeRange/index.js +67 -0
- package/lib/components/productCard/lineItem/index.js +5 -2
- package/lib/components/productCard/lineItem/index.less +21 -10
- package/lib/components/productCard/types.d.ts +1 -0
- package/lib/index.d.ts +6 -2
- package/lib/index.js +11 -0
- package/lib/pisell-materials.tw.css +3 -0
- package/package.json +6 -3
|
@@ -0,0 +1,956 @@
|
|
|
1
|
+
const require_layoutMeasurements = require("./layoutMeasurements.js");
|
|
2
|
+
//#region src/components/pisellFloorMapLayout/utils/edgeRouting.ts
|
|
3
|
+
const ANCHORS = [
|
|
4
|
+
"top",
|
|
5
|
+
"right",
|
|
6
|
+
"bottom",
|
|
7
|
+
"left"
|
|
8
|
+
];
|
|
9
|
+
/**
|
|
10
|
+
* 将图元逻辑坐标转为内容区像素矩形(与 EditableItemLayer 一致)
|
|
11
|
+
*/
|
|
12
|
+
function getItemPixelRect(item, options) {
|
|
13
|
+
const { cellSize, itemUnit, itemGap = 0, override } = options;
|
|
14
|
+
const g = require_layoutMeasurements.gapToPx(itemGap);
|
|
15
|
+
let left;
|
|
16
|
+
let top;
|
|
17
|
+
let width;
|
|
18
|
+
let height;
|
|
19
|
+
if (itemUnit === "pixel") {
|
|
20
|
+
var _item$width, _item$height;
|
|
21
|
+
width = (_item$width = item.width) !== null && _item$width !== void 0 ? _item$width : cellSize;
|
|
22
|
+
height = (_item$height = item.height) !== null && _item$height !== void 0 ? _item$height : cellSize;
|
|
23
|
+
left = item.x;
|
|
24
|
+
top = item.y;
|
|
25
|
+
} else {
|
|
26
|
+
var _item$width2, _item$height2;
|
|
27
|
+
const w = (_item$width2 = item.width) !== null && _item$width2 !== void 0 ? _item$width2 : 1;
|
|
28
|
+
const h = (_item$height2 = item.height) !== null && _item$height2 !== void 0 ? _item$height2 : 1;
|
|
29
|
+
left = g > 0 ? item.x * (cellSize + g) + g : item.x * cellSize;
|
|
30
|
+
top = g > 0 ? item.y * (cellSize + g) + g : item.y * cellSize;
|
|
31
|
+
width = g > 0 ? w * cellSize + (w - 1) * g : w * cellSize;
|
|
32
|
+
height = g > 0 ? h * cellSize + (h - 1) * g : h * cellSize;
|
|
33
|
+
}
|
|
34
|
+
if (override) {
|
|
35
|
+
if (override.left != null) left = override.left;
|
|
36
|
+
if (override.top != null) top = override.top;
|
|
37
|
+
if (override.width != null) width = override.width;
|
|
38
|
+
if (override.height != null) height = override.height;
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
left,
|
|
42
|
+
top,
|
|
43
|
+
width,
|
|
44
|
+
height
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function rectCenter(rect) {
|
|
48
|
+
return {
|
|
49
|
+
x: rect.left + rect.width / 2,
|
|
50
|
+
y: rect.top + rect.height / 2
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 锚点在矩形边框上的坐标
|
|
55
|
+
*/
|
|
56
|
+
function getAnchorPoint(rect, anchor) {
|
|
57
|
+
const cx = rect.left + rect.width / 2;
|
|
58
|
+
const cy = rect.top + rect.height / 2;
|
|
59
|
+
switch (anchor) {
|
|
60
|
+
case "top": return {
|
|
61
|
+
x: cx,
|
|
62
|
+
y: rect.top
|
|
63
|
+
};
|
|
64
|
+
case "right": return {
|
|
65
|
+
x: rect.left + rect.width,
|
|
66
|
+
y: cy
|
|
67
|
+
};
|
|
68
|
+
case "bottom": return {
|
|
69
|
+
x: cx,
|
|
70
|
+
y: rect.top + rect.height
|
|
71
|
+
};
|
|
72
|
+
case "left": return {
|
|
73
|
+
x: rect.left,
|
|
74
|
+
y: cy
|
|
75
|
+
};
|
|
76
|
+
default: return {
|
|
77
|
+
x: cx,
|
|
78
|
+
y: cy
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 编辑态锚点圆点 UI 位置(相对图元局部坐标),沿外法线外移,连线几何仍用 {@link getAnchorPoint}。
|
|
84
|
+
*/
|
|
85
|
+
function getPortMarkerDisplayPoint(rect, anchor, outset = 16) {
|
|
86
|
+
const base = getAnchorPoint(rect, anchor);
|
|
87
|
+
const v = sideVector(anchor);
|
|
88
|
+
return {
|
|
89
|
+
x: base.x + v.dx * outset,
|
|
90
|
+
y: base.y + v.dy * outset
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function getPortLabelPosition(rect, anchor, options) {
|
|
94
|
+
var _options$labelHeight, _options$gap;
|
|
95
|
+
const labelHeight = (_options$labelHeight = options === null || options === void 0 ? void 0 : options.labelHeight) !== null && _options$labelHeight !== void 0 ? _options$labelHeight : 18;
|
|
96
|
+
const gap = (_options$gap = options === null || options === void 0 ? void 0 : options.gap) !== null && _options$gap !== void 0 ? _options$gap : 6;
|
|
97
|
+
const base = getAnchorPoint(rect, anchor);
|
|
98
|
+
const v = sideVector(anchor);
|
|
99
|
+
const offset = labelHeight / 2 + gap;
|
|
100
|
+
return {
|
|
101
|
+
x: base.x + v.dx * offset,
|
|
102
|
+
y: base.y + v.dy * offset
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/** 与 EdgeLayer PortLabel 估算宽度一致 */
|
|
106
|
+
function estimateEdgeLabelWidth(text) {
|
|
107
|
+
return Math.max(32, text.length * 6.5 + 12);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* 在折线指定段上计算标签中心(水平段:标签在线上方;竖直段:标签在线左侧)。
|
|
111
|
+
*/
|
|
112
|
+
function getEdgeCenterLabelPositionOnSegment(points, segIndex, options) {
|
|
113
|
+
var _options$offsetPx, _options$perpOffsetPx, _options$along;
|
|
114
|
+
if (points.length < 2 || segIndex < 0 || segIndex >= points.length - 1) return null;
|
|
115
|
+
const offset = (_options$offsetPx = options === null || options === void 0 ? void 0 : options.offsetPx) !== null && _options$offsetPx !== void 0 ? _options$offsetPx : 8;
|
|
116
|
+
const perp = (_options$perpOffsetPx = options === null || options === void 0 ? void 0 : options.perpOffsetPx) !== null && _options$perpOffsetPx !== void 0 ? _options$perpOffsetPx : 0;
|
|
117
|
+
const [x1, y1] = points[segIndex];
|
|
118
|
+
const [x2, y2] = points[segIndex + 1];
|
|
119
|
+
const along = (_options$along = options === null || options === void 0 ? void 0 : options.along) !== null && _options$along !== void 0 ? _options$along : .5;
|
|
120
|
+
const x = x1 + (x2 - x1) * along;
|
|
121
|
+
const y = y1 + (y2 - y1) * along;
|
|
122
|
+
if (y1 === y2 && x1 !== x2) return {
|
|
123
|
+
x,
|
|
124
|
+
y: y - offset - perp
|
|
125
|
+
};
|
|
126
|
+
if (x1 === x2 && y1 !== y2) return {
|
|
127
|
+
x: x - offset - perp,
|
|
128
|
+
y
|
|
129
|
+
};
|
|
130
|
+
return {
|
|
131
|
+
x,
|
|
132
|
+
y: y - offset - perp
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/** 拓扑水平总线段最短可用长度(px) */
|
|
136
|
+
const LABEL_HORIZONTAL_MIN_LEN = 20;
|
|
137
|
+
function clampAlong(value) {
|
|
138
|
+
return Math.max(.12, Math.min(.9, value));
|
|
139
|
+
}
|
|
140
|
+
/** 取路径中最长水平段(拓扑总线段),供多边汇入同一目标时错开标签 */
|
|
141
|
+
function pickLongestHorizontalSegmentIndex(points, minLen = LABEL_HORIZONTAL_MIN_LEN) {
|
|
142
|
+
let bestIndex = null;
|
|
143
|
+
let bestLen = 0;
|
|
144
|
+
for (let i = 0; i < points.length - 1; i += 1) {
|
|
145
|
+
const [x1, y1] = points[i];
|
|
146
|
+
const [x2, y2] = points[i + 1];
|
|
147
|
+
if (y1 !== y2 || x1 === x2) continue;
|
|
148
|
+
const len = Math.abs(x2 - x1);
|
|
149
|
+
if (len >= minLen && len > bestLen) {
|
|
150
|
+
bestLen = len;
|
|
151
|
+
bestIndex = i;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return bestIndex;
|
|
155
|
+
}
|
|
156
|
+
function horizontalSegmentKey(points, segIndex) {
|
|
157
|
+
const [x1, y1] = points[segIndex];
|
|
158
|
+
const [x2, y2] = points[segIndex + 1];
|
|
159
|
+
return `h:${Math.round(y1)}:${Math.round(Math.min(x1, x2))}-${Math.round(Math.max(x1, x2))}`;
|
|
160
|
+
}
|
|
161
|
+
function labelBoxesOverlap(a, b) {
|
|
162
|
+
const margin = 4;
|
|
163
|
+
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;
|
|
164
|
+
}
|
|
165
|
+
function buildPlacementState(item, segIndex, along, perpOffsetPx, offsetPx) {
|
|
166
|
+
var _item$label$trim, _item$label;
|
|
167
|
+
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 : "";
|
|
168
|
+
const pos = getEdgeCenterLabelPositionOnSegment(item.points, segIndex, {
|
|
169
|
+
along,
|
|
170
|
+
offsetPx,
|
|
171
|
+
perpOffsetPx
|
|
172
|
+
});
|
|
173
|
+
if (!pos) return null;
|
|
174
|
+
return {
|
|
175
|
+
edgeId: item.edgeId,
|
|
176
|
+
targetId: item.targetId,
|
|
177
|
+
label,
|
|
178
|
+
points: item.points,
|
|
179
|
+
segIndex,
|
|
180
|
+
along,
|
|
181
|
+
perpOffsetPx,
|
|
182
|
+
x: pos.x,
|
|
183
|
+
y: pos.y,
|
|
184
|
+
w: estimateEdgeLabelWidth(label),
|
|
185
|
+
h: 18
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* 多边汇入同一目标且仍重叠时:把标签竖排在「目标图元正左侧」(紧贴设备外轮廓),
|
|
190
|
+
* pill 中心 x 严格落在设备左边框之外,避免压到设备身上;纵向居中对齐到设备中心。
|
|
191
|
+
*/
|
|
192
|
+
function placeLabelsBesideTarget(targetRect, group) {
|
|
193
|
+
const sorted = [...group].sort((a, b) => a.edgeId.localeCompare(b.edgeId));
|
|
194
|
+
const out = /* @__PURE__ */ new Map();
|
|
195
|
+
const h = 18;
|
|
196
|
+
const gap = 4;
|
|
197
|
+
const maxW = sorted.reduce((acc, it) => {
|
|
198
|
+
var _it$label$trim, _it$label;
|
|
199
|
+
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 : ""));
|
|
200
|
+
}, 32);
|
|
201
|
+
const cx = targetRect.left - 6 - maxW / 2;
|
|
202
|
+
const stackH = sorted.length * (h + gap) - gap;
|
|
203
|
+
const startY = targetRect.top + targetRect.height / 2 - stackH / 2 + h / 2;
|
|
204
|
+
for (let i = 0; i < sorted.length; i += 1) out.set(sorted[i].edgeId, {
|
|
205
|
+
x: cx,
|
|
206
|
+
y: startY + i * (h + gap)
|
|
207
|
+
});
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* 为多条边的连接类型标签分配互不重叠的位置。
|
|
212
|
+
* - 多边汇入同一目标:优先标在各自「水平总线」段中点(天然错开)
|
|
213
|
+
* - 仍重叠时按 pill 宽高做 bbox 分离;必要时在目标图元左侧纵向堆叠
|
|
214
|
+
*/
|
|
215
|
+
function resolveEdgeCenterLabelPositions(items, options) {
|
|
216
|
+
var _options$placement2, _options$offsetPx3;
|
|
217
|
+
const placement = (_options$placement2 = options === null || options === void 0 ? void 0 : options.placement) !== null && _options$placement2 !== void 0 ? _options$placement2 : "target-tail";
|
|
218
|
+
const offsetPx = (_options$offsetPx3 = options === null || options === void 0 ? void 0 : options.offsetPx) !== null && _options$offsetPx3 !== void 0 ? _options$offsetPx3 : 8;
|
|
219
|
+
const result = /* @__PURE__ */ new Map();
|
|
220
|
+
const states = [];
|
|
221
|
+
const labeled = items.filter((it) => {
|
|
222
|
+
var _it$label2;
|
|
223
|
+
return (_it$label2 = it.label) === null || _it$label2 === void 0 ? void 0 : _it$label2.trim();
|
|
224
|
+
});
|
|
225
|
+
const byTarget = /* @__PURE__ */ new Map();
|
|
226
|
+
for (const it of labeled) {
|
|
227
|
+
var _byTarget$get;
|
|
228
|
+
const list = (_byTarget$get = byTarget.get(it.targetId)) !== null && _byTarget$get !== void 0 ? _byTarget$get : [];
|
|
229
|
+
list.push(it);
|
|
230
|
+
byTarget.set(it.targetId, list);
|
|
231
|
+
}
|
|
232
|
+
for (const group of byTarget.values()) {
|
|
233
|
+
const sorted = [...group].sort((a, b) => a.edgeId.localeCompare(b.edgeId));
|
|
234
|
+
/** 同一水平段上多条边:按 x 顺序分配 along */
|
|
235
|
+
const alongBySegKey = /* @__PURE__ */ new Map();
|
|
236
|
+
for (let i = 0; i < sorted.length; i += 1) {
|
|
237
|
+
const item = sorted[i];
|
|
238
|
+
const hSeg = pickLongestHorizontalSegmentIndex(item.points);
|
|
239
|
+
const segIndex = hSeg !== null && hSeg !== void 0 ? hSeg : placement === "source-tail" ? 0 : Math.max(0, item.points.length - 2);
|
|
240
|
+
let along = hSeg != null ? .5 : placement === "source-tail" ? .35 : .68;
|
|
241
|
+
if (hSeg != null) {
|
|
242
|
+
var _alongBySegKey$get;
|
|
243
|
+
const key = horizontalSegmentKey(item.points, hSeg);
|
|
244
|
+
const count = (_alongBySegKey$get = alongBySegKey.get(key)) !== null && _alongBySegKey$get !== void 0 ? _alongBySegKey$get : 0;
|
|
245
|
+
alongBySegKey.set(key, count + 1);
|
|
246
|
+
if (count > 0) {
|
|
247
|
+
const [x1] = item.points[hSeg];
|
|
248
|
+
const [x2] = item.points[hSeg + 1];
|
|
249
|
+
const segLen = Math.abs(x2 - x1);
|
|
250
|
+
const step = Math.min(.2, 24 / Math.max(segLen, 40));
|
|
251
|
+
along = clampAlong(.5 + (count - .5) * step);
|
|
252
|
+
}
|
|
253
|
+
} else if (sorted.length > 1) {
|
|
254
|
+
const segLen = Math.hypot(item.points[segIndex + 1][0] - item.points[segIndex][0], item.points[segIndex + 1][1] - item.points[segIndex][1]);
|
|
255
|
+
const step = Math.min(.18, 26 / Math.max(segLen, 36));
|
|
256
|
+
along = clampAlong((placement === "source-tail" ? .35 : .68) + (i - (sorted.length - 1) / 2) * step);
|
|
257
|
+
}
|
|
258
|
+
const perpLane = Math.floor((i + 1) / 2) * 16;
|
|
259
|
+
const perpOffsetPx = sorted.length > 1 ? (i % 2 === 0 ? -1 : 1) * perpLane : 0;
|
|
260
|
+
const state = buildPlacementState(item, segIndex, along, perpOffsetPx, offsetPx);
|
|
261
|
+
if (state) states.push(state);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/** 全局 bbox 分离(按 pill 宽高,而非中心点距离) */
|
|
265
|
+
for (let iter = 0; iter < 12; iter += 1) {
|
|
266
|
+
let adjusted = false;
|
|
267
|
+
for (let i = 0; i < states.length; i += 1) for (let j = i + 1; j < states.length; j += 1) {
|
|
268
|
+
const a = states[i];
|
|
269
|
+
const b = states[j];
|
|
270
|
+
if (!labelBoxesOverlap(a, b)) continue;
|
|
271
|
+
const mover = states[j];
|
|
272
|
+
mover.along = clampAlong(mover.along + .08);
|
|
273
|
+
mover.perpOffsetPx += (j % 2 === 0 ? -1 : 1) * 12;
|
|
274
|
+
const pos = getEdgeCenterLabelPositionOnSegment(mover.points, mover.segIndex, {
|
|
275
|
+
along: mover.along,
|
|
276
|
+
offsetPx,
|
|
277
|
+
perpOffsetPx: mover.perpOffsetPx
|
|
278
|
+
});
|
|
279
|
+
if (!pos) continue;
|
|
280
|
+
mover.x = pos.x;
|
|
281
|
+
mover.y = pos.y;
|
|
282
|
+
adjusted = true;
|
|
283
|
+
}
|
|
284
|
+
if (!adjusted) break;
|
|
285
|
+
}
|
|
286
|
+
for (const state of states) result.set(state.edgeId, {
|
|
287
|
+
x: state.x,
|
|
288
|
+
y: state.y
|
|
289
|
+
});
|
|
290
|
+
/**
|
|
291
|
+
* 多边汇入同一目标(≥3):直接改用「目标上方竖排」方案。
|
|
292
|
+
* 经验上 ≥3 条线时即使 channelOffset 错开了线路 y,标签 pill 仍会有视觉重叠,
|
|
293
|
+
* 不如规整堆在设备正上方,与连线对齐到同一根末段竖线,更易读。
|
|
294
|
+
*/
|
|
295
|
+
if (options === null || options === void 0 ? void 0 : options.rectsById) for (const group of byTarget.values()) {
|
|
296
|
+
if (group.length < 3) continue;
|
|
297
|
+
const rect = options.rectsById.get(group[0].targetId);
|
|
298
|
+
if (!rect) continue;
|
|
299
|
+
const stacked = placeLabelsBesideTarget(rect, group);
|
|
300
|
+
for (const [id, pos] of stacked) result.set(id, pos);
|
|
301
|
+
}
|
|
302
|
+
return result;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* 未指定锚点时,选朝向对方最近的一侧
|
|
306
|
+
*/
|
|
307
|
+
function pickAutoAnchor(from, to) {
|
|
308
|
+
const a = rectCenter(from);
|
|
309
|
+
const b = rectCenter(to);
|
|
310
|
+
const dx = b.x - a.x;
|
|
311
|
+
const dy = b.y - a.y;
|
|
312
|
+
if (Math.abs(dx) >= Math.abs(dy)) return dx >= 0 ? "right" : "left";
|
|
313
|
+
return dy >= 0 ? "bottom" : "top";
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* 距矩形四边锚点最近的一侧
|
|
317
|
+
*/
|
|
318
|
+
function pickNearestAnchorOnRect(pt, rect) {
|
|
319
|
+
let best = "top";
|
|
320
|
+
let minDist = Infinity;
|
|
321
|
+
for (const anchor of ANCHORS) {
|
|
322
|
+
const ap = getAnchorPoint(rect, anchor);
|
|
323
|
+
const d = Math.hypot(pt.x - ap.x, pt.y - ap.y);
|
|
324
|
+
if (d < minDist) {
|
|
325
|
+
minDist = d;
|
|
326
|
+
best = anchor;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return best;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* 拉线过程中解析目标:优先吸附到端口圆点,否则落在图元内时取最近端口。
|
|
333
|
+
*/
|
|
334
|
+
function resolveDrawingTargetAtPoint(pt, instanceIds, rectsById, excludeInstanceId, snapThreshold = 16) {
|
|
335
|
+
let bestSnap = null;
|
|
336
|
+
let bestSnapDist = snapThreshold;
|
|
337
|
+
for (const id of instanceIds) {
|
|
338
|
+
if (id === excludeInstanceId) continue;
|
|
339
|
+
const rect = rectsById.get(id);
|
|
340
|
+
if (!rect) continue;
|
|
341
|
+
for (const anchor of ANCHORS) {
|
|
342
|
+
const ap = getAnchorPoint(rect, anchor);
|
|
343
|
+
const d = Math.hypot(pt.x - ap.x, pt.y - ap.y);
|
|
344
|
+
if (d <= bestSnapDist) {
|
|
345
|
+
bestSnapDist = d;
|
|
346
|
+
bestSnap = {
|
|
347
|
+
instanceId: id,
|
|
348
|
+
anchor
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (bestSnap) return bestSnap;
|
|
354
|
+
for (const id of instanceIds) {
|
|
355
|
+
if (id === excludeInstanceId) continue;
|
|
356
|
+
const rect = rectsById.get(id);
|
|
357
|
+
if (!rect) continue;
|
|
358
|
+
const right = rect.left + rect.width;
|
|
359
|
+
const bottom = rect.top + rect.height;
|
|
360
|
+
if (pt.x >= rect.left && pt.x <= right && pt.y >= rect.top && pt.y <= bottom) return {
|
|
361
|
+
instanceId: id,
|
|
362
|
+
anchor: pickNearestAnchorOnRect(pt, rect)
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
function sideVector(anchor) {
|
|
368
|
+
switch (anchor) {
|
|
369
|
+
case "top": return {
|
|
370
|
+
dx: 0,
|
|
371
|
+
dy: -1
|
|
372
|
+
};
|
|
373
|
+
case "bottom": return {
|
|
374
|
+
dx: 0,
|
|
375
|
+
dy: 1
|
|
376
|
+
};
|
|
377
|
+
case "left": return {
|
|
378
|
+
dx: -1,
|
|
379
|
+
dy: 0
|
|
380
|
+
};
|
|
381
|
+
default: return {
|
|
382
|
+
dx: 1,
|
|
383
|
+
dy: 0
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
/** 线段是否与矩形内部区域相交(水平/竖直在边框上不算) */
|
|
388
|
+
function horizontalSegmentCrossesRect(y, x1, x2, rect) {
|
|
389
|
+
const minX = Math.min(x1, x2);
|
|
390
|
+
const maxX = Math.max(x1, x2);
|
|
391
|
+
const bottom = rect.top + rect.height;
|
|
392
|
+
const right = rect.left + rect.width;
|
|
393
|
+
if (y <= rect.top || y >= bottom) return false;
|
|
394
|
+
return maxX > rect.left && minX < right;
|
|
395
|
+
}
|
|
396
|
+
function verticalSegmentCrossesRect(x, y1, y2, rect) {
|
|
397
|
+
const minY = Math.min(y1, y2);
|
|
398
|
+
const maxY = Math.max(y1, y2);
|
|
399
|
+
const bottom = rect.top + rect.height;
|
|
400
|
+
const right = rect.left + rect.width;
|
|
401
|
+
if (x <= rect.left || x >= right) return false;
|
|
402
|
+
return maxY > rect.top && minY < bottom;
|
|
403
|
+
}
|
|
404
|
+
function channelYOutsideRect(rect, preferY, clearance) {
|
|
405
|
+
const above = rect.top - clearance;
|
|
406
|
+
const below = rect.top + rect.height + clearance;
|
|
407
|
+
return Math.abs(preferY - above) <= Math.abs(preferY - below) ? above : below;
|
|
408
|
+
}
|
|
409
|
+
function channelXOutsideRect(rect, preferX, clearance) {
|
|
410
|
+
const left = rect.left - clearance;
|
|
411
|
+
const right = rect.left + rect.width + clearance;
|
|
412
|
+
return Math.abs(preferX - left) <= Math.abs(preferX - right) ? left : right;
|
|
413
|
+
}
|
|
414
|
+
function pushRoutePoint(out, point) {
|
|
415
|
+
const last = out[out.length - 1];
|
|
416
|
+
if (!last || last[0] !== point[0] || last[1] !== point[1]) out.push(point);
|
|
417
|
+
}
|
|
418
|
+
/** 折线是否有线段穿过矩形内部 */
|
|
419
|
+
function pathCrossesRectInterior(points, rect) {
|
|
420
|
+
const bottom = rect.top + rect.height;
|
|
421
|
+
const right = rect.left + rect.width;
|
|
422
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
423
|
+
const [x1, y1] = points[i];
|
|
424
|
+
const [x2, y2] = points[i + 1];
|
|
425
|
+
if (y1 === y2) {
|
|
426
|
+
const minX = Math.min(x1, x2);
|
|
427
|
+
const maxX = Math.max(x1, x2);
|
|
428
|
+
if (y1 <= rect.top || y1 >= bottom) continue;
|
|
429
|
+
if (maxX > rect.left && minX < right) return true;
|
|
430
|
+
}
|
|
431
|
+
if (x1 === x2) {
|
|
432
|
+
const minY = Math.min(y1, y2);
|
|
433
|
+
const maxY = Math.max(y1, y2);
|
|
434
|
+
if (x1 <= rect.left || x1 >= right) continue;
|
|
435
|
+
if (maxY > rect.top && minY < bottom) return true;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
function pathCrossesAnyRectInterior(points, rects) {
|
|
441
|
+
return rects.some((rect) => pathCrossesRectInterior(points, rect));
|
|
442
|
+
}
|
|
443
|
+
function manhattanPathLength(points) {
|
|
444
|
+
let sum = 0;
|
|
445
|
+
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]);
|
|
446
|
+
return sum;
|
|
447
|
+
}
|
|
448
|
+
/** 去除连续共线冗余点,得到最简折线(每个内部点都是真正的拐角) */
|
|
449
|
+
function compressCollinearPoints(points) {
|
|
450
|
+
if (points.length < 3) return points.map((p) => [...p]);
|
|
451
|
+
const out = [points[0]];
|
|
452
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
453
|
+
const a = out[out.length - 1];
|
|
454
|
+
const b = points[i];
|
|
455
|
+
const c = points[i + 1];
|
|
456
|
+
const collinearX = a[0] === b[0] && b[0] === c[0];
|
|
457
|
+
const collinearY = a[1] === b[1] && b[1] === c[1];
|
|
458
|
+
if (collinearX || collinearY) continue;
|
|
459
|
+
out.push(b);
|
|
460
|
+
}
|
|
461
|
+
out.push(points[points.length - 1]);
|
|
462
|
+
return out;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* 路径首末两段长度差(压缩冗余后衡量「Z 型两边对称度」),值越小越对称。
|
|
466
|
+
*/
|
|
467
|
+
function pathAsymmetry(points) {
|
|
468
|
+
const compressed = compressCollinearPoints(points);
|
|
469
|
+
if (compressed.length < 4) return 0;
|
|
470
|
+
const segLen = (a, b) => Math.abs(b[0] - a[0]) + Math.abs(b[1] - a[1]);
|
|
471
|
+
const first = segLen(compressed[0], compressed[1]);
|
|
472
|
+
const last = segLen(compressed[compressed.length - 2], compressed[compressed.length - 1]);
|
|
473
|
+
return Math.abs(first - last);
|
|
474
|
+
}
|
|
475
|
+
/** 拐角数(压缩共线冗余后的内部拐点数量) */
|
|
476
|
+
function pathTurnCount(points) {
|
|
477
|
+
const compressed = compressCollinearPoints(points);
|
|
478
|
+
return Math.max(0, compressed.length - 2);
|
|
479
|
+
}
|
|
480
|
+
/** 端口沿外法线走到图元外侧 clearance 处 */
|
|
481
|
+
function outwardClearancePoint(pt, side, rect, clearance) {
|
|
482
|
+
const bottom = rect.top + rect.height;
|
|
483
|
+
const right = rect.left + rect.width;
|
|
484
|
+
switch (side) {
|
|
485
|
+
case "right": return [right + clearance, pt.y];
|
|
486
|
+
case "left": return [rect.left - clearance, pt.y];
|
|
487
|
+
case "top": return [pt.x, rect.top - clearance];
|
|
488
|
+
case "bottom": return [pt.x, bottom + clearance];
|
|
489
|
+
default: return [pt.x, pt.y];
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
/** 校验正交:连续两点必须共享 x 或 y */
|
|
493
|
+
function isOrthogonalPath(points) {
|
|
494
|
+
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;
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* 端口出口 stub 不能反向:从源 sourcePt 到下一拐点必须沿外法线方向;目标同理。
|
|
499
|
+
*/
|
|
500
|
+
function exitsAlignWithSides(sourcePt, sourceSide, targetPt, targetSide, pts) {
|
|
501
|
+
if (pts.length < 2) return true;
|
|
502
|
+
const sv = sideVector(sourceSide);
|
|
503
|
+
const sNext = pts[1];
|
|
504
|
+
const sDx = sNext[0] - sourcePt.x;
|
|
505
|
+
const sDy = sNext[1] - sourcePt.y;
|
|
506
|
+
if (sv.dx !== 0) {
|
|
507
|
+
if (Math.sign(sDx) !== sv.dx) return false;
|
|
508
|
+
if (sDy !== 0) return false;
|
|
509
|
+
} else {
|
|
510
|
+
if (Math.sign(sDy) !== sv.dy) return false;
|
|
511
|
+
if (sDx !== 0) return false;
|
|
512
|
+
}
|
|
513
|
+
const tv = sideVector(targetSide);
|
|
514
|
+
const tPrev = pts[pts.length - 2];
|
|
515
|
+
const tDx = targetPt.x - tPrev[0];
|
|
516
|
+
const tDy = targetPt.y - tPrev[1];
|
|
517
|
+
if (tv.dx !== 0) {
|
|
518
|
+
if (Math.sign(tDx) !== -tv.dx) return false;
|
|
519
|
+
if (tDy !== 0) return false;
|
|
520
|
+
} else {
|
|
521
|
+
if (Math.sign(tDy) !== -tv.dy) return false;
|
|
522
|
+
if (tDx !== 0) return false;
|
|
523
|
+
}
|
|
524
|
+
return true;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* 候选正交路径:直 L、Z 型、外侧通道;端点已沿外法线出 clearance 距离。
|
|
528
|
+
* 选「不穿过源/目标矩形 + 端口方向正确」且曼哈顿距离最短的一条。
|
|
529
|
+
*/
|
|
530
|
+
function buildOrthogonalPathAvoidingRects(sourcePt, sourceSide, targetPt, targetSide, sourceRect, targetRect, clearance, channelOffset = 0) {
|
|
531
|
+
const rects = [sourceRect, targetRect];
|
|
532
|
+
const sOut = outwardClearancePoint(sourcePt, sourceSide, sourceRect, clearance);
|
|
533
|
+
const tOut = outwardClearancePoint(targetPt, targetSide, targetRect, clearance);
|
|
534
|
+
/** 中段通道偏移:让共享通道的多条线错开,避免视觉重叠 */
|
|
535
|
+
const channelGap = channelOffset;
|
|
536
|
+
const outerGap = Math.abs(channelOffset);
|
|
537
|
+
const aboveY = Math.min(...rects.map((r) => r.top)) - clearance - outerGap;
|
|
538
|
+
const belowY = Math.max(...rects.map((r) => r.top + r.height)) + clearance + outerGap;
|
|
539
|
+
const leftX = Math.min(...rects.map((r) => r.left)) - clearance - outerGap;
|
|
540
|
+
const rightX = Math.max(...rects.map((r) => r.left + r.width)) + clearance + outerGap;
|
|
541
|
+
/** s_out → t_out 之间的拐点序列;不含端点本身 */
|
|
542
|
+
const connectors = [];
|
|
543
|
+
/** 直连:先横后竖 / 先竖后横 */
|
|
544
|
+
connectors.push([[tOut[0], sOut[1]]]);
|
|
545
|
+
connectors.push([[sOut[0], tOut[1]]]);
|
|
546
|
+
/** Z 型中点(共享中段加偏移以错开多条线) */
|
|
547
|
+
const midX = (sOut[0] + tOut[0]) / 2 + channelGap;
|
|
548
|
+
const midY = (sOut[1] + tOut[1]) / 2 + channelGap;
|
|
549
|
+
connectors.push([[midX, sOut[1]], [midX, tOut[1]]]);
|
|
550
|
+
connectors.push([[sOut[0], midY], [tOut[0], midY]]);
|
|
551
|
+
/** 外侧公共通道(上/下/左/右),用于源、目标位于直连冲突区时绕过去 */
|
|
552
|
+
for (const channelY of [aboveY, belowY]) connectors.push([[sOut[0], channelY], [tOut[0], channelY]]);
|
|
553
|
+
for (const channelX of [leftX, rightX]) connectors.push([[channelX, sOut[1]], [channelX, tOut[1]]]);
|
|
554
|
+
let bestPath = null;
|
|
555
|
+
let bestLen = Infinity;
|
|
556
|
+
let bestTurns = Infinity;
|
|
557
|
+
let bestAsym = Infinity;
|
|
558
|
+
for (const inner of connectors) {
|
|
559
|
+
const trial = [];
|
|
560
|
+
pushRoutePoint(trial, [sourcePt.x, sourcePt.y]);
|
|
561
|
+
pushRoutePoint(trial, sOut);
|
|
562
|
+
for (const p of inner) pushRoutePoint(trial, p);
|
|
563
|
+
pushRoutePoint(trial, tOut);
|
|
564
|
+
pushRoutePoint(trial, [targetPt.x, targetPt.y]);
|
|
565
|
+
if (!isOrthogonalPath(trial)) continue;
|
|
566
|
+
if (!exitsAlignWithSides(sourcePt, sourceSide, targetPt, targetSide, trial)) continue;
|
|
567
|
+
if (pathCrossesAnyRectInterior(trial, rects)) continue;
|
|
568
|
+
const len = manhattanPathLength(trial);
|
|
569
|
+
const turns = pathTurnCount(trial);
|
|
570
|
+
const asym = pathAsymmetry(trial);
|
|
571
|
+
if (len < bestLen || len === bestLen && turns < bestTurns || len === bestLen && turns === bestTurns && asym < bestAsym) {
|
|
572
|
+
bestLen = len;
|
|
573
|
+
bestTurns = turns;
|
|
574
|
+
bestAsym = asym;
|
|
575
|
+
bestPath = trial;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
if (bestPath) return bestPath;
|
|
579
|
+
/** 兜底:沿外侧上方通道兜一圈(极端情况下两个矩形重叠/相邻仍能输出一条路径) */
|
|
580
|
+
return [
|
|
581
|
+
[sourcePt.x, sourcePt.y],
|
|
582
|
+
sOut,
|
|
583
|
+
[sOut[0], aboveY],
|
|
584
|
+
[tOut[0], aboveY],
|
|
585
|
+
tOut,
|
|
586
|
+
[targetPt.x, targetPt.y]
|
|
587
|
+
];
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* 最短正交折线:从端口锚点直接转折,无固定长度 stub。
|
|
591
|
+
*/
|
|
592
|
+
function buildShortestOrthogonalPath(sourcePt, sourceSide, targetPt, targetSide) {
|
|
593
|
+
const sourceExitsVertical = sideVector(sourceSide).dx === 0;
|
|
594
|
+
const sameHoriz = (sourceSide === "left" || sourceSide === "right") && (targetSide === "left" || targetSide === "right");
|
|
595
|
+
const sameVert = (sourceSide === "top" || sourceSide === "bottom") && (targetSide === "top" || targetSide === "bottom");
|
|
596
|
+
const out = [];
|
|
597
|
+
if (sameHoriz) {
|
|
598
|
+
const midX = (sourcePt.x + targetPt.x) / 2;
|
|
599
|
+
pushRoutePoint(out, [sourcePt.x, sourcePt.y]);
|
|
600
|
+
pushRoutePoint(out, [midX, sourcePt.y]);
|
|
601
|
+
pushRoutePoint(out, [midX, targetPt.y]);
|
|
602
|
+
pushRoutePoint(out, [targetPt.x, targetPt.y]);
|
|
603
|
+
return out;
|
|
604
|
+
}
|
|
605
|
+
if (sameVert) {
|
|
606
|
+
const midY = (sourcePt.y + targetPt.y) / 2;
|
|
607
|
+
pushRoutePoint(out, [sourcePt.x, sourcePt.y]);
|
|
608
|
+
pushRoutePoint(out, [sourcePt.x, midY]);
|
|
609
|
+
pushRoutePoint(out, [targetPt.x, midY]);
|
|
610
|
+
pushRoutePoint(out, [targetPt.x, targetPt.y]);
|
|
611
|
+
return out;
|
|
612
|
+
}
|
|
613
|
+
const pathA = sourceExitsVertical ? [
|
|
614
|
+
[sourcePt.x, sourcePt.y],
|
|
615
|
+
[targetPt.x, sourcePt.y],
|
|
616
|
+
[targetPt.x, targetPt.y]
|
|
617
|
+
] : [
|
|
618
|
+
[sourcePt.x, sourcePt.y],
|
|
619
|
+
[targetPt.x, sourcePt.y],
|
|
620
|
+
[targetPt.x, targetPt.y]
|
|
621
|
+
];
|
|
622
|
+
const pathB = sourceExitsVertical ? [
|
|
623
|
+
[sourcePt.x, sourcePt.y],
|
|
624
|
+
[sourcePt.x, targetPt.y],
|
|
625
|
+
[targetPt.x, targetPt.y]
|
|
626
|
+
] : [
|
|
627
|
+
[sourcePt.x, sourcePt.y],
|
|
628
|
+
[sourcePt.x, targetPt.y],
|
|
629
|
+
[targetPt.x, targetPt.y]
|
|
630
|
+
];
|
|
631
|
+
const len = (pts) => {
|
|
632
|
+
let sum = 0;
|
|
633
|
+
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]);
|
|
634
|
+
return sum;
|
|
635
|
+
};
|
|
636
|
+
const chosen = len(pathA) <= len(pathB) ? pathA : pathB;
|
|
637
|
+
for (const p of chosen) pushRoutePoint(out, p);
|
|
638
|
+
return out;
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* 单段穿框时插入最小 U 型绕行(多一折:先垂/平到通道,再平/垂接到对端)。
|
|
642
|
+
*/
|
|
643
|
+
function buildWrapAroundPoints(a, b, rect, clearance) {
|
|
644
|
+
if (a[1] === b[1]) {
|
|
645
|
+
if (!horizontalSegmentCrossesRect(a[1], a[0], b[0], rect)) return [];
|
|
646
|
+
const bypassY = channelYOutsideRect(rect, a[1], clearance);
|
|
647
|
+
return [[a[0], bypassY], [b[0], bypassY]];
|
|
648
|
+
}
|
|
649
|
+
if (a[0] === b[0]) {
|
|
650
|
+
if (!verticalSegmentCrossesRect(a[0], a[1], b[1], rect)) return [];
|
|
651
|
+
const bypassX = channelXOutsideRect(rect, a[0], clearance);
|
|
652
|
+
return [[bypassX, a[1]], [bypassX, b[1]]];
|
|
653
|
+
}
|
|
654
|
+
return [];
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* 在「最短路径」基础上,对穿过障碍矩形的线段做包围式绕行(仅跳过离开源锚点的第一段)。
|
|
658
|
+
*/
|
|
659
|
+
function wrapPathAroundRects(points, obstacleRects, clearance) {
|
|
660
|
+
if (points.length < 3 || obstacleRects.length === 0) return points;
|
|
661
|
+
const wrapped = [];
|
|
662
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
663
|
+
const a = points[i];
|
|
664
|
+
const b = points[i + 1];
|
|
665
|
+
pushRoutePoint(wrapped, a);
|
|
666
|
+
if (!(i === 0)) for (const rect of obstacleRects) {
|
|
667
|
+
const detour = buildWrapAroundPoints(a, b, rect, clearance);
|
|
668
|
+
if (detour.length === 0) continue;
|
|
669
|
+
for (const p of detour) pushRoutePoint(wrapped, p);
|
|
670
|
+
break;
|
|
671
|
+
}
|
|
672
|
+
pushRoutePoint(wrapped, b);
|
|
673
|
+
}
|
|
674
|
+
return wrapped;
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* 正交连线:先最短路径,再对穿框段绕一圈。
|
|
678
|
+
*/
|
|
679
|
+
function routeOrthogonal(sourcePt, sourceSide, targetPt, targetSide, stubOrOptions, maybeOptions) {
|
|
680
|
+
var _options$clearance, _options$obstacleRect2;
|
|
681
|
+
const options = typeof stubOrOptions === "object" && stubOrOptions != null ? stubOrOptions : maybeOptions;
|
|
682
|
+
const clearance = (_options$clearance = options === null || options === void 0 ? void 0 : options.clearance) !== null && _options$clearance !== void 0 ? _options$clearance : 10;
|
|
683
|
+
const sourceRect = options === null || options === void 0 ? void 0 : options.sourceRect;
|
|
684
|
+
const targetRect = options === null || options === void 0 ? void 0 : options.targetRect;
|
|
685
|
+
if (sourceRect && targetRect) {
|
|
686
|
+
var _options$channelOffse, _options$obstacleRect;
|
|
687
|
+
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);
|
|
688
|
+
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);
|
|
689
|
+
if (extra.length > 0) path = wrapPathAroundRects(path, extra, clearance);
|
|
690
|
+
return path;
|
|
691
|
+
}
|
|
692
|
+
const shortest = buildShortestOrthogonalPath(sourcePt, sourceSide, targetPt, targetSide);
|
|
693
|
+
const obstacles = (_options$obstacleRect2 = options === null || options === void 0 ? void 0 : options.obstacleRects) !== null && _options$obstacleRect2 !== void 0 ? _options$obstacleRect2 : targetRect ? [targetRect] : sourceRect ? [sourceRect] : [];
|
|
694
|
+
if (obstacles.length === 0) return shortest;
|
|
695
|
+
return wrapPathAroundRects(shortest, obstacles, clearance);
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* 折线转带圆角的 SVG path d
|
|
699
|
+
*/
|
|
700
|
+
function pathFromPoints(points, radius = 4) {
|
|
701
|
+
if (points.length < 2) return "";
|
|
702
|
+
if (points.length === 2) return `M ${points[0][0]} ${points[0][1]} L ${points[1][0]} ${points[1][1]}`;
|
|
703
|
+
let d = `M ${points[0][0]} ${points[0][1]}`;
|
|
704
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
705
|
+
const prev = points[i - 1];
|
|
706
|
+
const curr = points[i];
|
|
707
|
+
const next = points[i + 1];
|
|
708
|
+
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);
|
|
709
|
+
const dx1 = curr[0] - prev[0];
|
|
710
|
+
const dy1 = curr[1] - prev[1];
|
|
711
|
+
const len1 = Math.hypot(dx1, dy1) || 1;
|
|
712
|
+
const dx2 = next[0] - curr[0];
|
|
713
|
+
const dy2 = next[1] - curr[1];
|
|
714
|
+
const len2 = Math.hypot(dx2, dy2) || 1;
|
|
715
|
+
const x1 = curr[0] - dx1 / len1 * r;
|
|
716
|
+
const y1 = curr[1] - dy1 / len1 * r;
|
|
717
|
+
const x2 = curr[0] + dx2 / len2 * r;
|
|
718
|
+
const y2 = curr[1] + dy2 / len2 * r;
|
|
719
|
+
d += ` L ${x1} ${y1} Q ${curr[0]} ${curr[1]} ${x2} ${y2}`;
|
|
720
|
+
}
|
|
721
|
+
const last = points[points.length - 1];
|
|
722
|
+
d += ` L ${last[0]} ${last[1]}`;
|
|
723
|
+
return d;
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* 把一条已存在的 path 快照(snapshot)的两端「平移对齐」到新的源/目标锚点位置,
|
|
727
|
+
* 中间路径完全保留。用于「手动编辑过的连线」在图元被拖动后只更新最近两端。
|
|
728
|
+
*
|
|
729
|
+
* 规则:
|
|
730
|
+
* - 首段(snapshot[0] → snapshot[1]):
|
|
731
|
+
* - 若是水平段(y0 === y1):新 snapshot[0] = srcPt;新 snapshot[1] = (原 snapshot[1].x, srcPt.y)
|
|
732
|
+
* - 若是竖直段(x0 === x1):新 snapshot[0] = srcPt;新 snapshot[1] = (srcPt.x, 原 snapshot[1].y)
|
|
733
|
+
* - 若是斜段(理论上 snapshot 都是正交,但兜底):仅平移 snapshot[0],snapshot[1] 保持不变
|
|
734
|
+
* - 末段(snapshot[n-2] → snapshot[n-1]):对称处理
|
|
735
|
+
* - 同时对 snapshot[2] / snapshot[n-3] 做"必要的轴向跟随":snapshot[1] → snapshot[2] 段必须保持
|
|
736
|
+
* 正交,因此 snapshot[2] 的相邻轴坐标需要跟随 snapshot[1] 更新(同理末端)。
|
|
737
|
+
*/
|
|
738
|
+
function alignSnapshotToAnchors(snapshot, srcPt, tgtPt) {
|
|
739
|
+
if (snapshot.length < 2) return snapshot;
|
|
740
|
+
const result = snapshot.map((p) => [p[0], p[1]]);
|
|
741
|
+
/** 首段 */
|
|
742
|
+
const [x0, y0] = result[0];
|
|
743
|
+
const [x1, y1] = result[1];
|
|
744
|
+
result[0] = [srcPt.x, srcPt.y];
|
|
745
|
+
if (y0 === y1 && x0 !== x1)
|
|
746
|
+
/** 首段水平 → 用新 y 替换 snapshot[1].y;snapshot[2] 的 x 与 snapshot[1].x 原本相等(正交垂直段),保持不变 */
|
|
747
|
+
result[1] = [x1, srcPt.y];
|
|
748
|
+
else if (x0 === x1 && y0 !== y1) result[1] = [srcPt.x, y1];
|
|
749
|
+
else
|
|
750
|
+
/** 首段退化为零长度:保留原 snapshot[1] 不变 */
|
|
751
|
+
result[1] = [x1, y1];
|
|
752
|
+
/** 末段 */
|
|
753
|
+
const n = result.length;
|
|
754
|
+
if (n >= 4) {
|
|
755
|
+
const [xnm2, ynm2] = result[n - 2];
|
|
756
|
+
const [xnm1, ynm1] = result[n - 1];
|
|
757
|
+
result[n - 1] = [tgtPt.x, tgtPt.y];
|
|
758
|
+
if (ynm2 === ynm1 && xnm2 !== xnm1) result[n - 2] = [xnm2, tgtPt.y];
|
|
759
|
+
else if (xnm2 === xnm1 && ynm2 !== ynm1) result[n - 2] = [tgtPt.x, ynm2];
|
|
760
|
+
else result[n - 2] = [xnm2, ynm2];
|
|
761
|
+
} else if (n === 3) {
|
|
762
|
+
/**
|
|
763
|
+
* L 型 3 点情况:result[1] 同时受首段和末段方向约束。
|
|
764
|
+
* - 若首段水平 + 末段竖直 → result[1] = (tgtPt.x, srcPt.y)
|
|
765
|
+
* - 若首段竖直 + 末段水平 → result[1] = (srcPt.x, tgtPt.y)
|
|
766
|
+
* 否则保留原 result[1]。
|
|
767
|
+
*/
|
|
768
|
+
const [xA, yA] = snapshot[0];
|
|
769
|
+
const [xB, yB] = snapshot[1];
|
|
770
|
+
const [xC, yC] = snapshot[2];
|
|
771
|
+
const firstHorizontal = yA === yB && xA !== xB;
|
|
772
|
+
const firstVertical = xA === xB && yA !== yB;
|
|
773
|
+
const lastHorizontal = yB === yC && xB !== xC;
|
|
774
|
+
const lastVertical = xB === xC && yB !== yC;
|
|
775
|
+
let rx = xB;
|
|
776
|
+
let ry = yB;
|
|
777
|
+
if (firstHorizontal) ry = srcPt.y;
|
|
778
|
+
if (firstVertical) rx = srcPt.x;
|
|
779
|
+
if (lastHorizontal) ry = tgtPt.y;
|
|
780
|
+
if (lastVertical) rx = tgtPt.x;
|
|
781
|
+
result[1] = [rx, ry];
|
|
782
|
+
result[2] = [tgtPt.x, tgtPt.y];
|
|
783
|
+
} else if (n === 2) result[1] = [tgtPt.x, tgtPt.y];
|
|
784
|
+
return result;
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* 在路径起点处插入 stub 转角:把首段变成「沿首段实际方向走 stub → 90° 转弯 →
|
|
788
|
+
* 走到原 corner」。
|
|
789
|
+
*
|
|
790
|
+
* 注意:方向取自 *path 首段实际指向*(points[0]→points[1]),而非 sourceAnchor 字段——
|
|
791
|
+
* 这样在 path 已经经过手动编辑、首段方向与锚点 outward 不一致时仍能正确插入。
|
|
792
|
+
*
|
|
793
|
+
* 路径点数 +2;新增 2 个内部段(stub 段 + 「被拖动段」)。被拖动段的初始另一轴
|
|
794
|
+
* 坐标与原首段重合(视觉直线),后续由拖动调整。
|
|
795
|
+
*/
|
|
796
|
+
function applySourceStub(points, stubLength, sourceAnchor) {
|
|
797
|
+
if (!stubLength || stubLength <= 0 || points.length < 2) return points;
|
|
798
|
+
const [x0, y0] = points[0];
|
|
799
|
+
const [x1, y1] = points[1];
|
|
800
|
+
const isHorizontal = y0 === y1 && x0 !== x1;
|
|
801
|
+
if (!isHorizontal && !(x0 === x1 && y0 !== y1)) return points;
|
|
802
|
+
const firstLen = isHorizontal ? Math.abs(x1 - x0) : Math.abs(y1 - y0);
|
|
803
|
+
if (firstLen <= 1) return points;
|
|
804
|
+
const stub = Math.min(stubLength, firstLen / 2);
|
|
805
|
+
if (stub <= 0) return points;
|
|
806
|
+
if (isHorizontal) {
|
|
807
|
+
const sx = x0 + (Math.sign(x1 - x0) || 1) * stub;
|
|
808
|
+
const stubEnd = [sx, y0];
|
|
809
|
+
const midCorner = [sx, y1];
|
|
810
|
+
const replacedP1 = [x1, y1];
|
|
811
|
+
return [
|
|
812
|
+
points[0],
|
|
813
|
+
stubEnd,
|
|
814
|
+
midCorner,
|
|
815
|
+
replacedP1,
|
|
816
|
+
...points.slice(2)
|
|
817
|
+
];
|
|
818
|
+
}
|
|
819
|
+
const sy = y0 + (Math.sign(y1 - y0) || 1) * stub;
|
|
820
|
+
const stubEnd = [x0, sy];
|
|
821
|
+
const midCorner = [x1, sy];
|
|
822
|
+
const replacedP1 = [x1, y1];
|
|
823
|
+
return [
|
|
824
|
+
points[0],
|
|
825
|
+
stubEnd,
|
|
826
|
+
midCorner,
|
|
827
|
+
replacedP1,
|
|
828
|
+
...points.slice(2)
|
|
829
|
+
];
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* 对称地在路径末端插入 stub 转角。方向取自 *path 末段实际指向*。
|
|
833
|
+
*
|
|
834
|
+
* 路径点数 +2;新增 2 个内部段(「被拖动段」 + stub 段)。
|
|
835
|
+
*/
|
|
836
|
+
function applyTargetStub(points, stubLength, targetAnchor) {
|
|
837
|
+
if (!stubLength || stubLength <= 0 || points.length < 2) return points;
|
|
838
|
+
const n = points.length;
|
|
839
|
+
const [xA, yA] = points[n - 2];
|
|
840
|
+
const [xT, yT] = points[n - 1];
|
|
841
|
+
const isHorizontal = yA === yT && xA !== xT;
|
|
842
|
+
if (!isHorizontal && !(xA === xT && yA !== yT)) return points;
|
|
843
|
+
const lastLen = isHorizontal ? Math.abs(xT - xA) : Math.abs(yT - yA);
|
|
844
|
+
if (lastLen <= 1) return points;
|
|
845
|
+
const stub = Math.min(stubLength, lastLen / 2);
|
|
846
|
+
if (stub <= 0) return points;
|
|
847
|
+
if (isHorizontal) {
|
|
848
|
+
const sx = xT - (Math.sign(xT - xA) || 1) * stub;
|
|
849
|
+
const aMod = [xA, yA];
|
|
850
|
+
const midCorner = [sx, yA];
|
|
851
|
+
const stubStart = [sx, yT];
|
|
852
|
+
return [
|
|
853
|
+
...points.slice(0, n - 2),
|
|
854
|
+
aMod,
|
|
855
|
+
midCorner,
|
|
856
|
+
stubStart,
|
|
857
|
+
points[n - 1]
|
|
858
|
+
];
|
|
859
|
+
}
|
|
860
|
+
const sy = yT - (Math.sign(yT - yA) || 1) * stub;
|
|
861
|
+
const aMod = [xA, yA];
|
|
862
|
+
const midCorner = [xA, sy];
|
|
863
|
+
const stubStart = [xT, sy];
|
|
864
|
+
return [
|
|
865
|
+
...points.slice(0, n - 2),
|
|
866
|
+
aMod,
|
|
867
|
+
midCorner,
|
|
868
|
+
stubStart,
|
|
869
|
+
points[n - 1]
|
|
870
|
+
];
|
|
871
|
+
}
|
|
872
|
+
function buildEdgeRoute(edge, rectsById, options) {
|
|
873
|
+
var _edge$sourceAnchor, _edge$targetAnchor, _options$routingOverr, _options$channelOffse2;
|
|
874
|
+
const srcRect = rectsById.get(edge.sourceId);
|
|
875
|
+
const tgtRect = rectsById.get(edge.targetId);
|
|
876
|
+
if (!srcRect || !tgtRect) return null;
|
|
877
|
+
const sourceAnchor = (_edge$sourceAnchor = edge.sourceAnchor) !== null && _edge$sourceAnchor !== void 0 ? _edge$sourceAnchor : pickAutoAnchor(srcRect, tgtRect);
|
|
878
|
+
const targetAnchor = (_edge$targetAnchor = edge.targetAnchor) !== null && _edge$targetAnchor !== void 0 ? _edge$targetAnchor : pickAutoAnchor(tgtRect, srcRect);
|
|
879
|
+
const sourcePt = getAnchorPoint(srcRect, sourceAnchor);
|
|
880
|
+
const targetPt = getAnchorPoint(tgtRect, targetAnchor);
|
|
881
|
+
const routing = (_options$routingOverr = options === null || options === void 0 ? void 0 : options.routingOverride) !== null && _options$routingOverr !== void 0 ? _options$routingOverr : edge.routing;
|
|
882
|
+
/** 手动编辑过:snapshot 优先,仅对齐两端 */
|
|
883
|
+
if ((routing === null || routing === void 0 ? void 0 : routing.pathSnapshot) && routing.pathSnapshot.length >= 2) {
|
|
884
|
+
const points = simplifyCollinearPath(alignSnapshotToAnchors(routing.pathSnapshot, sourcePt, targetPt));
|
|
885
|
+
return {
|
|
886
|
+
edgeId: edge.id,
|
|
887
|
+
points,
|
|
888
|
+
pathD: pathFromPoints(points)
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
const points = simplifyCollinearPath(routeOrthogonal(sourcePt, sourceAnchor, targetPt, targetAnchor, {
|
|
892
|
+
sourceRect: srcRect,
|
|
893
|
+
targetRect: tgtRect,
|
|
894
|
+
channelOffset: (_options$channelOffse2 = options === null || options === void 0 ? void 0 : options.channelOffset) !== null && _options$channelOffse2 !== void 0 ? _options$channelOffse2 : 0
|
|
895
|
+
}));
|
|
896
|
+
return {
|
|
897
|
+
edgeId: edge.id,
|
|
898
|
+
points,
|
|
899
|
+
pathD: pathFromPoints(points)
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* 去掉路径中"三点共线"的冗余中间点(比如 routeOrthogonal 里 srcPt → sOut →
|
|
904
|
+
* 中段折点都在同一 y 上时,sOut 是冗余的)。
|
|
905
|
+
*
|
|
906
|
+
* 保留零长度段:用户拖动 first/last 段创建 stub 时,stubEnd 与 midCorner 短暂
|
|
907
|
+
* 重合(零长度),不能被合并掉,否则会立刻丢失新插入的转角结构。
|
|
908
|
+
*/
|
|
909
|
+
function simplifyCollinearPath(points) {
|
|
910
|
+
if (points.length < 3) return points.map((p) => [p[0], p[1]]);
|
|
911
|
+
const out = [[points[0][0], points[0][1]]];
|
|
912
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
913
|
+
const prev = out[out.length - 1];
|
|
914
|
+
const curr = points[i];
|
|
915
|
+
const next = points[i + 1];
|
|
916
|
+
/** 零长度段(prev===curr 或 curr===next)一律保留:用户主动创建的结构 */
|
|
917
|
+
const zeroBefore = prev[0] === curr[0] && prev[1] === curr[1];
|
|
918
|
+
const zeroAfter = curr[0] === next[0] && curr[1] === next[1];
|
|
919
|
+
if (zeroBefore || zeroAfter) {
|
|
920
|
+
out.push([curr[0], curr[1]]);
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
const collinearH = prev[1] === curr[1] && curr[1] === next[1];
|
|
924
|
+
const collinearV = prev[0] === curr[0] && curr[0] === next[0];
|
|
925
|
+
if (collinearH || collinearV) continue;
|
|
926
|
+
out.push([curr[0], curr[1]]);
|
|
927
|
+
}
|
|
928
|
+
const last = points[points.length - 1];
|
|
929
|
+
out.push([last[0], last[1]]);
|
|
930
|
+
return out;
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* 按 edge 顺序分配交替偏移,让落到同一通道的多条线错开。
|
|
934
|
+
* 序列:0, +step, -step, +2*step, -2*step, ...
|
|
935
|
+
*/
|
|
936
|
+
function getEdgeChannelOffset(index, step = 12) {
|
|
937
|
+
if (index <= 0) return 0;
|
|
938
|
+
const k = Math.ceil(index / 2);
|
|
939
|
+
return index % 2 === 1 ? k * step : -k * step;
|
|
940
|
+
}
|
|
941
|
+
//#endregion
|
|
942
|
+
exports.ANCHORS = ANCHORS;
|
|
943
|
+
exports.applySourceStub = applySourceStub;
|
|
944
|
+
exports.applyTargetStub = applyTargetStub;
|
|
945
|
+
exports.buildEdgeRoute = buildEdgeRoute;
|
|
946
|
+
exports.getAnchorPoint = getAnchorPoint;
|
|
947
|
+
exports.getEdgeChannelOffset = getEdgeChannelOffset;
|
|
948
|
+
exports.getItemPixelRect = getItemPixelRect;
|
|
949
|
+
exports.getPortLabelPosition = getPortLabelPosition;
|
|
950
|
+
exports.getPortMarkerDisplayPoint = getPortMarkerDisplayPoint;
|
|
951
|
+
exports.pathFromPoints = pathFromPoints;
|
|
952
|
+
exports.pickAutoAnchor = pickAutoAnchor;
|
|
953
|
+
exports.resolveDrawingTargetAtPoint = resolveDrawingTargetAtPoint;
|
|
954
|
+
exports.resolveEdgeCenterLabelPositions = resolveEdgeCenterLabelPositions;
|
|
955
|
+
exports.routeOrthogonal = routeOrthogonal;
|
|
956
|
+
exports.simplifyCollinearPath = simplifyCollinearPath;
|