@cardenelabs/cdl 0.5.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/LICENSE +21 -0
- package/README.md +343 -0
- package/SPEC.md +374 -0
- package/dist/index.cjs +28085 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +3415 -0
- package/dist/index.d.ts +3415 -0
- package/dist/index.js +27962 -0
- package/dist/index.js.map +1 -0
- package/dist/react.cjs +23043 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +2 -0
- package/dist/react.d.ts +2 -0
- package/dist/react.js +23041 -0
- package/dist/react.js.map +1 -0
- package/dist/render-C78lIXeC.d.cts +2449 -0
- package/dist/render-C78lIXeC.d.ts +2449 -0
- package/examples/quick-start.md +88 -0
- package/package.json +79 -0
- package/src/anim/core/easing.ts +77 -0
- package/src/anim/core/timeline.ts +335 -0
- package/src/anim/core/types.ts +42 -0
- package/src/anim/index.ts +20 -0
- package/src/anim/react/index.ts +3 -0
- package/src/anim/react/useReducedMotion.ts +27 -0
- package/src/anim/react/useTimeline.ts +48 -0
- package/src/assert-never.ts +16 -0
- package/src/author-intent-verify.ts +587 -0
- package/src/builder.ts +3740 -0
- package/src/compile.ts +12 -0
- package/src/dom-verify-core.ts +121 -0
- package/src/dom-verify-types.ts +21 -0
- package/src/dom-verify.ts +948 -0
- package/src/event-handler/index.ts +184 -0
- package/src/formula/ast.ts +46 -0
- package/src/formula/evaluator.ts +163 -0
- package/src/formula/formula-computeds.ts +83 -0
- package/src/formula/index.ts +5 -0
- package/src/formula/parser.ts +399 -0
- package/src/index.ts +284 -0
- package/src/input-signals.ts +74 -0
- package/src/kinds/actor.tsx +79 -0
- package/src/kinds/card.tsx +61 -0
- package/src/kinds/chart-bar.tsx +121 -0
- package/src/kinds/chart-line.tsx +163 -0
- package/src/kinds/chart-pie.tsx +107 -0
- package/src/kinds/compact-title.ts +164 -0
- package/src/kinds/dyn-shape.tsx +394 -0
- package/src/kinds/event.tsx +70 -0
- package/src/kinds/function.tsx +53 -0
- package/src/kinds/funnel.tsx +122 -0
- package/src/kinds/gantt.tsx +193 -0
- package/src/kinds/generic.tsx +368 -0
- package/src/kinds/mind-map.tsx +367 -0
- package/src/kinds/mind-radial.tsx +209 -0
- package/src/kinds/node-tone.ts +18 -0
- package/src/kinds/quadrant.tsx +164 -0
- package/src/kinds/row-align.ts +179 -0
- package/src/kinds/shape-basement.tsx +683 -0
- package/src/kinds/shape-blockchain.tsx +750 -0
- package/src/kinds/shape-commerce.tsx +553 -0
- package/src/kinds/shape-finance.tsx +706 -0
- package/src/kinds/shape-hardware.tsx +862 -0
- package/src/kinds/shape-people.tsx +439 -0
- package/src/kinds/shape-region.tsx +383 -0
- package/src/kinds/shape-software.tsx +859 -0
- package/src/kinds/storage.tsx +180 -0
- package/src/kinds/text-width.ts +73 -0
- package/src/kinds/tree.tsx +275 -0
- package/src/kinds/user-journey.tsx +240 -0
- package/src/label-text.ts +71 -0
- package/src/layout/clearance-constants.ts +47 -0
- package/src/layout/collisions.ts +2078 -0
- package/src/layout/edges.ts +2498 -0
- package/src/layout/footer-shape.ts +97 -0
- package/src/layout/geometry.ts +135 -0
- package/src/layout/label-shift.ts +28 -0
- package/src/layout/lanes.ts +328 -0
- package/src/layout/nodes.ts +190 -0
- package/src/layout/predict-bbox.ts +76 -0
- package/src/layout/px-projection.ts +176 -0
- package/src/layout/spec.ts +872 -0
- package/src/layout/text-width.ts +133 -0
- package/src/layout/tokens.ts +181 -0
- package/src/layout/viewbox.ts +47 -0
- package/src/layout-with-validation.ts +175 -0
- package/src/layout.ts +381 -0
- package/src/presets.ts +2108 -0
- package/src/reactive/batch.ts +48 -0
- package/src/reactive/computed.ts +85 -0
- package/src/reactive/effect.ts +145 -0
- package/src/reactive/index.ts +6 -0
- package/src/reactive/internal.ts +109 -0
- package/src/reactive/signal.ts +113 -0
- package/src/render/edges.tsx +318 -0
- package/src/render/header.tsx +146 -0
- package/src/render/interactive-panel.tsx +9620 -0
- package/src/render/nodes.tsx +319 -0
- package/src/render/stage.tsx +377 -0
- package/src/render/tone.ts +64 -0
- package/src/render/utils.ts +238 -0
- package/src/render.tsx +221 -0
- package/src/scroll-trigger/index.ts +109 -0
- package/src/scroll-trigger/progress.ts +49 -0
- package/src/thumbnail.tsx +114 -0
- package/src/types.ts +2371 -0
- package/src/validate.ts +268 -0
- package/src/visual-validate.ts +4381 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { LaidLane, LaidNode } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `shape-` の絵が箱の下端から上へどこまで伸びるか (world unit)。
|
|
5
|
+
*
|
|
6
|
+
* これらの種別は中身を **箱の下端から固定の距離** で組み立てる (実測 = `shape-robot-arm` は
|
|
7
|
+
* base の中心を `cy + h/2 - 60` に置き、 そこから腕を上へ積む)。 箱がこの値より低いと絵が
|
|
8
|
+
* 箱の上へはみ出す。
|
|
9
|
+
*
|
|
10
|
+
* ## 値の出どころ
|
|
11
|
+
*
|
|
12
|
+
* 箱 140x72 で描いて絵の外接矩形を測り、 `72 + 上へのはみ出し` を伸びる量とした。 載せるのは
|
|
13
|
+
* はみ出しが実測で 0 より大きかった種別だけで、 それ以外は箱の中に収まるため不要。
|
|
14
|
+
*/
|
|
15
|
+
export const SHAPE_DRAW_UP_EXTENT: Readonly<Record<string, number>> = {
|
|
16
|
+
"shape-robot-arm": 201, // 実測 上 129
|
|
17
|
+
"shape-iot-sensor": 120, // 48
|
|
18
|
+
"shape-credit-card": 113, // 40.8
|
|
19
|
+
"shape-exchange": 107, // 35
|
|
20
|
+
"shape-storefront": 104, // 32
|
|
21
|
+
"shape-cdn-edge": 103, // 30.8
|
|
22
|
+
"shape-payment-provider": 101, // 29
|
|
23
|
+
"shape-gear": 96, // 23.9
|
|
24
|
+
"shape-rpc-node": 94, // 22
|
|
25
|
+
"shape-wallet": 84, // 12.1
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 縦線を持つ lane の **下端の名札** を集める。
|
|
30
|
+
*
|
|
31
|
+
* 順序図は lane ごとに上端と下端へ名札を置く。 node が 1 つ以下の lane は下端の名札を持たない
|
|
32
|
+
* (上端だけ、 もしくは空) ため対象から外す。
|
|
33
|
+
*
|
|
34
|
+
* 縦線の終点 (`CdlStage`) と絵を下げる量 (`footerShapeDrops`) と図の下端 (`computeViewBox`) の
|
|
35
|
+
* 3 箇所が同じ名札を指す必要があるため、 「どれが下端の名札か」 の判定をここに 1 つだけ置く。
|
|
36
|
+
*/
|
|
37
|
+
export function lifelineFooterNodes(lanes: LaidLane[], nodes: LaidNode[]): LaidNode[] {
|
|
38
|
+
// lane ごとに node を集める索引を 1 度だけ作る。 lane ごとに全 node を走査すると
|
|
39
|
+
// lane 数 × node 数に膨らみ、 `CdlStage` は動きの 1 コマごとに描き直すため毎コマ払う
|
|
40
|
+
const byLane = new Map<string, LaidNode[]>();
|
|
41
|
+
for (const n of nodes) {
|
|
42
|
+
const inside = byLane.get(n.lane);
|
|
43
|
+
if (inside) inside.push(n);
|
|
44
|
+
else byLane.set(n.lane, [n]);
|
|
45
|
+
}
|
|
46
|
+
const footers: LaidNode[] = [];
|
|
47
|
+
for (const lane of lanes) {
|
|
48
|
+
if (!lane.lifeline) continue;
|
|
49
|
+
const inside = byLane.get(lane.id);
|
|
50
|
+
if (inside === undefined || inside.length < 2) continue;
|
|
51
|
+
// 一番下 = cy が最大。 並べ替えずに 1 回の走査で選ぶ。 同じ cy が複数ある時は
|
|
52
|
+
// **後に現れた方** を採る (安定な並べ替えで末尾を取るのと同じ結果にする)
|
|
53
|
+
let footer = inside[0]!;
|
|
54
|
+
for (const n of inside) {
|
|
55
|
+
if (n.cy >= footer.cy) footer = n;
|
|
56
|
+
}
|
|
57
|
+
footers.push(footer);
|
|
58
|
+
}
|
|
59
|
+
return footers;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 下端の名札で絵を下げる量。 箱に収まる種別と、 収まる高さを持つ箱では 0 を返す。
|
|
64
|
+
*
|
|
65
|
+
* 下げた結果、 絵の上端が箱の上端に揃う = 縦線 (箱の上端で止まる) と重ならなくなる。
|
|
66
|
+
*/
|
|
67
|
+
export function footerShapeDropY(kind: string, h: number): number {
|
|
68
|
+
const extent = SHAPE_DRAW_UP_EXTENT[kind];
|
|
69
|
+
if (extent === undefined) return 0;
|
|
70
|
+
if (!Number.isFinite(h) || h <= 0) return 0;
|
|
71
|
+
return Math.max(0, extent - h);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 下端の名札の id から下げる量を引く表。 下げない名札は載せない。
|
|
76
|
+
*
|
|
77
|
+
* **下げるのは下端の名札だけ**。 上端の名札を下げると絵が箱の下端より下に出て、 そこから
|
|
78
|
+
* 下りる縦線が絵を貫く = 直そうとしている状態を上下逆に作ってしまう。
|
|
79
|
+
*/
|
|
80
|
+
export function footerShapeDrops(lanes: LaidLane[], nodes: LaidNode[]): Map<string, number> {
|
|
81
|
+
return footerShapeDropsOf(lifelineFooterNodes(lanes, nodes));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 集め終えた下端の名札から、 下げる量の表を作る。
|
|
86
|
+
*
|
|
87
|
+
* 呼出側が既に名札を集めている場合はこちらを使う。 `footerShapeDrops` を呼ぶと名札を
|
|
88
|
+
* もう一度集めることになり、 動きの 1 コマごとに 2 度払う (`CdlStage`)。
|
|
89
|
+
*/
|
|
90
|
+
export function footerShapeDropsOf(footers: LaidNode[]): Map<string, number> {
|
|
91
|
+
const drops = new Map<string, number>();
|
|
92
|
+
for (const footer of footers) {
|
|
93
|
+
const dy = footerShapeDropY(footer.kind, footer.h);
|
|
94
|
+
if (dy > 0) drops.set(footer.id, dy);
|
|
95
|
+
}
|
|
96
|
+
return drops;
|
|
97
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 幾何演算 helper SSOT。
|
|
3
|
+
*
|
|
4
|
+
* visual-validate / collisions / dragon test で共通利用する
|
|
5
|
+
* 2D geometry primitive を 1 箇所に集約。 前 visual-validate.ts 内 local helper と
|
|
6
|
+
* Playwright test 内 local helper で 2 度書きだったのを engine から export で統一。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface Segment {
|
|
10
|
+
x1: number;
|
|
11
|
+
y1: number;
|
|
12
|
+
x2: number;
|
|
13
|
+
y2: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface Rect {
|
|
17
|
+
x: number;
|
|
18
|
+
y: number;
|
|
19
|
+
w: number;
|
|
20
|
+
h: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 点 (px, py) と矩形の縁 (外周) までの最短距離。 内側なら 0、 外側なら実距離。 */
|
|
24
|
+
export function pointRectEdgeDistance(px: number, py: number, rect: Rect): number {
|
|
25
|
+
const dx = Math.max(rect.x - px, 0, px - (rect.x + rect.w));
|
|
26
|
+
const dy = Math.max(rect.y - py, 0, py - (rect.y + rect.h));
|
|
27
|
+
return Math.hypot(dx, dy);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 矩形同士の重なり面積 (world 単位)。 重ならなければ 0。 */
|
|
31
|
+
export function rectRectOverlapArea(a: Rect, b: Rect): number {
|
|
32
|
+
const dx = Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x);
|
|
33
|
+
const dy = Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y);
|
|
34
|
+
if (dx <= 0 || dy <= 0) return 0;
|
|
35
|
+
return dx * dy;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 矩形同士の clearance (最短距離)。 重なっていれば 0、 離れていれば実距離。 */
|
|
39
|
+
export function rectRectClearance(a: Rect, b: Rect): number {
|
|
40
|
+
const dx = Math.max(0, Math.max(a.x - (b.x + b.w), b.x - (a.x + a.w)));
|
|
41
|
+
const dy = Math.max(0, Math.max(a.y - (b.y + b.h), b.y - (a.y + a.h)));
|
|
42
|
+
return Math.hypot(dx, dy);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 2 線分の交差判定 (端点共有は交差扱いしない)。 */
|
|
46
|
+
export function segmentsIntersect(a: Segment, b: Segment): boolean {
|
|
47
|
+
const d1 = (b.x2 - b.x1) * (a.y1 - b.y1) - (b.y2 - b.y1) * (a.x1 - b.x1);
|
|
48
|
+
const d2 = (b.x2 - b.x1) * (a.y2 - b.y1) - (b.y2 - b.y1) * (a.x2 - b.x1);
|
|
49
|
+
const d3 = (a.x2 - a.x1) * (b.y1 - a.y1) - (a.y2 - a.y1) * (b.x1 - a.x1);
|
|
50
|
+
const d4 = (a.x2 - a.x1) * (b.y2 - a.y1) - (a.y2 - a.y1) * (b.x2 - a.x1);
|
|
51
|
+
return ((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) && ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 線分の交点。 */
|
|
55
|
+
export interface Point {
|
|
56
|
+
x: number;
|
|
57
|
+
y: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 2 線分の交点を返す。 交わらなければ `null`。 **端点での接触も交点として返す**。
|
|
62
|
+
*
|
|
63
|
+
* `segmentsIntersect` は端点の接触を交差に数えない。 曲線を折れ線に開いて数える時、 交差が
|
|
64
|
+
* 分割点にちょうど乗ると隣り合う 2 本の分割片が両方とも端点接触になり、 **交差が消える**
|
|
65
|
+
* (実測 = `M 0 0 Q 80 80, 160 160` と `y=40` / `y=80` / `y=120` の 3 本で 3 件 → 0 件)。
|
|
66
|
+
*
|
|
67
|
+
* 端点を含めて返すので、 呼出側が「元の辺の端点で接しているだけ」 を除く。 同じ節から出る辺
|
|
68
|
+
* どうしの共有端点を交差として数えないため。
|
|
69
|
+
*
|
|
70
|
+
* 平行 / 重なりは `null` を返す。 重なりの交点は 1 点に定まらず、 数えると同じ線を 2 度引いた
|
|
71
|
+
* 図で件数が跳ねる。
|
|
72
|
+
*/
|
|
73
|
+
export function segmentIntersection(a: Segment, b: Segment): Point | null {
|
|
74
|
+
const ax = a.x2 - a.x1;
|
|
75
|
+
const ay = a.y2 - a.y1;
|
|
76
|
+
const bx = b.x2 - b.x1;
|
|
77
|
+
const by = b.y2 - b.y1;
|
|
78
|
+
const denom = ax * by - ay * bx;
|
|
79
|
+
if (denom === 0) return null;
|
|
80
|
+
const t = ((b.x1 - a.x1) * by - (b.y1 - a.y1) * bx) / denom;
|
|
81
|
+
const u = ((b.x1 - a.x1) * ay - (b.y1 - a.y1) * ax) / denom;
|
|
82
|
+
if (t < 0 || t > 1 || u < 0 || u > 1) return null;
|
|
83
|
+
return { x: a.x1 + t * ax, y: a.y1 + t * ay };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 線分の集まりを覆う矩形。 空なら `null`。 */
|
|
87
|
+
export function segmentsBBox(segs: ReadonlyArray<Segment>): Rect | null {
|
|
88
|
+
if (segs.length === 0) return null;
|
|
89
|
+
let minX = Infinity;
|
|
90
|
+
let minY = Infinity;
|
|
91
|
+
let maxX = -Infinity;
|
|
92
|
+
let maxY = -Infinity;
|
|
93
|
+
for (const s of segs) {
|
|
94
|
+
minX = Math.min(minX, s.x1, s.x2);
|
|
95
|
+
minY = Math.min(minY, s.y1, s.y2);
|
|
96
|
+
maxX = Math.max(maxX, s.x1, s.x2);
|
|
97
|
+
maxY = Math.max(maxY, s.y1, s.y2);
|
|
98
|
+
}
|
|
99
|
+
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 矩形どうしが触れているか。 縁の接触も含める。
|
|
104
|
+
*
|
|
105
|
+
* 交差の事前除外に使う。 縁で接する形を落とすと、 縁上の交点を見逃す。
|
|
106
|
+
*/
|
|
107
|
+
export function bboxOverlaps(a: Rect | null, b: Rect | null): boolean {
|
|
108
|
+
if (!a || !b) return false;
|
|
109
|
+
return a.x <= b.x + b.w && b.x <= a.x + a.w && a.y <= b.y + b.h && b.y <= a.y + a.h;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** 点が候補のどれかと `tol` 以内で一致するか。 */
|
|
113
|
+
export function isNearAny(p: Point, candidates: ReadonlyArray<Point>, tol: number): boolean {
|
|
114
|
+
return candidates.some((c) => Math.abs(c.x - p.x) <= tol && Math.abs(c.y - p.y) <= tol);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** 点 (px, py) と線分 (x1,y1)-(x2,y2) の最短距離。 */
|
|
118
|
+
export function pointToSegmentDistance(
|
|
119
|
+
px: number,
|
|
120
|
+
py: number,
|
|
121
|
+
x1: number,
|
|
122
|
+
y1: number,
|
|
123
|
+
x2: number,
|
|
124
|
+
y2: number,
|
|
125
|
+
): number {
|
|
126
|
+
const dx = x2 - x1;
|
|
127
|
+
const dy = y2 - y1;
|
|
128
|
+
const lenSq = dx * dx + dy * dy;
|
|
129
|
+
if (lenSq === 0) return Math.hypot(px - x1, py - y1);
|
|
130
|
+
let t = ((px - x1) * dx + (py - y1) * dy) / lenSq;
|
|
131
|
+
t = Math.max(0, Math.min(1, t));
|
|
132
|
+
const cx = x1 + t * dx;
|
|
133
|
+
const cy = y1 + t * dy;
|
|
134
|
+
return Math.hypot(px - cx, py - cy);
|
|
135
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { LaidEdge, LaidLane, LaidNode } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Edge label の post-pass shift 探索は CAR-430 で削除済 (no-op passthrough)。
|
|
5
|
+
*
|
|
6
|
+
* 旧実装 (v10 系) は 16 方向 × 26 段階の全探索型 shift + Tier 0-3 fallback +
|
|
7
|
+
* PROXIMITY_HARD_CAP + computeFanTShift 等の場当たり logic で label 位置を再計算していた。
|
|
8
|
+
* CAR-426 chain 伝搬 shift の label 層拡張 (CAR-426-A / CAR-428) 完了で
|
|
9
|
+
* `resolveEdgeLabelOverlapsWithChain` (`layout/collisions.ts`) が label overlap 解消の
|
|
10
|
+
* 単一 SSOT になったため、 本 file の shift 探索は 二重権限 として削除された。
|
|
11
|
+
*
|
|
12
|
+
* label 配置の権限フロー (CAR-430 SSOT)。
|
|
13
|
+
* 1. `edges.ts routePath()` = 初期 label 位置 (path 中点 ± 30 world 等) を SSOT で決定
|
|
14
|
+
* 2. `resolveEdgeLabelOverlapsWithChain` (post-pass 2) = overlap 検出時のみ chain 伝搬 shift
|
|
15
|
+
* 3. 本 function = 通過 no-op (旧 API caller の後方互換のため signature 保持)
|
|
16
|
+
*
|
|
17
|
+
* 後方互換のため export は残置、 内部で input を そのまま返す。
|
|
18
|
+
* caller (`layout.ts` post-pass 1) は CAR-430 同 PR で削除済で、 本 function は
|
|
19
|
+
* pipeline 経由では呼ばれない。 外部 caller (dragon 等の direct import) がある可能性を
|
|
20
|
+
* 考慮して signature のみ維持する。
|
|
21
|
+
*/
|
|
22
|
+
export function shiftLabelsAwayFromNodes(
|
|
23
|
+
edges: LaidEdge[],
|
|
24
|
+
_nodes: LaidNode[],
|
|
25
|
+
_lanes: LaidLane[],
|
|
26
|
+
): LaidEdge[] {
|
|
27
|
+
return edges;
|
|
28
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import type { CdlDiagram, CdlEdge, LaidLane, LaidNode } from "../types";
|
|
2
|
+
import { CLEARANCE_NODE_LABEL } from "./clearance-constants";
|
|
3
|
+
import { computeLabelBoxW, EDGE_STUB_OUT, targetNodeNodeClearance } from "./spec";
|
|
4
|
+
import { CONTAIN_PAD, DEFAULT_LANE_GAP, HEADER_RESERVE, LANE_LABEL_H, LANE_PAD_MIN } from "./tokens";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* lane.x を「動いた」 と見なす最小差分 (world unit)。
|
|
8
|
+
*
|
|
9
|
+
* pitch 均一化は先頭 lane から targetPitch を累積加算して x を再計算するため、
|
|
10
|
+
* 小数座標の図では既に均一でも 1e-13 規模の丸め誤差が乗る。 厳密比較だと
|
|
11
|
+
* この誤差を移動と誤判定し、 描画の変わらない図で lane 配列を再生成してしまう。
|
|
12
|
+
*/
|
|
13
|
+
const LANE_POSITION_EPSILON = 1e-9;
|
|
14
|
+
|
|
15
|
+
export function layoutLanes(diag: CdlDiagram): LaidLane[] {
|
|
16
|
+
// contain なし lane = label 高だけ確保、 contain あり lane は後で recompute で高さを node から決める。
|
|
17
|
+
// lane.x が未指定 (undefined) なら 前 lane の右端 + laneGap で auto 計算 (mermaid 風 auto-layout)。
|
|
18
|
+
// laneGap = viewport.laneGap → viewport.gap → DEFAULT_LANE_GAP の優先順で解決 (author 細粒度上書き可)。
|
|
19
|
+
//
|
|
20
|
+
// 注意 = ここで決めた x / laneGap は最終値ではない。 後段の `expandLaneGapsForEdgeLabels` が
|
|
21
|
+
// edge label 収納幅から requiredGap を算出し、 gap がそれを下回る場合は lane を右へ shift する。
|
|
22
|
+
// 結果として lanes 間の実効 gap は `max(laneGap, requiredGap)` になり、
|
|
23
|
+
// author が明示した `lane.x` も shift 対象になる (CdlViewport.laneGap の doc comment に実測例あり)。
|
|
24
|
+
//
|
|
25
|
+
// canvas pivot 新 spec (dragon canvas pivot spec §layout-role-conversion)。
|
|
26
|
+
// posX / posY / posW / posH が set 済 lane は「user が固定した絶対座標」 = 後続 auto layout の
|
|
27
|
+
// cursor 進行にも参加しない (座標 skip)。 cursorX 進行と切り離すことで他 lane の位置に副作用を
|
|
28
|
+
// 与えず、 完全に独立した free-form 座標として扱う。 未指定 lane は従来通り。
|
|
29
|
+
const laneGap = diag.viewport?.laneGap ?? diag.viewport?.gap ?? DEFAULT_LANE_GAP;
|
|
30
|
+
let cursorX = 0;
|
|
31
|
+
return diag.lanes.map((lane) => {
|
|
32
|
+
if (lane.posX !== undefined && lane.posY !== undefined) {
|
|
33
|
+
return {
|
|
34
|
+
...lane,
|
|
35
|
+
x: lane.posX,
|
|
36
|
+
y: lane.posY,
|
|
37
|
+
width: lane.posW ?? lane.width,
|
|
38
|
+
height: lane.posH ?? LANE_LABEL_H,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const resolvedX = lane.x ?? cursorX;
|
|
42
|
+
cursorX = resolvedX + lane.width + laneGap;
|
|
43
|
+
return {
|
|
44
|
+
...lane,
|
|
45
|
+
x: resolvedX,
|
|
46
|
+
y: HEADER_RESERVE,
|
|
47
|
+
height: LANE_LABEL_H,
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* node が lane.width より広い場合、 lane.width を node 最大幅 + 余裕に自動拡張する。
|
|
54
|
+
* 拡張後、 後続 lane の x を shift して被りを回避。
|
|
55
|
+
* 著者が lane.width を node より狭く指定しても engine が自動修正、 label 侵食を防ぐ。
|
|
56
|
+
*/
|
|
57
|
+
export function expandLanesForNodes(lanes: LaidLane[], nodes: LaidNode[]): LaidLane[] {
|
|
58
|
+
// 隣り合う node の間隔は「著者が空けた lane 間隔 + 左右 lane の余白」 で決まる。
|
|
59
|
+
//
|
|
60
|
+
// node 間隔 = lane 間隔 + (lane 幅 - node 幅) / 2 × 2 本分 = lane 間隔 + PAD × 2
|
|
61
|
+
//
|
|
62
|
+
// したがって PAD は「policy が要求する node 間隔を、 著者が詰めて置いた分だけ埋め合わせる」
|
|
63
|
+
// 値でなければならない。 PAD を定数で持つと、 著者が lane を詰めた図で policy を割る。
|
|
64
|
+
//
|
|
65
|
+
// 経緯 = 定数 25 だった頃、 lane 間隔 20 の図でちょうど 70 (policy ぴったり) になり、
|
|
66
|
+
// 間隔を 10 にした図は 60 で policy 割れを起こしていた (engine は何も補正しなかった)。
|
|
67
|
+
// 間隔 20 が偶然 policy を満たしていただけで、 policy を守る仕組みは無かった。
|
|
68
|
+
//
|
|
69
|
+
// 本 function は policy から必要 PAD を逆算する。 policy 値を変えれば PAD も追従する。
|
|
70
|
+
//
|
|
71
|
+
// canvas pivot 新 spec (dragon canvas pivot spec §layout-role-conversion)。
|
|
72
|
+
// posX / posY set 済 lane は cumulative shift 連鎖から除外、 自身の x / width を固定する。
|
|
73
|
+
// user drag / resize で明示的に置いた lane が他 lane の auto layout 再計算に副作用を与えない
|
|
74
|
+
// = 「座標 set 済 skip」 の SSOT。
|
|
75
|
+
const targetNodeGap = targetNodeNodeClearance();
|
|
76
|
+
|
|
77
|
+
// 隣接は「配列の並び」 ではなく x 座標で決める。 著者は lane を x 昇順に宣言するとは限らず、
|
|
78
|
+
// 配列順で隣を取ると実際には隣り合っていない対を見てしまう。
|
|
79
|
+
//
|
|
80
|
+
// 座標を固定した lane (posX / posY) は auto layout の連鎖から外れる既存仕様に合わせ、
|
|
81
|
+
// ここでも鎖から除く。 固定 lane は幅も位置も engine が動かせないため、 その隣で policy を
|
|
82
|
+
// 満たせるかは著者の配置次第になる = 本 function の保証の外。 除いた上で「auto lane 同士の
|
|
83
|
+
// 隣接」 を取り直すので、 固定 lane が間に挟まっても auto lane 同士の関係は切れない。
|
|
84
|
+
// 重ねることが目的の lane (role: "overlay") も鎖から除く。 単に「その対を見ない」 だけだと、
|
|
85
|
+
// 帯を挟んだ両隣の column lane 同士が隣接と判定されず調整が効かなくなる。
|
|
86
|
+
const isAuto = (l: LaidLane): boolean => l.posX === undefined || l.posY === undefined;
|
|
87
|
+
const isOverlay = (l: LaidLane): boolean => l.role === "overlay";
|
|
88
|
+
const autoSortedByX = lanes.filter(isAuto).sort((a, b) => a.x - b.x);
|
|
89
|
+
const columnChain = autoSortedByX.filter((l) => !isOverlay(l));
|
|
90
|
+
|
|
91
|
+
// 拡張は lane 間隔を変えない (幅の増分をそのまま後続 lane の x に足すため)。
|
|
92
|
+
// よって必要な余白は元の lane 配置から先に決められる。
|
|
93
|
+
//
|
|
94
|
+
// 重ねることが目的の lane (role: "overlay") が絡む対は対象外。 group を囲む枠や gantt の
|
|
95
|
+
// timeline 帯がこれにあたり、 引き剥がすのは配置の意図に反する。
|
|
96
|
+
//
|
|
97
|
+
// それ以外の対は重なっていても埋め合わせるが、 **重なり幅そのものは埋めない**。
|
|
98
|
+
// 重なり幅を足し込むと余白が青天井になり lane が異常に太る (実測 = 重なり 460 の対で
|
|
99
|
+
// 余白 270、 幅 480 の lane が 540)。 role の付け忘れがこの形で図を壊すのを防ぐため、
|
|
100
|
+
// 重なりは「間隔 0」 として扱い、 余白の上限を「間隔 0 で並べた時に要る値」 で頭打ちにする。
|
|
101
|
+
const gapById = new Map<string, number>();
|
|
102
|
+
for (let i = 0; i + 1 < columnChain.length; i++) {
|
|
103
|
+
const left = columnChain[i]!;
|
|
104
|
+
const right = columnChain[i + 1]!;
|
|
105
|
+
const gap = Math.max(0, right.x - (left.x + left.width));
|
|
106
|
+
// 左右それぞれの lane に対して、 自分が関わる対のうち最も狭い gap を覚える。
|
|
107
|
+
for (const l of [left, right]) {
|
|
108
|
+
const prev = gapById.get(l.id);
|
|
109
|
+
if (prev === undefined || gap < prev) gapById.set(l.id, gap);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const padFor = (lane: LaidLane): number => {
|
|
114
|
+
const narrowest = gapById.get(lane.id);
|
|
115
|
+
if (narrowest === undefined) return LANE_PAD_MIN;
|
|
116
|
+
// node 間隔 = lane 間隔 + PAD * 2 を targetNodeGap 以上にする PAD。
|
|
117
|
+
// narrowest は 0 以上に丸めてあるので、 上限は ceil(targetNodeGap / 2) で頭打ちになる。
|
|
118
|
+
return Math.max(LANE_PAD_MIN, Math.ceil((targetNodeGap - narrowest) / 2));
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// 幅が増えた lane の右側にある lane を、 増えた分だけ右へ動かして間隔を保つ。
|
|
122
|
+
//
|
|
123
|
+
// 「右側」 は x 座標で決める。 著者は lane を x 昇順に宣言するとは限らず、 配列順で
|
|
124
|
+
// 累積すると、 実際には左にある lane まで押してしまい配置が崩れる。
|
|
125
|
+
const widthOf = new Map<string, number>();
|
|
126
|
+
const deltaOf = new Map<string, number>();
|
|
127
|
+
for (const lane of lanes) {
|
|
128
|
+
if (lane.posX !== undefined && lane.posY !== undefined) continue;
|
|
129
|
+
const inside = nodes.filter((n) => n.lane === lane.id);
|
|
130
|
+
const maxNodeW = inside.length > 0 ? Math.max(...inside.map((n) => n.w)) : 0;
|
|
131
|
+
const newWidth = Math.max(lane.width, maxNodeW + padFor(lane) * 2);
|
|
132
|
+
widthOf.set(lane.id, newWidth);
|
|
133
|
+
deltaOf.set(lane.id, newWidth - lane.width);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const shiftOf = new Map<string, number>();
|
|
137
|
+
let cumulativeShift = 0;
|
|
138
|
+
for (const lane of autoSortedByX) {
|
|
139
|
+
shiftOf.set(lane.id, cumulativeShift);
|
|
140
|
+
cumulativeShift += deltaOf.get(lane.id) ?? 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return lanes.map((lane) => {
|
|
144
|
+
if (lane.posX !== undefined && lane.posY !== undefined) {
|
|
145
|
+
return { ...lane };
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
...lane,
|
|
149
|
+
x: lane.x + (shiftOf.get(lane.id) ?? 0),
|
|
150
|
+
width: widthOf.get(lane.id) ?? lane.width,
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* lane 間 gap を edge label 収納幅 + 両側 clearance で先手拡張する (CAR-470 SSOT)。
|
|
157
|
+
*
|
|
158
|
+
* 動機 = CAR-468 で routePath init labelY を LABEL_INIT_CLEARANCE (26 world、 CAR-570 で 34 → 26 縮小) に SSOT 準拠したが、
|
|
159
|
+
* lane 間 gap < label 幅 + 両側 clearance の場合、 edges.ts routePath L 字 分岐で
|
|
160
|
+
* `labelXOverlapsNode = true` 判定が発火 (edges.ts:354-356) し、 label が node 上端外側
|
|
161
|
+
* (`A.y - nodeMaxH/2 - CLEARANCE_NODE_LABEL - labelHalfH`) に押し出されて init 位置から乖離する。
|
|
162
|
+
* 実測 = stateMachine submit / success で label が path から 125 world 離れて配置される。
|
|
163
|
+
*
|
|
164
|
+
* 本 function は隣接 lane 対を跨ぐ edge が存在する場合、 label bbox が両 node bbox から
|
|
165
|
+
* CLEARANCE_NODE_LABEL 分離れて収まる gap を engine 側で確保する。 拡張後、 後続 lane は
|
|
166
|
+
* cumulative shift で連鎖 shift される (`expandLanesForNodes` と同じ設計)。
|
|
167
|
+
*
|
|
168
|
+
* 拡張式 = `labelBoxW + CLEARANCE_NODE_LABEL * 2 + EDGE_STUB_OUT * 2`
|
|
169
|
+
* - labelBoxW ... SSOT `computeLabelBoxW(edge.label, edge.sub)` (spec.ts)
|
|
170
|
+
* - CLEARANCE_NODE_LABEL ... node × edge-label 誤読下限 32 world (clearance-constants.ts)
|
|
171
|
+
* - EDGE_STUB_OUT ... edge 起点から水平直進最小距離 40 world (spec.ts)、 L 字 detour path
|
|
172
|
+
* で label が corner Y 上に置かれるため両側 stub 分の余白を確保する
|
|
173
|
+
*
|
|
174
|
+
* 結果 = lane 対を跨ぐ全 edge の label が `labelXOverlapsNode = false` 分岐を通り、
|
|
175
|
+
* routePath init labelY = A.y - LABEL_INIT_CLEARANCE (34、 CAR-630 で 26 → 34 復帰) がそのまま最終 label 位置となる。
|
|
176
|
+
*
|
|
177
|
+
* 非隣接 lane を跨ぐ edge の扱い (Issue #202)。
|
|
178
|
+
* edge は隣接 lane 対だけでなく、 間の lane を横断することがある (例 user(0) → api(2))。
|
|
179
|
+
* 初版は隣接対しか見ておらず、 横断される中間 lane の gap に label 幅が反映されないため
|
|
180
|
+
* label が非発着 lane の border を貫通していた。 現在は edge が跨ぐ全ての隣接対に対して
|
|
181
|
+
* その edge の labelBoxW を要求し、 label が span 内のどの対に置かれても収まるようにする。
|
|
182
|
+
*
|
|
183
|
+
* pitch 均一化 (Issue #202)。
|
|
184
|
+
* 上記の要求 gap を対ごとに個別適用すると lane 中心間距離 (pitch) が不揃いになる。
|
|
185
|
+
* lane diagram の node.cx は lane 中心に固定されるため、 pitch のばらつきは
|
|
186
|
+
* そのまま「同 stack 内 node の cx 差のばらつき」 として visualValidate の
|
|
187
|
+
* Axis 65 (row-gap-uniform) に検出される。 本 function は全対の必要 pitch を求めた上で
|
|
188
|
+
* その最大値に全対を揃え、 lifeline 間隔を均一にする。
|
|
189
|
+
*
|
|
190
|
+
* 実効仕様 = `pitch = max(全対の max(currentGap, requiredGap) + 両 lane の半幅和)`
|
|
191
|
+
* 先頭 lane の x は固定 (viewBox 原点維持)、 以降の lane が右へ shift される。
|
|
192
|
+
*/
|
|
193
|
+
export function expandLaneGapsForEdgeLabels(
|
|
194
|
+
lanes: LaidLane[],
|
|
195
|
+
nodes: LaidNode[],
|
|
196
|
+
edges: readonly CdlEdge[],
|
|
197
|
+
): LaidLane[] {
|
|
198
|
+
if (lanes.length < 2) return lanes;
|
|
199
|
+
// canvas pivot 新 spec (dragon canvas pivot spec §layout-role-conversion)。
|
|
200
|
+
// posX / posY set 済 lane は pitch uniform 化計算から完全除外し、 座標固定のまま返す。
|
|
201
|
+
// sortedByX に含めないことで隣接対にも入らず、 他 lane の layout に副作用を与えない。
|
|
202
|
+
// 未指定 lane のみ従来の pitch 均一化 logic に参加、 catalog 100+ backward compat 保証。
|
|
203
|
+
const layoutedLanes = lanes.filter((l) => !(l.posX !== undefined && l.posY !== undefined));
|
|
204
|
+
if (layoutedLanes.length < 2) return lanes;
|
|
205
|
+
// lane を x 昇順に並べる (author が非順序 index / x 手動指定した場合の safe fallback)。
|
|
206
|
+
// shifted x を lane.id で lookup、 最後に元 lane 配列の順序で返却する。
|
|
207
|
+
const nodeById = new Map(nodes.map((n) => [n.id, n]));
|
|
208
|
+
const sortedByX = [...layoutedLanes].sort((a, b) => a.x - b.x);
|
|
209
|
+
const laneIndexById = new Map(sortedByX.map((lane, i) => [lane.id, i]));
|
|
210
|
+
|
|
211
|
+
// 各 隣接 lane 対 (i, i+1) が確保すべき labelBoxW の最大値を求める。
|
|
212
|
+
//
|
|
213
|
+
// CAR-470 初版は「隣接 lane 対を跨ぐ edge」 だけを見ていた。 しかし edge は
|
|
214
|
+
// 非隣接 lane を跨ぐことがある (例 jwt-auth の user(0) → api(2))。 この場合
|
|
215
|
+
// label は中間 lane (login) の領域に描かれうるが、 その lane 対の gap 計算には
|
|
216
|
+
// 一切反映されていなかった。 結果 label が非発着 lane の border を貫通する
|
|
217
|
+
// (visualValidate の lane-border-clearance が検出)。
|
|
218
|
+
//
|
|
219
|
+
// 修正 = edge が跨ぐ全ての隣接 lane 対に対して、 その edge の labelBoxW を要求する。
|
|
220
|
+
// label が span 内のどの対に置かれても収まることを保証する (routing 前なので
|
|
221
|
+
// 実際の label x は未定、 保守的に全対へ課す)。
|
|
222
|
+
const requiredBoxWByPair = new Array<number>(Math.max(0, sortedByX.length - 1)).fill(0);
|
|
223
|
+
for (const edge of edges) {
|
|
224
|
+
const fromLane = nodeById.get(edge.from)?.lane;
|
|
225
|
+
const toLane = nodeById.get(edge.to)?.lane;
|
|
226
|
+
if (fromLane === undefined || toLane === undefined) continue;
|
|
227
|
+
const fi = laneIndexById.get(fromLane);
|
|
228
|
+
const ti = laneIndexById.get(toLane);
|
|
229
|
+
if (fi === undefined || ti === undefined || fi === ti) continue;
|
|
230
|
+
const labelBoxW = computeLabelBoxW(edge.label, edge.sub);
|
|
231
|
+
if (labelBoxW === 0) continue;
|
|
232
|
+
const lo = Math.min(fi, ti);
|
|
233
|
+
const hi = Math.max(fi, ti);
|
|
234
|
+
for (let pair = lo; pair < hi; pair++) {
|
|
235
|
+
if (labelBoxW > requiredBoxWByPair[pair]!) requiredBoxWByPair[pair] = labelBoxW;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// 各 隣接対が必要とする pitch (lane 中心間距離) を求め、 その最大値に全対を揃える。
|
|
240
|
+
//
|
|
241
|
+
// pitch を対ごとではなく全体で揃える理由 (Issue #202)。
|
|
242
|
+
// lane diagram の node.cx は lane 中心に固定される。 よって visualValidate の Axis 65
|
|
243
|
+
// (row-gap-uniform) が見る「同 stack 内 node の cx 差」 は pitch と恒等的に一致する。
|
|
244
|
+
// 対ごとに requiredGap だけ拡張する初版は pitch を不均一にし、 label 幅の差がそのまま
|
|
245
|
+
// node 間隔のばらつきとして現れていた (実測 = catalog で min 572 / max 733 world)。
|
|
246
|
+
//
|
|
247
|
+
// 代償 = 最も広い対に全対が引き上げられ図幅が増える。 各対の label 収納幅は下限として
|
|
248
|
+
// 保証し続けるため、 描画の正しさは損なわない。
|
|
249
|
+
const requiredPitchByPair: number[] = [];
|
|
250
|
+
for (let i = 0; i < sortedByX.length - 1; i++) {
|
|
251
|
+
const lane = sortedByX[i]!;
|
|
252
|
+
const rightLane = sortedByX[i + 1]!;
|
|
253
|
+
const halfSum = lane.width / 2 + rightLane.width / 2;
|
|
254
|
+
const currentPitch = rightLane.x + rightLane.width / 2 - (lane.x + lane.width / 2);
|
|
255
|
+
const maxLabelBoxW = requiredBoxWByPair[i]!;
|
|
256
|
+
// edge が跨がない対は label 収納の要求がない。 下限を課さず現状の pitch を維持する。
|
|
257
|
+
// author が意図的に lane を重ねた図 (resolveOverlaps が y 軸 shift で解消する edge case) を
|
|
258
|
+
// engine が勝手に引き離さないため。
|
|
259
|
+
if (maxLabelBoxW === 0) {
|
|
260
|
+
requiredPitchByPair.push(currentPitch);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
const requiredGap = maxLabelBoxW + CLEARANCE_NODE_LABEL * 2 + EDGE_STUB_OUT * 2;
|
|
264
|
+
requiredPitchByPair.push(Math.max(currentPitch, requiredGap + halfSum));
|
|
265
|
+
}
|
|
266
|
+
// 整数に切り上げる。 pitch は label 幅から決まり、 label 幅は 1 文字あたりの送り幅
|
|
267
|
+
// (1.08 em 等) × font size で出すため小数になる。 その小数がそのまま lane の x に乗り、
|
|
268
|
+
// node 中心 → edge label の x へ順に伝わって、 図全体の座標が非整数になっていた
|
|
269
|
+
// (実測 = dragon catalog 412 図で subpixel warn 936 件、 うち 9 割超が x 軸)。
|
|
270
|
+
//
|
|
271
|
+
// SVG は非整数座標を subpixel で描くため線や文字の輪郭がにじむ。 切り上げなので
|
|
272
|
+
// label 収納に必要な幅は必ず確保される (切り捨てると label がはみ出す)。
|
|
273
|
+
const targetPitch = Math.ceil(Math.max(...requiredPitchByPair));
|
|
274
|
+
|
|
275
|
+
// 先頭 lane の x を固定し (viewBox 原点維持)、 以降を targetPitch 間隔で再配置する。
|
|
276
|
+
const shiftedXById = new Map<string, number>();
|
|
277
|
+
const firstLane = sortedByX[0]!;
|
|
278
|
+
shiftedXById.set(firstLane.id, firstLane.x);
|
|
279
|
+
let center = firstLane.x + firstLane.width / 2;
|
|
280
|
+
for (let i = 1; i < sortedByX.length; i++) {
|
|
281
|
+
const lane = sortedByX[i]!;
|
|
282
|
+
center += targetPitch;
|
|
283
|
+
shiftedXById.set(lane.id, center - lane.width / 2);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// targetPitch の累積加算で 1e-13 規模の丸め誤差が乗る。 これを「動いた」 と判定すると
|
|
287
|
+
// 見た目の変わらない図でも lane を再配置してしまい、 snapshot に無意味な差分が出る。
|
|
288
|
+
const moved = sortedByX.some(
|
|
289
|
+
(lane) => Math.abs((shiftedXById.get(lane.id) ?? lane.x) - lane.x) > LANE_POSITION_EPSILON,
|
|
290
|
+
);
|
|
291
|
+
if (!moved) return lanes;
|
|
292
|
+
return lanes.map((lane) => ({ ...lane, x: shiftedXById.get(lane.id) ?? lane.x }));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function recomputeContainedLanes(lanes: LaidLane[], nodes: LaidNode[]): LaidLane[] {
|
|
296
|
+
// contain: true な lane は内部 node を囲む height を再計算
|
|
297
|
+
// lifeline: true な lane は全 node 最下端まで height を拡張 (lifeline が全 box を貫通するよう)
|
|
298
|
+
// sequence diagram は全 lifeline 同じ長さである必要があるので、 lifeline lane 群の bottom を統一する
|
|
299
|
+
const lifelineLanes = lanes.filter((l) => l.lifeline);
|
|
300
|
+
let lifelineBottom = 0;
|
|
301
|
+
if (lifelineLanes.length > 0) {
|
|
302
|
+
for (const lane of lifelineLanes) {
|
|
303
|
+
const inside = nodes.filter((n) => n.lane === lane.id);
|
|
304
|
+
if (inside.length === 0) continue;
|
|
305
|
+
const bottom = Math.max(...inside.map((n) => n.cy + n.h / 2));
|
|
306
|
+
lifelineBottom = Math.max(lifelineBottom, bottom);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return lanes.map((lane) => {
|
|
311
|
+
if (lane.lifeline) {
|
|
312
|
+
return {
|
|
313
|
+
...lane,
|
|
314
|
+
height: lifelineBottom - lane.y + CONTAIN_PAD,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
if (!lane.contain) return lane;
|
|
318
|
+
const inside = nodes.filter((n) => n.lane === lane.id);
|
|
319
|
+
if (inside.length === 0) return lane;
|
|
320
|
+
const top = Math.min(...inside.map((n) => n.cy - n.h / 2));
|
|
321
|
+
const bottom = Math.max(...inside.map((n) => n.cy + n.h / 2));
|
|
322
|
+
return {
|
|
323
|
+
...lane,
|
|
324
|
+
y: top - CONTAIN_PAD - LANE_LABEL_H,
|
|
325
|
+
height: bottom - top + CONTAIN_PAD * 2 + LANE_LABEL_H,
|
|
326
|
+
};
|
|
327
|
+
});
|
|
328
|
+
}
|