@cardenelabs/cdl 0.124.0 → 0.125.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/dist/index.cjs CHANGED
@@ -12332,6 +12332,8 @@ function diagram(id, options) {
12332
12332
  ...options.textMetrics !== void 0 ? { textMetrics: options.textMetrics } : {},
12333
12333
  // 種類の名前も同じ経路で渡し、空文字の扱いを `layout` の 1 箇所に揃える
12334
12334
  ...options.type !== void 0 ? { type: options.type } : {},
12335
+ // 空の並びは書かなかった図と同じ形にする。 凡例なしの JSON と配置を変えない
12336
+ ...options.legend && options.legend.length > 0 ? { legend: options.legend.map((item) => ({ ...item })) } : {},
12335
12337
  // inputs / formulas / scrollTriggers / eventBindings は空なら省略 (static diagram の JSON 形状維持)
12336
12338
  ...inputs.length > 0 ? { inputs: [...inputs] } : {},
12337
12339
  ...formulas.length > 0 ? { formulas: [...formulas] } : {},
@@ -12444,7 +12446,7 @@ function drawsTitleOutsideBox(kind) {
12444
12446
  }
12445
12447
  var TITLE_FONT_SIZE_DEFAULT = 18;
12446
12448
  var TITLE_FONT_SIZE = new Map([
12447
- ...["dyn-rect", "dyn-circle", "dyn-arc", "dyn-wave", "dyn-polygon", "station"].map(
12449
+ ...["dyn-rect", "dyn-circle", "dyn-arc", "dyn-wave", "dyn-polygon"].map(
12448
12450
  (k) => [k, 13]
12449
12451
  ),
12450
12452
  ...[
@@ -12489,8 +12491,10 @@ var TITLE_FONT_SIZE = new Map([
12489
12491
  "webhook"
12490
12492
  ].map((k) => [k, 20]),
12491
12493
  ...["card", "event", "function"].map((k) => [k, 24]),
12494
+ ["station", 25],
12492
12495
  ["actor", 32],
12493
- ["storage", 34]
12496
+ ["storage", 34],
12497
+ ["timeline-number", 18]
12494
12498
  ]);
12495
12499
  function titleFontSize(kind) {
12496
12500
  return TITLE_FONT_SIZE.get(kind ?? "") ?? TITLE_FONT_SIZE_DEFAULT;
@@ -12500,9 +12504,17 @@ function skipsNodeTitleWidthCheck(kind) {
12500
12504
  return KINDS_WITH_TITLE_WIDTH_OUTSIDE_NODE.has(kind ?? "");
12501
12505
  }
12502
12506
  var TITLE_BOX_PADDING = 52;
12507
+ var TIMELINE_NUMBER_STROKE = 4;
12508
+ function titleUsableWidth(kind, w, h) {
12509
+ if (kind === "timeline-number") {
12510
+ return Math.min(w, h) - 2 * TIMELINE_NUMBER_STROKE;
12511
+ }
12512
+ return w - (drawsTitleOutsideBox(kind) ? 0 : TITLE_BOX_PADDING);
12513
+ }
12503
12514
  var KINDS_SCALING_TITLE_WHEN_LOW = /* @__PURE__ */ new Set(["actor", "event", "function", "storage"]);
12504
12515
  var KINDS_SCALING_TITLE_ALWAYS = /* @__PURE__ */ new Set([
12505
12516
  "card",
12517
+ "timeline-number",
12506
12518
  "shape-file",
12507
12519
  "shape-folder",
12508
12520
  "shape-cloud",
@@ -12586,14 +12598,16 @@ var KINDS_DRAWING_ONLY_SHAPE = [
12586
12598
  ];
12587
12599
  var KINDS_WITHOUT_EYEBROW_TEXT = /* @__PURE__ */ new Set([
12588
12600
  ...KINDS_DRAWING_ONLY_SHAPE,
12589
- ...KINDS_WITHOUT_TITLE_TEXT
12601
+ ...KINDS_WITHOUT_TITLE_TEXT,
12602
+ "timeline-number"
12590
12603
  ]);
12591
12604
  function rendersEyebrow(kind) {
12592
12605
  return !KINDS_WITHOUT_EYEBROW_TEXT.has(kind ?? "");
12593
12606
  }
12594
12607
  var KINDS_WITHOUT_SUBTITLE_TEXT = /* @__PURE__ */ new Set([
12595
12608
  ...KINDS_DRAWING_OWN_PAYLOAD,
12596
- ...KINDS_WITHOUT_TITLE_TEXT
12609
+ ...KINDS_WITHOUT_TITLE_TEXT,
12610
+ "timeline-number"
12597
12611
  ]);
12598
12612
  function rendersSubtitle(kind) {
12599
12613
  return !KINDS_WITHOUT_SUBTITLE_TEXT.has(kind ?? "");
@@ -14700,6 +14714,14 @@ function curvePath(from, to, edge, labelMarginExtra = 0, textMetrics = "proporti
14700
14714
 
14701
14715
  // src/layout/metro-path.ts
14702
14716
  var METRO_CORNER_RADIUS = 24;
14717
+ var METRO_BRANCH_CORNER_RADIUS = 22;
14718
+ var METRO_BRANCH_STUB = 30;
14719
+ var METRO_DECISION_HEIGHT = 88;
14720
+ var METRO_DECISION_WIDTH = 116;
14721
+ var METRO_RETURN_END_OFFSET = 43;
14722
+ var METRO_RETURN_START_OFFSET = 24;
14723
+ var METRO_LINE_WIDTH = 10;
14724
+ var METRO_LINE_WIDTH_CSS = `var(--cdl-metro-line-width, ${METRO_LINE_WIDTH}px)`;
14703
14725
  var T = Math.SQRT2 - 1;
14704
14726
  function round2(v) {
14705
14727
  return Math.round(v * 100) / 100;
@@ -14717,7 +14739,63 @@ function polyline(points) {
14717
14739
  });
14718
14740
  return distinct.map((p, i) => `${i === 0 ? "M" : "L"} ${point(p)}`).join(" ");
14719
14741
  }
14720
- function metroPath(from, to, edge, labelMarginExtra = 0, textMetrics = "proportional") {
14742
+ function quadraticCornerPath(start, corner, end, radius) {
14743
+ const horizontalDir = Math.sign(corner.x - start.x);
14744
+ const verticalDir = Math.sign(end.y - corner.y);
14745
+ const r = Math.min(
14746
+ radius,
14747
+ Math.abs(corner.x - start.x),
14748
+ Math.abs(end.y - corner.y)
14749
+ );
14750
+ const before = { x: corner.x - horizontalDir * r, y: corner.y };
14751
+ const after = { x: corner.x, y: corner.y + verticalDir * r };
14752
+ return [
14753
+ `M ${point(start)}`,
14754
+ `L ${point(before)}`,
14755
+ `Q ${point(corner)} ${point(after)}`,
14756
+ `L ${point(end)}`
14757
+ ].join(" ");
14758
+ }
14759
+ function decisionBranchPath(start, end) {
14760
+ const horizontalDir = Math.sign(end.x - start.x);
14761
+ const verticalDir = Math.sign(end.y - start.y);
14762
+ const turnX = start.x + horizontalDir * (METRO_BRANCH_STUB + METRO_BRANCH_CORNER_RADIUS);
14763
+ const radius = Math.min(
14764
+ METRO_BRANCH_CORNER_RADIUS,
14765
+ Math.abs(end.y - start.y) / 2,
14766
+ Math.abs(end.x - turnX)
14767
+ );
14768
+ const firstCorner = { x: turnX, y: start.y };
14769
+ const secondCorner = { x: turnX, y: end.y };
14770
+ const beforeFirst = {
14771
+ x: turnX - horizontalDir * radius,
14772
+ y: start.y
14773
+ };
14774
+ const afterFirst = {
14775
+ x: turnX,
14776
+ y: start.y + verticalDir * radius
14777
+ };
14778
+ const beforeSecond = {
14779
+ x: turnX,
14780
+ y: end.y - verticalDir * radius
14781
+ };
14782
+ const afterSecond = {
14783
+ x: turnX + horizontalDir * radius,
14784
+ y: end.y
14785
+ };
14786
+ return {
14787
+ d: [
14788
+ `M ${point(start)}`,
14789
+ `L ${point(beforeFirst)}`,
14790
+ `Q ${point(firstCorner)} ${point(afterFirst)}`,
14791
+ `L ${point(beforeSecond)}`,
14792
+ `Q ${point(secondCorner)} ${point(afterSecond)}`,
14793
+ `L ${point(end)}`
14794
+ ].join(" "),
14795
+ turnX
14796
+ };
14797
+ }
14798
+ function metroPath(from, to, edge, _labelMarginExtra = 0, _textMetrics = "proportional") {
14721
14799
  const fromLeft = from.cx - from.w / 2;
14722
14800
  const fromRight = from.cx + from.w / 2;
14723
14801
  const toLeft = to.cx - to.w / 2;
@@ -14752,11 +14830,45 @@ function metroPath(from, to, edge, labelMarginExtra = 0, textMetrics = "proporti
14752
14830
  const sgn = Math.sign(dy);
14753
14831
  let d;
14754
14832
  let mid;
14755
- let tangent;
14756
- if (Math.abs(dy) < SAME_HEIGHT_THRESHOLD) {
14833
+ let metroReturn = false;
14834
+ if (from.kind === "decision" && Math.abs(dy) >= SAME_HEIGHT_THRESHOLD) {
14835
+ const verticalDir = Math.sign(dy);
14836
+ if (Math.abs(to.cx - from.cx) < SAME_HEIGHT_THRESHOLD) {
14837
+ const start = { x: from.cx, y: from.cy + verticalDir * from.h / 2 };
14838
+ const end = { x: to.cx, y: to.cy };
14839
+ d = polyline([start, end]);
14840
+ fromSide = verticalDir < 0 ? "top" : "bottom";
14841
+ toSide = verticalDir < 0 ? "bottom" : "top";
14842
+ mid = { x: start.x, y: (start.y + end.y) / 2 };
14843
+ } else {
14844
+ const horizontalDir = Math.sign(to.cx - from.cx);
14845
+ const start = {
14846
+ x: from.cx + horizontalDir * from.w / 2,
14847
+ y: from.cy
14848
+ };
14849
+ const end = {
14850
+ x: to.cx - horizontalDir * to.w / 2,
14851
+ y: to.cy
14852
+ };
14853
+ const branch = decisionBranchPath(start, end);
14854
+ d = branch.d;
14855
+ fromSide = horizontalDir > 0 ? "right" : "left";
14856
+ toSide = horizontalDir > 0 ? "left" : "right";
14857
+ mid = { x: branch.turnX, y: (start.y + end.y) / 2 };
14858
+ }
14859
+ } else if (edge?.style === "dashed" && from.kind === "station" && to.kind === "station" && to.cx < from.cx && Math.abs(dy) >= SAME_HEIGHT_THRESHOLD) {
14860
+ const verticalDir = Math.sign(to.cy - from.cy);
14861
+ const start = { x: from.cx - METRO_RETURN_START_OFFSET, y: from.cy };
14862
+ const end = { x: to.cx, y: to.cy - verticalDir * METRO_RETURN_END_OFFSET };
14863
+ const corner = { x: end.x, y: start.y };
14864
+ fromSide = "left";
14865
+ toSide = verticalDir < 0 ? "bottom" : "top";
14866
+ d = quadraticCornerPath(start, corner, end, METRO_BRANCH_CORNER_RADIUS);
14867
+ mid = { x: (start.x + corner.x) / 2, y: start.y };
14868
+ metroReturn = true;
14869
+ } else if (Math.abs(dy) < SAME_HEIGHT_THRESHOLD) {
14757
14870
  d = `M ${point({ x: x1, y: y1 })} L ${point({ x: x2, y: y2 })}`;
14758
14871
  mid = { x: (x1 + x2) / 2, y: (y1 + y2) / 2 };
14759
- tangent = { x: x2 - x1, y: y2 - y1 };
14760
14872
  } else if (fromSide !== toSide && Math.abs(x2 - x1) >= Math.abs(dy)) {
14761
14873
  const horizontal = Math.abs(x2 - x1);
14762
14874
  const s = (horizontal - Math.abs(dy)) / 2;
@@ -14786,7 +14898,6 @@ function metroPath(from, to, edge, labelMarginExtra = 0, textMetrics = "proporti
14786
14898
  d = polyline([p0, p1, p2, p3]);
14787
14899
  }
14788
14900
  mid = { x: (x1 + x2) / 2, y: (y1 + y2) / 2 };
14789
- tangent = w;
14790
14901
  } else {
14791
14902
  fromSide = "right";
14792
14903
  toSide = "right";
@@ -14816,33 +14927,32 @@ function metroPath(from, to, edge, labelMarginExtra = 0, textMetrics = "proporti
14816
14927
  `L ${point(p4)}`
14817
14928
  ].join(" ");
14818
14929
  mid = { x: p2.x - T * r, y: p2.y };
14819
- tangent = { x: 0, y: sgn };
14820
- }
14821
- if (edge?.overlay) {
14822
- return { d, fromSide, toSide, labelX: mid.x, labelY: mid.y, labelAnchor: "middle" };
14823
- }
14824
- const tangentLength = Math.hypot(tangent.x, tangent.y);
14825
- let normalX = tangent.y / tangentLength;
14826
- let normalY = -tangent.x / tangentLength;
14827
- if (normalY > Number.EPSILON || Math.abs(normalY) <= Number.EPSILON && normalX < 0) {
14828
- normalX *= -1;
14829
- normalY *= -1;
14830
14930
  }
14831
- const labelW = computeLabelBoxW(edge?.label, edge?.sub, textMetrics);
14832
- const labelH = labelPillH(edge?.sub);
14833
- const distance = labelPathClearanceHorizontal(edge) + (Math.abs(normalX) * labelW + Math.abs(normalY) * labelH) / 2 + labelMarginExtra;
14834
14931
  return {
14835
14932
  d,
14836
14933
  fromSide,
14837
14934
  toSide,
14838
- labelX: mid.x + normalX * distance,
14839
- labelY: mid.y + normalY * distance,
14840
- labelAnchor: "middle"
14935
+ labelX: mid.x,
14936
+ labelY: mid.y,
14937
+ labelAnchor: "middle",
14938
+ ...metroReturn ? { metroReturn: true } : {}
14841
14939
  };
14842
14940
  }
14843
14941
 
14844
14942
  // src/types.ts
14845
14943
  var TONES = ["accent", "teal", "success", "error", "warning", "info"];
14944
+ var LEGEND_MARKS = [
14945
+ "diamond",
14946
+ "filled-circle",
14947
+ "double-circle",
14948
+ "dotted-line",
14949
+ "solid-line",
14950
+ "curved-line",
14951
+ "rounded-label",
14952
+ "numbered-circle",
14953
+ "station",
14954
+ "arrow"
14955
+ ];
14846
14956
  var NODE_KINDS = [
14847
14957
  // dynamic shape (CAR: shape primitive) ... signal 値で内部 fill / radius / angle 等が
14848
14958
  // リアルタイム変化する汎用 container、 shape spec は node.shape に格納
@@ -14964,12 +15074,14 @@ var NODE_KINDS = [
14964
15074
  "shape-lawyer",
14965
15075
  "shape-trader",
14966
15076
  "shape-customer-service",
14967
- // 流れの始まりと終わりの印 2 種 (#560) ... 塗った丸と輪で囲んだ丸。
14968
- // 状態遷移図が使う。 印そのものが意味を持つので文字は描かない
15077
+ // 流れの始まりと終わりの印 2 種 (#560) ... 始点と終点は線の端から推測せず、著者が
15078
+ // `kind: "mark-start"` / `kind: "mark-end"` の node を明示する。印は文字を描かない
14969
15079
  "mark-start",
14970
15080
  "mark-end",
14971
15081
  // 路線図の駅 (#996) ... 線路上の点を小さな丸で示し、名前と説明は丸の外に置く
14972
15082
  "station",
15083
+ // 時間軸の番号 (#1017) ... 実線の軸上に番号を入れた丸を置く。路線図の駅とは別の意味を持つ。
15084
+ "timeline-number",
14973
15085
  // 時系列のやり取り (#578) ... 1 つの箱が図を丸ごと描く。 縦が時間で、参加者は箱を持たない
14974
15086
  "sequence-board"
14975
15087
  ];
@@ -17703,7 +17815,7 @@ function runAxes(laid, diag, violations, counts, push, profile, skippedAxes, \u5
17703
17815
  const LIFELINE_W_THRESHOLD = 8;
17704
17816
  for (const n of laid.nodes) {
17705
17817
  if (n.w < LIFELINE_W_THRESHOLD) continue;
17706
- if (n.kind === "station") continue;
17818
+ if (n.kind === "station" || n.kind === "timeline-number") continue;
17707
17819
  if (n.w < MIN_NODE_W || n.h < MIN_NODE_H) {
17708
17820
  push(
17709
17821
  "node-visibility",
@@ -17729,8 +17841,7 @@ function runAxes(laid, diag, violations, counts, push, profile, skippedAxes, \u5
17729
17841
  if (!isRenderedNode(n)) continue;
17730
17842
  const titleTrimmed = n.title?.trim() ?? "";
17731
17843
  if (!titleTrimmed) continue;
17732
- const \u4F59\u767D = drawsTitleOutsideBox(n.kind) ? 0 : TITLE_BOX_PADDING;
17733
- const \u4F7F\u3048\u308B\u5E452 = n.w - \u4F59\u767D;
17844
+ const \u4F7F\u3048\u308B\u5E452 = titleUsableWidth(n.kind, n.w, n.h);
17734
17845
  const \u5B9F\u5E45 = measureTextWidth(titleTrimmed, {
17735
17846
  fontSize: titleFontSize(n.kind),
17736
17847
  bound: "drawn",
@@ -17749,7 +17860,7 @@ function runAxes(laid, diag, violations, counts, push, profile, skippedAxes, \u5
17749
17860
  push(
17750
17861
  "text-readability",
17751
17862
  // 字ごとの実幅は小数になるので、読む人に出す時だけ丸める (判定は丸めない)
17752
- `node "${n.id}" title "${n.title}" \u304C node \u5E45 ${n.w}px \u3092\u8D85\u904E\u3059\u308B\u53EF\u80FD\u6027 (\u671F\u5F85 ${Math.round(\u5B9F\u5E45 + \u4F59\u767D)}px)`,
17863
+ `node "${n.id}" title "${n.title}" \u304C node \u5E45 ${n.w}px \u3092\u8D85\u904E\u3059\u308B\u53EF\u80FD\u6027 (\u671F\u5F85 ${Math.round(\u5B9F\u5E45 + (n.w - \u4F7F\u3048\u308B\u5E452))}px)`,
17753
17864
  "warn"
17754
17865
  );
17755
17866
  }
@@ -19563,6 +19674,8 @@ var TOKENS = {
19563
19674
  lane_label_h: 40,
19564
19675
  lane_top_pad: 100,
19565
19676
  contain_pad: 28,
19677
+ stage_column_gap: 48,
19678
+ stage_card_text_inset: 18,
19566
19679
  // node sizes (kind ごと、 unified row h は engine が max() で算出)
19567
19680
  node: {
19568
19681
  actor: { w: 340, h: 165 },
@@ -19689,7 +19802,10 @@ var TOKENS = {
19689
19802
  "mark-start": { w: 96, h: 96 },
19690
19803
  "mark-end": { w: 96, h: 96 },
19691
19804
  // 路線図の駅 (#996) ... 正方形にして小さな丸が縦横どちらにも歪まないようにする。
19692
- station: { w: 24, h: 24 },
19805
+ // 半径 16 と縁 6 の外径を、線路との接点を含めて収める。
19806
+ station: { w: 38, h: 38 },
19807
+ // 時間軸の番号 (#1017) ... 半径 24 の丸と太さ 4 の縁を収める。
19808
+ "timeline-number": { w: 52, h: 52 },
19693
19809
  // 時系列のやり取り (#578)。 実寸は中身から決まる (`sequenceBoardMetrics`) ので、
19694
19810
  // ここは書かなかった時の受け皿
19695
19811
  "sequence-board": { w: 960, h: 640 }
@@ -19704,6 +19820,8 @@ var LANE_TOP_PAD = TOKENS.lane_top_pad;
19704
19820
  var STACK_GAP = TOKENS.row_gap;
19705
19821
  var NODE_SIZE = TOKENS.node;
19706
19822
  var CONTAIN_PAD = TOKENS.contain_pad;
19823
+ var STAGE_COLUMN_GAP = TOKENS.stage_column_gap;
19824
+ var STAGE_CARD_TEXT_INSET = TOKENS.stage_card_text_inset;
19707
19825
  var VIEW_PAD = TOKENS.view_pad;
19708
19826
  var HEADER_RESERVE = TOKENS.header_reserve;
19709
19827
  var DEFAULT_LANE_GAP = 80;
@@ -19804,12 +19922,24 @@ function layoutEdges(diag, nodes, lanes = [], textMetrics = "proportional") {
19804
19922
  if (!from) throw new Error(`cdl layout: edge "${e.id}" \u306E from "${e.from}" \u304C node \u306B\u7121\u3044`);
19805
19923
  if (!to) throw new Error(`cdl layout: edge "${e.id}" \u306E to "${e.to}" \u304C node \u306B\u7121\u3044`);
19806
19924
  const c = curvePath(from, to, e, labelMarginExtra, textMetrics);
19925
+ const fromLane = lmap.get(from.lane);
19926
+ const toLane = lmap.get(to.lane);
19927
+ let labelX = c.labelX;
19928
+ if (fromLane?.stage !== void 0 && toLane?.stage !== void 0 && fromLane.id !== toLane.id && (e.label || e.sub)) {
19929
+ const [leftLane, rightLane] = fromLane.x <= toLane.x ? [fromLane, toLane] : [toLane, fromLane];
19930
+ const gapLeft = leftLane.x + leftLane.width - STAGE_COLUMN_GAP / 2;
19931
+ const gapRight = rightLane.x + STAGE_COLUMN_GAP / 2;
19932
+ const labelHalfWidth = computeLabelBoxW(e.label, e.sub, textMetrics) / 2;
19933
+ const minX = gapLeft + labelHalfWidth;
19934
+ const maxX = gapRight - labelHalfWidth;
19935
+ if (minX <= maxX) labelX = Math.min(maxX, Math.max(minX, labelX));
19936
+ }
19807
19937
  positionedEdges.push({
19808
19938
  ...e,
19809
19939
  d: c.d,
19810
19940
  fromSide: c.fromSide,
19811
19941
  toSide: c.toSide,
19812
- labelX: c.labelX + (e.labelOffsetX ?? 0),
19942
+ labelX: labelX + (e.labelOffsetX ?? 0),
19813
19943
  labelY: c.labelY + (e.labelOffsetY ?? 0),
19814
19944
  labelAnchor: c.labelAnchor
19815
19945
  });
@@ -19826,7 +19956,8 @@ function layoutEdges(diag, nodes, lanes = [], textMetrics = "proportional") {
19826
19956
  toSide: m.toSide,
19827
19957
  labelX: m.labelX + (e.labelOffsetX ?? 0),
19828
19958
  labelY: m.labelY + (e.labelOffsetY ?? 0),
19829
- labelAnchor: m.labelAnchor
19959
+ labelAnchor: m.labelAnchor,
19960
+ ...m.metroReturn ? { metroReturn: true } : {}
19830
19961
  });
19831
19962
  } else {
19832
19963
  autoEdges.push(e);
@@ -20518,20 +20649,22 @@ function countPathObstacleCrossings(d, obstacles) {
20518
20649
  const segments = flattenPathSegments(extractPathSegments(d));
20519
20650
  return obstacles.filter((obstacle) => segmentsCrossRect(segments, nodeRect(obstacle))).length;
20520
20651
  }
20521
- function makeBackDetourCandidate(A, B, from, to, obstacles, detourY, side) {
20652
+ function makeBackDetourCandidate(A, B, from, to, obstacles, detourY, side, columnXOverride) {
20522
20653
  const lowIsA = A.y >= B.y;
20523
20654
  const low = lowIsA ? A : B;
20524
20655
  const lowBendY = low.y - VERTICAL_DETOUR_EXIT;
20525
20656
  const bandTop = Math.min(detourY, lowBendY);
20526
20657
  const bandBottom = Math.max(detourY, lowBendY);
20527
20658
  const bandObstacles = obstacles.map(nodeRect).filter((rect) => rect.y + rect.h >= bandTop && rect.y <= bandBottom);
20528
- let columnX = detourColumnFor([from, to], side);
20529
- while (true) {
20530
- const blockers = bandObstacles.filter(
20531
- (rect) => rect.x - VERTICAL_DETOUR_GAP < columnX && columnX < rect.x + rect.w + VERTICAL_DETOUR_GAP
20532
- );
20533
- if (blockers.length === 0) break;
20534
- columnX = side === "left" ? Math.min(...blockers.map((rect) => rect.x - VERTICAL_DETOUR_GAP)) : Math.max(...blockers.map((rect) => rect.x + rect.w + VERTICAL_DETOUR_GAP));
20659
+ let columnX = columnXOverride ?? detourColumnFor([from, to], side);
20660
+ if (columnXOverride === void 0) {
20661
+ while (true) {
20662
+ const blockers = bandObstacles.filter(
20663
+ (rect) => rect.x - VERTICAL_DETOUR_GAP < columnX && columnX < rect.x + rect.w + VERTICAL_DETOUR_GAP
20664
+ );
20665
+ if (blockers.length === 0) break;
20666
+ columnX = side === "left" ? Math.min(...blockers.map((rect) => rect.x - VERTICAL_DETOUR_GAP)) : Math.max(...blockers.map((rect) => rect.x + rect.w + VERTICAL_DETOUR_GAP));
20667
+ }
20535
20668
  }
20536
20669
  const bendAY = lowIsA ? lowBendY : detourY;
20537
20670
  const bendBY = lowIsA ? detourY : lowBendY;
@@ -20640,7 +20773,7 @@ function sidePoint(n, side) {
20640
20773
  return { x: n.cx + n.w / 2, y: n.cy };
20641
20774
  }
20642
20775
  }
20643
- function routePath(from, to, fromSide, toSide, obstacles = [], detourSide, _fromLane, _toLane, edge, labelObstacles = obstacles, fromOffset = { x: 0, y: 0 }, toOffset = { x: 0, y: 0 }, avoidRects = [], textMetrics = "proportional") {
20776
+ function routePath(from, to, fromSide, toSide, obstacles = [], detourSide, fromLane, toLane, edge, labelObstacles = obstacles, fromOffset = { x: 0, y: 0 }, toOffset = { x: 0, y: 0 }, avoidRects = [], textMetrics = "proportional") {
20644
20777
  const labelBoxW = edge ? computeLabelBoxW(edge.label, edge.sub, textMetrics) : 0;
20645
20778
  const labelHalfW = labelBoxW / 2;
20646
20779
  const labelInit = labelInitClearance(edge);
@@ -21010,17 +21143,27 @@ function routePath(from, to, fromSide, toSide, obstacles = [], detourSide, _from
21010
21143
  };
21011
21144
  }
21012
21145
  if (topTopPair) {
21013
- const detourY = Math.min(A.y, B.y) - 100;
21146
+ const stagePoint = (point2, node, lane) => lane?.stage === void 0 ? point2 : {
21147
+ x: Math.min(node.cx + node.w / 2 - 24, lane.x + lane.width - 24),
21148
+ y: point2.y
21149
+ };
21150
+ const backA = stagePoint(A, from, fromLane);
21151
+ const backB = stagePoint(B, to, toLane);
21152
+ const stageTops = [fromLane, toLane].filter((lane) => lane?.stage !== void 0).map((lane) => lane.y);
21153
+ const detourY = Math.min(
21154
+ Math.min(backA.y, backB.y) - 100,
21155
+ ...stageTops.map((top) => top - VERTICAL_DETOUR_GAP)
21156
+ );
21014
21157
  const corner = 14;
21015
- const xDir = B.x > A.x ? 1 : -1;
21158
+ const xDir = backB.x > backA.x ? 1 : -1;
21016
21159
  const existing = {
21017
- d: `M ${A.x} ${A.y}
21018
- L ${A.x} ${detourY + corner}
21019
- Q ${A.x} ${detourY}, ${A.x + xDir * corner} ${detourY}
21020
- L ${B.x - xDir * corner} ${detourY}
21021
- Q ${B.x} ${detourY}, ${B.x} ${detourY + corner}
21022
- L ${B.x} ${B.y}`,
21023
- labelX: (A.x + B.x) / 2,
21160
+ d: `M ${backA.x} ${backA.y}
21161
+ L ${backA.x} ${detourY + corner}
21162
+ Q ${backA.x} ${detourY}, ${backA.x + xDir * corner} ${detourY}
21163
+ L ${backB.x - xDir * corner} ${detourY}
21164
+ Q ${backB.x} ${detourY}, ${backB.x} ${detourY + corner}
21165
+ L ${backB.x} ${backB.y}`,
21166
+ labelX: (backA.x + backB.x) / 2,
21024
21167
  // CAR-468 SSOT ... 旧 detourY - 30 magic number を LABEL_INIT_CLEARANCE (26、 CAR-570 で 34 → 26 縮小) に置換。
21025
21168
  // CAR-518 SSOT ... dotted-flow は 62 world (marker glow radius 分拡張) を labelInit で参照。
21026
21169
  // top-top detour の水平 segment 上方 labelInit world = spec 準拠距離。
@@ -21029,7 +21172,16 @@ function routePath(from, to, fromSide, toSide, obstacles = [], detourSide, _from
21029
21172
  };
21030
21173
  const existingCrossings = countPathObstacleCrossings(existing.d, obstacles);
21031
21174
  if (existingCrossings === 0) return existing;
21032
- const candidate = chooseBackDetourCandidate(A, B, from, to, obstacles, detourY);
21175
+ const candidate = from.lane === to.lane && fromLane?.stage !== void 0 ? makeBackDetourCandidate(
21176
+ backA,
21177
+ backB,
21178
+ from,
21179
+ to,
21180
+ obstacles,
21181
+ detourY,
21182
+ "right",
21183
+ fromLane.x + fromLane.width
21184
+ ) : chooseBackDetourCandidate(backA, backB, from, to, obstacles, detourY);
21033
21185
  if (candidate.crossings >= existingCrossings) return existing;
21034
21186
  return {
21035
21187
  d: candidate.d,
@@ -21963,12 +22115,56 @@ function \u76F4\u524D\u306E\u6298\u308C\u3054\u3068\u305A\u3089\u3059(segs, \u6A
21963
22115
  return parts.join(" ");
21964
22116
  }
21965
22117
 
22118
+ // src/layout/legend.ts
22119
+ var LEGEND_FONT_SIZE = 17;
22120
+ var LEGEND_MARK_SCALE = LEGEND_FONT_SIZE / 21;
22121
+ var LEGEND_MARK_REFERENCE_WIDTH = 46;
22122
+ var LEGEND_MARK_WIDTH = LEGEND_MARK_REFERENCE_WIDTH * LEGEND_MARK_SCALE;
22123
+ var LEGEND_MARK_TEXT_GAP = 12 * LEGEND_MARK_SCALE;
22124
+ var LEGEND_ITEM_GAP_RATIO = 0.07;
22125
+ var LEGEND_BASELINE_GAP = 32;
22126
+ var LEGEND_BOTTOM_EXTENT = 10;
22127
+ function layoutLegend(items, viewBox, textMetrics, nodes) {
22128
+ const renderedNodes = nodes.filter(isRenderedNode);
22129
+ const startX = renderedNodes.length > 0 ? Math.min(...renderedNodes.map((node) => node.cx - node.w / 2)) : viewBox.x + VIEW_PAD;
22130
+ const diagramRight = renderedNodes.length > 0 ? Math.max(...renderedNodes.map((node) => node.cx + node.w / 2)) : viewBox.x + viewBox.w - VIEW_PAD;
22131
+ const itemGap = (diagramRight - startX) * LEGEND_ITEM_GAP_RATIO;
22132
+ const y = viewBox.y + viewBox.h - VIEW_PAD + LEGEND_BASELINE_GAP;
22133
+ let x = startX;
22134
+ const legend = items.map((item) => {
22135
+ const laid = { ...item, x, y };
22136
+ x += LEGEND_MARK_WIDTH + LEGEND_MARK_TEXT_GAP + textWidth(item.text, LEGEND_FONT_SIZE, 0, textMetrics) + itemGap;
22137
+ return laid;
22138
+ });
22139
+ const right = x - itemGap;
22140
+ const bottom = y + LEGEND_BOTTOM_EXTENT + VIEW_PAD;
22141
+ return {
22142
+ legend,
22143
+ viewBox: {
22144
+ ...viewBox,
22145
+ w: Math.max(viewBox.w, right + VIEW_PAD - viewBox.x),
22146
+ h: Math.max(viewBox.h, bottom - viewBox.y)
22147
+ }
22148
+ };
22149
+ }
22150
+
21966
22151
  // src/layout/lanes.ts
21967
22152
  var LANE_POSITION_EPSILON = 1e-9;
22153
+ function equalizeStageColumns(lanes, nodes = []) {
22154
+ const stages = lanes.filter((lane) => lane.stage !== void 0);
22155
+ if (stages.length === 0) return lanes;
22156
+ const top = Math.min(...stages.map((lane) => lane.y));
22157
+ const stageIds = new Set(stages.map((lane) => lane.id));
22158
+ const nodeBottoms = nodes.filter((node) => stageIds.has(node.lane)).map((node) => node.cy + node.h / 2 + CONTAIN_PAD);
22159
+ const bottom = Math.max(...stages.map((lane) => lane.y + lane.height), ...nodeBottoms);
22160
+ return lanes.map(
22161
+ (lane) => lane.stage === void 0 ? lane : { ...lane, y: top, height: bottom - top }
22162
+ );
22163
+ }
21968
22164
  function layoutLanes(diag) {
21969
22165
  const laneGap = diag.viewport?.laneGap ?? diag.viewport?.gap ?? DEFAULT_LANE_GAP;
21970
22166
  let cursorX = 0;
21971
- return diag.lanes.map((lane) => {
22167
+ const lanes = diag.lanes.map((lane, index) => {
21972
22168
  if (lane.posX !== void 0 && lane.posY !== void 0) {
21973
22169
  return {
21974
22170
  ...lane,
@@ -21979,7 +22175,9 @@ function layoutLanes(diag) {
21979
22175
  };
21980
22176
  }
21981
22177
  const resolvedX = lane.x ?? cursorX;
21982
- cursorX = resolvedX + lane.width + laneGap;
22178
+ const nextLane = diag.lanes[index + 1];
22179
+ const gapAfter = lane.stage !== void 0 && nextLane?.stage !== void 0 ? 0 : laneGap;
22180
+ cursorX = resolvedX + lane.width + gapAfter;
21983
22181
  return {
21984
22182
  ...lane,
21985
22183
  x: resolvedX,
@@ -21987,6 +22185,7 @@ function layoutLanes(diag) {
21987
22185
  height: LANE_LABEL_H
21988
22186
  };
21989
22187
  });
22188
+ return equalizeStageColumns(lanes);
21990
22189
  }
21991
22190
  function expandLanesForNodes(lanes, nodes) {
21992
22191
  const targetNodeGap = targetNodeNodeClearance();
@@ -22015,7 +22214,8 @@ function expandLanesForNodes(lanes, nodes) {
22015
22214
  if (lane.posX !== void 0 && lane.posY !== void 0) continue;
22016
22215
  const inside = nodes.filter((n) => n.lane === lane.id);
22017
22216
  const maxNodeW = inside.length > 0 ? Math.max(...inside.map((n) => n.w)) : 0;
22018
- const newWidth = Math.max(lane.width, maxNodeW + padFor(lane) * 2);
22217
+ const stageMinWidth = lane.stage === void 0 ? 0 : maxNodeW + CONTAIN_PAD + STAGE_COLUMN_GAP;
22218
+ const newWidth = Math.max(lane.width, maxNodeW + padFor(lane) * 2, stageMinWidth);
22019
22219
  widthOf.set(lane.id, newWidth);
22020
22220
  deltaOf.set(lane.id, newWidth - lane.width);
22021
22221
  }
@@ -22040,6 +22240,34 @@ function expandLaneGapsForEdgeLabels(lanes, nodes, edges, textMetrics = "proport
22040
22240
  if (lanes.length < 2) return lanes;
22041
22241
  const layoutedLanes = lanes.filter((l) => !(l.posX !== void 0 && l.posY !== void 0));
22042
22242
  if (layoutedLanes.length < 2) return lanes;
22243
+ if (layoutedLanes.length === lanes.length && layoutedLanes.every((lane) => lane.stage !== void 0)) {
22244
+ const sortedStages = [...layoutedLanes].sort((a, b) => a.x - b.x);
22245
+ const stageIndexById = new Map(sortedStages.map((lane, index) => [lane.id, index]));
22246
+ const nodeLaneById = new Map(nodes.map((node) => [node.id, node.lane]));
22247
+ const requiredSurfaceGaps = new Array(sortedStages.length - 1).fill(STAGE_COLUMN_GAP);
22248
+ for (const edge of edges) {
22249
+ if (edge.routing !== "curve" || !edge.label && !edge.sub) continue;
22250
+ const fromIndex = stageIndexById.get(nodeLaneById.get(edge.from) ?? "");
22251
+ const toIndex = stageIndexById.get(nodeLaneById.get(edge.to) ?? "");
22252
+ if (fromIndex === void 0 || toIndex === void 0 || fromIndex === toIndex) continue;
22253
+ const lo = Math.min(fromIndex, toIndex);
22254
+ const hi = Math.max(fromIndex, toIndex);
22255
+ const labelWidth = computeLabelBoxW(edge.label, edge.sub, textMetrics);
22256
+ for (let pair = lo; pair < hi; pair++) {
22257
+ requiredSurfaceGaps[pair] = Math.max(requiredSurfaceGaps[pair], labelWidth);
22258
+ }
22259
+ }
22260
+ const shiftedXById2 = /* @__PURE__ */ new Map([[sortedStages[0].id, sortedStages[0].x]]);
22261
+ for (let index = 1; index < sortedStages.length; index++) {
22262
+ const previous = sortedStages[index - 1];
22263
+ const lane = sortedStages[index];
22264
+ const previousX = shiftedXById2.get(previous.id);
22265
+ const authoredGap = Math.max(0, lane.x - (previous.x + previous.width));
22266
+ const laneGap = Math.max(authoredGap, requiredSurfaceGaps[index - 1] - STAGE_COLUMN_GAP);
22267
+ shiftedXById2.set(lane.id, previousX + previous.width + laneGap);
22268
+ }
22269
+ return lanes.map((lane) => ({ ...lane, x: shiftedXById2.get(lane.id) ?? lane.x }));
22270
+ }
22043
22271
  const nodeById = new Map(nodes.map((n) => [n.id, n]));
22044
22272
  const sortedByX = [...layoutedLanes].sort((a, b) => a.x - b.x);
22045
22273
  const laneIndexById = new Map(sortedByX.map((lane, i) => [lane.id, i]));
@@ -22111,7 +22339,7 @@ function recomputeContainedLanes(lanes, nodes) {
22111
22339
  lifelineBottom = Math.max(lifelineBottom, bottom);
22112
22340
  }
22113
22341
  }
22114
- return lanes.map((lane) => {
22342
+ const contained = lanes.map((lane) => {
22115
22343
  if (lane.lifeline) {
22116
22344
  return {
22117
22345
  ...lane,
@@ -22129,6 +22357,7 @@ function recomputeContainedLanes(lanes, nodes) {
22129
22357
  height: bottom - top + CONTAIN_PAD * 2 + LANE_LABEL_H
22130
22358
  };
22131
22359
  });
22360
+ return equalizeStageColumns(contained, nodes);
22132
22361
  }
22133
22362
 
22134
22363
  // src/layout/head-label.ts
@@ -22278,11 +22507,15 @@ function requiredStackGapForEdgeLabels(diag, allStacks) {
22278
22507
  }
22279
22508
  function layoutNodes(diag, lanes, textMetrics) {
22280
22509
  const laneMap = new Map(lanes.map((l) => [l.id, l]));
22510
+ const metroDecisionIds = new Set(
22511
+ diag.edges.filter((edge) => edge.routing === "metro").flatMap((edge) => [edge.from, edge.to]).filter((id) => diag.nodes.some((node) => node.id === id && node.kind === "decision"))
22512
+ );
22513
+ const nodeSize = (node) => metroDecisionIds.has(node.id) ? { w: METRO_DECISION_WIDTH, h: METRO_DECISION_HEIGHT } : NODE_SIZE[node.kind];
22281
22514
  const allStacks = Array.from(new Set(diag.nodes.map((n) => n.stack))).sort((a, b) => a - b);
22282
22515
  const rowH = /* @__PURE__ */ new Map();
22283
22516
  for (const stack of allStacks) {
22284
22517
  const nodesAtStack = diag.nodes.filter((n) => n.stack === stack);
22285
- const h = Math.max(...nodesAtStack.map((n) => \u7BB1\u306E\u9AD8\u30552(n, NODE_SIZE[n.kind].h)));
22518
+ const h = Math.max(...nodesAtStack.map((n) => \u7BB1\u306E\u9AD8\u30552(n, nodeSize(n).h)));
22286
22519
  rowH.set(stack, h);
22287
22520
  }
22288
22521
  const rowCy = /* @__PURE__ */ new Map();
@@ -22318,7 +22551,7 @@ function layoutNodes(diag, lanes, textMetrics) {
22318
22551
  if (!lane) {
22319
22552
  throw new Error(`cdl layout: node "${n.id}" \u306E lane "${n.lane}" \u304C\u5B9A\u7FA9\u3055\u308C\u3066\u3044\u306A\u3044`);
22320
22553
  }
22321
- const size = NODE_SIZE[n.kind];
22554
+ const size = nodeSize(n);
22322
22555
  if (n.posX !== void 0 && n.posY !== void 0) {
22323
22556
  const w2 = n.posW ?? n.w ?? autoRowsWidth(n, size.w, textMetrics);
22324
22557
  const h2 = n.posH ?? n.h ?? rowH.get(n.stack) ?? size.h;
@@ -22441,6 +22674,9 @@ function dropUnsafeLaneDx(dxByLane, nodes) {
22441
22674
  function applyRowLevelNodeShifts(nodes, lanes, nodeShifts) {
22442
22675
  const dyByStack = aggregateRowDy(nodes, nodeShifts);
22443
22676
  const dxByLane = dropUnsafeLaneDx(aggregateLaneDx(nodes, nodeShifts), nodes);
22677
+ for (const lane of lanes) {
22678
+ if (lane.stage !== void 0) dxByLane.delete(lane.id);
22679
+ }
22444
22680
  let moved = false;
22445
22681
  const outNodes = nodes.map((n) => {
22446
22682
  const dx = dxByLane.get(n.lane) ?? 0;
@@ -22560,7 +22796,15 @@ function layout(diag) {
22560
22796
  );
22561
22797
  }
22562
22798
  }
22563
- const viewBox = computeViewBox(containedLanesFinal, nodesFinal, edges, bboxes, diag.viewport);
22799
+ const contentViewBox = computeViewBox(
22800
+ containedLanesFinal,
22801
+ nodesFinal,
22802
+ edges,
22803
+ bboxes,
22804
+ diag.viewport
22805
+ );
22806
+ const legendLayout = diag.legend?.length ? layoutLegend(diag.legend, contentViewBox, textMetrics, nodesFinal) : void 0;
22807
+ const viewBox = legendLayout?.viewBox ?? contentViewBox;
22564
22808
  return {
22565
22809
  id: diag.id,
22566
22810
  topic: diag.topic,
@@ -22587,6 +22831,7 @@ function layout(diag) {
22587
22831
  ...textMetrics === "monospace" ? { textMetrics: "monospace" } : {},
22588
22832
  // 種類も配色と同じく空の目印を出さない。 遮断をここに揃えると手書きの図にも効く
22589
22833
  ...diag.type ? { type: diag.type } : {},
22834
+ ...legendLayout ? { legend: legendLayout.legend } : {},
22590
22835
  bboxes,
22591
22836
  collisions,
22592
22837
  nearCollisions,
@@ -22775,6 +23020,11 @@ function validate(diag) {
22775
23020
  \u2192 \u4FEE\u6B63\u4F8B: .lane("${n.lane}", { x: 0, width: 400 }) \u3092 node \u5BA3\u8A00\u524D\u306B\u8FFD\u52A0`
22776
23021
  );
22777
23022
  }
23023
+ if (n.leaderTo !== void 0 && !nodeIds.has(n.leaderTo)) {
23024
+ errors.push(
23025
+ `[cdl] unknown-ref: node "${n.id}" \u306E leaderTo "${n.leaderTo}" \u304C node \u306B\u5B58\u5728\u3057\u307E\u305B\u3093${suggest(n.leaderTo, nodeIds, "node")}`
23026
+ );
23027
+ }
22778
23028
  }
22779
23029
  for (const e of diag.edges) {
22780
23030
  if (!nodeIds.has(e.from)) {
@@ -24610,6 +24860,10 @@ function uniformLifelineEndY(lanes, nodes, starts = lifelineStartYs(nodes)) {
24610
24860
 
24611
24861
  // src/render/edge-head.ts
24612
24862
  var EDGE_HEAD_MUTED_MARKER_ID = "cdl-arrow-muted";
24863
+ var METRO_RETURN_ARROW_D = "M 21 0 L 4 9.5 L 4 -9.5 Z";
24864
+ function metroReturnArrowMarkerId(tone) {
24865
+ return `cdl-metro-return-arrow-${tone}`;
24866
+ }
24613
24867
  var EDGE_HEAD_SHAPE = {
24614
24868
  // 何も描かない端。 marker 自体を出さないので `d` は読まれない
24615
24869
  none: { d: "", w: 10, h: 10, refX: 0, filled: false },
@@ -24664,6 +24918,7 @@ function \u7B49\u5E45\u306E\u5B57(text, fontSize) {
24664
24918
  });
24665
24919
  return out;
24666
24920
  }
24921
+ var METRO_PENDING_ATTRIBUTE = "data-cdl-pending";
24667
24922
  function defaultEdgeWidth(active) {
24668
24923
  return active ? 2.5 : 1.25;
24669
24924
  }
@@ -24682,12 +24937,14 @@ function CdlEdgeView({
24682
24937
  drawing,
24683
24938
  progress,
24684
24939
  stateValues,
24685
- flowing = false
24940
+ flowing = false,
24941
+ pending = false
24686
24942
  }) {
24687
24943
  const color = edgeColor(edge);
24688
- const marker = edgeHeadMarkerName(edge.tone, edge.head, !active, edge.headFill);
24689
- const tailMarker = edge.tailHead === void 0 ? void 0 : edgeHeadMarkerName(edge.tone, edge.tailHead, !active, edge.tailHeadFill);
24690
24944
  const style = edge.style ?? "solid";
24945
+ const isMetroReturn = edge.metroReturn === true;
24946
+ const marker = isMetroReturn ? metroReturnArrowMarkerId(edge.tone) : edgeHeadMarkerName(edge.tone, edge.head, !active, edge.headFill);
24947
+ const tailMarker = edge.tailHead === void 0 ? void 0 : edgeHeadMarkerName(edge.tone, edge.tailHead, !active, edge.tailHeadFill);
24691
24948
  const isDotted = style === "dotted-flow";
24692
24949
  const isPassThrough = isDotted && Boolean(edge.dThrough);
24693
24950
  const lineDotsD = isDotted ? shrinkPathEnd(edge.d, 12) : edge.d;
@@ -24697,6 +24954,7 @@ function CdlEdgeView({
24697
24954
  return /* @__PURE__ */ jsxRuntime.jsxs(
24698
24955
  "g",
24699
24956
  {
24957
+ ...pending ? { [METRO_PENDING_ATTRIBUTE]: "true" } : {},
24700
24958
  "data-cdl-edge": edge.id,
24701
24959
  "data-cdl-routing": edge.routing === "metro" ? "metro" : void 0,
24702
24960
  "data-cdl-from": edge.from,
@@ -24722,11 +24980,12 @@ function CdlEdgeView({
24722
24980
  fill: "none",
24723
24981
  stroke: color,
24724
24982
  strokeOpacity: active ? 0.95 : 0.9,
24725
- strokeWidth: active ? 5 : 4,
24983
+ strokeWidth: edge.routing === "metro" ? METRO_LINE_WIDTH : active ? 5 : 4,
24726
24984
  strokeDasharray: "0 18",
24727
24985
  strokeLinecap: "round",
24728
24986
  style: {
24729
- transition: "stroke 280ms ease, stroke-opacity 280ms ease, stroke-width 280ms ease"
24987
+ transition: "stroke 280ms ease, stroke-opacity 280ms ease, stroke-width 280ms ease",
24988
+ strokeWidth: edge.routing === "metro" ? METRO_LINE_WIDTH_CSS : void 0
24730
24989
  }
24731
24990
  }
24732
24991
  ),
@@ -24781,7 +25040,9 @@ function SolidEdgePath({
24781
25040
  const boundWidth = edge.widthBind && stateValues ? resolveNumericAttr(edge.widthBind, stateValues, defaultWidth) : defaultWidth;
24782
25041
  const boundStroke = edge.strokeBind && stateValues ? interpolate(edge.strokeBind, stateValues) : color;
24783
25042
  const boundDashOffset = edge.dashOffsetBind && stateValues ? resolveNumericAttr(edge.dashOffsetBind, stateValues, 0) : void 0;
24784
- const dashArray = dashed ? "10 8" : active ? boundDashOffset !== void 0 ? "8 6" : void 0 : "4 5";
25043
+ const strokeWidth = edge.routing === "metro" && edge.widthBind === void 0 ? edge.metroReturn ? 6 : METRO_LINE_WIDTH : boundWidth;
25044
+ const strokeWidthCss = edge.routing === "metro" && !edge.metroReturn ? edge.widthBind === void 0 ? METRO_LINE_WIDTH_CSS : `var(--cdl-metro-line-width, ${strokeWidth}px)` : void 0;
25045
+ const dashArray = dashed ? edge.metroReturn ? "0 9.6" : "10 8" : active ? boundDashOffset !== void 0 ? "8 6" : void 0 : "4 5";
24785
25046
  return /* @__PURE__ */ jsxRuntime.jsx(
24786
25047
  "path",
24787
25048
  {
@@ -24790,14 +25051,15 @@ function SolidEdgePath({
24790
25051
  fill: "none",
24791
25052
  stroke: boundStroke,
24792
25053
  strokeOpacity: active ? 0.95 : 0.9,
24793
- strokeWidth: boundWidth,
25054
+ strokeWidth,
24794
25055
  strokeDasharray: dashArray,
24795
25056
  strokeDashoffset: boundDashOffset,
24796
25057
  strokeLinecap: "round",
24797
25058
  markerEnd: marker === void 0 ? void 0 : `url(#${marker})`,
24798
25059
  markerStart: tailMarker === void 0 ? void 0 : `url(#${tailMarker})`,
24799
25060
  style: {
24800
- transition: "stroke 280ms ease, stroke-opacity 280ms ease, stroke-width 280ms ease"
25061
+ transition: "stroke 280ms ease, stroke-opacity 280ms ease, stroke-width 280ms ease",
25062
+ strokeWidth: strokeWidthCss
24801
25063
  }
24802
25064
  }
24803
25065
  );
@@ -25094,7 +25356,11 @@ function \u5272\u308B\u5019\u88DC(text) {
25094
25356
  function \u5165\u3063\u3066\u3044\u308B([\u5DE6, \u53F3]) {
25095
25357
  return \u5DE6 !== "" && \u53F3 !== "";
25096
25358
  }
25097
- function CardNode({ node, active }) {
25359
+ function CardNode({
25360
+ node,
25361
+ active,
25362
+ compactTitleInset
25363
+ }) {
25098
25364
  const accent = nodeAccent(node, "var(--cdl-tone-accent, #2d6a8f)");
25099
25365
  const x = node.cx - node.w / 2;
25100
25366
  const y = node.cy - node.h / 2;
@@ -25112,12 +25378,12 @@ function CardNode({ node, active }) {
25112
25378
  });
25113
25379
  const \u305A\u3089\u3057 = \u7E70\u308A\u4E0A\u3052 - \u4F59\u308A;
25114
25380
  const titleY = compact ? \u5C0F\u578B\u306B\u547C\u3073\u540D ? node.h / 2 - 2 : node.h / 2 + 8 : 86 - \u305A\u3089\u3057;
25115
- const titleX = compact ? node.w / 2 : 26;
25116
- const paddingX = compact ? 16 : 52;
25381
+ const titleX = compact ? compactTitleInset ?? node.w / 2 : 26;
25382
+ const paddingX = compact ? (compactTitleInset ?? 8) * 2 : 52;
25117
25383
  const availableW = node.w - paddingX;
25118
25384
  const initialTitleSize = compact ? 20 : 24;
25119
25385
  const titleSize = fitTextSize(node.title, availableW, initialTitleSize, 11);
25120
- const textAnchor = compact ? "middle" : "start";
25386
+ const textAnchor = compact && compactTitleInset === void 0 ? "middle" : "start";
25121
25387
  const subtitleY = 124 - \u305A\u3089\u3057;
25122
25388
  const \u6700\u5927\u306E\u884C\u9001\u308A = SUBTITLE_SIZE * \u884C\u9001\u308A\u306E\u500D\u7387;
25123
25389
  const \u4E8C\u884C\u304C\u5165\u308B = !compact && subtitleY - \u6700\u5927\u306E\u884C\u9001\u308A / 2 - SUBTITLE_SIZE * ASCENT > titleY + titleSize * DESCENT && subtitleY + \u6700\u5927\u306E\u884C\u9001\u308A / 2 + SUBTITLE_SIZE * DESCENT <= node.h - 4;
@@ -25880,7 +26146,9 @@ function GenericNode({
25880
26146
  value,
25881
26147
  stateValues,
25882
26148
  \u5024\u306E\u540D\u524D,
25883
- textMetrics = "proportional"
26149
+ textMetrics = "proportional",
26150
+ showKindIcon = true,
26151
+ metroDecision = false
25884
26152
  }) {
25885
26153
  const base = STYLES[node.kind] ?? FALLBACK;
25886
26154
  const toned = toneColor3(node.tone);
@@ -25911,14 +26179,14 @@ function GenericNode({
25911
26179
  if (shape === "diamond") {
25912
26180
  const cx = w / 2;
25913
26181
  const cy = h / 2;
25914
- const pad = 8;
26182
+ const pad = metroDecision ? 0 : 8;
25915
26183
  return /* @__PURE__ */ jsxRuntime.jsx(
25916
26184
  "path",
25917
26185
  {
25918
26186
  d: `M ${cx} ${pad} L ${w - pad} ${cy} L ${cx} ${h - pad} L ${pad} ${cy} Z`,
25919
26187
  fill: "var(--cdl-node-fill, #ffffff)",
25920
26188
  stroke: active ? style.accent : "var(--cdl-divider, #d4d4d8)",
25921
- strokeWidth: active ? 3 : 1.25
26189
+ strokeWidth: metroDecision ? 1.5 : active ? 3 : 1.25
25922
26190
  }
25923
26191
  );
25924
26192
  }
@@ -25982,11 +26250,11 @@ function GenericNode({
25982
26250
  const \u305A\u3089\u3057 = \u7E70\u308A\u4E0A\u3052 - \u4F59\u308A;
25983
26251
  const \u7D75\u306E\u82AF = centered && STYLES[node.kind]?.shape === "cloud" ? h * CLOUD_ART_CENTER_RISE : 0;
25984
26252
  const eyebrowY = centered ? h / 2 - 26 - \u7D75\u306E\u82AF : 32 + cylinderShift + \u4F59\u308A;
25985
- const titleY = genericTitleBaselineY(node.kind, h, hasRows) - \u305A\u3089\u3057 - \u7D75\u306E\u82AF;
26253
+ const titleY = metroDecision ? h / 2 + 9 : genericTitleBaselineY(node.kind, h, hasRows) - \u305A\u3089\u3057 - \u7D75\u306E\u82AF;
25986
26254
  const subtitleY = centered ? h / 2 + 28 - \u7D75\u306E\u82AF : 96 + cylinderShift - \u305A\u3089\u3057;
25987
26255
  const iconX = centered ? w / 2 + 40 : w - 48;
25988
26256
  const iconY = centered ? h / 2 - 44 - \u7D75\u306E\u82AF : 18 + cylinderShift;
25989
- const dominantBaseline = centered ? "middle" : void 0;
26257
+ const dominantBaseline = centered && !metroDecision ? "middle" : void 0;
25990
26258
  return /* @__PURE__ */ jsxRuntime.jsxs(
25991
26259
  "g",
25992
26260
  {
@@ -25995,12 +26263,12 @@ function GenericNode({
25995
26263
  "data-cdl-look": style.look,
25996
26264
  children: [
25997
26265
  shapeEl,
25998
- /* @__PURE__ */ jsxRuntime.jsxs("g", { transform: `translate(${iconX} ${iconY}) scale(1)`, "data-cdl-role": "node-kind-icon", children: [
26266
+ showKindIcon && /* @__PURE__ */ jsxRuntime.jsxs("g", { transform: `translate(${iconX} ${iconY}) scale(1)`, "data-cdl-role": "node-kind-icon", children: [
25999
26267
  /* @__PURE__ */ jsxRuntime.jsx("rect", { width: 24, height: 24, fill: "transparent" }),
26000
26268
  /* @__PURE__ */ jsxRuntime.jsx("path", { d: style.icon, fill: style.eyebrowColor, opacity: 0.7, transform: "scale(1)" })
26001
26269
  ] }),
26002
26270
  node.eyebrow && /* @__PURE__ */ jsxRuntime.jsx("text", { x: textX, y: eyebrowY, fontSize: 17, fontWeight: 700, letterSpacing: 1.5, fill: paint.onSolid ? "var(--cdl-node-fill, #ffffff)" : style.eyebrowColor, textAnchor, dominantBaseline, children: node.eyebrow }),
26003
- /* @__PURE__ */ jsxRuntime.jsx("text", { "data-cdl-role": "node-label", x: textX, y: titleY, fontSize: 20, fontWeight: 700, fill: paint.onSolid ? "var(--cdl-node-fill, #ffffff)" : "var(--cdl-text, #1a1f2a)", textAnchor, dominantBaseline, style: node.kind === "function" || node.kind === "api" ? { fontFamily: "'JetBrains Mono', monospace" } : void 0, children: node.kind === "function" || node.kind === "api" ? \u7B49\u5E45\u306E\u5B57(node.title, 20) : node.title }),
26271
+ /* @__PURE__ */ jsxRuntime.jsx("text", { "data-cdl-role": "node-label", x: textX, y: titleY, fontSize: metroDecision ? 26 : 20, fontWeight: 700, fill: paint.onSolid ? "var(--cdl-node-fill, #ffffff)" : "var(--cdl-text, #1a1f2a)", textAnchor, dominantBaseline, style: node.kind === "function" || node.kind === "api" ? { fontFamily: "'JetBrains Mono', monospace" } : void 0, children: node.kind === "function" || node.kind === "api" ? \u7B49\u5E45\u306E\u5B57(node.title, 20) : node.title }),
26004
26272
  node.subtitle && /* @__PURE__ */ jsxRuntime.jsx("text", { x: textX, y: subtitleY, fontSize: 17, fill: paint.onSolid ? "var(--cdl-node-fill, #ffffff)" : "var(--cdl-text-dim, #5a6270)", textAnchor, dominantBaseline, style: { fontFamily: "'JetBrains Mono', monospace" }, children: \u7B49\u5E45\u306E\u5B57(node.subtitle, 17) }),
26005
26273
  resolvedValue && /* @__PURE__ */ jsxRuntime.jsx(
26006
26274
  "text",
@@ -26049,50 +26317,176 @@ function GenericNode({
26049
26317
  var MARK_INNER_RATIO = 0.56;
26050
26318
  var RING_WIDTH_ACTIVE = 6;
26051
26319
  var RING_WIDTH_IDLE = 4;
26320
+ var METRO_START_RADIUS = 17;
26321
+ var METRO_END_RADIUS = 19;
26322
+ var METRO_END_RING_WIDTH = 3.5;
26323
+ var METRO_END_INNER_RADIUS = 10;
26052
26324
  var MARK_DEFAULT = "var(--cdl-tone-accent, #2d6a8f)";
26053
26325
  function markRadius(node) {
26054
26326
  return Math.min(node.w, node.h) / 2;
26055
26327
  }
26056
- function MarkStartNode({ node }) {
26328
+ function MarkStartNode({
26329
+ node,
26330
+ metro = false
26331
+ }) {
26057
26332
  const \u8272 = nodeAccent(node, MARK_DEFAULT);
26058
- return /* @__PURE__ */ jsxRuntime.jsx("g", { transform: `translate(${node.cx - node.w / 2} ${node.cy - node.h / 2})`, children: /* @__PURE__ */ jsxRuntime.jsx(
26059
- "circle",
26333
+ return /* @__PURE__ */ jsxRuntime.jsx(
26334
+ "g",
26060
26335
  {
26061
- "data-cdl-role": "node-body",
26062
- "data-cdl-mark": "start",
26063
- cx: node.w / 2,
26064
- cy: node.h / 2,
26065
- r: markRadius(node),
26066
- fill: \u8272
26336
+ "data-cdl-role": metro ? "mark-start" : void 0,
26337
+ transform: `translate(${node.cx - node.w / 2} ${node.cy - node.h / 2})`,
26338
+ children: /* @__PURE__ */ jsxRuntime.jsx(
26339
+ "circle",
26340
+ {
26341
+ "data-cdl-role": "node-body",
26342
+ "data-cdl-mark": "start",
26343
+ cx: node.w / 2,
26344
+ cy: node.h / 2,
26345
+ r: metro ? METRO_START_RADIUS : markRadius(node),
26346
+ fill: \u8272
26347
+ }
26348
+ )
26067
26349
  }
26068
- ) });
26350
+ );
26069
26351
  }
26070
- function MarkEndNode({ node, active }) {
26352
+ function MarkEndNode({
26353
+ node,
26354
+ active,
26355
+ metro = false
26356
+ }) {
26071
26357
  const \u8272 = nodeAccent(node, MARK_DEFAULT);
26072
- const r = markRadius(node);
26073
- const \u8F2A\u306E\u592A\u3055 = active ? RING_WIDTH_ACTIVE : RING_WIDTH_IDLE;
26074
- return /* @__PURE__ */ jsxRuntime.jsxs("g", { transform: `translate(${node.cx - node.w / 2} ${node.cy - node.h / 2})`, children: [
26358
+ const r = metro ? METRO_END_RADIUS : markRadius(node);
26359
+ const \u8F2A\u306E\u592A\u3055 = metro ? METRO_END_RING_WIDTH : active ? RING_WIDTH_ACTIVE : RING_WIDTH_IDLE;
26360
+ return /* @__PURE__ */ jsxRuntime.jsxs(
26361
+ "g",
26362
+ {
26363
+ "data-cdl-role": metro ? "mark-end" : void 0,
26364
+ transform: `translate(${node.cx - node.w / 2} ${node.cy - node.h / 2})`,
26365
+ children: [
26366
+ /* @__PURE__ */ jsxRuntime.jsx(
26367
+ "circle",
26368
+ {
26369
+ "data-cdl-role": "node-body",
26370
+ "data-cdl-mark": "end",
26371
+ cx: node.w / 2,
26372
+ cy: node.h / 2,
26373
+ r: metro ? r : Math.max(0, r - \u8F2A\u306E\u592A\u3055 / 2),
26374
+ fill: "var(--cdl-node-fill, #ffffff)",
26375
+ stroke: \u8272,
26376
+ strokeWidth: \u8F2A\u306E\u592A\u3055
26377
+ }
26378
+ ),
26379
+ /* @__PURE__ */ jsxRuntime.jsx(
26380
+ "circle",
26381
+ {
26382
+ "data-cdl-role": "node-inner",
26383
+ cx: node.w / 2,
26384
+ cy: node.h / 2,
26385
+ r: metro ? METRO_END_INNER_RADIUS : Math.max(0, r * MARK_INNER_RATIO),
26386
+ fill: \u8272
26387
+ }
26388
+ )
26389
+ ]
26390
+ }
26391
+ );
26392
+ }
26393
+ var METRO_STATION_RADIUS = 16;
26394
+ var METRO_STATION_STROKE_WIDTH = 6;
26395
+ var METRO_STATION_RADIUS_CSS = `var(--cdl-metro-station-radius, ${METRO_STATION_RADIUS}px)`;
26396
+ var METRO_STATION_STROKE_WIDTH_CSS = `var(--cdl-metro-station-stroke-width, ${METRO_STATION_STROKE_WIDTH}px)`;
26397
+ var STATION_DEFAULT = "var(--cdl-tone-accent, #2d6a8f)";
26398
+ var STATION_NAME_FONT_SIZE = 25;
26399
+ var STATION_NAME_GAP = 18;
26400
+ var STATION_NAME_RIGHT_GAP = 32;
26401
+ var STATION_NAME_RIGHT_BASELINE = 9;
26402
+ var STATION_SUBTITLE_FONT_SIZE = 18;
26403
+ var STATION_SUBTITLE_GAP = 22;
26404
+ function StationNode({
26405
+ node,
26406
+ nameRight = false
26407
+ }) {
26408
+ const name = node.title.trim();
26409
+ const subtitle = node.subtitle?.trim() ?? "";
26410
+ return /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
26075
26411
  /* @__PURE__ */ jsxRuntime.jsx(
26076
26412
  "circle",
26077
26413
  {
26078
26414
  "data-cdl-role": "node-body",
26079
- "data-cdl-mark": "end",
26080
- cx: node.w / 2,
26081
- cy: node.h / 2,
26082
- r: Math.max(0, r - \u8F2A\u306E\u592A\u3055 / 2),
26415
+ "data-cdl-mark": "station",
26416
+ cx: node.cx,
26417
+ cy: node.cy,
26418
+ r: METRO_STATION_RADIUS,
26083
26419
  fill: "var(--cdl-node-fill, #ffffff)",
26084
- stroke: \u8272,
26085
- strokeWidth: \u8F2A\u306E\u592A\u3055
26420
+ stroke: nodeAccent(node, STATION_DEFAULT),
26421
+ strokeWidth: METRO_STATION_STROKE_WIDTH,
26422
+ style: { r: METRO_STATION_RADIUS_CSS, strokeWidth: METRO_STATION_STROKE_WIDTH_CSS }
26086
26423
  }
26087
26424
  ),
26425
+ name !== "" && /* @__PURE__ */ jsxRuntime.jsx(
26426
+ "text",
26427
+ {
26428
+ "data-cdl-role": "shape-name",
26429
+ x: nameRight ? node.cx + STATION_NAME_RIGHT_GAP : node.cx,
26430
+ y: nameRight ? node.cy + STATION_NAME_RIGHT_BASELINE : node.cy - METRO_STATION_RADIUS - STATION_NAME_GAP,
26431
+ textAnchor: nameRight ? "start" : "middle",
26432
+ fontSize: STATION_NAME_FONT_SIZE,
26433
+ fontWeight: 700,
26434
+ fill: "var(--cdl-text, #23201b)",
26435
+ stroke: "var(--cdl-node-fill, #ffffff)",
26436
+ strokeWidth: 8,
26437
+ strokeLinejoin: "round",
26438
+ paintOrder: "stroke",
26439
+ children: name
26440
+ }
26441
+ ),
26442
+ subtitle !== "" && /* @__PURE__ */ jsxRuntime.jsx(
26443
+ "text",
26444
+ {
26445
+ "data-cdl-role": "shape-value",
26446
+ x: node.cx,
26447
+ y: node.cy + METRO_STATION_RADIUS + STATION_SUBTITLE_GAP,
26448
+ textAnchor: "middle",
26449
+ fontSize: STATION_SUBTITLE_FONT_SIZE,
26450
+ fill: "var(--cdl-text-dim, #5a6270)",
26451
+ children: subtitle
26452
+ }
26453
+ )
26454
+ ] });
26455
+ }
26456
+ function TimelineNumberNode({ node }) {
26457
+ const radius = Math.max(0, Math.min(node.w, node.h) / 2 - TIMELINE_NUMBER_STROKE / 2);
26458
+ const fontSize = fitTextSize(
26459
+ node.title,
26460
+ titleUsableWidth(node.kind, node.w, node.h),
26461
+ 18,
26462
+ MIN_TEXT_SIZE
26463
+ );
26464
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
26088
26465
  /* @__PURE__ */ jsxRuntime.jsx(
26089
26466
  "circle",
26090
26467
  {
26091
- "data-cdl-role": "node-inner",
26092
- cx: node.w / 2,
26093
- cy: node.h / 2,
26094
- r: Math.max(0, r * MARK_INNER_RATIO),
26095
- fill: \u8272
26468
+ "data-cdl-role": "node-body",
26469
+ "data-cdl-mark": "timeline-number",
26470
+ cx: node.cx,
26471
+ cy: node.cy,
26472
+ r: radius,
26473
+ fill: "var(--cdl-node-fill, #ffffff)",
26474
+ stroke: nodeAccent(node, STATION_DEFAULT),
26475
+ strokeWidth: TIMELINE_NUMBER_STROKE
26476
+ }
26477
+ ),
26478
+ /* @__PURE__ */ jsxRuntime.jsx(
26479
+ "text",
26480
+ {
26481
+ "data-cdl-role": "node-label",
26482
+ x: node.cx,
26483
+ y: node.cy,
26484
+ textAnchor: "middle",
26485
+ dominantBaseline: "central",
26486
+ fontSize,
26487
+ fontWeight: 700,
26488
+ fill: "var(--cdl-text, #1a1f2a)",
26489
+ children: node.title
26096
26490
  }
26097
26491
  )
26098
26492
  ] });
@@ -26424,27 +26818,6 @@ function interpolateColor(template, stateValues, fallback) {
26424
26818
  if (!template) return fallback;
26425
26819
  return interpolate(template, stateValues);
26426
26820
  }
26427
- var STATION_STROKE_ACTIVE = 4;
26428
- var STATION_STROKE_IDLE = 3;
26429
- var STATION_DEFAULT = "var(--cdl-tone-accent, #2d6a8f)";
26430
- function StationNode({ node, active }) {
26431
- const strokeWidth = active ? STATION_STROKE_ACTIVE : STATION_STROKE_IDLE;
26432
- const radius = Math.max(0, Math.min(node.w, node.h) / 2 - strokeWidth / 2);
26433
- const \u5F62 = /* @__PURE__ */ jsxRuntime.jsx(
26434
- "circle",
26435
- {
26436
- "data-cdl-role": "node-body",
26437
- "data-cdl-mark": "station",
26438
- cx: node.cx,
26439
- cy: node.cy,
26440
- r: radius,
26441
- fill: "var(--cdl-node-fill, #ffffff)",
26442
- stroke: nodeAccent(node, STATION_DEFAULT),
26443
- strokeWidth
26444
- }
26445
- );
26446
- return renderShapeWithOutsideLabels(\u5F62, node, "middle");
26447
- }
26448
26821
 
26449
26822
  // src/kinds/label-stack.ts
26450
26823
  function \u540D\u672D\u306E\u6BB5\u3092\u307B\u3069\u304F(\u771F\u306Ey, \u9593\u9694, \u4E0A\u9650, \u4E0B\u9650) {
@@ -33606,10 +33979,14 @@ function CdlNodeView({
33606
33979
  stateValues,
33607
33980
  currentPhaseId,
33608
33981
  chartLineScale,
33982
+ compactTitleInset,
33609
33983
  \u95A2\u4FC2\u306E\u53D7\u3051\u53E3,
33610
33984
  lang,
33611
33985
  textMetrics,
33612
- \u5024\u306E\u540D\u524D
33986
+ \u5024\u306E\u540D\u524D,
33987
+ metroDiagram = false,
33988
+ metroDecision = false,
33989
+ stationNameRight = false
33613
33990
  }) {
33614
33991
  const value = node.value ? interpolate(node.value, stateValues, \u5024\u306E\u540D\u524D) : void 0;
33615
33992
  const rows = (node.rows ?? []).map((r) => interpolate(r, stateValues, \u5024\u306E\u540D\u524D));
@@ -33650,7 +34027,7 @@ function CdlNodeView({
33650
34027
  case "event":
33651
34028
  return /* @__PURE__ */ jsxRuntime.jsx(EventNode, { node: resolvedNode, active, progress });
33652
34029
  case "card":
33653
- return /* @__PURE__ */ jsxRuntime.jsx(CardNode, { node: resolvedNode, active });
34030
+ return /* @__PURE__ */ jsxRuntime.jsx(CardNode, { node: resolvedNode, active, compactTitleInset });
33654
34031
  // 時系列のやり取り (#578)。 1 つの箱が図を丸ごと描く = 縦が時間で、参加者は箱を持たない
33655
34032
  case "sequence-board":
33656
34033
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -33937,11 +34314,13 @@ function CdlNodeView({
33937
34314
  case "shape-customer-service":
33938
34315
  return /* @__PURE__ */ jsxRuntime.jsx(ShapeCustomerServiceNode, { node: resolvedNode, active });
33939
34316
  case "mark-start":
33940
- return /* @__PURE__ */ jsxRuntime.jsx(MarkStartNode, { node: resolvedNode, active });
34317
+ return /* @__PURE__ */ jsxRuntime.jsx(MarkStartNode, { node: resolvedNode, active, metro: metroDiagram });
33941
34318
  case "mark-end":
33942
- return /* @__PURE__ */ jsxRuntime.jsx(MarkEndNode, { node: resolvedNode, active });
34319
+ return /* @__PURE__ */ jsxRuntime.jsx(MarkEndNode, { node: resolvedNode, active, metro: metroDiagram });
33943
34320
  case "station":
33944
- return /* @__PURE__ */ jsxRuntime.jsx(StationNode, { node: resolvedNode, active });
34321
+ return /* @__PURE__ */ jsxRuntime.jsx(StationNode, { node: resolvedNode, active, nameRight: stationNameRight });
34322
+ case "timeline-number":
34323
+ return /* @__PURE__ */ jsxRuntime.jsx(TimelineNumberNode, { node: resolvedNode });
33945
34324
  default:
33946
34325
  return /* @__PURE__ */ jsxRuntime.jsx(
33947
34326
  GenericNode,
@@ -33951,7 +34330,9 @@ function CdlNodeView({
33951
34330
  value: node.value,
33952
34331
  stateValues,
33953
34332
  \u5024\u306E\u540D\u524D,
33954
- textMetrics
34333
+ textMetrics,
34334
+ showKindIcon: !(metroDiagram && node.kind === "decision"),
34335
+ metroDecision
33955
34336
  }
33956
34337
  );
33957
34338
  }
@@ -34065,6 +34446,112 @@ function \u7BB1\u306E\u4E0A\u306E\u8A9E(node, textMetrics = "proportional") {
34065
34446
  )
34066
34447
  ] });
34067
34448
  }
34449
+ var ink = "var(--cdl-text-accent, #143a52)";
34450
+ var paper = "var(--cdl-stage-bg, #f8fafc)";
34451
+ var LEGEND_MARK_RENDERERS = {
34452
+ diamond: () => /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M 23 -26 L 46 -8 L 23 10 L 0 -8 Z", fill: "none", stroke: ink, strokeWidth: 2 }),
34453
+ "filled-circle": () => /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: 23, cy: -8, r: 11, fill: ink }),
34454
+ "double-circle": () => /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
34455
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: 23, cy: -8, r: 12, fill: "none", stroke: ink, strokeWidth: 2.5 }),
34456
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: 23, cy: -8, r: 6, fill: ink })
34457
+ ] }),
34458
+ "dotted-line": () => /* @__PURE__ */ jsxRuntime.jsx(
34459
+ "path",
34460
+ {
34461
+ d: `M 0 -8 H ${LEGEND_MARK_REFERENCE_WIDTH}`,
34462
+ fill: "none",
34463
+ stroke: ink,
34464
+ strokeWidth: 6,
34465
+ strokeLinecap: "round",
34466
+ strokeDasharray: "0 9.6"
34467
+ }
34468
+ ),
34469
+ "solid-line": () => /* @__PURE__ */ jsxRuntime.jsx(
34470
+ "path",
34471
+ {
34472
+ d: `M 0 -8 H ${LEGEND_MARK_REFERENCE_WIDTH}`,
34473
+ fill: "none",
34474
+ stroke: ink,
34475
+ strokeWidth: 4,
34476
+ strokeLinecap: "round"
34477
+ }
34478
+ ),
34479
+ "curved-line": () => /* @__PURE__ */ jsxRuntime.jsx(
34480
+ "path",
34481
+ {
34482
+ d: `M 0 6 C 12 -22, 34 -22, ${LEGEND_MARK_REFERENCE_WIDTH} 6`,
34483
+ fill: "none",
34484
+ stroke: ink,
34485
+ strokeWidth: 4,
34486
+ strokeLinecap: "round"
34487
+ }
34488
+ ),
34489
+ "rounded-label": () => /* @__PURE__ */ jsxRuntime.jsx(
34490
+ "rect",
34491
+ {
34492
+ x: 0,
34493
+ y: -23,
34494
+ width: LEGEND_MARK_REFERENCE_WIDTH,
34495
+ height: 30,
34496
+ rx: 9,
34497
+ fill: paper,
34498
+ stroke: ink,
34499
+ strokeWidth: 2
34500
+ }
34501
+ ),
34502
+ "numbered-circle": () => /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
34503
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: 23, cy: -8, r: 13, fill: paper, stroke: ink, strokeWidth: 2.5 }),
34504
+ /* @__PURE__ */ jsxRuntime.jsx("text", { x: 23, y: -2, textAnchor: "middle", fontSize: 17, fontWeight: 700, fill: ink, children: "1" })
34505
+ ] }),
34506
+ station: () => /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: 23, cy: -8, r: 11, fill: paper, stroke: ink, strokeWidth: 4 }),
34507
+ arrow: () => /* @__PURE__ */ jsxRuntime.jsx(
34508
+ "path",
34509
+ {
34510
+ d: `M 0 -8 H 42 M 32 -18 L ${LEGEND_MARK_REFERENCE_WIDTH} -8 L 32 2`,
34511
+ fill: "none",
34512
+ stroke: ink,
34513
+ strokeWidth: 3,
34514
+ strokeLinecap: "round",
34515
+ strokeLinejoin: "round"
34516
+ }
34517
+ )
34518
+ };
34519
+ function CdlLegend({ items }) {
34520
+ return /* @__PURE__ */ jsxRuntime.jsx("g", { "data-cdl-role": "legend", children: items.map((item, index) => {
34521
+ const Mark = LEGEND_MARK_RENDERERS[item.mark];
34522
+ return /* @__PURE__ */ jsxRuntime.jsxs(
34523
+ "g",
34524
+ {
34525
+ "data-cdl-role": "legend-item",
34526
+ "data-cdl-y": item.y,
34527
+ transform: `translate(${item.x} ${item.y})`,
34528
+ children: [
34529
+ /* @__PURE__ */ jsxRuntime.jsx(
34530
+ "g",
34531
+ {
34532
+ "data-cdl-role": "legend-mark",
34533
+ "data-cdl-legend-mark": item.mark,
34534
+ transform: `scale(${LEGEND_MARK_SCALE})`,
34535
+ children: /* @__PURE__ */ jsxRuntime.jsx(Mark, {})
34536
+ }
34537
+ ),
34538
+ /* @__PURE__ */ jsxRuntime.jsx(
34539
+ "text",
34540
+ {
34541
+ "data-cdl-role": "legend-text",
34542
+ x: LEGEND_MARK_WIDTH + LEGEND_MARK_TEXT_GAP,
34543
+ y: 0,
34544
+ fontSize: LEGEND_FONT_SIZE,
34545
+ fill: "var(--cdl-text-mute, #4d7187)",
34546
+ children: item.text
34547
+ }
34548
+ )
34549
+ ]
34550
+ },
34551
+ `${item.mark}-${index}`
34552
+ );
34553
+ }) });
34554
+ }
34068
34555
 
34069
34556
  // src/render/relation-focus.ts
34070
34557
  function relatedFocusSet(edges, nodeId) {
@@ -34077,10 +34564,17 @@ function relatedFocusSet(edges, nodeId) {
34077
34564
  }
34078
34565
  return \u96C6\u5408;
34079
34566
  }
34567
+ var METRO_LANE_BADGE_WIDTH = 200;
34568
+ var METRO_LANE_BADGE_HEIGHT = 88;
34569
+ var METRO_LANE_BADGE_INSET = 24;
34570
+ var METRO_LANE_GUIDE_GAP = 24;
34080
34571
  function \u63CF\u304F\u3082\u306E\u304C\u3042\u308B(laid) {
34572
+ if (laid.legend && laid.legend.length > 0) return true;
34081
34573
  if (laid.edges.length > 0) return true;
34082
34574
  if (laid.nodes.some((n) => isRenderedNode(n))) return true;
34083
- return laid.lanes.some((l) => l.contain === true || l.lifeline === true || Boolean(l.label));
34575
+ return laid.lanes.some(
34576
+ (l) => l.contain === true || l.lifeline === true || Boolean(l.label) || l.stage !== void 0 || l.metro !== void 0
34577
+ );
34084
34578
  }
34085
34579
  function CdlStage({
34086
34580
  laid,
@@ -34121,6 +34615,7 @@ function CdlStage({
34121
34615
  [\u95A2\u4FC2\u3092\u5149\u3089\u305B\u308B, \u7126\u70B9, laid.edges]
34122
34616
  );
34123
34617
  const activeSet = \u7126\u70B9\u306E\u96C6\u5408 ?? new Set(currentPhase?.activate ?? []);
34618
+ const \u8DEF\u7DDA\u56F3 = laid.edges.some((edge) => edge.routing === "metro") && laid.nodes.some((node) => node.kind === "station");
34124
34619
  const \u7BB1\u306E\u53D7\u3051\u53E3 = \u95A2\u4FC2\u3092\u5149\u3089\u305B\u308B ? { \u89E6\u308C\u305F, \u96E2\u308C\u305F, \u62BC\u3057\u305F, \u7126\u70B9: \u7126\u70B9?.id ?? null } : void 0;
34125
34620
  const \u77E2\u5370\u306E\u51FA\u756A = react.useMemo(() => {
34126
34621
  const \u521D\u51FA = /* @__PURE__ */ new Map();
@@ -34149,9 +34644,15 @@ function CdlStage({
34149
34644
  const first = \u521D\u51FA.get(edgeId);
34150
34645
  if (first === void 0) return false;
34151
34646
  return \u4ECA === first;
34647
+ },
34648
+ /** 初出の段より前にある区間だけを、通過前として選べるようにする。 */
34649
+ \u307E\u3060\u304B: (edgeId) => {
34650
+ const first = \u521D\u51FA.get(edgeId);
34651
+ return first !== void 0 && \u4ECA >= 0 && \u4ECA < first;
34152
34652
  }
34153
34653
  };
34154
34654
  }, [laid.phases, currentPhase, laid.edgeReveal]);
34655
+ const \u7DDA\u3092\u63CF\u304F = (edge) => edge.routing === "metro" || \u77E2\u5370\u306E\u51FA\u756A.\u51FA\u308B\u304B(edge.id);
34155
34656
  const drawSet = new Set(currentPhase?.draw ?? []);
34156
34657
  const chartLineScales = react.useMemo(() => {
34157
34658
  const nodes = laid.nodes.filter((node) => node.kind === "chart-line");
@@ -34239,7 +34740,53 @@ function CdlStage({
34239
34740
  )
34240
34741
  );
34241
34742
  }, [laid]);
34743
+ const metroReturnArrowMarkers = react.useMemo(() => {
34744
+ const tones = new Set(
34745
+ laid.edges.filter((edge) => edge.metroReturn === true).map((edge) => edge.tone)
34746
+ );
34747
+ return [...tones].map((tone) => /* @__PURE__ */ jsxRuntime.jsx(
34748
+ "marker",
34749
+ {
34750
+ id: metroReturnArrowMarkerId(tone),
34751
+ viewBox: "0 -9.5 21 19",
34752
+ refX: "0",
34753
+ refY: "0",
34754
+ markerWidth: "21",
34755
+ markerHeight: "19",
34756
+ markerUnits: "userSpaceOnUse",
34757
+ orient: "auto",
34758
+ children: /* @__PURE__ */ jsxRuntime.jsx(
34759
+ "path",
34760
+ {
34761
+ "data-cdl-role": "edge-arrowhead",
34762
+ "data-cdl-mark": "metro-return-arrow",
34763
+ d: METRO_RETURN_ARROW_D,
34764
+ fill: TONE[tone]
34765
+ }
34766
+ )
34767
+ },
34768
+ tone
34769
+ ));
34770
+ }, [laid.edges]);
34242
34771
  const diagramScale = laid.diagramScale ?? 1;
34772
+ const stageLaneIds = new Set(
34773
+ laid.lanes.filter((lane) => lane.stage !== void 0).map((lane) => lane.id)
34774
+ );
34775
+ const stageNoteNodes = laid.nodes.filter(
34776
+ (node) => node.kind === "card" && (stageLaneIds.has(node.lane) || node.leaderTo !== void 0) && isRenderedNode(node) && Boolean(node.subtitle)
34777
+ );
34778
+ const stageNoteNodeIds = new Set(stageNoteNodes.map((node) => node.id));
34779
+ const nodesById = new Map(laid.nodes.map((node) => [node.id, node]));
34780
+ const metroDecisionIds = /* @__PURE__ */ new Set();
34781
+ const stationNamesRight = /* @__PURE__ */ new Set();
34782
+ for (const edge of laid.edges) {
34783
+ if (edge.routing !== "metro") continue;
34784
+ if (nodesById.get(edge.from)?.kind === "decision") metroDecisionIds.add(edge.from);
34785
+ if (nodesById.get(edge.to)?.kind === "decision") metroDecisionIds.add(edge.to);
34786
+ if (edge.toSide === "top" && nodesById.get(edge.to)?.kind === "station") {
34787
+ stationNamesRight.add(edge.to);
34788
+ }
34789
+ }
34243
34790
  if (!\u63CF\u304F\u3082\u306E\u304C\u3042\u308B(laid)) return null;
34244
34791
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: compact ? "px-3 py-3" : "px-6 py-6", style: compact ? void 0 : { minHeight: 460 }, children: [
34245
34792
  /* @__PURE__ */ jsxRuntime.jsxs(
@@ -34259,6 +34806,7 @@ function CdlStage({
34259
34806
  children: [
34260
34807
  /* @__PURE__ */ jsxRuntime.jsxs("defs", { children: [
34261
34808
  edgeHeadMarkers,
34809
+ metroReturnArrowMarkers,
34262
34810
  /* @__PURE__ */ jsxRuntime.jsx(
34263
34811
  "marker",
34264
34812
  {
@@ -34295,7 +34843,61 @@ function CdlStage({
34295
34843
  pointerEvents: "none"
34296
34844
  }
34297
34845
  ),
34298
- lane.contain && /* @__PURE__ */ jsxRuntime.jsx(
34846
+ lane.stage && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
34847
+ /* @__PURE__ */ jsxRuntime.jsx(
34848
+ "rect",
34849
+ {
34850
+ "data-cdl-role": "stage-column",
34851
+ x: lane.x + STAGE_COLUMN_GAP / 2,
34852
+ y: lane.y,
34853
+ width: lane.width - STAGE_COLUMN_GAP,
34854
+ height: lane.height,
34855
+ rx: 16,
34856
+ fill: "var(--cdl-stage-bg, #fafafa)",
34857
+ fillOpacity: 0.5,
34858
+ stroke: "var(--cdl-text-mute, #8a8678)",
34859
+ strokeWidth: 1.5
34860
+ }
34861
+ ),
34862
+ /* @__PURE__ */ jsxRuntime.jsx(
34863
+ "text",
34864
+ {
34865
+ "data-cdl-role": "stage-number",
34866
+ x: lane.x + STAGE_COLUMN_GAP / 2 + 20,
34867
+ y: lane.y + 24,
34868
+ fontSize: 14,
34869
+ fontWeight: 500,
34870
+ letterSpacing: 1.5,
34871
+ fill: "var(--cdl-text-mute, #8a8678)",
34872
+ children: `\u6BB5\u968E ${lane.stage.number}`
34873
+ }
34874
+ ),
34875
+ /* @__PURE__ */ jsxRuntime.jsx(
34876
+ "text",
34877
+ {
34878
+ "data-cdl-role": "stage-name",
34879
+ x: lane.x + STAGE_COLUMN_GAP / 2 + 20,
34880
+ y: lane.y + 55,
34881
+ fontSize: 22,
34882
+ fontWeight: 700,
34883
+ fill: "var(--cdl-text, #1a1f2a)",
34884
+ children: lane.stage.name
34885
+ }
34886
+ ),
34887
+ /* @__PURE__ */ jsxRuntime.jsx(
34888
+ "line",
34889
+ {
34890
+ "data-cdl-role": "stage-divider",
34891
+ x1: lane.x + STAGE_COLUMN_GAP / 2,
34892
+ y1: lane.y + 72,
34893
+ x2: lane.x + lane.width - STAGE_COLUMN_GAP / 2,
34894
+ y2: lane.y + 72,
34895
+ stroke: "var(--cdl-divider, #d4d4d8)",
34896
+ strokeWidth: 1.5
34897
+ }
34898
+ )
34899
+ ] }),
34900
+ lane.contain && lane.stage === void 0 && /* @__PURE__ */ jsxRuntime.jsx(
34299
34901
  "rect",
34300
34902
  {
34301
34903
  "data-cdl-role": "lane-container",
@@ -34331,7 +34933,85 @@ function CdlStage({
34331
34933
  }
34332
34934
  );
34333
34935
  })(),
34334
- lane.label && /* @__PURE__ */ jsxRuntime.jsx(
34936
+ lane.timelineAxis && (() => {
34937
+ const ys = laid.nodes.filter((node) => node.lane === lane.id && isRenderedNode(node)).map((node) => node.cy);
34938
+ if (ys.length < 2) return null;
34939
+ const y1 = Math.min(...ys);
34940
+ const y2 = Math.max(...ys);
34941
+ if (y2 <= y1) return null;
34942
+ return /* @__PURE__ */ jsxRuntime.jsx(
34943
+ "line",
34944
+ {
34945
+ "data-cdl-role": "timeline-axis",
34946
+ x1: lane.x + lane.width / 2,
34947
+ y1,
34948
+ x2: lane.x + lane.width / 2,
34949
+ y2,
34950
+ stroke: "var(--cdl-tone-accent, #2d6a8f)",
34951
+ strokeWidth: TIMELINE_NUMBER_STROKE
34952
+ }
34953
+ );
34954
+ })(),
34955
+ lane.metro !== void 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
34956
+ /* @__PURE__ */ jsxRuntime.jsx(
34957
+ "line",
34958
+ {
34959
+ "data-cdl-role": "metro-lane-guide",
34960
+ x1: lane.x + METRO_LANE_BADGE_INSET + METRO_LANE_BADGE_WIDTH + METRO_LANE_GUIDE_GAP,
34961
+ y1: lane.y + lane.height / 2,
34962
+ x2: lane.x + lane.width - METRO_LANE_BADGE_INSET,
34963
+ y2: lane.y + lane.height / 2,
34964
+ stroke: "var(--cdl-line-mute, #a89d8f)",
34965
+ strokeWidth: 2,
34966
+ strokeDasharray: "22 7 5 7",
34967
+ opacity: 0.45
34968
+ }
34969
+ ),
34970
+ /* @__PURE__ */ jsxRuntime.jsxs(
34971
+ "g",
34972
+ {
34973
+ "data-cdl-role": "metro-lane-badge",
34974
+ transform: `translate(${lane.x + METRO_LANE_BADGE_INSET} ${lane.y + lane.height / 2 - METRO_LANE_BADGE_HEIGHT / 2})`,
34975
+ children: [
34976
+ /* @__PURE__ */ jsxRuntime.jsx(
34977
+ "rect",
34978
+ {
34979
+ width: METRO_LANE_BADGE_WIDTH,
34980
+ height: METRO_LANE_BADGE_HEIGHT,
34981
+ rx: 12,
34982
+ fill: "var(--cdl-node-fill, #ffffff)",
34983
+ stroke: "var(--cdl-tone-accent, #2d6a8f)",
34984
+ strokeWidth: 1.25
34985
+ }
34986
+ ),
34987
+ /* @__PURE__ */ jsxRuntime.jsx(
34988
+ "text",
34989
+ {
34990
+ x: METRO_LANE_BADGE_WIDTH / 2,
34991
+ y: lane.metro.subtitle ? 37 : 53,
34992
+ fontSize: 26,
34993
+ fontWeight: 700,
34994
+ fill: "var(--cdl-text, #1a1f2a)",
34995
+ textAnchor: "middle",
34996
+ children: lane.label
34997
+ }
34998
+ ),
34999
+ lane.metro.subtitle && /* @__PURE__ */ jsxRuntime.jsx(
35000
+ "text",
35001
+ {
35002
+ x: METRO_LANE_BADGE_WIDTH / 2,
35003
+ y: 67,
35004
+ fontSize: 18,
35005
+ fill: "var(--cdl-text-dim, #5a6270)",
35006
+ textAnchor: "middle",
35007
+ children: lane.metro.subtitle
35008
+ }
35009
+ )
35010
+ ]
35011
+ }
35012
+ )
35013
+ ] }),
35014
+ lane.label && lane.metro === void 0 && /* @__PURE__ */ jsxRuntime.jsx(
34335
35015
  "text",
34336
35016
  {
34337
35017
  "data-cdl-role": "lane-label",
@@ -34348,7 +35028,34 @@ function CdlStage({
34348
35028
  },
34349
35029
  lane.id
34350
35030
  )),
34351
- laid.edges.filter((edge) => \u77E2\u5370\u306E\u51FA\u756A.\u51FA\u308B\u304B(edge.id)).map((edge) => /* @__PURE__ */ jsxRuntime.jsx(
35031
+ laid.nodes.map((node) => {
35032
+ if (node.leaderTo === void 0) return null;
35033
+ const target = nodesById.get(node.leaderTo);
35034
+ if (target === void 0) return null;
35035
+ if (!isRenderedNode(node) || !isRenderedNode(target)) return null;
35036
+ const targetIsRight = target.cx >= node.cx;
35037
+ const x1 = node.cx + (targetIsRight ? node.w / 2 : -node.w / 2);
35038
+ const x2 = target.cx + (targetIsRight ? -target.w / 2 : target.w / 2);
35039
+ if (targetIsRight ? x2 <= x1 : x1 <= x2) return null;
35040
+ return /* @__PURE__ */ jsxRuntime.jsx(
35041
+ "line",
35042
+ {
35043
+ "data-cdl-role": "timeline-leader",
35044
+ "data-cdl-leader-from": node.id,
35045
+ "data-cdl-leader-to": node.leaderTo,
35046
+ x1,
35047
+ y1: node.cy,
35048
+ x2,
35049
+ y2: target.cy,
35050
+ stroke: "var(--cdl-line-mute, #a89d8f)",
35051
+ strokeWidth: 1.5,
35052
+ strokeDasharray: "2 4",
35053
+ strokeLinecap: "round"
35054
+ },
35055
+ `timeline-leader-${node.id}`
35056
+ );
35057
+ }),
35058
+ laid.edges.filter(\u7DDA\u3092\u63CF\u304F).map((edge) => /* @__PURE__ */ jsxRuntime.jsx(
34352
35059
  CdlEdgeView,
34353
35060
  {
34354
35061
  edge,
@@ -34356,16 +35063,19 @@ function CdlStage({
34356
35063
  flowing: \u7126\u70B9\u306E\u96C6\u5408?.has(edge.id) === true && !\u52D5\u304D\u3092\u63A7\u3048\u308B,
34357
35064
  drawing: \u77E2\u5370\u306E\u51FA\u756A.\u4F38\u3070\u3059\u304B(edge.id),
34358
35065
  progress: \u77E2\u5370\u306E\u51FA\u756A.\u4F38\u3070\u3059\u304B(edge.id) ? \u63CF\u304F\u9032\u307F : progress,
34359
- stateValues
35066
+ stateValues,
35067
+ pending: edge.routing === "metro" && \u77E2\u5370\u306E\u51FA\u756A.\u307E\u3060\u304B(edge.id)
34360
35068
  },
34361
35069
  edge.id
34362
35070
  )),
34363
35071
  laid.nodes.map((node) => {
34364
35072
  const \u63CF\u304F = drawSet.has(node.id);
35073
+ const \u6BB5\u968E\u306E\u88DC\u8DB3 = stageNoteNodeIds.has(node.id);
34365
35074
  const view = /* @__PURE__ */ jsxRuntime.jsx(
34366
35075
  CdlNodeView,
34367
35076
  {
34368
- node,
35077
+ node: \u6BB5\u968E\u306E\u88DC\u8DB3 ? { ...node, subtitle: void 0 } : node,
35078
+ compactTitleInset: \u6BB5\u968E\u306E\u88DC\u8DB3 ? STAGE_CARD_TEXT_INSET : void 0,
34369
35079
  active: activeSet.has(node.id),
34370
35080
  lang: laid.lang,
34371
35081
  textMetrics: laid.textMetrics,
@@ -34375,13 +35085,32 @@ function CdlStage({
34375
35085
  stateValues,
34376
35086
  \u5024\u306E\u540D\u524D,
34377
35087
  currentPhaseId: currentPhase?.id,
34378
- chartLineScale: chartLineScales.get(node.id)
35088
+ chartLineScale: chartLineScales.get(node.id),
35089
+ metroDiagram: \u8DEF\u7DDA\u56F3,
35090
+ metroDecision: metroDecisionIds.has(node.id),
35091
+ stationNameRight: stationNamesRight.has(node.id)
34379
35092
  }
34380
35093
  );
34381
35094
  return /* @__PURE__ */ jsxRuntime.jsx("g", { children: view }, node.id);
34382
35095
  }),
34383
- laid.edges.filter((edge) => Boolean(edge.dThrough) && \u77E2\u5370\u306E\u51FA\u756A.\u51FA\u308B\u304B(edge.id)).map((edge) => /* @__PURE__ */ jsxRuntime.jsx(CdlEdgeGlowView, { edge, active: activeSet.has(edge.id) }, `glow-${edge.id}`)),
34384
- laid.edges.filter((edge) => \u77E2\u5370\u306E\u51FA\u756A.\u51FA\u308B\u304B(edge.id)).map((edge) => /* @__PURE__ */ jsxRuntime.jsx(
35096
+ stageNoteNodes.map((node) => /* @__PURE__ */ jsxRuntime.jsx(
35097
+ "text",
35098
+ {
35099
+ "data-cdl-role": "stage-note",
35100
+ "data-cdl-stage-node": node.id,
35101
+ x: node.cx + node.w / 2 - STAGE_CARD_TEXT_INSET,
35102
+ y: node.cy + 1,
35103
+ fontSize: 14,
35104
+ fontWeight: 500,
35105
+ textAnchor: "end",
35106
+ dominantBaseline: "middle",
35107
+ fill: "var(--cdl-text-mute, #8a8678)",
35108
+ children: interpolate(node.subtitle, stateValues, \u5024\u306E\u540D\u524D)
35109
+ },
35110
+ `stage-note-${node.id}`
35111
+ )),
35112
+ laid.edges.filter((edge) => Boolean(edge.dThrough) && \u7DDA\u3092\u63CF\u304F(edge)).map((edge) => /* @__PURE__ */ jsxRuntime.jsx(CdlEdgeGlowView, { edge, active: activeSet.has(edge.id) }, `glow-${edge.id}`)),
35113
+ laid.edges.filter(\u7DDA\u3092\u63CF\u304F).map((edge) => /* @__PURE__ */ jsxRuntime.jsx(
34385
35114
  CdlEdgeLabelView,
34386
35115
  {
34387
35116
  edge,
@@ -34390,7 +35119,7 @@ function CdlStage({
34390
35119
  },
34391
35120
  `label-${edge.id}`
34392
35121
  )),
34393
- laid.edges.filter((edge) => (edge.headLabel || edge.tailLabel) && \u77E2\u5370\u306E\u51FA\u756A.\u51FA\u308B\u304B(edge.id)).flatMap(
35122
+ laid.edges.filter((edge) => (edge.headLabel || edge.tailLabel) && \u7DDA\u3092\u63CF\u304F(edge)).flatMap(
34394
35123
  (edge) => ["head", "tail"].map((\u7AEF) => /* @__PURE__ */ jsxRuntime.jsx(
34395
35124
  CdlEdgeEndLabelView,
34396
35125
  {
@@ -34402,6 +35131,7 @@ function CdlStage({
34402
35131
  `${\u7AEF}-label-${edge.id}`
34403
35132
  ))
34404
35133
  ),
35134
+ laid.legend && /* @__PURE__ */ jsxRuntime.jsx(CdlLegend, { items: laid.legend }),
34405
35135
  debug && /* @__PURE__ */ jsxRuntime.jsxs("g", { pointerEvents: "none", children: [
34406
35136
  laid.bboxes.map((bbox, idx) => {
34407
35137
  const color = bbox.kind === "node" ? "#3b82f6" : bbox.kind === "lane-label" ? "#a855f7" : bbox.kind === "edge-label" ? "#10b981" : "#f59e0b";
@@ -36066,6 +36796,7 @@ exports.LABEL_INIT_CLEARANCE_DOTTED_FLOW = LABEL_INIT_CLEARANCE_DOTTED_FLOW;
36066
36796
  exports.LABEL_SIZE_TOLERANCE_WORLD = LABEL_SIZE_TOLERANCE_WORLD;
36067
36797
  exports.LABEL_TO_PATH_CLEARANCE = LABEL_TO_PATH_CLEARANCE;
36068
36798
  exports.LANE_BORDER_CLEARANCE_TOL = LANE_BORDER_CLEARANCE_TOL;
36799
+ exports.LEGEND_MARKS = LEGEND_MARKS;
36069
36800
  exports.MARKER_GLOW_RADIUS = MARKER_GLOW_RADIUS;
36070
36801
  exports.MIN_COLUMN_ALIGNMENT_TOLERANCE = MIN_COLUMN_ALIGNMENT_TOLERANCE;
36071
36802
  exports.MIN_ROW_ALIGNMENT_TOLERANCE = MIN_ROW_ALIGNMENT_TOLERANCE;