@cardenelabs/cdl 0.121.0 → 0.123.0
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/CHANGELOG.md +51 -1
- package/README.md +137 -0
- package/SPEC.md +1 -1
- package/dist/index.cjs +1513 -667
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +51 -33
- package/dist/index.d.ts +51 -33
- package/dist/index.js +1513 -668
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +1271 -444
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1271 -444
- package/dist/react.js.map +1 -1
- package/dist/{render-A5AKjNuy.d.cts → render-CVczwfRb.d.cts} +40 -7
- package/dist/{render-A5AKjNuy.d.ts → render-CVczwfRb.d.ts} +40 -7
- package/package.json +9 -12
- package/src/builder.ts +8 -2
- package/src/index.ts +3 -0
- package/src/kinds/box-lines.ts +17 -4
- package/src/kinds/chart-bar.tsx +4 -0
- package/src/kinds/chart-gauge.tsx +20 -9
- package/src/kinds/chart-pie.tsx +67 -17
- package/src/kinds/chart-radial.tsx +7 -2
- package/src/kinds/chart-slope.tsx +26 -7
- package/src/kinds/chart-stacked-bar.tsx +8 -4
- package/src/kinds/chart-stat.tsx +19 -4
- package/src/kinds/chart-waffle.tsx +7 -2
- package/src/kinds/dyn-shape.tsx +22 -9
- package/src/kinds/funnel.tsx +9 -2
- package/src/kinds/generic.tsx +5 -2
- package/src/kinds/mind-map.tsx +26 -10
- package/src/kinds/quadrant.tsx +14 -4
- package/src/kinds/row-align.ts +19 -7
- package/src/kinds/sequence-board-metrics.ts +13 -3
- package/src/kinds/sequence-board.tsx +4 -2
- package/src/kinds/shape-basement.tsx +57 -32
- package/src/kinds/shape-blockchain.tsx +83 -34
- package/src/kinds/shape-commerce.tsx +65 -24
- package/src/kinds/shape-finance.tsx +90 -26
- package/src/kinds/shape-hardware.tsx +48 -17
- package/src/kinds/shape-region.tsx +16 -6
- package/src/kinds/shape-software.tsx +30 -8
- package/src/kinds/station.tsx +29 -0
- package/src/kinds/text-width.ts +17 -3
- package/src/kinds/tree.tsx +25 -7
- package/src/layout/collisions.ts +50 -28
- package/src/layout/curve-path.ts +126 -0
- package/src/layout/edges.ts +271 -58
- package/src/layout/head-label.ts +11 -5
- package/src/layout/lanes.ts +3 -2
- package/src/layout/metro-path.ts +185 -0
- package/src/layout/nodes.ts +14 -6
- package/src/layout/predict-bbox.ts +6 -3
- package/src/layout/self-loop.ts +10 -4
- package/src/layout/spec.ts +36 -7
- package/src/layout/text-width.ts +25 -8
- package/src/layout/tokens.ts +2 -0
- package/src/layout-with-validation.ts +10 -5
- package/src/layout.ts +55 -21
- package/src/presets.ts +69 -43
- package/src/render/edges.tsx +19 -3
- package/src/render/interactive-panel.tsx +34 -7
- package/src/render/nodes.tsx +100 -48
- package/src/render/stage.tsx +3 -0
- package/src/text-metrics.ts +9 -0
- package/src/types.ts +41 -4
- package/src/visual-validate.ts +91 -21
package/src/layout/edges.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
BBox,
|
|
3
|
+
CdlDiagram,
|
|
4
|
+
CdlEdge,
|
|
5
|
+
CdlTextMetrics,
|
|
6
|
+
LaidEdge,
|
|
7
|
+
LaidLane,
|
|
8
|
+
LaidNode,
|
|
9
|
+
Side,
|
|
10
|
+
} from "../types";
|
|
2
11
|
import {
|
|
3
12
|
CLEARANCE_LABEL_LABEL,
|
|
4
13
|
CLEARANCE_NODE_LABEL,
|
|
@@ -8,9 +17,14 @@ import {
|
|
|
8
17
|
extractPathSegments,
|
|
9
18
|
flattenPathSegments,
|
|
10
19
|
measurePathLabelGapSegs,
|
|
20
|
+
札の移動を塞ぐ相手を探す,
|
|
11
21
|
segmentsCrossRect,
|
|
12
22
|
segmentsExcludingOwnPath,
|
|
23
|
+
type 札の周り,
|
|
24
|
+
線の測り方を作る,
|
|
13
25
|
} from "./collisions";
|
|
26
|
+
import { curvePath } from "./curve-path";
|
|
27
|
+
import { metroPath } from "./metro-path";
|
|
14
28
|
import { countEdgeCrossings } from "../visual-validate";
|
|
15
29
|
import {
|
|
16
30
|
computeLabelBBoxWorld,
|
|
@@ -34,6 +48,8 @@ import { DEFAULT_LABEL_MARGIN } from "./tokens";
|
|
|
34
48
|
const VERTICAL_DETOUR_GAP = 50;
|
|
35
49
|
/** 縦 detour の角の丸め半径 (world unit)。 */
|
|
36
50
|
const VERTICAL_DETOUR_CORNER = 14;
|
|
51
|
+
/** overlay の札を「自分の線の上に載せた札」 とみなす、 札の中心から自分の線までの距離の上限 (world unit)。 */
|
|
52
|
+
const OVERLAY_LABEL_ON_PATH_TOLERANCE = 1;
|
|
37
53
|
|
|
38
54
|
/**
|
|
39
55
|
* 縦に進む区間の**進む向き** (下へ = 1、 上へ = -1)。 角の丸めの位置を決める時に使う。
|
|
@@ -204,7 +220,12 @@ function detourColumnFor(blockers: LaidNode[], side: DetourSide): number {
|
|
|
204
220
|
: Math.max(...blockers.map((n) => n.cx + n.w / 2)) + VERTICAL_DETOUR_GAP;
|
|
205
221
|
}
|
|
206
222
|
|
|
207
|
-
export function layoutEdges(
|
|
223
|
+
export function layoutEdges(
|
|
224
|
+
diag: CdlDiagram,
|
|
225
|
+
nodes: LaidNode[],
|
|
226
|
+
lanes: LaidLane[] = [],
|
|
227
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
228
|
+
): LaidEdge[] {
|
|
208
229
|
// labelMargin = author 追加余白、 path 中点 → label の垂直距離に加算 (上向き label は -=、 下向きは +=)。
|
|
209
230
|
// viewport.labelMargin 明示時はその差分、 未指定なら viewport.gap → DEFAULT_LABEL_MARGIN の順で fallback。
|
|
210
231
|
const labelMarginExtra =
|
|
@@ -214,6 +235,7 @@ export function layoutEdges(diag: CdlDiagram, nodes: LaidNode[], lanes: LaidLane
|
|
|
214
235
|
// label が lane の枠線を跨ぐかを見る 2 か所 (左への迂回と label の反転) が使う縦の範囲 (#863)。
|
|
215
236
|
const laneRangeById = new Map(lanes.map((l) => [l.id, laneVerticalRange(l, nodes)]));
|
|
216
237
|
|
|
238
|
+
// 経路を書き手が決めた線 = 座標を固定した線、曲線の線、路線図の線。
|
|
217
239
|
// canvas pivot 新 spec (dragon canvas pivot spec §layout-role-conversion)。
|
|
218
240
|
// posX1 / posY1 / posX2 / posY2 全て set 済 edge は「user が固定した絶対 path」 = 直線 path
|
|
219
241
|
// (posX1, posY1) → (posX2, posY2) を最終 path とする。 label 位置は path 中点、 fromSide / toSide
|
|
@@ -245,6 +267,36 @@ export function layoutEdges(diag: CdlDiagram, nodes: LaidNode[], lanes: LaidLane
|
|
|
245
267
|
});
|
|
246
268
|
} else if (isSelfLoop(e)) {
|
|
247
269
|
selfLoopEdges.push(e);
|
|
270
|
+
} else if (e.routing === "curve") {
|
|
271
|
+
const from = nmap.get(e.from);
|
|
272
|
+
const to = nmap.get(e.to);
|
|
273
|
+
if (!from) throw new Error(`cdl layout: edge "${e.id}" の from "${e.from}" が node に無い`);
|
|
274
|
+
if (!to) throw new Error(`cdl layout: edge "${e.id}" の to "${e.to}" が node に無い`);
|
|
275
|
+
const c = curvePath(from, to, e, labelMarginExtra, textMetrics);
|
|
276
|
+
positionedEdges.push({
|
|
277
|
+
...e,
|
|
278
|
+
d: c.d,
|
|
279
|
+
fromSide: c.fromSide,
|
|
280
|
+
toSide: c.toSide,
|
|
281
|
+
labelX: c.labelX + (e.labelOffsetX ?? 0),
|
|
282
|
+
labelY: c.labelY + (e.labelOffsetY ?? 0),
|
|
283
|
+
labelAnchor: c.labelAnchor,
|
|
284
|
+
});
|
|
285
|
+
} else if (e.routing === "metro") {
|
|
286
|
+
const from = nmap.get(e.from);
|
|
287
|
+
const to = nmap.get(e.to);
|
|
288
|
+
if (!from) throw new Error(`cdl layout: edge "${e.id}" の from "${e.from}" が node に無い`);
|
|
289
|
+
if (!to) throw new Error(`cdl layout: edge "${e.id}" の to "${e.to}" が node に無い`);
|
|
290
|
+
const m = metroPath(from, to, e, labelMarginExtra, textMetrics);
|
|
291
|
+
positionedEdges.push({
|
|
292
|
+
...e,
|
|
293
|
+
d: m.d,
|
|
294
|
+
fromSide: m.fromSide,
|
|
295
|
+
toSide: m.toSide,
|
|
296
|
+
labelX: m.labelX + (e.labelOffsetX ?? 0),
|
|
297
|
+
labelY: m.labelY + (e.labelOffsetY ?? 0),
|
|
298
|
+
labelAnchor: m.labelAnchor,
|
|
299
|
+
});
|
|
248
300
|
} else {
|
|
249
301
|
autoEdges.push(e);
|
|
250
302
|
}
|
|
@@ -291,7 +343,7 @@ export function layoutEdges(diag: CdlDiagram, nodes: LaidNode[], lanes: LaidLane
|
|
|
291
343
|
使う(le.from, le.fromSide);
|
|
292
344
|
使う(le.to, le.toSide);
|
|
293
345
|
}
|
|
294
|
-
const loopEdges = layoutSelfLoops(selfLoopEdges, nmap, occupiedBySide);
|
|
346
|
+
const loopEdges = layoutSelfLoops(selfLoopEdges, nmap, occupiedBySide, textMetrics);
|
|
295
347
|
|
|
296
348
|
// CAR-429 (CAR-426-B) ... 全 edge の起点 / 終点 / L 字 corner は node 辺 Y 中央固定 (offset=0 SSOT)。
|
|
297
349
|
// 旧 fan-in / fan-out / parallel の sidepoint 分散 (±60/±120) は削除済で、
|
|
@@ -649,7 +701,7 @@ export function layoutEdges(diag: CdlDiagram, nodes: LaidNode[], lanes: LaidLane
|
|
|
649
701
|
if (others.some((n) => segmentsCrossRect(segs, nodeRect(n)))) return true;
|
|
650
702
|
|
|
651
703
|
if (!hasRenderedLabel(c.e)) return false;
|
|
652
|
-
const boxW = computeLabelBoxW(c.e.label, c.e.sub);
|
|
704
|
+
const boxW = computeLabelBoxW(c.e.label, c.e.sub, textMetrics);
|
|
653
705
|
const boxH = labelPillH(c.e.sub);
|
|
654
706
|
const centerX = columnX - (labelPathClearanceHorizontal(c.e) + boxW / 2);
|
|
655
707
|
const pill = { x: centerX - boxW / 2, y: (exitY + returnY) / 2 - boxH / 2, w: boxW, h: boxH };
|
|
@@ -834,6 +886,7 @@ export function layoutEdges(diag: CdlDiagram, nodes: LaidNode[], lanes: LaidLane
|
|
|
834
886
|
{ x: endpointOffset.fromDx, y: endpointOffset.fromDy },
|
|
835
887
|
{ x: endpointOffset.toDx, y: endpointOffset.toDy },
|
|
836
888
|
avoidRects,
|
|
889
|
+
textMetrics,
|
|
837
890
|
);
|
|
838
891
|
const routed1 = route([]);
|
|
839
892
|
const { d, labelX, labelY, labelAnchor, labelAltX } = routed1;
|
|
@@ -915,7 +968,7 @@ export function layoutEdges(diag: CdlDiagram, nodes: LaidNode[], lanes: LaidLane
|
|
|
915
968
|
const pillById = new Map<string, Rect>();
|
|
916
969
|
for (const r of list) {
|
|
917
970
|
if (!hasRenderedLabel(r.e)) continue;
|
|
918
|
-
const w = computeLabelBoxW(r.e.label, r.e.sub);
|
|
971
|
+
const w = computeLabelBoxW(r.e.label, r.e.sub, textMetrics);
|
|
919
972
|
const h = labelPillH(r.e.sub);
|
|
920
973
|
pillById.set(r.e.id, { x: r.baseLabelX - w / 2, y: r.baseLabelY - h / 2, w, h });
|
|
921
974
|
}
|
|
@@ -923,7 +976,7 @@ export function layoutEdges(diag: CdlDiagram, nodes: LaidNode[], lanes: LaidLane
|
|
|
923
976
|
// 迂回線がその pill を貫く
|
|
924
977
|
for (const le of positionedEdges) {
|
|
925
978
|
if (!hasRenderedLabel(le)) continue;
|
|
926
|
-
const w = computeLabelBoxW(le.label, le.sub);
|
|
979
|
+
const w = computeLabelBoxW(le.label, le.sub, textMetrics);
|
|
927
980
|
const h = labelPillH(le.sub);
|
|
928
981
|
pillById.set(le.id, { x: le.labelX - w / 2, y: le.labelY - h / 2, w, h });
|
|
929
982
|
}
|
|
@@ -980,7 +1033,7 @@ export function layoutEdges(diag: CdlDiagram, nodes: LaidNode[], lanes: LaidLane
|
|
|
980
1033
|
|
|
981
1034
|
for (const r of flippable) {
|
|
982
1035
|
const ownSegs = segsByEdgeId.get(r.e.id)!;
|
|
983
|
-
const boxW = computeLabelBoxW(r.e.label, r.e.sub);
|
|
1036
|
+
const boxW = computeLabelBoxW(r.e.label, r.e.sub, textMetrics);
|
|
984
1037
|
const boxH = labelPillH(r.e.sub);
|
|
985
1038
|
const pillAt = (centerX: number) => ({
|
|
986
1039
|
x: centerX - boxW / 2,
|
|
@@ -1056,7 +1109,7 @@ export function layoutEdges(diag: CdlDiagram, nodes: LaidNode[], lanes: LaidLane
|
|
|
1056
1109
|
// 横の重なりで連結成分に割り、 成分ごとに縦を解く。
|
|
1057
1110
|
const boxes = list.map((r) => ({
|
|
1058
1111
|
cx: r.baseLabelX,
|
|
1059
|
-
halfW: computeLabelBoxW(r.e.label, r.e.sub) / 2,
|
|
1112
|
+
halfW: computeLabelBoxW(r.e.label, r.e.sub, textMetrics) / 2,
|
|
1060
1113
|
}));
|
|
1061
1114
|
const overlapsX = (i: number, j: number): boolean =>
|
|
1062
1115
|
Math.abs(boxes[i]!.cx - boxes[j]!.cx) < boxes[i]!.halfW + boxes[j]!.halfW + CLEARANCE_LABEL_LABEL;
|
|
@@ -1134,7 +1187,7 @@ export function layoutEdges(diag: CdlDiagram, nodes: LaidNode[], lanes: LaidLane
|
|
|
1134
1187
|
r,
|
|
1135
1188
|
baseX: r.baseLabelX,
|
|
1136
1189
|
baseY: r.baseLabelY,
|
|
1137
|
-
w: computeLabelBoxW(r.e.label, r.e.sub),
|
|
1190
|
+
w: computeLabelBoxW(r.e.label, r.e.sub, textMetrics),
|
|
1138
1191
|
h: labelPillH(r.e.sub),
|
|
1139
1192
|
}));
|
|
1140
1193
|
items.sort((a, b) => a.baseX - b.baseX);
|
|
@@ -1336,6 +1389,7 @@ export function bowSharedStraightPaths(
|
|
|
1336
1389
|
edges: LaidEdge[],
|
|
1337
1390
|
nodes: LaidNode[],
|
|
1338
1391
|
skipIds: ReadonlySet<string>,
|
|
1392
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
1339
1393
|
): void {
|
|
1340
1394
|
/**
|
|
1341
1395
|
* 向きを無視した線分の key。 A→B と B→A を同じ群にする。
|
|
@@ -1407,7 +1461,7 @@ export function bowSharedStraightPaths(
|
|
|
1407
1461
|
const groupIds = new Set(group.map((g) => g.e.id));
|
|
1408
1462
|
const labelRects = edges
|
|
1409
1463
|
.filter((x) => hasRenderedLabel(x) && !groupIds.has(x.id))
|
|
1410
|
-
.map((x) => computeLabelBBoxWorld(x));
|
|
1464
|
+
.map((x) => computeLabelBBoxWorld(x, textMetrics));
|
|
1411
1465
|
const nodeRects = nodes.map((n) => ({ x: n.cx - n.w / 2, y: n.cy - n.h / 2, w: n.w, h: n.h }));
|
|
1412
1466
|
const build = (seg: Entry["seg"], offset: number): string => {
|
|
1413
1467
|
// 2 次曲線は制御点の半分の高さを通るので、 狙いの膨らみの 2 倍を制御点に与える。
|
|
@@ -1577,7 +1631,7 @@ function sidePoint(n: LaidNode, side: Side): { x: number; y: number } {
|
|
|
1577
1631
|
/**
|
|
1578
1632
|
* orthogonal routing。
|
|
1579
1633
|
* - 横水平 (left/right pair で y 揃い) → 真横直線
|
|
1580
|
-
* - 横水平 (y 不一致) →
|
|
1634
|
+
* - 横水平 (y 不一致) → 角を丸めた L 字
|
|
1581
1635
|
* - 縦垂直 (top/bottom pair で x 揃い) → 真縦直線
|
|
1582
1636
|
* - 縦垂直 (x 不一致) → L 字 (途中で 90° 曲げ + 角丸)
|
|
1583
1637
|
* - その他 (top→right 等の交差) → L 字
|
|
@@ -1608,6 +1662,7 @@ function routePath(
|
|
|
1608
1662
|
* `widenDetourStubs` が同じ引数に本 list を足して呼び直す。
|
|
1609
1663
|
*/
|
|
1610
1664
|
avoidRects: readonly Rect[] = [],
|
|
1665
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
1611
1666
|
): {
|
|
1612
1667
|
d: string;
|
|
1613
1668
|
labelX: number;
|
|
@@ -1631,7 +1686,7 @@ function routePath(
|
|
|
1631
1686
|
// label 幅 = SSOT = spec.ts の computeLabelBoxW (CAR-431 で collections.ts labelBBox /
|
|
1632
1687
|
// render/edges.tsx / predict-bbox.ts の 4 箇所計算を集約)。 L 字 midX / midY を label 収納可能な
|
|
1633
1688
|
// 位置に押し出す判定で使う。 CLEARANCE_NODE_LABEL は clearance-constants.ts SSOT import。
|
|
1634
|
-
const labelBoxW = edge ? computeLabelBoxW(edge.label, edge.sub) : 0;
|
|
1689
|
+
const labelBoxW = edge ? computeLabelBoxW(edge.label, edge.sub, textMetrics) : 0;
|
|
1635
1690
|
const labelHalfW = labelBoxW / 2;
|
|
1636
1691
|
// CAR-518 SSOT ... solid = LABEL_INIT_CLEARANCE (34、 CAR-630 で 26 → 34 復帰)、 dotted-flow = LABEL_INIT_CLEARANCE_DOTTED_FLOW (62、 CAR-630 で 54 → 62 復帰)。
|
|
1637
1692
|
// routePath 内 labelY 計算 8 箇所で参照、 style 分岐を helper 経由で単一 SSOT 化する。
|
|
@@ -2380,7 +2435,11 @@ function routePath(
|
|
|
2380
2435
|
* @param edges 確定済 path を持つ辺。 `labelY` のみを書き換える
|
|
2381
2436
|
* @param nodes 節。 間隔が悪化する置き方を捨てるために使う
|
|
2382
2437
|
*/
|
|
2383
|
-
export function repositionParallelLabels(
|
|
2438
|
+
export function repositionParallelLabels(
|
|
2439
|
+
edges: LaidEdge[],
|
|
2440
|
+
nodes: LaidNode[],
|
|
2441
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
2442
|
+
): void {
|
|
2384
2443
|
// 図の全ての線 (実際に描かれる形)。 1 度だけ開いて使い回す。
|
|
2385
2444
|
const allSegs = new Map(
|
|
2386
2445
|
edges.map((e) => [e.id, flattenPathSegments(extractPathSegments(e.d))] as const),
|
|
@@ -2413,7 +2472,7 @@ export function repositionParallelLabels(edges: LaidEdge[], nodes: LaidNode[]):
|
|
|
2413
2472
|
const pairOrder = [...pair].sort(compareSharedPairOrder);
|
|
2414
2473
|
|
|
2415
2474
|
// 横に重なる label ごとに塊を作る (1 つだけの塊も含む)。
|
|
2416
|
-
const boxes = pair.map((e) => computeLabelBBoxWorld(e));
|
|
2475
|
+
const boxes = pair.map((e) => computeLabelBBoxWorld(e, textMetrics));
|
|
2417
2476
|
const componentOf = pair.map((_, i) => i);
|
|
2418
2477
|
for (let i = 0; i < pair.length; i++) {
|
|
2419
2478
|
for (let j = i + 1; j < pair.length; j++) {
|
|
@@ -2441,6 +2500,7 @@ export function repositionParallelLabels(edges: LaidEdge[], nodes: LaidNode[]):
|
|
|
2441
2500
|
allEdges: edges,
|
|
2442
2501
|
allSegs,
|
|
2443
2502
|
xRange,
|
|
2503
|
+
textMetrics,
|
|
2444
2504
|
});
|
|
2445
2505
|
for (let i = 0; i < members.length; i++) members[i]!.labelY = placed[i]!;
|
|
2446
2506
|
}
|
|
@@ -2473,11 +2533,21 @@ function placeAvoidingSiblingPaths(args: {
|
|
|
2473
2533
|
allEdges: LaidEdge[];
|
|
2474
2534
|
allSegs: Map<string, Array<{ x1: number; y1: number; x2: number; y2: number }>>;
|
|
2475
2535
|
xRange: Map<string, { min: number; max: number }>;
|
|
2536
|
+
textMetrics?: CdlTextMetrics;
|
|
2476
2537
|
}): number[] {
|
|
2477
|
-
const {
|
|
2538
|
+
const {
|
|
2539
|
+
members,
|
|
2540
|
+
pair,
|
|
2541
|
+
pairOrder,
|
|
2542
|
+
nodes,
|
|
2543
|
+
allEdges,
|
|
2544
|
+
allSegs,
|
|
2545
|
+
xRange,
|
|
2546
|
+
textMetrics = "proportional",
|
|
2547
|
+
} = args;
|
|
2478
2548
|
const sizes = members.map((e) => labelPillH(e.sub));
|
|
2479
2549
|
const wants = members.map((e) => e.labelY);
|
|
2480
|
-
const boxes = members.map((e) => computeLabelBBoxWorld(e));
|
|
2550
|
+
const boxes = members.map((e) => computeLabelBBoxWorld(e, textMetrics));
|
|
2481
2551
|
const all = members.map((_, i) => i);
|
|
2482
2552
|
const spread = (idx: number[]): number[] =>
|
|
2483
2553
|
spreadWithMinimalShift(
|
|
@@ -2761,7 +2831,11 @@ function inflate(r: Rect, pad: number): Rect {
|
|
|
2761
2831
|
* 余白は `LABEL_SLIDE_CLEARANCES` を大きい方から試し、 その余白を確保できる位置のうち
|
|
2762
2832
|
* 今の位置に一番近いものを採る。 どの余白でも置けない場合は動かさない。
|
|
2763
2833
|
*/
|
|
2764
|
-
export function slideLabelsAlongOwnPath(
|
|
2834
|
+
export function slideLabelsAlongOwnPath(
|
|
2835
|
+
edges: LaidEdge[],
|
|
2836
|
+
nodes: readonly LaidNode[],
|
|
2837
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
2838
|
+
): void {
|
|
2765
2839
|
const withLabel = edges.filter((e) => hasRenderedLabel(e));
|
|
2766
2840
|
if (withLabel.length === 0) return;
|
|
2767
2841
|
const segsById = new Map<string, FlatSeg[]>();
|
|
@@ -2769,7 +2843,7 @@ export function slideLabelsAlongOwnPath(edges: LaidEdge[], nodes: readonly LaidN
|
|
|
2769
2843
|
const nodeRects = nodes.map(nodeRect);
|
|
2770
2844
|
|
|
2771
2845
|
for (const e of withLabel) {
|
|
2772
|
-
const w = computeLabelBoxW(e.label, e.sub);
|
|
2846
|
+
const w = computeLabelBoxW(e.label, e.sub, textMetrics);
|
|
2773
2847
|
const h = labelPillH(e.sub);
|
|
2774
2848
|
if (w === 0 || h === 0) continue;
|
|
2775
2849
|
const own = segsById.get(e.id) ?? [];
|
|
@@ -2887,10 +2961,12 @@ export function hopEdgeCrossings(edges: LaidEdge[]): void {
|
|
|
2887
2961
|
}
|
|
2888
2962
|
|
|
2889
2963
|
for (const e of edges) {
|
|
2964
|
+
if (e.routing === "metro") continue;
|
|
2890
2965
|
// 部分経路が 2 つ以上ある形 (`M` が複数) は組み直せないので触らない。
|
|
2891
2966
|
if ((e.d.match(/M/g) ?? []).length !== 1) continue;
|
|
2892
2967
|
const own = extractPathSegments(e.d);
|
|
2893
2968
|
if (own.length === 0) continue;
|
|
2969
|
+
if (own.some((s) => s.cmd === "A")) continue;
|
|
2894
2970
|
const myEnds = ends.get(e.id) ?? [];
|
|
2895
2971
|
|
|
2896
2972
|
/** 区間 index → その区間で跨ぐ点 */
|
|
@@ -2995,7 +3071,7 @@ type Vertex = { x: number; y: number };
|
|
|
2995
3071
|
* 丸めた角は `L (手前の切り口) Q (角), (向こうの切り口)` の形で書かれており、 `Q` の制御点が
|
|
2996
3072
|
* 角そのものになる。 折れずに続く `L` は角ではないので点にしない。
|
|
2997
3073
|
*
|
|
2998
|
-
* 開けない形は 3 つ。 部分経路が 2 つ以上ある (`M` が複数)、
|
|
3074
|
+
* 開けない形は 3 つ。 部分経路が 2 つ以上ある (`M` が複数)、 弓か円弧 (`C` / `A`) を含む、 `Q` に制御点が
|
|
2999
3075
|
* 無い。 いずれも触らずに返す = 形そのものが意味を持つため、 組み直すと別の図になる。
|
|
3000
3076
|
*/
|
|
3001
3077
|
function pathVertices(d: string): Vertex[] | null {
|
|
@@ -3006,7 +3082,7 @@ function pathVertices(d: string): Vertex[] | null {
|
|
|
3006
3082
|
const out: Vertex[] = [{ x: first.x1, y: first.y1 }];
|
|
3007
3083
|
for (let i = 0; i < segs.length; i++) {
|
|
3008
3084
|
const s = segs[i]!;
|
|
3009
|
-
if (s.cmd === "C") return null;
|
|
3085
|
+
if (s.cmd === "C" || s.cmd === "A") return null;
|
|
3010
3086
|
if (s.cmd === "Q") {
|
|
3011
3087
|
const c = s.ctrl?.[0];
|
|
3012
3088
|
if (!c) return null;
|
|
@@ -3106,7 +3182,7 @@ export function separateSharedStraightRuns(edges: LaidEdge[], nodes: readonly La
|
|
|
3106
3182
|
const v = pathVertices(e.d);
|
|
3107
3183
|
if (v && v.length >= 2) verts.set(e.id, v);
|
|
3108
3184
|
}
|
|
3109
|
-
if (verts.size
|
|
3185
|
+
if (verts.size === 0) return;
|
|
3110
3186
|
|
|
3111
3187
|
const moved = new Set<string>();
|
|
3112
3188
|
|
|
@@ -3129,36 +3205,50 @@ export function separateSharedStraightRuns(edges: LaidEdge[], nodes: readonly La
|
|
|
3129
3205
|
for (let j = i + 1; j < edges.length; j++) {
|
|
3130
3206
|
const 前 = edges[i]!;
|
|
3131
3207
|
const 後 = edges[j]!;
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3208
|
+
if (前.routing === "metro" && 後.routing === "metro") continue;
|
|
3209
|
+
// 書き手が形を決めた路線図の線と重なる時は、自動線の方を動かす。
|
|
3210
|
+
const 固定 = 後.routing === "metro" ? 後 : 前;
|
|
3211
|
+
const 動かす = 後.routing === "metro" ? 前 : 後;
|
|
3212
|
+
const B = verts.get(動かす.id);
|
|
3213
|
+
if (!B) continue;
|
|
3214
|
+
const 固定の角 = verts.get(固定.id);
|
|
3215
|
+
const 固定の経路 = 固定の角
|
|
3216
|
+
? [固定の角]
|
|
3217
|
+
: 固定.routing === "metro"
|
|
3218
|
+
? extractPathSegments(固定.d)
|
|
3219
|
+
.filter((s) => s.cmd === "L")
|
|
3220
|
+
.map((s) => [{ x: s.x1, y: s.y1 }, { x: s.x2, y: s.y2 }])
|
|
3221
|
+
: [];
|
|
3222
|
+
if (固定の経路.length === 0) continue;
|
|
3135
3223
|
// 同じ節から出る線は幹線として重ねる (#211)。
|
|
3136
|
-
if (
|
|
3224
|
+
if (固定.from === 動かす.from) continue;
|
|
3137
3225
|
for (const ax of [0, 1] as const) {
|
|
3138
|
-
for (
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3226
|
+
for (const A of 固定の経路) {
|
|
3227
|
+
for (let ka = 0; ka + 1 < A.length; ka++) {
|
|
3228
|
+
if (!runIsAligned(A, ka, ax)) continue;
|
|
3229
|
+
for (let kb = 0; kb + 1 < B.length; kb++) {
|
|
3230
|
+
if (!runIsAligned(B, kb, ax)) continue;
|
|
3231
|
+
// 端点を含む区間は動かさない (線の根元が辺の中央から外れる)。
|
|
3232
|
+
if (kb === 0 || kb + 1 === B.length - 1) continue;
|
|
3233
|
+
const 座標A = ax === 0 ? A[ka]!.x : A[ka]!.y;
|
|
3234
|
+
const 座標B = ax === 0 ? B[kb]!.x : B[kb]!.y;
|
|
3235
|
+
if (Math.abs(座標A - 座標B) > SHARED_RUN_EPS) continue;
|
|
3236
|
+
const sa = runSpan(A, ka, ax);
|
|
3237
|
+
const sb = runSpan(B, kb, ax);
|
|
3238
|
+
if (Math.min(sa.hi, sb.hi) - Math.max(sa.lo, sb.lo) < SHARED_RUN_MIN) continue;
|
|
3239
|
+
|
|
3240
|
+
for (const delta of [SHARED_RUN_SEPARATION, -SHARED_RUN_SEPARATION]) {
|
|
3241
|
+
if (!fits(動かす, B, kb, ax, delta)) continue;
|
|
3242
|
+
if (ax === 0) {
|
|
3243
|
+
B[kb]!.x += delta;
|
|
3244
|
+
B[kb + 1]!.x += delta;
|
|
3245
|
+
} else {
|
|
3246
|
+
B[kb]!.y += delta;
|
|
3247
|
+
B[kb + 1]!.y += delta;
|
|
3248
|
+
}
|
|
3249
|
+
moved.add(動かす.id);
|
|
3250
|
+
break;
|
|
3159
3251
|
}
|
|
3160
|
-
moved.add(後.id);
|
|
3161
|
-
break;
|
|
3162
3252
|
}
|
|
3163
3253
|
}
|
|
3164
3254
|
}
|
|
@@ -3178,7 +3268,11 @@ export function separateSharedStraightRuns(edges: LaidEdge[], nodes: readonly La
|
|
|
3178
3268
|
* 起点は `fan-origin-single-point` のため動かさず、 最初の折れ直後の区間を平行移動する。
|
|
3179
3269
|
* 1 本しか出ない辺は `detourStub` が決めた箱との中間位置をそのまま保つ。
|
|
3180
3270
|
*/
|
|
3181
|
-
export function pullOutgoingFirstCorner(
|
|
3271
|
+
export function pullOutgoingFirstCorner(
|
|
3272
|
+
edges: LaidEdge[],
|
|
3273
|
+
nodes: readonly LaidNode[],
|
|
3274
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
3275
|
+
): void {
|
|
3182
3276
|
if (edges.length < 2) return;
|
|
3183
3277
|
|
|
3184
3278
|
const 束 = new Map<string, LaidEdge[]>();
|
|
@@ -3195,16 +3289,25 @@ export function pullOutgoingFirstCorner(edges: LaidEdge[], nodes: readonly LaidN
|
|
|
3195
3289
|
let 札を旧経路へ追従済み = false;
|
|
3196
3290
|
for (const list of 束.values()) {
|
|
3197
3291
|
if (list.length < 2) continue;
|
|
3292
|
+
type 最初の折れ候補 = { e: LaidEdge; pts: Vertex[] | null; 最初の長さ: number };
|
|
3198
3293
|
const 並び = list
|
|
3199
|
-
.
|
|
3294
|
+
.map((e): 最初の折れ候補 | null => {
|
|
3200
3295
|
const pts = pathVertices(e.d);
|
|
3201
|
-
if (
|
|
3202
|
-
|
|
3296
|
+
if (pts && pts.length >= 2) {
|
|
3297
|
+
return { e, pts, 最初の長さ: Math.hypot(pts[1]!.x - pts[0]!.x, pts[1]!.y - pts[0]!.y) };
|
|
3298
|
+
}
|
|
3299
|
+
if (e.routing !== "metro") return null;
|
|
3300
|
+
const first = extractPathSegments(e.d)[0];
|
|
3301
|
+
if (!first) return null;
|
|
3302
|
+
return { e, pts: null, 最初の長さ: Math.hypot(first.x2 - first.x1, first.y2 - first.y1) };
|
|
3203
3303
|
})
|
|
3304
|
+
.filter((x): x is 最初の折れ候補 => x !== null)
|
|
3204
3305
|
.sort((a, b) => b.最初の長さ - a.最初の長さ);
|
|
3205
3306
|
|
|
3206
3307
|
// 最長の線を引いても共有区間は縮まらないので、 2 本目以降だけを動かす。
|
|
3207
3308
|
for (const { e, pts, 最初の長さ } of 並び.slice(1)) {
|
|
3309
|
+
if (e.routing === "metro") continue;
|
|
3310
|
+
if (!pts) continue;
|
|
3208
3311
|
// 同じ 2 節を結ぶ線の分離は `bowSharedStraightPaths` の範囲。 迂回経路は重ねて残す。
|
|
3209
3312
|
if ((同じ組の数.get(`${e.from}->${e.to}`) ?? 0) > 1) continue;
|
|
3210
3313
|
if (pts.length < 3 || 最初の長さ <= EDGE_STUB_OUT + VERTICAL_DETOUR_CORNER) continue;
|
|
@@ -3240,10 +3343,10 @@ export function pullOutgoingFirstCorner(edges: LaidEdge[], nodes: readonly LaidN
|
|
|
3240
3343
|
// 旧経路に重なっていた札を先に確定する。 経路を動かした後では重なりが消え、 札だけが
|
|
3241
3344
|
// 元の位置に取り残される。
|
|
3242
3345
|
if (!札を旧経路へ追従済み) {
|
|
3243
|
-
slideLabelsAlongOwnPath(edges, nodes);
|
|
3346
|
+
slideLabelsAlongOwnPath(edges, nodes, textMetrics);
|
|
3244
3347
|
札を旧経路へ追従済み = true;
|
|
3245
3348
|
}
|
|
3246
|
-
const labelBox = computeLabelBBoxWorld(e);
|
|
3349
|
+
const labelBox = computeLabelBBoxWorld(e, textMetrics);
|
|
3247
3350
|
const label = { x: labelBox.x + labelBox.w / 2, y: labelBox.y + labelBox.h / 2 };
|
|
3248
3351
|
const 線分までの距離2 = (a: Vertex, b: Vertex): number => {
|
|
3249
3352
|
const dx = b.x - a.x;
|
|
@@ -3306,9 +3409,16 @@ const MIXED_ROOT_OVERLAP_MIN = 24;
|
|
|
3306
3409
|
* 分かれるので、 分かれる点が交差として数えられる。 端点の接触を除く数え方も試したが、
|
|
3307
3410
|
* 離れる本数は増えないまま別の図で交差が出た。 収束する前の段階で分けるには経路の引き方
|
|
3308
3411
|
* そのものを変える必要があり、 それは後処理では届かない。
|
|
3412
|
+
*
|
|
3413
|
+
* @returns 差し替えを残した線ごとの終点移動量。 札は経路を動かす pass が全て終わった後に
|
|
3414
|
+
* `followLabelsToFannedTails` がこの量へ追従させる (#1000)。
|
|
3309
3415
|
*/
|
|
3310
|
-
export function fanIncomingEndpoints(
|
|
3311
|
-
|
|
3416
|
+
export function fanIncomingEndpoints(
|
|
3417
|
+
edges: LaidEdge[],
|
|
3418
|
+
nodes: readonly LaidNode[],
|
|
3419
|
+
): ReadonlyMap<string, { dx: number; dy: number }> {
|
|
3420
|
+
const 終点の移動 = new Map<string, { dx: number; dy: number }>();
|
|
3421
|
+
if (edges.length < 2) return 終点の移動;
|
|
3312
3422
|
const nmap = new Map(nodes.map((n) => [n.id, n]));
|
|
3313
3423
|
const 平坦な区間 = new Map(edges.map((e) => [e.id, flattenPathSegments(extractPathSegments(e.d))] as const));
|
|
3314
3424
|
|
|
@@ -3399,6 +3509,7 @@ export function fanIncomingEndpoints(edges: LaidEdge[], nodes: readonly LaidNode
|
|
|
3399
3509
|
: k * 間隔 - 中心;
|
|
3400
3510
|
if (Math.abs(d) < 0.001) continue;
|
|
3401
3511
|
const e = 並び[k]!.e;
|
|
3512
|
+
if (e.routing === "metro") continue;
|
|
3402
3513
|
// 交差が増えるなら戻す。 判定は軸と同じ数え方を使う = 独自の数え方は軸と食い違い、
|
|
3403
3514
|
// 軸が 3 件を数えている状況で 0 件と判定して通してしまった (実測)。
|
|
3404
3515
|
const 前 = e.d;
|
|
@@ -3412,6 +3523,108 @@ export function fanIncomingEndpoints(edges: LaidEdge[], nodes: readonly LaidNode
|
|
|
3412
3523
|
if (countEdgeCrossings(edges).count <= 前の交差) break;
|
|
3413
3524
|
e.d = 前;
|
|
3414
3525
|
}
|
|
3526
|
+
if (e.d !== 前) 終点の移動.set(e.id, 横 ? { dx: 0, dy: d } : { dx: d, dy: 0 });
|
|
3527
|
+
}
|
|
3528
|
+
}
|
|
3529
|
+
|
|
3530
|
+
return 終点の移動;
|
|
3531
|
+
}
|
|
3532
|
+
|
|
3533
|
+
/**
|
|
3534
|
+
* 同じ辺へ入るために終点をずらした線の札を、 最後の区間から離れた位置へ追従させる (#1000)。
|
|
3535
|
+
*
|
|
3536
|
+
* 経路を動かす pass が全て終わった後の形で、 最後の区間を覆っている札だけを見る。
|
|
3537
|
+
* まず終点と同じ量だけ動かし、 置けなければ最後の区間の反対側へ折り返す。
|
|
3538
|
+
* 追従側で足す条件は、 候補が最後の区間と交わらないことだけ。
|
|
3539
|
+
*
|
|
3540
|
+
* 置けるかは後段の `resolveLabelClearanceAfterPathPasses` と同じ
|
|
3541
|
+
* `札の移動を塞ぐ相手を探す` で決める。 別の判定で余白を割る位置へ置くと、 後段が同じ札を
|
|
3542
|
+
* 押し直すため (#1002)。
|
|
3543
|
+
*
|
|
3544
|
+
* 自分の線上に載っている overlay は判定の先頭で外す。 線から押し出された overlay は追従させる =
|
|
3545
|
+
* 一律に外すと、 #1000 で直した「押し出された overlay の札が最後の区間を覆う」 形が戻る。
|
|
3546
|
+
*
|
|
3547
|
+
* @param obstacles 箱と縦列の題の矩形。 後段の逃がし pass に渡すものと同じ集まり。
|
|
3548
|
+
*/
|
|
3549
|
+
export function followLabelsToFannedTails(
|
|
3550
|
+
edges: LaidEdge[],
|
|
3551
|
+
obstacles: readonly BBox[],
|
|
3552
|
+
shifts: ReadonlyMap<string, { dx: number; dy: number }>,
|
|
3553
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
3554
|
+
): void {
|
|
3555
|
+
if (shifts.size === 0) return;
|
|
3556
|
+
const 測り方 = 線の測り方を作る();
|
|
3557
|
+
const labels: BBox[] = edges.filter(hasRenderedLabel).map((e) => ({
|
|
3558
|
+
kind: "edge-label",
|
|
3559
|
+
id: e.id,
|
|
3560
|
+
...computeLabelBBoxWorld(e, textMetrics),
|
|
3561
|
+
}));
|
|
3562
|
+
const labelIndexByEdgeId = new Map(labels.map((label, index) => [label.id, index] as const));
|
|
3563
|
+
const edgeById = new Map(edges.map((e) => [e.id, e] as const));
|
|
3564
|
+
const 周り: 札の周り = {
|
|
3565
|
+
箱たち: obstacles.filter((o) => o.kind === "node" || o.kind === "lane-label"),
|
|
3566
|
+
labels,
|
|
3567
|
+
edges,
|
|
3568
|
+
edgeById,
|
|
3569
|
+
測り方,
|
|
3570
|
+
};
|
|
3571
|
+
|
|
3572
|
+
for (const e of edges) {
|
|
3573
|
+
const 移動 = shifts.get(e.id);
|
|
3574
|
+
if (!移動 || !hasRenderedLabel(e)) continue;
|
|
3575
|
+
|
|
3576
|
+
const labelIndex = labelIndexByEdgeId.get(e.id);
|
|
3577
|
+
if (labelIndex === undefined) continue;
|
|
3578
|
+
const 元の札 = labels[labelIndex]!;
|
|
3579
|
+
const 自分の区間 = 測り方.区間(e.id, e.d);
|
|
3580
|
+
if (
|
|
3581
|
+
e.overlay &&
|
|
3582
|
+
measurePathLabelGapSegs(自分の区間, {
|
|
3583
|
+
x: 元の札.x + 元の札.w / 2,
|
|
3584
|
+
y: 元の札.y + 元の札.h / 2,
|
|
3585
|
+
w: 0,
|
|
3586
|
+
h: 0,
|
|
3587
|
+
}) <= OVERLAY_LABEL_ON_PATH_TOLERANCE
|
|
3588
|
+
) continue;
|
|
3589
|
+
|
|
3590
|
+
const 最後 = 自分の区間.at(-1);
|
|
3591
|
+
if (!最後) continue;
|
|
3592
|
+
if (!segmentsCrossRect([最後], 元の札)) continue;
|
|
3593
|
+
|
|
3594
|
+
const 候補a = {
|
|
3595
|
+
labelX: e.labelX + 移動.dx,
|
|
3596
|
+
labelY: e.labelY + 移動.dy,
|
|
3597
|
+
labelAnchor: e.labelAnchor,
|
|
3598
|
+
};
|
|
3599
|
+
const 候補aの箱 = computeLabelBBoxWorld({ ...e, ...候補a }, textMetrics);
|
|
3600
|
+
const 最後は縦 = Math.abs(最後.x2 - 最後.x1) <= Math.abs(最後.y2 - 最後.y1);
|
|
3601
|
+
const 候補 = [
|
|
3602
|
+
候補a,
|
|
3603
|
+
{
|
|
3604
|
+
labelX: 最後は縦
|
|
3605
|
+
? 最後.x1 * 2 - (候補aの箱.x + 候補aの箱.w / 2)
|
|
3606
|
+
: 候補aの箱.x + 候補aの箱.w / 2,
|
|
3607
|
+
labelY: 最後は縦
|
|
3608
|
+
? 候補aの箱.y + 候補aの箱.h / 2
|
|
3609
|
+
: 最後.y1 * 2 - (候補aの箱.y + 候補aの箱.h / 2),
|
|
3610
|
+
labelAnchor: "middle" as const,
|
|
3611
|
+
},
|
|
3612
|
+
];
|
|
3613
|
+
|
|
3614
|
+
for (const c of 候補) {
|
|
3615
|
+
const 札: BBox = {
|
|
3616
|
+
kind: "edge-label",
|
|
3617
|
+
id: e.id,
|
|
3618
|
+
...computeLabelBBoxWorld({ ...e, ...c }, textMetrics),
|
|
3619
|
+
};
|
|
3620
|
+
if (segmentsCrossRect([最後], 札)) continue;
|
|
3621
|
+
if (札の移動を塞ぐ相手を探す(周り, 元の札, 札) !== null) continue;
|
|
3622
|
+
|
|
3623
|
+
e.labelX = c.labelX;
|
|
3624
|
+
e.labelY = c.labelY;
|
|
3625
|
+
e.labelAnchor = c.labelAnchor;
|
|
3626
|
+
labels[labelIndex] = 札;
|
|
3627
|
+
break;
|
|
3415
3628
|
}
|
|
3416
3629
|
}
|
|
3417
3630
|
}
|
|
@@ -3437,7 +3650,7 @@ function 辺から離れた側の位置(e: LaidEdge, 横: boolean): number | nul
|
|
|
3437
3650
|
/**
|
|
3438
3651
|
* 経路の末尾を「辺に沿って助走 → 辺へ垂直に入る」 の形に差し替える。
|
|
3439
3652
|
*
|
|
3440
|
-
* 差し替えられない形なら `null` を返す。
|
|
3653
|
+
* 差し替えられない形なら `null` を返す。 弓か円弧 (`C` / `A`) を含む経路、 部分経路が 2 つ以上ある形、
|
|
3441
3654
|
* 最後が辺へ垂直に入っていない形、 助走を取れないほど短い経路、 差し替えた先が箱に当たる
|
|
3442
3655
|
* 場合が該当する。
|
|
3443
3656
|
*/
|
|
@@ -3451,7 +3664,7 @@ function 末尾を辺に沿ってずらす(
|
|
|
3451
3664
|
if ((e.d.match(/M/g) ?? []).length !== 1) return null;
|
|
3452
3665
|
const segs = extractPathSegments(e.d);
|
|
3453
3666
|
if (segs.length === 0) return null;
|
|
3454
|
-
if (segs.some((s) => s.cmd === "C")) return null;
|
|
3667
|
+
if (segs.some((s) => s.cmd === "C" || s.cmd === "A")) return null;
|
|
3455
3668
|
|
|
3456
3669
|
const last = segs[segs.length - 1]!;
|
|
3457
3670
|
if (last.cmd !== "L") return null;
|
package/src/layout/head-label.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* 使う。 弦の向きを使うと、大きく膨らむ曲線で線が端へ入ってくる向きと別の向きが出る。
|
|
18
18
|
*/
|
|
19
19
|
import { EDGE_LABEL_TEXT } from "../label-text";
|
|
20
|
-
import { EDGE_HEAD_MARKER_SIZE } from "../types";
|
|
20
|
+
import { EDGE_HEAD_MARKER_SIZE, type CdlTextMetrics } from "../types";
|
|
21
21
|
import { extractPathSegments } from "./collisions";
|
|
22
22
|
import { hasRenderedLabel } from "./spec";
|
|
23
23
|
import { measureTextWidth } from "./text-width";
|
|
@@ -132,12 +132,12 @@ function 向きを取る(end: 点, 候補: ReadonlyArray<点 | undefined>): { en
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
/**
|
|
135
|
-
* 端の点と、線がその端へ入ってくる向き。
|
|
135
|
+
* 端の点と、線がその端へ入ってくる向き。 SVG が端の印を回す向きと同じ規則で取る。
|
|
136
136
|
*
|
|
137
137
|
* 行き先の端は最後の区間から、出どころの端は最初の区間を逆にたどって取る。 端の区間の長さが
|
|
138
138
|
* 0 の時は隣の区間へ進む。 向きが取れない (区間が無い / 全て長さ 0) 時は `null`。
|
|
139
139
|
*/
|
|
140
|
-
function
|
|
140
|
+
export function 線の端の向き(d: string, 端: 線の端): { end: 点; ux: number; uy: number } | null {
|
|
141
141
|
const segs = extractPathSegments(d);
|
|
142
142
|
if (segs.length === 0) return null;
|
|
143
143
|
if (端 === "head") {
|
|
@@ -146,7 +146,11 @@ function 端での向き(d: string, 端: 線の端): { end: 点; ux: number; uy:
|
|
|
146
146
|
const 候補 = segs
|
|
147
147
|
.slice()
|
|
148
148
|
.reverse()
|
|
149
|
-
.flatMap((s) =>
|
|
149
|
+
.flatMap((s) => {
|
|
150
|
+
const 制御点 =
|
|
151
|
+
s.cmd === "C" ? [s.ctrl?.[1], s.ctrl?.[0]] : s.cmd === "Q" ? [s.ctrl?.[0]] : [];
|
|
152
|
+
return [...制御点, { x: s.x1, y: s.y1 }];
|
|
153
|
+
});
|
|
150
154
|
return 向きを取る(end, 候補);
|
|
151
155
|
}
|
|
152
156
|
const first = segs[0]!;
|
|
@@ -167,10 +171,11 @@ export function 端の字を置く(
|
|
|
167
171
|
labelY?: number;
|
|
168
172
|
},
|
|
169
173
|
端: 線の端 = "head",
|
|
174
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
170
175
|
): 端の字の置き場所 | null {
|
|
171
176
|
const text = (端 === "head" ? edge.headLabel : edge.tailLabel)?.trim();
|
|
172
177
|
if (!text) return null;
|
|
173
|
-
const 向き =
|
|
178
|
+
const 向き = 線の端の向き(edge.d, 端);
|
|
174
179
|
if (!向き) return null;
|
|
175
180
|
const { end, ux, uy } = 向き;
|
|
176
181
|
const bx = end.x - ux * 端の字を戻す距離;
|
|
@@ -179,6 +184,7 @@ export function 端の字を置く(
|
|
|
179
184
|
fontSize: 端の字の大きさ,
|
|
180
185
|
fontWeight: EDGE_LABEL_TEXT.sub.fontWeight,
|
|
181
186
|
fontFamily: "mono",
|
|
187
|
+
textMetrics,
|
|
182
188
|
});
|
|
183
189
|
const h = 箱の高さ;
|
|
184
190
|
// 札の中心。 札を描かない線と、位置がまだ無い線は `null`
|