@cardenelabs/cdl 0.122.0 → 0.124.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 +28 -1
- package/README.md +70 -1
- package/dist/index.cjs +1125 -618
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +38 -26
- package/dist/index.d.ts +38 -26
- package/dist/index.js +1125 -619
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +894 -404
- 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 +894 -404
- package/dist/react.js.map +1 -1
- package/dist/{render-U_pC-Ksh.d.cts → render-U6omSNH2.d.cts} +23 -4
- package/dist/{render-U_pC-Ksh.d.ts → render-U6omSNH2.d.ts} +23 -4
- package/package.json +9 -12
- package/src/builder.ts +5 -0
- package/src/index.ts +2 -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/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/text-width.ts +17 -3
- package/src/kinds/tree.tsx +25 -7
- package/src/layout/collisions.ts +21 -8
- package/src/layout/curve-path.ts +3 -2
- package/src/layout/edges.ts +173 -29
- package/src/layout/head-label.ts +3 -1
- package/src/layout/lanes.ts +3 -2
- package/src/layout/metro-path.ts +3 -2
- 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 +20 -6
- package/src/layout/text-width.ts +25 -8
- package/src/layout.ts +39 -15
- package/src/presets.ts +69 -43
- package/src/render/edges.tsx +18 -3
- package/src/render/interactive-panel.tsx +34 -7
- package/src/render/nodes.tsx +97 -48
- package/src/render/stage.tsx +3 -0
- package/src/text-metrics.ts +9 -0
- package/src/types.ts +22 -2
- package/src/visual-validate.ts +67 -17
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,
|
|
@@ -211,7 +220,12 @@ function detourColumnFor(blockers: LaidNode[], side: DetourSide): number {
|
|
|
211
220
|
: Math.max(...blockers.map((n) => n.cx + n.w / 2)) + VERTICAL_DETOUR_GAP;
|
|
212
221
|
}
|
|
213
222
|
|
|
214
|
-
export function layoutEdges(
|
|
223
|
+
export function layoutEdges(
|
|
224
|
+
diag: CdlDiagram,
|
|
225
|
+
nodes: LaidNode[],
|
|
226
|
+
lanes: LaidLane[] = [],
|
|
227
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
228
|
+
): LaidEdge[] {
|
|
215
229
|
// labelMargin = author 追加余白、 path 中点 → label の垂直距離に加算 (上向き label は -=、 下向きは +=)。
|
|
216
230
|
// viewport.labelMargin 明示時はその差分、 未指定なら viewport.gap → DEFAULT_LABEL_MARGIN の順で fallback。
|
|
217
231
|
const labelMarginExtra =
|
|
@@ -258,7 +272,7 @@ export function layoutEdges(diag: CdlDiagram, nodes: LaidNode[], lanes: LaidLane
|
|
|
258
272
|
const to = nmap.get(e.to);
|
|
259
273
|
if (!from) throw new Error(`cdl layout: edge "${e.id}" の from "${e.from}" が node に無い`);
|
|
260
274
|
if (!to) throw new Error(`cdl layout: edge "${e.id}" の to "${e.to}" が node に無い`);
|
|
261
|
-
const c = curvePath(from, to, e, labelMarginExtra);
|
|
275
|
+
const c = curvePath(from, to, e, labelMarginExtra, textMetrics);
|
|
262
276
|
positionedEdges.push({
|
|
263
277
|
...e,
|
|
264
278
|
d: c.d,
|
|
@@ -273,7 +287,7 @@ export function layoutEdges(diag: CdlDiagram, nodes: LaidNode[], lanes: LaidLane
|
|
|
273
287
|
const to = nmap.get(e.to);
|
|
274
288
|
if (!from) throw new Error(`cdl layout: edge "${e.id}" の from "${e.from}" が node に無い`);
|
|
275
289
|
if (!to) throw new Error(`cdl layout: edge "${e.id}" の to "${e.to}" が node に無い`);
|
|
276
|
-
const m = metroPath(from, to, e, labelMarginExtra);
|
|
290
|
+
const m = metroPath(from, to, e, labelMarginExtra, textMetrics);
|
|
277
291
|
positionedEdges.push({
|
|
278
292
|
...e,
|
|
279
293
|
d: m.d,
|
|
@@ -329,7 +343,7 @@ export function layoutEdges(diag: CdlDiagram, nodes: LaidNode[], lanes: LaidLane
|
|
|
329
343
|
使う(le.from, le.fromSide);
|
|
330
344
|
使う(le.to, le.toSide);
|
|
331
345
|
}
|
|
332
|
-
const loopEdges = layoutSelfLoops(selfLoopEdges, nmap, occupiedBySide);
|
|
346
|
+
const loopEdges = layoutSelfLoops(selfLoopEdges, nmap, occupiedBySide, textMetrics);
|
|
333
347
|
|
|
334
348
|
// CAR-429 (CAR-426-B) ... 全 edge の起点 / 終点 / L 字 corner は node 辺 Y 中央固定 (offset=0 SSOT)。
|
|
335
349
|
// 旧 fan-in / fan-out / parallel の sidepoint 分散 (±60/±120) は削除済で、
|
|
@@ -687,7 +701,7 @@ export function layoutEdges(diag: CdlDiagram, nodes: LaidNode[], lanes: LaidLane
|
|
|
687
701
|
if (others.some((n) => segmentsCrossRect(segs, nodeRect(n)))) return true;
|
|
688
702
|
|
|
689
703
|
if (!hasRenderedLabel(c.e)) return false;
|
|
690
|
-
const boxW = computeLabelBoxW(c.e.label, c.e.sub);
|
|
704
|
+
const boxW = computeLabelBoxW(c.e.label, c.e.sub, textMetrics);
|
|
691
705
|
const boxH = labelPillH(c.e.sub);
|
|
692
706
|
const centerX = columnX - (labelPathClearanceHorizontal(c.e) + boxW / 2);
|
|
693
707
|
const pill = { x: centerX - boxW / 2, y: (exitY + returnY) / 2 - boxH / 2, w: boxW, h: boxH };
|
|
@@ -872,6 +886,7 @@ export function layoutEdges(diag: CdlDiagram, nodes: LaidNode[], lanes: LaidLane
|
|
|
872
886
|
{ x: endpointOffset.fromDx, y: endpointOffset.fromDy },
|
|
873
887
|
{ x: endpointOffset.toDx, y: endpointOffset.toDy },
|
|
874
888
|
avoidRects,
|
|
889
|
+
textMetrics,
|
|
875
890
|
);
|
|
876
891
|
const routed1 = route([]);
|
|
877
892
|
const { d, labelX, labelY, labelAnchor, labelAltX } = routed1;
|
|
@@ -953,7 +968,7 @@ export function layoutEdges(diag: CdlDiagram, nodes: LaidNode[], lanes: LaidLane
|
|
|
953
968
|
const pillById = new Map<string, Rect>();
|
|
954
969
|
for (const r of list) {
|
|
955
970
|
if (!hasRenderedLabel(r.e)) continue;
|
|
956
|
-
const w = computeLabelBoxW(r.e.label, r.e.sub);
|
|
971
|
+
const w = computeLabelBoxW(r.e.label, r.e.sub, textMetrics);
|
|
957
972
|
const h = labelPillH(r.e.sub);
|
|
958
973
|
pillById.set(r.e.id, { x: r.baseLabelX - w / 2, y: r.baseLabelY - h / 2, w, h });
|
|
959
974
|
}
|
|
@@ -961,7 +976,7 @@ export function layoutEdges(diag: CdlDiagram, nodes: LaidNode[], lanes: LaidLane
|
|
|
961
976
|
// 迂回線がその pill を貫く
|
|
962
977
|
for (const le of positionedEdges) {
|
|
963
978
|
if (!hasRenderedLabel(le)) continue;
|
|
964
|
-
const w = computeLabelBoxW(le.label, le.sub);
|
|
979
|
+
const w = computeLabelBoxW(le.label, le.sub, textMetrics);
|
|
965
980
|
const h = labelPillH(le.sub);
|
|
966
981
|
pillById.set(le.id, { x: le.labelX - w / 2, y: le.labelY - h / 2, w, h });
|
|
967
982
|
}
|
|
@@ -1018,7 +1033,7 @@ export function layoutEdges(diag: CdlDiagram, nodes: LaidNode[], lanes: LaidLane
|
|
|
1018
1033
|
|
|
1019
1034
|
for (const r of flippable) {
|
|
1020
1035
|
const ownSegs = segsByEdgeId.get(r.e.id)!;
|
|
1021
|
-
const boxW = computeLabelBoxW(r.e.label, r.e.sub);
|
|
1036
|
+
const boxW = computeLabelBoxW(r.e.label, r.e.sub, textMetrics);
|
|
1022
1037
|
const boxH = labelPillH(r.e.sub);
|
|
1023
1038
|
const pillAt = (centerX: number) => ({
|
|
1024
1039
|
x: centerX - boxW / 2,
|
|
@@ -1094,7 +1109,7 @@ export function layoutEdges(diag: CdlDiagram, nodes: LaidNode[], lanes: LaidLane
|
|
|
1094
1109
|
// 横の重なりで連結成分に割り、 成分ごとに縦を解く。
|
|
1095
1110
|
const boxes = list.map((r) => ({
|
|
1096
1111
|
cx: r.baseLabelX,
|
|
1097
|
-
halfW: computeLabelBoxW(r.e.label, r.e.sub) / 2,
|
|
1112
|
+
halfW: computeLabelBoxW(r.e.label, r.e.sub, textMetrics) / 2,
|
|
1098
1113
|
}));
|
|
1099
1114
|
const overlapsX = (i: number, j: number): boolean =>
|
|
1100
1115
|
Math.abs(boxes[i]!.cx - boxes[j]!.cx) < boxes[i]!.halfW + boxes[j]!.halfW + CLEARANCE_LABEL_LABEL;
|
|
@@ -1172,7 +1187,7 @@ export function layoutEdges(diag: CdlDiagram, nodes: LaidNode[], lanes: LaidLane
|
|
|
1172
1187
|
r,
|
|
1173
1188
|
baseX: r.baseLabelX,
|
|
1174
1189
|
baseY: r.baseLabelY,
|
|
1175
|
-
w: computeLabelBoxW(r.e.label, r.e.sub),
|
|
1190
|
+
w: computeLabelBoxW(r.e.label, r.e.sub, textMetrics),
|
|
1176
1191
|
h: labelPillH(r.e.sub),
|
|
1177
1192
|
}));
|
|
1178
1193
|
items.sort((a, b) => a.baseX - b.baseX);
|
|
@@ -1374,6 +1389,7 @@ export function bowSharedStraightPaths(
|
|
|
1374
1389
|
edges: LaidEdge[],
|
|
1375
1390
|
nodes: LaidNode[],
|
|
1376
1391
|
skipIds: ReadonlySet<string>,
|
|
1392
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
1377
1393
|
): void {
|
|
1378
1394
|
/**
|
|
1379
1395
|
* 向きを無視した線分の key。 A→B と B→A を同じ群にする。
|
|
@@ -1445,7 +1461,7 @@ export function bowSharedStraightPaths(
|
|
|
1445
1461
|
const groupIds = new Set(group.map((g) => g.e.id));
|
|
1446
1462
|
const labelRects = edges
|
|
1447
1463
|
.filter((x) => hasRenderedLabel(x) && !groupIds.has(x.id))
|
|
1448
|
-
.map((x) => computeLabelBBoxWorld(x));
|
|
1464
|
+
.map((x) => computeLabelBBoxWorld(x, textMetrics));
|
|
1449
1465
|
const nodeRects = nodes.map((n) => ({ x: n.cx - n.w / 2, y: n.cy - n.h / 2, w: n.w, h: n.h }));
|
|
1450
1466
|
const build = (seg: Entry["seg"], offset: number): string => {
|
|
1451
1467
|
// 2 次曲線は制御点の半分の高さを通るので、 狙いの膨らみの 2 倍を制御点に与える。
|
|
@@ -1506,10 +1522,98 @@ interface Rect {
|
|
|
1506
1522
|
h: number;
|
|
1507
1523
|
}
|
|
1508
1524
|
|
|
1525
|
+
/** 経路の角の点。 */
|
|
1526
|
+
type Vertex = { x: number; y: number };
|
|
1527
|
+
|
|
1509
1528
|
function nodeRect(n: LaidNode): Rect {
|
|
1510
1529
|
return { x: n.cx - n.w / 2, y: n.cy - n.h / 2, w: n.w, h: n.h };
|
|
1511
1530
|
}
|
|
1512
1531
|
|
|
1532
|
+
/** 絵の `edge-node-cross` と同じ判定で、経路が貫く箱を 1 箱 1 回で数える。 */
|
|
1533
|
+
function countPathObstacleCrossings(d: string, obstacles: readonly LaidNode[]): number {
|
|
1534
|
+
const segments = flattenPathSegments(extractPathSegments(d));
|
|
1535
|
+
return obstacles.filter((obstacle) => segmentsCrossRect(segments, nodeRect(obstacle))).length;
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
type BackDetourCandidate = {
|
|
1539
|
+
d: string;
|
|
1540
|
+
labelX: number;
|
|
1541
|
+
crossings: number;
|
|
1542
|
+
length: number;
|
|
1543
|
+
};
|
|
1544
|
+
|
|
1545
|
+
/** top-top の戻り線を、帯に掛かる箱を避けた最も近い外側の列へ回す候補を作る。 */
|
|
1546
|
+
function makeBackDetourCandidate(
|
|
1547
|
+
A: Vertex,
|
|
1548
|
+
B: Vertex,
|
|
1549
|
+
from: LaidNode,
|
|
1550
|
+
to: LaidNode,
|
|
1551
|
+
obstacles: readonly LaidNode[],
|
|
1552
|
+
detourY: number,
|
|
1553
|
+
side: DetourSide,
|
|
1554
|
+
): BackDetourCandidate {
|
|
1555
|
+
const lowIsA = A.y >= B.y;
|
|
1556
|
+
const low = lowIsA ? A : B;
|
|
1557
|
+
const lowBendY = low.y - VERTICAL_DETOUR_EXIT;
|
|
1558
|
+
const bandTop = Math.min(detourY, lowBendY);
|
|
1559
|
+
const bandBottom = Math.max(detourY, lowBendY);
|
|
1560
|
+
const bandObstacles = obstacles
|
|
1561
|
+
.map(nodeRect)
|
|
1562
|
+
.filter((rect) => rect.y + rect.h >= bandTop && rect.y <= bandBottom);
|
|
1563
|
+
let columnX = detourColumnFor([from, to], side);
|
|
1564
|
+
while (true) {
|
|
1565
|
+
const blockers = bandObstacles.filter(
|
|
1566
|
+
(rect) => rect.x - VERTICAL_DETOUR_GAP < columnX
|
|
1567
|
+
&& columnX < rect.x + rect.w + VERTICAL_DETOUR_GAP,
|
|
1568
|
+
);
|
|
1569
|
+
if (blockers.length === 0) break;
|
|
1570
|
+
columnX = side === "left"
|
|
1571
|
+
? Math.min(...blockers.map((rect) => rect.x - VERTICAL_DETOUR_GAP))
|
|
1572
|
+
: Math.max(...blockers.map((rect) => rect.x + rect.w + VERTICAL_DETOUR_GAP));
|
|
1573
|
+
}
|
|
1574
|
+
const bendAY = lowIsA ? lowBendY : detourY;
|
|
1575
|
+
const bendBY = lowIsA ? detourY : lowBendY;
|
|
1576
|
+
const vertices: Vertex[] = [
|
|
1577
|
+
A,
|
|
1578
|
+
{ x: A.x, y: bendAY },
|
|
1579
|
+
{ x: columnX, y: bendAY },
|
|
1580
|
+
{ x: columnX, y: bendBY },
|
|
1581
|
+
{ x: B.x, y: bendBY },
|
|
1582
|
+
B,
|
|
1583
|
+
];
|
|
1584
|
+
const d = verticesToPath(vertices);
|
|
1585
|
+
const high = lowIsA ? B : A;
|
|
1586
|
+
return {
|
|
1587
|
+
d,
|
|
1588
|
+
labelX: (high.x + columnX) / 2,
|
|
1589
|
+
crossings: countPathObstacleCrossings(d, obstacles),
|
|
1590
|
+
length: vertices.slice(1).reduce((sum, vertex, index) => {
|
|
1591
|
+
const previous = vertices[index]!;
|
|
1592
|
+
return sum + Math.hypot(vertex.x - previous.x, vertex.y - previous.y);
|
|
1593
|
+
}, 0),
|
|
1594
|
+
};
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
/** 交差数、長さ、終点へ入る線の並びを保つ側の順で、左右候補を 1 つに決める。 */
|
|
1598
|
+
function chooseBackDetourCandidate(
|
|
1599
|
+
A: Vertex,
|
|
1600
|
+
B: Vertex,
|
|
1601
|
+
from: LaidNode,
|
|
1602
|
+
to: LaidNode,
|
|
1603
|
+
obstacles: readonly LaidNode[],
|
|
1604
|
+
detourY: number,
|
|
1605
|
+
): BackDetourCandidate {
|
|
1606
|
+
const left = makeBackDetourCandidate(A, B, from, to, obstacles, detourY, "left");
|
|
1607
|
+
const right = makeBackDetourCandidate(A, B, from, to, obstacles, detourY, "right");
|
|
1608
|
+
if (left.crossings !== right.crossings) return left.crossings < right.crossings ? left : right;
|
|
1609
|
+
if (Math.abs(left.length - right.length) >= 0.5) return left.length < right.length ? left : right;
|
|
1610
|
+
|
|
1611
|
+
// 同じ長さなら始点側へ回すと、同じ上辺へ入る線の左右の並びを従来から入れ替えずに済む。
|
|
1612
|
+
// 同じ列は左右の基準を持たないため、縦 detour と同じ右を既定にする。
|
|
1613
|
+
const preferred: DetourSide = Math.abs(A.x - B.x) < 0.5 ? "right" : A.x < B.x ? "left" : "right";
|
|
1614
|
+
return preferred === "left" ? left : right;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1513
1617
|
/** 2 rect の最短距離 (world unit)。 重なっていれば 0。 */
|
|
1514
1618
|
function rectGap(a: Rect, b: Rect): number {
|
|
1515
1619
|
const dx = Math.max(0, a.x - (b.x + b.w), b.x - (a.x + a.w));
|
|
@@ -1646,6 +1750,7 @@ function routePath(
|
|
|
1646
1750
|
* `widenDetourStubs` が同じ引数に本 list を足して呼び直す。
|
|
1647
1751
|
*/
|
|
1648
1752
|
avoidRects: readonly Rect[] = [],
|
|
1753
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
1649
1754
|
): {
|
|
1650
1755
|
d: string;
|
|
1651
1756
|
labelX: number;
|
|
@@ -1669,7 +1774,7 @@ function routePath(
|
|
|
1669
1774
|
// label 幅 = SSOT = spec.ts の computeLabelBoxW (CAR-431 で collections.ts labelBBox /
|
|
1670
1775
|
// render/edges.tsx / predict-bbox.ts の 4 箇所計算を集約)。 L 字 midX / midY を label 収納可能な
|
|
1671
1776
|
// 位置に押し出す判定で使う。 CLEARANCE_NODE_LABEL は clearance-constants.ts SSOT import。
|
|
1672
|
-
const labelBoxW = edge ? computeLabelBoxW(edge.label, edge.sub) : 0;
|
|
1777
|
+
const labelBoxW = edge ? computeLabelBoxW(edge.label, edge.sub, textMetrics) : 0;
|
|
1673
1778
|
const labelHalfW = labelBoxW / 2;
|
|
1674
1779
|
// CAR-518 SSOT ... solid = LABEL_INIT_CLEARANCE (34、 CAR-630 で 26 → 34 復帰)、 dotted-flow = LABEL_INIT_CLEARANCE_DOTTED_FLOW (62、 CAR-630 で 54 → 62 復帰)。
|
|
1675
1780
|
// routePath 内 labelY 計算 8 箇所で参照、 style 分岐を helper 経由で単一 SSOT 化する。
|
|
@@ -2357,7 +2462,7 @@ function routePath(
|
|
|
2357
2462
|
const detourY = Math.min(A.y, B.y) - 100;
|
|
2358
2463
|
const corner = 14;
|
|
2359
2464
|
const xDir = B.x > A.x ? 1 : -1;
|
|
2360
|
-
|
|
2465
|
+
const existing = {
|
|
2361
2466
|
d: `M ${A.x} ${A.y}
|
|
2362
2467
|
L ${A.x} ${detourY + corner}
|
|
2363
2468
|
Q ${A.x} ${detourY}, ${A.x + xDir * corner} ${detourY}
|
|
@@ -2369,6 +2474,20 @@ function routePath(
|
|
|
2369
2474
|
// CAR-518 SSOT ... dotted-flow は 62 world (marker glow radius 分拡張) を labelInit で参照。
|
|
2370
2475
|
// top-top detour の水平 segment 上方 labelInit world = spec 準拠距離。
|
|
2371
2476
|
labelY: detourY - labelInit,
|
|
2477
|
+
labelAnchor: "middle" as const,
|
|
2478
|
+
};
|
|
2479
|
+
const existingCrossings = countPathObstacleCrossings(existing.d, obstacles);
|
|
2480
|
+
if (existingCrossings === 0) return existing;
|
|
2481
|
+
|
|
2482
|
+
// 帯に掛かる箱を避けた最も近い外側の列なら、経路に関わらない遠い箱を足しても列を動かさない。
|
|
2483
|
+
// 遠回りは交差を実際に減らす時だけ採り、減らない時は短く読める従来経路を保って、
|
|
2484
|
+
// 残る交差を `edge-node-cross` に報告させる。
|
|
2485
|
+
const candidate = chooseBackDetourCandidate(A, B, from, to, obstacles, detourY);
|
|
2486
|
+
if (candidate.crossings >= existingCrossings) return existing;
|
|
2487
|
+
return {
|
|
2488
|
+
d: candidate.d,
|
|
2489
|
+
labelX: candidate.labelX,
|
|
2490
|
+
labelY: detourY - labelInit,
|
|
2372
2491
|
labelAnchor: "middle",
|
|
2373
2492
|
};
|
|
2374
2493
|
}
|
|
@@ -2418,7 +2537,11 @@ function routePath(
|
|
|
2418
2537
|
* @param edges 確定済 path を持つ辺。 `labelY` のみを書き換える
|
|
2419
2538
|
* @param nodes 節。 間隔が悪化する置き方を捨てるために使う
|
|
2420
2539
|
*/
|
|
2421
|
-
export function repositionParallelLabels(
|
|
2540
|
+
export function repositionParallelLabels(
|
|
2541
|
+
edges: LaidEdge[],
|
|
2542
|
+
nodes: LaidNode[],
|
|
2543
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
2544
|
+
): void {
|
|
2422
2545
|
// 図の全ての線 (実際に描かれる形)。 1 度だけ開いて使い回す。
|
|
2423
2546
|
const allSegs = new Map(
|
|
2424
2547
|
edges.map((e) => [e.id, flattenPathSegments(extractPathSegments(e.d))] as const),
|
|
@@ -2451,7 +2574,7 @@ export function repositionParallelLabels(edges: LaidEdge[], nodes: LaidNode[]):
|
|
|
2451
2574
|
const pairOrder = [...pair].sort(compareSharedPairOrder);
|
|
2452
2575
|
|
|
2453
2576
|
// 横に重なる label ごとに塊を作る (1 つだけの塊も含む)。
|
|
2454
|
-
const boxes = pair.map((e) => computeLabelBBoxWorld(e));
|
|
2577
|
+
const boxes = pair.map((e) => computeLabelBBoxWorld(e, textMetrics));
|
|
2455
2578
|
const componentOf = pair.map((_, i) => i);
|
|
2456
2579
|
for (let i = 0; i < pair.length; i++) {
|
|
2457
2580
|
for (let j = i + 1; j < pair.length; j++) {
|
|
@@ -2479,6 +2602,7 @@ export function repositionParallelLabels(edges: LaidEdge[], nodes: LaidNode[]):
|
|
|
2479
2602
|
allEdges: edges,
|
|
2480
2603
|
allSegs,
|
|
2481
2604
|
xRange,
|
|
2605
|
+
textMetrics,
|
|
2482
2606
|
});
|
|
2483
2607
|
for (let i = 0; i < members.length; i++) members[i]!.labelY = placed[i]!;
|
|
2484
2608
|
}
|
|
@@ -2511,11 +2635,21 @@ function placeAvoidingSiblingPaths(args: {
|
|
|
2511
2635
|
allEdges: LaidEdge[];
|
|
2512
2636
|
allSegs: Map<string, Array<{ x1: number; y1: number; x2: number; y2: number }>>;
|
|
2513
2637
|
xRange: Map<string, { min: number; max: number }>;
|
|
2638
|
+
textMetrics?: CdlTextMetrics;
|
|
2514
2639
|
}): number[] {
|
|
2515
|
-
const {
|
|
2640
|
+
const {
|
|
2641
|
+
members,
|
|
2642
|
+
pair,
|
|
2643
|
+
pairOrder,
|
|
2644
|
+
nodes,
|
|
2645
|
+
allEdges,
|
|
2646
|
+
allSegs,
|
|
2647
|
+
xRange,
|
|
2648
|
+
textMetrics = "proportional",
|
|
2649
|
+
} = args;
|
|
2516
2650
|
const sizes = members.map((e) => labelPillH(e.sub));
|
|
2517
2651
|
const wants = members.map((e) => e.labelY);
|
|
2518
|
-
const boxes = members.map((e) => computeLabelBBoxWorld(e));
|
|
2652
|
+
const boxes = members.map((e) => computeLabelBBoxWorld(e, textMetrics));
|
|
2519
2653
|
const all = members.map((_, i) => i);
|
|
2520
2654
|
const spread = (idx: number[]): number[] =>
|
|
2521
2655
|
spreadWithMinimalShift(
|
|
@@ -2799,7 +2933,11 @@ function inflate(r: Rect, pad: number): Rect {
|
|
|
2799
2933
|
* 余白は `LABEL_SLIDE_CLEARANCES` を大きい方から試し、 その余白を確保できる位置のうち
|
|
2800
2934
|
* 今の位置に一番近いものを採る。 どの余白でも置けない場合は動かさない。
|
|
2801
2935
|
*/
|
|
2802
|
-
export function slideLabelsAlongOwnPath(
|
|
2936
|
+
export function slideLabelsAlongOwnPath(
|
|
2937
|
+
edges: LaidEdge[],
|
|
2938
|
+
nodes: readonly LaidNode[],
|
|
2939
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
2940
|
+
): void {
|
|
2803
2941
|
const withLabel = edges.filter((e) => hasRenderedLabel(e));
|
|
2804
2942
|
if (withLabel.length === 0) return;
|
|
2805
2943
|
const segsById = new Map<string, FlatSeg[]>();
|
|
@@ -2807,7 +2945,7 @@ export function slideLabelsAlongOwnPath(edges: LaidEdge[], nodes: readonly LaidN
|
|
|
2807
2945
|
const nodeRects = nodes.map(nodeRect);
|
|
2808
2946
|
|
|
2809
2947
|
for (const e of withLabel) {
|
|
2810
|
-
const w = computeLabelBoxW(e.label, e.sub);
|
|
2948
|
+
const w = computeLabelBoxW(e.label, e.sub, textMetrics);
|
|
2811
2949
|
const h = labelPillH(e.sub);
|
|
2812
2950
|
if (w === 0 || h === 0) continue;
|
|
2813
2951
|
const own = segsById.get(e.id) ?? [];
|
|
@@ -3026,9 +3164,6 @@ const SHARED_RUN_CORNER = VERTICAL_DETOUR_CORNER;
|
|
|
3026
3164
|
/** 同じ直線上とみなす座標の差 (world unit)。 */
|
|
3027
3165
|
const SHARED_RUN_EPS = 0.5;
|
|
3028
3166
|
|
|
3029
|
-
/** 経路の角の点。 */
|
|
3030
|
-
type Vertex = { x: number; y: number };
|
|
3031
|
-
|
|
3032
3167
|
/**
|
|
3033
3168
|
* 経路を「角の点の並び」 に開く。 開けない形なら `null` を返す。
|
|
3034
3169
|
*
|
|
@@ -3232,7 +3367,11 @@ export function separateSharedStraightRuns(edges: LaidEdge[], nodes: readonly La
|
|
|
3232
3367
|
* 起点は `fan-origin-single-point` のため動かさず、 最初の折れ直後の区間を平行移動する。
|
|
3233
3368
|
* 1 本しか出ない辺は `detourStub` が決めた箱との中間位置をそのまま保つ。
|
|
3234
3369
|
*/
|
|
3235
|
-
export function pullOutgoingFirstCorner(
|
|
3370
|
+
export function pullOutgoingFirstCorner(
|
|
3371
|
+
edges: LaidEdge[],
|
|
3372
|
+
nodes: readonly LaidNode[],
|
|
3373
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
3374
|
+
): void {
|
|
3236
3375
|
if (edges.length < 2) return;
|
|
3237
3376
|
|
|
3238
3377
|
const 束 = new Map<string, LaidEdge[]>();
|
|
@@ -3303,10 +3442,10 @@ export function pullOutgoingFirstCorner(edges: LaidEdge[], nodes: readonly LaidN
|
|
|
3303
3442
|
// 旧経路に重なっていた札を先に確定する。 経路を動かした後では重なりが消え、 札だけが
|
|
3304
3443
|
// 元の位置に取り残される。
|
|
3305
3444
|
if (!札を旧経路へ追従済み) {
|
|
3306
|
-
slideLabelsAlongOwnPath(edges, nodes);
|
|
3445
|
+
slideLabelsAlongOwnPath(edges, nodes, textMetrics);
|
|
3307
3446
|
札を旧経路へ追従済み = true;
|
|
3308
3447
|
}
|
|
3309
|
-
const labelBox = computeLabelBBoxWorld(e);
|
|
3448
|
+
const labelBox = computeLabelBBoxWorld(e, textMetrics);
|
|
3310
3449
|
const label = { x: labelBox.x + labelBox.w / 2, y: labelBox.y + labelBox.h / 2 };
|
|
3311
3450
|
const 線分までの距離2 = (a: Vertex, b: Vertex): number => {
|
|
3312
3451
|
const dx = b.x - a.x;
|
|
@@ -3510,13 +3649,14 @@ export function followLabelsToFannedTails(
|
|
|
3510
3649
|
edges: LaidEdge[],
|
|
3511
3650
|
obstacles: readonly BBox[],
|
|
3512
3651
|
shifts: ReadonlyMap<string, { dx: number; dy: number }>,
|
|
3652
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
3513
3653
|
): void {
|
|
3514
3654
|
if (shifts.size === 0) return;
|
|
3515
3655
|
const 測り方 = 線の測り方を作る();
|
|
3516
3656
|
const labels: BBox[] = edges.filter(hasRenderedLabel).map((e) => ({
|
|
3517
3657
|
kind: "edge-label",
|
|
3518
3658
|
id: e.id,
|
|
3519
|
-
...computeLabelBBoxWorld(e),
|
|
3659
|
+
...computeLabelBBoxWorld(e, textMetrics),
|
|
3520
3660
|
}));
|
|
3521
3661
|
const labelIndexByEdgeId = new Map(labels.map((label, index) => [label.id, index] as const));
|
|
3522
3662
|
const edgeById = new Map(edges.map((e) => [e.id, e] as const));
|
|
@@ -3555,7 +3695,7 @@ export function followLabelsToFannedTails(
|
|
|
3555
3695
|
labelY: e.labelY + 移動.dy,
|
|
3556
3696
|
labelAnchor: e.labelAnchor,
|
|
3557
3697
|
};
|
|
3558
|
-
const 候補aの箱 = computeLabelBBoxWorld({ ...e, ...候補a });
|
|
3698
|
+
const 候補aの箱 = computeLabelBBoxWorld({ ...e, ...候補a }, textMetrics);
|
|
3559
3699
|
const 最後は縦 = Math.abs(最後.x2 - 最後.x1) <= Math.abs(最後.y2 - 最後.y1);
|
|
3560
3700
|
const 候補 = [
|
|
3561
3701
|
候補a,
|
|
@@ -3571,7 +3711,11 @@ export function followLabelsToFannedTails(
|
|
|
3571
3711
|
];
|
|
3572
3712
|
|
|
3573
3713
|
for (const c of 候補) {
|
|
3574
|
-
const 札: BBox = {
|
|
3714
|
+
const 札: BBox = {
|
|
3715
|
+
kind: "edge-label",
|
|
3716
|
+
id: e.id,
|
|
3717
|
+
...computeLabelBBoxWorld({ ...e, ...c }, textMetrics),
|
|
3718
|
+
};
|
|
3575
3719
|
if (segmentsCrossRect([最後], 札)) continue;
|
|
3576
3720
|
if (札の移動を塞ぐ相手を探す(周り, 元の札, 札) !== null) continue;
|
|
3577
3721
|
|
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";
|
|
@@ -171,6 +171,7 @@ export function 端の字を置く(
|
|
|
171
171
|
labelY?: number;
|
|
172
172
|
},
|
|
173
173
|
端: 線の端 = "head",
|
|
174
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
174
175
|
): 端の字の置き場所 | null {
|
|
175
176
|
const text = (端 === "head" ? edge.headLabel : edge.tailLabel)?.trim();
|
|
176
177
|
if (!text) return null;
|
|
@@ -183,6 +184,7 @@ export function 端の字を置く(
|
|
|
183
184
|
fontSize: 端の字の大きさ,
|
|
184
185
|
fontWeight: EDGE_LABEL_TEXT.sub.fontWeight,
|
|
185
186
|
fontFamily: "mono",
|
|
187
|
+
textMetrics,
|
|
186
188
|
});
|
|
187
189
|
const h = 箱の高さ;
|
|
188
190
|
// 札の中心。 札を描かない線と、位置がまだ無い線は `null`
|
package/src/layout/lanes.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CdlDiagram, CdlEdge, LaidLane, LaidNode } from "../types";
|
|
1
|
+
import type { CdlDiagram, CdlEdge, CdlTextMetrics, LaidLane, LaidNode } from "../types";
|
|
2
2
|
import { CLEARANCE_NODE_LABEL } from "./clearance-constants";
|
|
3
3
|
import { computeLabelBoxW, EDGE_STUB_OUT, targetNodeNodeClearance } from "./spec";
|
|
4
4
|
import { CONTAIN_PAD, DEFAULT_LANE_GAP, HEADER_RESERVE, LANE_LABEL_H, LANE_PAD_MIN } from "./tokens";
|
|
@@ -194,6 +194,7 @@ export function expandLaneGapsForEdgeLabels(
|
|
|
194
194
|
lanes: LaidLane[],
|
|
195
195
|
nodes: LaidNode[],
|
|
196
196
|
edges: readonly CdlEdge[],
|
|
197
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
197
198
|
): LaidLane[] {
|
|
198
199
|
if (lanes.length < 2) return lanes;
|
|
199
200
|
// canvas pivot 新 spec (dragon canvas pivot spec §layout-role-conversion)。
|
|
@@ -240,7 +241,7 @@ export function expandLaneGapsForEdgeLabels(
|
|
|
240
241
|
const fi = laneIndexById.get(fromLane);
|
|
241
242
|
const ti = laneIndexById.get(toLane);
|
|
242
243
|
if (fi === undefined || ti === undefined || fi === ti) continue;
|
|
243
|
-
const labelBoxW = computeLabelBoxW(edge.label, edge.sub);
|
|
244
|
+
const labelBoxW = computeLabelBoxW(edge.label, edge.sub, textMetrics);
|
|
244
245
|
if (labelBoxW === 0) continue;
|
|
245
246
|
const lo = Math.min(fi, ti);
|
|
246
247
|
const hi = Math.max(fi, ti);
|
package/src/layout/metro-path.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CdlEdge, LaidNode, Side } from "../types";
|
|
1
|
+
import type { CdlEdge, CdlTextMetrics, LaidNode, Side } from "../types";
|
|
2
2
|
import { SAME_HEIGHT_THRESHOLD } from "./curve-path";
|
|
3
3
|
import { computeLabelBoxW, EDGE_STUB_OUT, labelPathClearanceHorizontal, labelPillH } from "./spec";
|
|
4
4
|
|
|
@@ -49,6 +49,7 @@ export function metroPath(
|
|
|
49
49
|
to: 箱,
|
|
50
50
|
edge?: Pick<CdlEdge, "label" | "sub" | "style" | "overlay">,
|
|
51
51
|
labelMarginExtra = 0,
|
|
52
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
52
53
|
): 路線図の線の結果 {
|
|
53
54
|
const fromLeft = from.cx - from.w / 2;
|
|
54
55
|
const fromRight = from.cx + from.w / 2;
|
|
@@ -166,7 +167,7 @@ export function metroPath(
|
|
|
166
167
|
normalX *= -1;
|
|
167
168
|
normalY *= -1;
|
|
168
169
|
}
|
|
169
|
-
const labelW = computeLabelBoxW(edge?.label, edge?.sub);
|
|
170
|
+
const labelW = computeLabelBoxW(edge?.label, edge?.sub, textMetrics);
|
|
170
171
|
const labelH = labelPillH(edge?.sub);
|
|
171
172
|
const distance =
|
|
172
173
|
labelPathClearanceHorizontal(edge) +
|
package/src/layout/nodes.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { requiredRowsWidth } from "../kinds/row-align";
|
|
2
|
-
import type { CdlDiagram, LaidLane, LaidNode } from "../types";
|
|
2
|
+
import type { CdlDiagram, CdlTextMetrics, LaidLane, LaidNode } from "../types";
|
|
3
3
|
import { 端の字に要る線の長さ } from "./head-label";
|
|
4
4
|
import {
|
|
5
5
|
hasRenderedLabel,
|
|
@@ -77,10 +77,14 @@ function autoStorageHeight(
|
|
|
77
77
|
*
|
|
78
78
|
* 著者が n.w を明示している場合は尊重 (snapshot 互換 / catalog thumbnail 縮小指定を壊さない)。
|
|
79
79
|
*/
|
|
80
|
-
function autoRowsWidth(
|
|
80
|
+
function autoRowsWidth(
|
|
81
|
+
n: { kind: string; rows?: string[]; w?: number },
|
|
82
|
+
defaultW: number,
|
|
83
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
84
|
+
): number {
|
|
81
85
|
if (n.w != null) return n.w;
|
|
82
86
|
if (!n.rows || n.rows.length === 0) return defaultW;
|
|
83
|
-
const required = requiredRowsWidth(n.rows, n.kind);
|
|
87
|
+
const required = requiredRowsWidth(n.rows, n.kind, textMetrics);
|
|
84
88
|
return Math.max(defaultW, required);
|
|
85
89
|
}
|
|
86
90
|
|
|
@@ -171,7 +175,11 @@ function requiredStackGapForEdgeLabels(diag: CdlDiagram, allStacks: readonly num
|
|
|
171
175
|
return required;
|
|
172
176
|
}
|
|
173
177
|
|
|
174
|
-
export function layoutNodes(
|
|
178
|
+
export function layoutNodes(
|
|
179
|
+
diag: CdlDiagram,
|
|
180
|
+
lanes: LaidLane[],
|
|
181
|
+
textMetrics: CdlTextMetrics,
|
|
182
|
+
): LaidNode[] {
|
|
175
183
|
const laneMap = new Map(lanes.map((l) => [l.id, l]));
|
|
176
184
|
|
|
177
185
|
// 全 lane を跨いで stack 番号で row を共有する。
|
|
@@ -232,7 +240,7 @@ export function layoutNodes(diag: CdlDiagram, lanes: LaidLane[]): LaidNode[] {
|
|
|
232
240
|
}
|
|
233
241
|
const size = NODE_SIZE[n.kind];
|
|
234
242
|
if (n.posX !== undefined && n.posY !== undefined) {
|
|
235
|
-
const w = n.posW ?? n.w ?? autoRowsWidth(n, size.w);
|
|
243
|
+
const w = n.posW ?? n.w ?? autoRowsWidth(n, size.w, textMetrics);
|
|
236
244
|
const h = n.posH ?? n.h ?? rowH.get(n.stack) ?? size.h;
|
|
237
245
|
out.push({
|
|
238
246
|
...n,
|
|
@@ -270,7 +278,7 @@ export function layoutNodes(diag: CdlDiagram, lanes: LaidLane[]): LaidNode[] {
|
|
|
270
278
|
const h = n.h ?? (自前の高さ ? 箱の高さ(n, size.h) : 段の高さ);
|
|
271
279
|
const cy = 段の芯;
|
|
272
280
|
// n.w 明示時は尊重、 未指定で rows[] を持つ node は内容長に合わせて auto 拡張。
|
|
273
|
-
const w = autoRowsWidth(n, size.w);
|
|
281
|
+
const w = autoRowsWidth(n, size.w, textMetrics);
|
|
274
282
|
out.push({
|
|
275
283
|
...n,
|
|
276
284
|
cx,
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { computeLabelBBoxWorld } from "./spec";
|
|
12
|
-
import type { LaidEdge, LaidNode } from "../types";
|
|
12
|
+
import type { CdlTextMetrics, LaidEdge, LaidNode } from "../types";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Edge label の world 単位 bbox 予測 (spec.ts SSOT の re-export)。
|
|
@@ -22,8 +22,11 @@ import type { LaidEdge, LaidNode } from "../types";
|
|
|
22
22
|
* @param edge LaidEdge (labelX, labelY, labelAnchor, label, sub 既確定)
|
|
23
23
|
* @returns { x, y, w, h } の world 座標 bbox (render 実 rect 位置と一致)
|
|
24
24
|
*/
|
|
25
|
-
export function predictLabelBBoxWorld(
|
|
26
|
-
|
|
25
|
+
export function predictLabelBBoxWorld(
|
|
26
|
+
edge: LaidEdge,
|
|
27
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
28
|
+
): { x: number; y: number; w: number; h: number } {
|
|
29
|
+
return computeLabelBBoxWorld(edge, textMetrics);
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
/**
|
package/src/layout/self-loop.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CdlEdge, LaidEdge, LaidNode, Side } from "../types";
|
|
1
|
+
import type { CdlEdge, CdlTextMetrics, LaidEdge, LaidNode, Side } from "../types";
|
|
2
2
|
import { CLEARANCE_LABEL_LABEL } from "./clearance-constants";
|
|
3
3
|
import { computeLabelBoxW, LABEL_TO_PATH_CLEARANCE, labelPillH } from "./spec";
|
|
4
4
|
|
|
@@ -120,7 +120,12 @@ export function pickFreeSides(occupied: ReadonlySet<Side> | undefined, count: nu
|
|
|
120
120
|
*
|
|
121
121
|
* 1 本だけの時は積む必要が無いので、辺の中央の真横に 1 つ置くだけになる。
|
|
122
122
|
*/
|
|
123
|
-
export function routeSelfLoopGroup(
|
|
123
|
+
export function routeSelfLoopGroup(
|
|
124
|
+
edges: CdlEdge[],
|
|
125
|
+
node: LaidNode,
|
|
126
|
+
side: Side,
|
|
127
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
128
|
+
): LaidEdge[] {
|
|
124
129
|
const horizontal = side === "left" || side === "right";
|
|
125
130
|
const g = sideGeometry(node, side);
|
|
126
131
|
const span = fitSpan(horizontal ? node.h : node.w, SELF_LOOP_SPAN);
|
|
@@ -135,7 +140,7 @@ export function routeSelfLoopGroup(edges: CdlEdge[], node: LaidNode, side: Side)
|
|
|
135
140
|
|
|
136
141
|
// 積む向きの語の大きさ。 横の辺なら縦に積むので高さ、縦の辺なら横に積むので幅
|
|
137
142
|
const 語幅 = edges.map((e) =>
|
|
138
|
-
horizontal ? labelPillH(e.sub) : computeLabelBoxW(e.label, e.sub),
|
|
143
|
+
horizontal ? labelPillH(e.sub) : computeLabelBoxW(e.label, e.sub, textMetrics),
|
|
139
144
|
);
|
|
140
145
|
const 位置: number[] = [];
|
|
141
146
|
let 積 = 0;
|
|
@@ -181,6 +186,7 @@ export function layoutSelfLoops(
|
|
|
181
186
|
edges: CdlEdge[],
|
|
182
187
|
nodes: Map<string, LaidNode>,
|
|
183
188
|
occupiedBySide: ReadonlyMap<string, ReadonlySet<Side>>,
|
|
189
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
184
190
|
): LaidEdge[] {
|
|
185
191
|
// 辺の割り当ては節ごとに 1 度で決める。 輪を 1 本ずつ処理して選び直すと、2 本目が
|
|
186
192
|
// 1 本目の辺を「空いている」 と見て同じ場所に重なる
|
|
@@ -213,5 +219,5 @@ export function layoutSelfLoops(
|
|
|
213
219
|
}
|
|
214
220
|
// 語の置き場所は束の中の並びで決まるので、束ごとにまとめて経路にする。
|
|
215
221
|
// 返す並びは呼出側が id で引き直すため、ここでは束の順で構わない
|
|
216
|
-
return [...束.values()].flatMap((b) => routeSelfLoopGroup(b.edges, b.node, b.side));
|
|
222
|
+
return [...束.values()].flatMap((b) => routeSelfLoopGroup(b.edges, b.node, b.side, textMetrics));
|
|
217
223
|
}
|
package/src/layout/spec.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import { CLEARANCE_LABEL_LABEL, CLEARANCE_NODE_LABEL } from "./clearance-constants";
|
|
19
19
|
import { measureTextWidth } from "./text-width";
|
|
20
20
|
import { EDGE_LABEL_TEXT } from "../label-text";
|
|
21
|
-
import type { CdlEdge, LaidEdge, LaidLane, LaidNode } from "../types";
|
|
21
|
+
import type { CdlEdge, CdlTextMetrics, LaidEdge, LaidLane, LaidNode } from "../types";
|
|
22
22
|
|
|
23
23
|
// ─── edge label pill bbox SSOT (CAR-431 で 3 定義箇所を集約) ──
|
|
24
24
|
|
|
@@ -965,10 +965,21 @@ export function requiredVerticalLabelGap(pillHs: readonly number[]): number {
|
|
|
965
965
|
* @param sub sub text (undefined なら 0)
|
|
966
966
|
* @returns pill 全体の world 幅 (偶数)
|
|
967
967
|
*/
|
|
968
|
-
export function computeLabelBoxW(
|
|
969
|
-
|
|
968
|
+
export function computeLabelBoxW(
|
|
969
|
+
label: string | undefined,
|
|
970
|
+
sub: string | undefined,
|
|
971
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
972
|
+
): number {
|
|
973
|
+
const mainW = measureTextWidth(label ?? "", {
|
|
974
|
+
fontSize: EDGE_LABEL_TEXT.main.fontSize,
|
|
975
|
+
textMetrics,
|
|
976
|
+
});
|
|
970
977
|
const subW = sub
|
|
971
|
-
? measureTextWidth(sub, {
|
|
978
|
+
? measureTextWidth(sub, {
|
|
979
|
+
fontSize: EDGE_LABEL_TEXT.sub.fontSize,
|
|
980
|
+
fontFamily: "mono",
|
|
981
|
+
textMetrics,
|
|
982
|
+
})
|
|
972
983
|
: 0;
|
|
973
984
|
const raw = Math.max(mainW, subW) + LABEL_PILL_PAD_X * 2;
|
|
974
985
|
// 切り上げる前に浮動小数の誤差を落とす。 文字幅は係数の掛け算と足し算を重ねるので、
|
|
@@ -999,8 +1010,11 @@ export function computeLabelBoxW(label: string | undefined, sub: string | undefi
|
|
|
999
1010
|
* @param edge LaidEdge (labelX / labelY / labelAnchor / label / sub 既確定)
|
|
1000
1011
|
* @returns { x, y, w, h } の world 座標 bbox (render 実 rect 位置と一致)
|
|
1001
1012
|
*/
|
|
1002
|
-
export function computeLabelBBoxWorld(
|
|
1003
|
-
|
|
1013
|
+
export function computeLabelBBoxWorld(
|
|
1014
|
+
edge: LaidEdge,
|
|
1015
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
1016
|
+
): { x: number; y: number; w: number; h: number } {
|
|
1017
|
+
const boxW = computeLabelBoxW(edge.label, edge.sub, textMetrics);
|
|
1004
1018
|
const boxH = edge.sub ? LABEL_PILL_H_WITH_SUB : LABEL_PILL_H_MAIN_ONLY;
|
|
1005
1019
|
const anchor = edge.labelAnchor;
|
|
1006
1020
|
const boxLeftX = anchor === "start" ? 0 : anchor === "end" ? -boxW : -boxW / 2;
|