@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/react.js CHANGED
@@ -1004,7 +1004,7 @@ function drawsTitleOutsideBox(kind) {
1004
1004
  }
1005
1005
  var TITLE_FONT_SIZE_DEFAULT = 18;
1006
1006
  var TITLE_FONT_SIZE = new Map([
1007
- ...["dyn-rect", "dyn-circle", "dyn-arc", "dyn-wave", "dyn-polygon", "station"].map(
1007
+ ...["dyn-rect", "dyn-circle", "dyn-arc", "dyn-wave", "dyn-polygon"].map(
1008
1008
  (k) => [k, 13]
1009
1009
  ),
1010
1010
  ...[
@@ -1049,8 +1049,10 @@ var TITLE_FONT_SIZE = new Map([
1049
1049
  "webhook"
1050
1050
  ].map((k) => [k, 20]),
1051
1051
  ...["card", "event", "function"].map((k) => [k, 24]),
1052
+ ["station", 25],
1052
1053
  ["actor", 32],
1053
- ["storage", 34]
1054
+ ["storage", 34],
1055
+ ["timeline-number", 18]
1054
1056
  ]);
1055
1057
  function titleFontSize(kind) {
1056
1058
  return TITLE_FONT_SIZE.get(kind ?? "") ?? TITLE_FONT_SIZE_DEFAULT;
@@ -1060,9 +1062,17 @@ function skipsNodeTitleWidthCheck(kind) {
1060
1062
  return KINDS_WITH_TITLE_WIDTH_OUTSIDE_NODE.has(kind ?? "");
1061
1063
  }
1062
1064
  var TITLE_BOX_PADDING = 52;
1065
+ var TIMELINE_NUMBER_STROKE = 4;
1066
+ function titleUsableWidth(kind, w, h) {
1067
+ if (kind === "timeline-number") {
1068
+ return Math.min(w, h) - 2 * TIMELINE_NUMBER_STROKE;
1069
+ }
1070
+ return w - (drawsTitleOutsideBox(kind) ? 0 : TITLE_BOX_PADDING);
1071
+ }
1063
1072
  var KINDS_SCALING_TITLE_WHEN_LOW = /* @__PURE__ */ new Set(["actor", "event", "function", "storage"]);
1064
1073
  var KINDS_SCALING_TITLE_ALWAYS = /* @__PURE__ */ new Set([
1065
1074
  "card",
1075
+ "timeline-number",
1066
1076
  "shape-file",
1067
1077
  "shape-folder",
1068
1078
  "shape-cloud",
@@ -1146,14 +1156,16 @@ var KINDS_DRAWING_ONLY_SHAPE = [
1146
1156
  ];
1147
1157
  var KINDS_WITHOUT_EYEBROW_TEXT = /* @__PURE__ */ new Set([
1148
1158
  ...KINDS_DRAWING_ONLY_SHAPE,
1149
- ...KINDS_WITHOUT_TITLE_TEXT
1159
+ ...KINDS_WITHOUT_TITLE_TEXT,
1160
+ "timeline-number"
1150
1161
  ]);
1151
1162
  function rendersEyebrow(kind) {
1152
1163
  return !KINDS_WITHOUT_EYEBROW_TEXT.has(kind ?? "");
1153
1164
  }
1154
1165
  var KINDS_WITHOUT_SUBTITLE_TEXT = /* @__PURE__ */ new Set([
1155
1166
  ...KINDS_DRAWING_OWN_PAYLOAD,
1156
- ...KINDS_WITHOUT_TITLE_TEXT
1167
+ ...KINDS_WITHOUT_TITLE_TEXT,
1168
+ "timeline-number"
1157
1169
  ]);
1158
1170
  function rendersSubtitle(kind) {
1159
1171
  return !KINDS_WITHOUT_SUBTITLE_TEXT.has(kind ?? "");
@@ -3087,6 +3099,14 @@ function curvePath(from, to, edge, labelMarginExtra = 0, textMetrics = "proporti
3087
3099
 
3088
3100
  // src/layout/metro-path.ts
3089
3101
  var METRO_CORNER_RADIUS = 24;
3102
+ var METRO_BRANCH_CORNER_RADIUS = 22;
3103
+ var METRO_BRANCH_STUB = 30;
3104
+ var METRO_DECISION_HEIGHT = 88;
3105
+ var METRO_DECISION_WIDTH = 116;
3106
+ var METRO_RETURN_END_OFFSET = 43;
3107
+ var METRO_RETURN_START_OFFSET = 24;
3108
+ var METRO_LINE_WIDTH = 10;
3109
+ var METRO_LINE_WIDTH_CSS = `var(--cdl-metro-line-width, ${METRO_LINE_WIDTH}px)`;
3090
3110
  var T = Math.SQRT2 - 1;
3091
3111
  function round2(v) {
3092
3112
  return Math.round(v * 100) / 100;
@@ -3104,7 +3124,63 @@ function polyline(points) {
3104
3124
  });
3105
3125
  return distinct.map((p, i) => `${i === 0 ? "M" : "L"} ${point(p)}`).join(" ");
3106
3126
  }
3107
- function metroPath(from, to, edge, labelMarginExtra = 0, textMetrics = "proportional") {
3127
+ function quadraticCornerPath(start, corner, end, radius) {
3128
+ const horizontalDir = Math.sign(corner.x - start.x);
3129
+ const verticalDir = Math.sign(end.y - corner.y);
3130
+ const r = Math.min(
3131
+ radius,
3132
+ Math.abs(corner.x - start.x),
3133
+ Math.abs(end.y - corner.y)
3134
+ );
3135
+ const before = { x: corner.x - horizontalDir * r, y: corner.y };
3136
+ const after = { x: corner.x, y: corner.y + verticalDir * r };
3137
+ return [
3138
+ `M ${point(start)}`,
3139
+ `L ${point(before)}`,
3140
+ `Q ${point(corner)} ${point(after)}`,
3141
+ `L ${point(end)}`
3142
+ ].join(" ");
3143
+ }
3144
+ function decisionBranchPath(start, end) {
3145
+ const horizontalDir = Math.sign(end.x - start.x);
3146
+ const verticalDir = Math.sign(end.y - start.y);
3147
+ const turnX = start.x + horizontalDir * (METRO_BRANCH_STUB + METRO_BRANCH_CORNER_RADIUS);
3148
+ const radius = Math.min(
3149
+ METRO_BRANCH_CORNER_RADIUS,
3150
+ Math.abs(end.y - start.y) / 2,
3151
+ Math.abs(end.x - turnX)
3152
+ );
3153
+ const firstCorner = { x: turnX, y: start.y };
3154
+ const secondCorner = { x: turnX, y: end.y };
3155
+ const beforeFirst = {
3156
+ x: turnX - horizontalDir * radius,
3157
+ y: start.y
3158
+ };
3159
+ const afterFirst = {
3160
+ x: turnX,
3161
+ y: start.y + verticalDir * radius
3162
+ };
3163
+ const beforeSecond = {
3164
+ x: turnX,
3165
+ y: end.y - verticalDir * radius
3166
+ };
3167
+ const afterSecond = {
3168
+ x: turnX + horizontalDir * radius,
3169
+ y: end.y
3170
+ };
3171
+ return {
3172
+ d: [
3173
+ `M ${point(start)}`,
3174
+ `L ${point(beforeFirst)}`,
3175
+ `Q ${point(firstCorner)} ${point(afterFirst)}`,
3176
+ `L ${point(beforeSecond)}`,
3177
+ `Q ${point(secondCorner)} ${point(afterSecond)}`,
3178
+ `L ${point(end)}`
3179
+ ].join(" "),
3180
+ turnX
3181
+ };
3182
+ }
3183
+ function metroPath(from, to, edge, _labelMarginExtra = 0, _textMetrics = "proportional") {
3108
3184
  const fromLeft = from.cx - from.w / 2;
3109
3185
  const fromRight = from.cx + from.w / 2;
3110
3186
  const toLeft = to.cx - to.w / 2;
@@ -3139,11 +3215,45 @@ function metroPath(from, to, edge, labelMarginExtra = 0, textMetrics = "proporti
3139
3215
  const sgn = Math.sign(dy);
3140
3216
  let d;
3141
3217
  let mid;
3142
- let tangent;
3143
- if (Math.abs(dy) < SAME_HEIGHT_THRESHOLD) {
3218
+ let metroReturn = false;
3219
+ if (from.kind === "decision" && Math.abs(dy) >= SAME_HEIGHT_THRESHOLD) {
3220
+ const verticalDir = Math.sign(dy);
3221
+ if (Math.abs(to.cx - from.cx) < SAME_HEIGHT_THRESHOLD) {
3222
+ const start = { x: from.cx, y: from.cy + verticalDir * from.h / 2 };
3223
+ const end = { x: to.cx, y: to.cy };
3224
+ d = polyline([start, end]);
3225
+ fromSide = verticalDir < 0 ? "top" : "bottom";
3226
+ toSide = verticalDir < 0 ? "bottom" : "top";
3227
+ mid = { x: start.x, y: (start.y + end.y) / 2 };
3228
+ } else {
3229
+ const horizontalDir = Math.sign(to.cx - from.cx);
3230
+ const start = {
3231
+ x: from.cx + horizontalDir * from.w / 2,
3232
+ y: from.cy
3233
+ };
3234
+ const end = {
3235
+ x: to.cx - horizontalDir * to.w / 2,
3236
+ y: to.cy
3237
+ };
3238
+ const branch = decisionBranchPath(start, end);
3239
+ d = branch.d;
3240
+ fromSide = horizontalDir > 0 ? "right" : "left";
3241
+ toSide = horizontalDir > 0 ? "left" : "right";
3242
+ mid = { x: branch.turnX, y: (start.y + end.y) / 2 };
3243
+ }
3244
+ } else if (edge?.style === "dashed" && from.kind === "station" && to.kind === "station" && to.cx < from.cx && Math.abs(dy) >= SAME_HEIGHT_THRESHOLD) {
3245
+ const verticalDir = Math.sign(to.cy - from.cy);
3246
+ const start = { x: from.cx - METRO_RETURN_START_OFFSET, y: from.cy };
3247
+ const end = { x: to.cx, y: to.cy - verticalDir * METRO_RETURN_END_OFFSET };
3248
+ const corner = { x: end.x, y: start.y };
3249
+ fromSide = "left";
3250
+ toSide = verticalDir < 0 ? "bottom" : "top";
3251
+ d = quadraticCornerPath(start, corner, end, METRO_BRANCH_CORNER_RADIUS);
3252
+ mid = { x: (start.x + corner.x) / 2, y: start.y };
3253
+ metroReturn = true;
3254
+ } else if (Math.abs(dy) < SAME_HEIGHT_THRESHOLD) {
3144
3255
  d = `M ${point({ x: x1, y: y1 })} L ${point({ x: x2, y: y2 })}`;
3145
3256
  mid = { x: (x1 + x2) / 2, y: (y1 + y2) / 2 };
3146
- tangent = { x: x2 - x1, y: y2 - y1 };
3147
3257
  } else if (fromSide !== toSide && Math.abs(x2 - x1) >= Math.abs(dy)) {
3148
3258
  const horizontal = Math.abs(x2 - x1);
3149
3259
  const s = (horizontal - Math.abs(dy)) / 2;
@@ -3173,7 +3283,6 @@ function metroPath(from, to, edge, labelMarginExtra = 0, textMetrics = "proporti
3173
3283
  d = polyline([p0, p1, p2, p3]);
3174
3284
  }
3175
3285
  mid = { x: (x1 + x2) / 2, y: (y1 + y2) / 2 };
3176
- tangent = w;
3177
3286
  } else {
3178
3287
  fromSide = "right";
3179
3288
  toSide = "right";
@@ -3203,28 +3312,15 @@ function metroPath(from, to, edge, labelMarginExtra = 0, textMetrics = "proporti
3203
3312
  `L ${point(p4)}`
3204
3313
  ].join(" ");
3205
3314
  mid = { x: p2.x - T * r, y: p2.y };
3206
- tangent = { x: 0, y: sgn };
3207
- }
3208
- if (edge?.overlay) {
3209
- return { d, fromSide, toSide, labelX: mid.x, labelY: mid.y, labelAnchor: "middle" };
3210
- }
3211
- const tangentLength = Math.hypot(tangent.x, tangent.y);
3212
- let normalX = tangent.y / tangentLength;
3213
- let normalY = -tangent.x / tangentLength;
3214
- if (normalY > Number.EPSILON || Math.abs(normalY) <= Number.EPSILON && normalX < 0) {
3215
- normalX *= -1;
3216
- normalY *= -1;
3217
3315
  }
3218
- const labelW = computeLabelBoxW(edge?.label, edge?.sub, textMetrics);
3219
- const labelH = labelPillH(edge?.sub);
3220
- const distance = labelPathClearanceHorizontal(edge) + (Math.abs(normalX) * labelW + Math.abs(normalY) * labelH) / 2 + labelMarginExtra;
3221
3316
  return {
3222
3317
  d,
3223
3318
  fromSide,
3224
3319
  toSide,
3225
- labelX: mid.x + normalX * distance,
3226
- labelY: mid.y + normalY * distance,
3227
- labelAnchor: "middle"
3320
+ labelX: mid.x,
3321
+ labelY: mid.y,
3322
+ labelAnchor: "middle",
3323
+ ...metroReturn ? { metroReturn: true } : {}
3228
3324
  };
3229
3325
  }
3230
3326
 
@@ -3351,12 +3447,14 @@ var NODE_KINDS = [
3351
3447
  "shape-lawyer",
3352
3448
  "shape-trader",
3353
3449
  "shape-customer-service",
3354
- // 流れの始まりと終わりの印 2 種 (#560) ... 塗った丸と輪で囲んだ丸。
3355
- // 状態遷移図が使う。 印そのものが意味を持つので文字は描かない
3450
+ // 流れの始まりと終わりの印 2 種 (#560) ... 始点と終点は線の端から推測せず、著者が
3451
+ // `kind: "mark-start"` / `kind: "mark-end"` の node を明示する。印は文字を描かない
3356
3452
  "mark-start",
3357
3453
  "mark-end",
3358
3454
  // 路線図の駅 (#996) ... 線路上の点を小さな丸で示し、名前と説明は丸の外に置く
3359
3455
  "station",
3456
+ // 時間軸の番号 (#1017) ... 実線の軸上に番号を入れた丸を置く。路線図の駅とは別の意味を持つ。
3457
+ "timeline-number",
3360
3458
  // 時系列のやり取り (#578) ... 1 つの箱が図を丸ごと描く。 縦が時間で、参加者は箱を持たない
3361
3459
  "sequence-board"
3362
3460
  ];
@@ -6548,7 +6646,7 @@ function runAxes(laid, diag, violations, counts, push, profile, skippedAxes, \u5
6548
6646
  const LIFELINE_W_THRESHOLD = 8;
6549
6647
  for (const n of laid.nodes) {
6550
6648
  if (n.w < LIFELINE_W_THRESHOLD) continue;
6551
- if (n.kind === "station") continue;
6649
+ if (n.kind === "station" || n.kind === "timeline-number") continue;
6552
6650
  if (n.w < MIN_NODE_W || n.h < MIN_NODE_H) {
6553
6651
  push(
6554
6652
  "node-visibility",
@@ -6574,8 +6672,7 @@ function runAxes(laid, diag, violations, counts, push, profile, skippedAxes, \u5
6574
6672
  if (!isRenderedNode(n)) continue;
6575
6673
  const titleTrimmed = n.title?.trim() ?? "";
6576
6674
  if (!titleTrimmed) continue;
6577
- const \u4F59\u767D = drawsTitleOutsideBox(n.kind) ? 0 : TITLE_BOX_PADDING;
6578
- const \u4F7F\u3048\u308B\u5E452 = n.w - \u4F59\u767D;
6675
+ const \u4F7F\u3048\u308B\u5E452 = titleUsableWidth(n.kind, n.w, n.h);
6579
6676
  const \u5B9F\u5E45 = measureTextWidth(titleTrimmed, {
6580
6677
  fontSize: titleFontSize(n.kind),
6581
6678
  bound: "drawn",
@@ -6594,7 +6691,7 @@ function runAxes(laid, diag, violations, counts, push, profile, skippedAxes, \u5
6594
6691
  push(
6595
6692
  "text-readability",
6596
6693
  // 字ごとの実幅は小数になるので、読む人に出す時だけ丸める (判定は丸めない)
6597
- `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)`,
6694
+ `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)`,
6598
6695
  "warn"
6599
6696
  );
6600
6697
  }
@@ -8154,6 +8251,8 @@ var TOKENS = {
8154
8251
  lane_label_h: 40,
8155
8252
  lane_top_pad: 100,
8156
8253
  contain_pad: 28,
8254
+ stage_column_gap: 48,
8255
+ stage_card_text_inset: 18,
8157
8256
  // node sizes (kind ごと、 unified row h は engine が max() で算出)
8158
8257
  node: {
8159
8258
  actor: { w: 340, h: 165 },
@@ -8280,7 +8379,10 @@ var TOKENS = {
8280
8379
  "mark-start": { w: 96, h: 96 },
8281
8380
  "mark-end": { w: 96, h: 96 },
8282
8381
  // 路線図の駅 (#996) ... 正方形にして小さな丸が縦横どちらにも歪まないようにする。
8283
- station: { w: 24, h: 24 },
8382
+ // 半径 16 と縁 6 の外径を、線路との接点を含めて収める。
8383
+ station: { w: 38, h: 38 },
8384
+ // 時間軸の番号 (#1017) ... 半径 24 の丸と太さ 4 の縁を収める。
8385
+ "timeline-number": { w: 52, h: 52 },
8284
8386
  // 時系列のやり取り (#578)。 実寸は中身から決まる (`sequenceBoardMetrics`) ので、
8285
8387
  // ここは書かなかった時の受け皿
8286
8388
  "sequence-board": { w: 960, h: 640 }
@@ -8295,6 +8397,8 @@ var LANE_TOP_PAD = TOKENS.lane_top_pad;
8295
8397
  var STACK_GAP = TOKENS.row_gap;
8296
8398
  var NODE_SIZE = TOKENS.node;
8297
8399
  var CONTAIN_PAD = TOKENS.contain_pad;
8400
+ var STAGE_COLUMN_GAP = TOKENS.stage_column_gap;
8401
+ var STAGE_CARD_TEXT_INSET = TOKENS.stage_card_text_inset;
8298
8402
  var VIEW_PAD = TOKENS.view_pad;
8299
8403
  var HEADER_RESERVE = TOKENS.header_reserve;
8300
8404
  var DEFAULT_LANE_GAP = 80;
@@ -8395,12 +8499,24 @@ function layoutEdges(diag, nodes, lanes = [], textMetrics = "proportional") {
8395
8499
  if (!from) throw new Error(`cdl layout: edge "${e.id}" \u306E from "${e.from}" \u304C node \u306B\u7121\u3044`);
8396
8500
  if (!to) throw new Error(`cdl layout: edge "${e.id}" \u306E to "${e.to}" \u304C node \u306B\u7121\u3044`);
8397
8501
  const c = curvePath(from, to, e, labelMarginExtra, textMetrics);
8502
+ const fromLane = lmap.get(from.lane);
8503
+ const toLane = lmap.get(to.lane);
8504
+ let labelX = c.labelX;
8505
+ if (fromLane?.stage !== void 0 && toLane?.stage !== void 0 && fromLane.id !== toLane.id && (e.label || e.sub)) {
8506
+ const [leftLane, rightLane] = fromLane.x <= toLane.x ? [fromLane, toLane] : [toLane, fromLane];
8507
+ const gapLeft = leftLane.x + leftLane.width - STAGE_COLUMN_GAP / 2;
8508
+ const gapRight = rightLane.x + STAGE_COLUMN_GAP / 2;
8509
+ const labelHalfWidth = computeLabelBoxW(e.label, e.sub, textMetrics) / 2;
8510
+ const minX = gapLeft + labelHalfWidth;
8511
+ const maxX = gapRight - labelHalfWidth;
8512
+ if (minX <= maxX) labelX = Math.min(maxX, Math.max(minX, labelX));
8513
+ }
8398
8514
  positionedEdges.push({
8399
8515
  ...e,
8400
8516
  d: c.d,
8401
8517
  fromSide: c.fromSide,
8402
8518
  toSide: c.toSide,
8403
- labelX: c.labelX + (e.labelOffsetX ?? 0),
8519
+ labelX: labelX + (e.labelOffsetX ?? 0),
8404
8520
  labelY: c.labelY + (e.labelOffsetY ?? 0),
8405
8521
  labelAnchor: c.labelAnchor
8406
8522
  });
@@ -8417,7 +8533,8 @@ function layoutEdges(diag, nodes, lanes = [], textMetrics = "proportional") {
8417
8533
  toSide: m.toSide,
8418
8534
  labelX: m.labelX + (e.labelOffsetX ?? 0),
8419
8535
  labelY: m.labelY + (e.labelOffsetY ?? 0),
8420
- labelAnchor: m.labelAnchor
8536
+ labelAnchor: m.labelAnchor,
8537
+ ...m.metroReturn ? { metroReturn: true } : {}
8421
8538
  });
8422
8539
  } else {
8423
8540
  autoEdges.push(e);
@@ -9109,20 +9226,22 @@ function countPathObstacleCrossings(d, obstacles) {
9109
9226
  const segments = flattenPathSegments(extractPathSegments(d));
9110
9227
  return obstacles.filter((obstacle) => segmentsCrossRect(segments, nodeRect(obstacle))).length;
9111
9228
  }
9112
- function makeBackDetourCandidate(A, B, from, to, obstacles, detourY, side) {
9229
+ function makeBackDetourCandidate(A, B, from, to, obstacles, detourY, side, columnXOverride) {
9113
9230
  const lowIsA = A.y >= B.y;
9114
9231
  const low = lowIsA ? A : B;
9115
9232
  const lowBendY = low.y - VERTICAL_DETOUR_EXIT;
9116
9233
  const bandTop = Math.min(detourY, lowBendY);
9117
9234
  const bandBottom = Math.max(detourY, lowBendY);
9118
9235
  const bandObstacles = obstacles.map(nodeRect).filter((rect) => rect.y + rect.h >= bandTop && rect.y <= bandBottom);
9119
- let columnX = detourColumnFor([from, to], side);
9120
- while (true) {
9121
- const blockers = bandObstacles.filter(
9122
- (rect) => rect.x - VERTICAL_DETOUR_GAP < columnX && columnX < rect.x + rect.w + VERTICAL_DETOUR_GAP
9123
- );
9124
- if (blockers.length === 0) break;
9125
- 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));
9236
+ let columnX = columnXOverride ?? detourColumnFor([from, to], side);
9237
+ if (columnXOverride === void 0) {
9238
+ while (true) {
9239
+ const blockers = bandObstacles.filter(
9240
+ (rect) => rect.x - VERTICAL_DETOUR_GAP < columnX && columnX < rect.x + rect.w + VERTICAL_DETOUR_GAP
9241
+ );
9242
+ if (blockers.length === 0) break;
9243
+ 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));
9244
+ }
9126
9245
  }
9127
9246
  const bendAY = lowIsA ? lowBendY : detourY;
9128
9247
  const bendBY = lowIsA ? detourY : lowBendY;
@@ -9231,7 +9350,7 @@ function sidePoint(n, side) {
9231
9350
  return { x: n.cx + n.w / 2, y: n.cy };
9232
9351
  }
9233
9352
  }
9234
- 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") {
9353
+ 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") {
9235
9354
  const labelBoxW = edge ? computeLabelBoxW(edge.label, edge.sub, textMetrics) : 0;
9236
9355
  const labelHalfW = labelBoxW / 2;
9237
9356
  const labelInit = labelInitClearance(edge);
@@ -9601,17 +9720,27 @@ function routePath(from, to, fromSide, toSide, obstacles = [], detourSide, _from
9601
9720
  };
9602
9721
  }
9603
9722
  if (topTopPair) {
9604
- const detourY = Math.min(A.y, B.y) - 100;
9723
+ const stagePoint = (point2, node, lane) => lane?.stage === void 0 ? point2 : {
9724
+ x: Math.min(node.cx + node.w / 2 - 24, lane.x + lane.width - 24),
9725
+ y: point2.y
9726
+ };
9727
+ const backA = stagePoint(A, from, fromLane);
9728
+ const backB = stagePoint(B, to, toLane);
9729
+ const stageTops = [fromLane, toLane].filter((lane) => lane?.stage !== void 0).map((lane) => lane.y);
9730
+ const detourY = Math.min(
9731
+ Math.min(backA.y, backB.y) - 100,
9732
+ ...stageTops.map((top) => top - VERTICAL_DETOUR_GAP)
9733
+ );
9605
9734
  const corner = 14;
9606
- const xDir = B.x > A.x ? 1 : -1;
9735
+ const xDir = backB.x > backA.x ? 1 : -1;
9607
9736
  const existing = {
9608
- d: `M ${A.x} ${A.y}
9609
- L ${A.x} ${detourY + corner}
9610
- Q ${A.x} ${detourY}, ${A.x + xDir * corner} ${detourY}
9611
- L ${B.x - xDir * corner} ${detourY}
9612
- Q ${B.x} ${detourY}, ${B.x} ${detourY + corner}
9613
- L ${B.x} ${B.y}`,
9614
- labelX: (A.x + B.x) / 2,
9737
+ d: `M ${backA.x} ${backA.y}
9738
+ L ${backA.x} ${detourY + corner}
9739
+ Q ${backA.x} ${detourY}, ${backA.x + xDir * corner} ${detourY}
9740
+ L ${backB.x - xDir * corner} ${detourY}
9741
+ Q ${backB.x} ${detourY}, ${backB.x} ${detourY + corner}
9742
+ L ${backB.x} ${backB.y}`,
9743
+ labelX: (backA.x + backB.x) / 2,
9615
9744
  // CAR-468 SSOT ... 旧 detourY - 30 magic number を LABEL_INIT_CLEARANCE (26、 CAR-570 で 34 → 26 縮小) に置換。
9616
9745
  // CAR-518 SSOT ... dotted-flow は 62 world (marker glow radius 分拡張) を labelInit で参照。
9617
9746
  // top-top detour の水平 segment 上方 labelInit world = spec 準拠距離。
@@ -9620,7 +9749,16 @@ function routePath(from, to, fromSide, toSide, obstacles = [], detourSide, _from
9620
9749
  };
9621
9750
  const existingCrossings = countPathObstacleCrossings(existing.d, obstacles);
9622
9751
  if (existingCrossings === 0) return existing;
9623
- const candidate = chooseBackDetourCandidate(A, B, from, to, obstacles, detourY);
9752
+ const candidate = from.lane === to.lane && fromLane?.stage !== void 0 ? makeBackDetourCandidate(
9753
+ backA,
9754
+ backB,
9755
+ from,
9756
+ to,
9757
+ obstacles,
9758
+ detourY,
9759
+ "right",
9760
+ fromLane.x + fromLane.width
9761
+ ) : chooseBackDetourCandidate(backA, backB, from, to, obstacles, detourY);
9624
9762
  if (candidate.crossings >= existingCrossings) return existing;
9625
9763
  return {
9626
9764
  d: candidate.d,
@@ -10554,12 +10692,56 @@ function \u76F4\u524D\u306E\u6298\u308C\u3054\u3068\u305A\u3089\u3059(segs, \u6A
10554
10692
  return parts.join(" ");
10555
10693
  }
10556
10694
 
10695
+ // src/layout/legend.ts
10696
+ var LEGEND_FONT_SIZE = 17;
10697
+ var LEGEND_MARK_SCALE = LEGEND_FONT_SIZE / 21;
10698
+ var LEGEND_MARK_REFERENCE_WIDTH = 46;
10699
+ var LEGEND_MARK_WIDTH = LEGEND_MARK_REFERENCE_WIDTH * LEGEND_MARK_SCALE;
10700
+ var LEGEND_MARK_TEXT_GAP = 12 * LEGEND_MARK_SCALE;
10701
+ var LEGEND_ITEM_GAP_RATIO = 0.07;
10702
+ var LEGEND_BASELINE_GAP = 32;
10703
+ var LEGEND_BOTTOM_EXTENT = 10;
10704
+ function layoutLegend(items, viewBox, textMetrics, nodes) {
10705
+ const renderedNodes = nodes.filter(isRenderedNode);
10706
+ const startX = renderedNodes.length > 0 ? Math.min(...renderedNodes.map((node) => node.cx - node.w / 2)) : viewBox.x + VIEW_PAD;
10707
+ const diagramRight = renderedNodes.length > 0 ? Math.max(...renderedNodes.map((node) => node.cx + node.w / 2)) : viewBox.x + viewBox.w - VIEW_PAD;
10708
+ const itemGap = (diagramRight - startX) * LEGEND_ITEM_GAP_RATIO;
10709
+ const y = viewBox.y + viewBox.h - VIEW_PAD + LEGEND_BASELINE_GAP;
10710
+ let x = startX;
10711
+ const legend = items.map((item) => {
10712
+ const laid = { ...item, x, y };
10713
+ x += LEGEND_MARK_WIDTH + LEGEND_MARK_TEXT_GAP + textWidth(item.text, LEGEND_FONT_SIZE, 0, textMetrics) + itemGap;
10714
+ return laid;
10715
+ });
10716
+ const right = x - itemGap;
10717
+ const bottom = y + LEGEND_BOTTOM_EXTENT + VIEW_PAD;
10718
+ return {
10719
+ legend,
10720
+ viewBox: {
10721
+ ...viewBox,
10722
+ w: Math.max(viewBox.w, right + VIEW_PAD - viewBox.x),
10723
+ h: Math.max(viewBox.h, bottom - viewBox.y)
10724
+ }
10725
+ };
10726
+ }
10727
+
10557
10728
  // src/layout/lanes.ts
10558
10729
  var LANE_POSITION_EPSILON = 1e-9;
10730
+ function equalizeStageColumns(lanes, nodes = []) {
10731
+ const stages = lanes.filter((lane) => lane.stage !== void 0);
10732
+ if (stages.length === 0) return lanes;
10733
+ const top = Math.min(...stages.map((lane) => lane.y));
10734
+ const stageIds = new Set(stages.map((lane) => lane.id));
10735
+ const nodeBottoms = nodes.filter((node) => stageIds.has(node.lane)).map((node) => node.cy + node.h / 2 + CONTAIN_PAD);
10736
+ const bottom = Math.max(...stages.map((lane) => lane.y + lane.height), ...nodeBottoms);
10737
+ return lanes.map(
10738
+ (lane) => lane.stage === void 0 ? lane : { ...lane, y: top, height: bottom - top }
10739
+ );
10740
+ }
10559
10741
  function layoutLanes(diag) {
10560
10742
  const laneGap = diag.viewport?.laneGap ?? diag.viewport?.gap ?? DEFAULT_LANE_GAP;
10561
10743
  let cursorX = 0;
10562
- return diag.lanes.map((lane) => {
10744
+ const lanes = diag.lanes.map((lane, index) => {
10563
10745
  if (lane.posX !== void 0 && lane.posY !== void 0) {
10564
10746
  return {
10565
10747
  ...lane,
@@ -10570,7 +10752,9 @@ function layoutLanes(diag) {
10570
10752
  };
10571
10753
  }
10572
10754
  const resolvedX = lane.x ?? cursorX;
10573
- cursorX = resolvedX + lane.width + laneGap;
10755
+ const nextLane = diag.lanes[index + 1];
10756
+ const gapAfter = lane.stage !== void 0 && nextLane?.stage !== void 0 ? 0 : laneGap;
10757
+ cursorX = resolvedX + lane.width + gapAfter;
10574
10758
  return {
10575
10759
  ...lane,
10576
10760
  x: resolvedX,
@@ -10578,6 +10762,7 @@ function layoutLanes(diag) {
10578
10762
  height: LANE_LABEL_H
10579
10763
  };
10580
10764
  });
10765
+ return equalizeStageColumns(lanes);
10581
10766
  }
10582
10767
  function expandLanesForNodes(lanes, nodes) {
10583
10768
  const targetNodeGap = targetNodeNodeClearance();
@@ -10606,7 +10791,8 @@ function expandLanesForNodes(lanes, nodes) {
10606
10791
  if (lane.posX !== void 0 && lane.posY !== void 0) continue;
10607
10792
  const inside = nodes.filter((n) => n.lane === lane.id);
10608
10793
  const maxNodeW = inside.length > 0 ? Math.max(...inside.map((n) => n.w)) : 0;
10609
- const newWidth = Math.max(lane.width, maxNodeW + padFor(lane) * 2);
10794
+ const stageMinWidth = lane.stage === void 0 ? 0 : maxNodeW + CONTAIN_PAD + STAGE_COLUMN_GAP;
10795
+ const newWidth = Math.max(lane.width, maxNodeW + padFor(lane) * 2, stageMinWidth);
10610
10796
  widthOf.set(lane.id, newWidth);
10611
10797
  deltaOf.set(lane.id, newWidth - lane.width);
10612
10798
  }
@@ -10631,6 +10817,34 @@ function expandLaneGapsForEdgeLabels(lanes, nodes, edges, textMetrics = "proport
10631
10817
  if (lanes.length < 2) return lanes;
10632
10818
  const layoutedLanes = lanes.filter((l) => !(l.posX !== void 0 && l.posY !== void 0));
10633
10819
  if (layoutedLanes.length < 2) return lanes;
10820
+ if (layoutedLanes.length === lanes.length && layoutedLanes.every((lane) => lane.stage !== void 0)) {
10821
+ const sortedStages = [...layoutedLanes].sort((a, b) => a.x - b.x);
10822
+ const stageIndexById = new Map(sortedStages.map((lane, index) => [lane.id, index]));
10823
+ const nodeLaneById = new Map(nodes.map((node) => [node.id, node.lane]));
10824
+ const requiredSurfaceGaps = new Array(sortedStages.length - 1).fill(STAGE_COLUMN_GAP);
10825
+ for (const edge of edges) {
10826
+ if (edge.routing !== "curve" || !edge.label && !edge.sub) continue;
10827
+ const fromIndex = stageIndexById.get(nodeLaneById.get(edge.from) ?? "");
10828
+ const toIndex = stageIndexById.get(nodeLaneById.get(edge.to) ?? "");
10829
+ if (fromIndex === void 0 || toIndex === void 0 || fromIndex === toIndex) continue;
10830
+ const lo = Math.min(fromIndex, toIndex);
10831
+ const hi = Math.max(fromIndex, toIndex);
10832
+ const labelWidth = computeLabelBoxW(edge.label, edge.sub, textMetrics);
10833
+ for (let pair = lo; pair < hi; pair++) {
10834
+ requiredSurfaceGaps[pair] = Math.max(requiredSurfaceGaps[pair], labelWidth);
10835
+ }
10836
+ }
10837
+ const shiftedXById2 = /* @__PURE__ */ new Map([[sortedStages[0].id, sortedStages[0].x]]);
10838
+ for (let index = 1; index < sortedStages.length; index++) {
10839
+ const previous = sortedStages[index - 1];
10840
+ const lane = sortedStages[index];
10841
+ const previousX = shiftedXById2.get(previous.id);
10842
+ const authoredGap = Math.max(0, lane.x - (previous.x + previous.width));
10843
+ const laneGap = Math.max(authoredGap, requiredSurfaceGaps[index - 1] - STAGE_COLUMN_GAP);
10844
+ shiftedXById2.set(lane.id, previousX + previous.width + laneGap);
10845
+ }
10846
+ return lanes.map((lane) => ({ ...lane, x: shiftedXById2.get(lane.id) ?? lane.x }));
10847
+ }
10634
10848
  const nodeById = new Map(nodes.map((n) => [n.id, n]));
10635
10849
  const sortedByX = [...layoutedLanes].sort((a, b) => a.x - b.x);
10636
10850
  const laneIndexById = new Map(sortedByX.map((lane, i) => [lane.id, i]));
@@ -10702,7 +10916,7 @@ function recomputeContainedLanes(lanes, nodes) {
10702
10916
  lifelineBottom = Math.max(lifelineBottom, bottom);
10703
10917
  }
10704
10918
  }
10705
- return lanes.map((lane) => {
10919
+ const contained = lanes.map((lane) => {
10706
10920
  if (lane.lifeline) {
10707
10921
  return {
10708
10922
  ...lane,
@@ -10720,6 +10934,7 @@ function recomputeContainedLanes(lanes, nodes) {
10720
10934
  height: bottom - top + CONTAIN_PAD * 2 + LANE_LABEL_H
10721
10935
  };
10722
10936
  });
10937
+ return equalizeStageColumns(contained, nodes);
10723
10938
  }
10724
10939
 
10725
10940
  // src/layout/head-label.ts
@@ -10869,11 +11084,15 @@ function requiredStackGapForEdgeLabels(diag, allStacks) {
10869
11084
  }
10870
11085
  function layoutNodes(diag, lanes, textMetrics) {
10871
11086
  const laneMap = new Map(lanes.map((l) => [l.id, l]));
11087
+ const metroDecisionIds = new Set(
11088
+ 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"))
11089
+ );
11090
+ const nodeSize = (node) => metroDecisionIds.has(node.id) ? { w: METRO_DECISION_WIDTH, h: METRO_DECISION_HEIGHT } : NODE_SIZE[node.kind];
10872
11091
  const allStacks = Array.from(new Set(diag.nodes.map((n) => n.stack))).sort((a, b) => a - b);
10873
11092
  const rowH = /* @__PURE__ */ new Map();
10874
11093
  for (const stack of allStacks) {
10875
11094
  const nodesAtStack = diag.nodes.filter((n) => n.stack === stack);
10876
- const h = Math.max(...nodesAtStack.map((n) => \u7BB1\u306E\u9AD8\u30552(n, NODE_SIZE[n.kind].h)));
11095
+ const h = Math.max(...nodesAtStack.map((n) => \u7BB1\u306E\u9AD8\u30552(n, nodeSize(n).h)));
10877
11096
  rowH.set(stack, h);
10878
11097
  }
10879
11098
  const rowCy = /* @__PURE__ */ new Map();
@@ -10909,7 +11128,7 @@ function layoutNodes(diag, lanes, textMetrics) {
10909
11128
  if (!lane) {
10910
11129
  throw new Error(`cdl layout: node "${n.id}" \u306E lane "${n.lane}" \u304C\u5B9A\u7FA9\u3055\u308C\u3066\u3044\u306A\u3044`);
10911
11130
  }
10912
- const size = NODE_SIZE[n.kind];
11131
+ const size = nodeSize(n);
10913
11132
  if (n.posX !== void 0 && n.posY !== void 0) {
10914
11133
  const w2 = n.posW ?? n.w ?? autoRowsWidth(n, size.w, textMetrics);
10915
11134
  const h2 = n.posH ?? n.h ?? rowH.get(n.stack) ?? size.h;
@@ -11032,6 +11251,9 @@ function dropUnsafeLaneDx(dxByLane, nodes) {
11032
11251
  function applyRowLevelNodeShifts(nodes, lanes, nodeShifts) {
11033
11252
  const dyByStack = aggregateRowDy(nodes, nodeShifts);
11034
11253
  const dxByLane = dropUnsafeLaneDx(aggregateLaneDx(nodes, nodeShifts), nodes);
11254
+ for (const lane of lanes) {
11255
+ if (lane.stage !== void 0) dxByLane.delete(lane.id);
11256
+ }
11035
11257
  let moved = false;
11036
11258
  const outNodes = nodes.map((n) => {
11037
11259
  const dx = dxByLane.get(n.lane) ?? 0;
@@ -11151,7 +11373,15 @@ function layout(diag) {
11151
11373
  );
11152
11374
  }
11153
11375
  }
11154
- const viewBox = computeViewBox(containedLanesFinal, nodesFinal, edges, bboxes, diag.viewport);
11376
+ const contentViewBox = computeViewBox(
11377
+ containedLanesFinal,
11378
+ nodesFinal,
11379
+ edges,
11380
+ bboxes,
11381
+ diag.viewport
11382
+ );
11383
+ const legendLayout = diag.legend?.length ? layoutLegend(diag.legend, contentViewBox, textMetrics, nodesFinal) : void 0;
11384
+ const viewBox = legendLayout?.viewBox ?? contentViewBox;
11155
11385
  return {
11156
11386
  id: diag.id,
11157
11387
  topic: diag.topic,
@@ -11178,6 +11408,7 @@ function layout(diag) {
11178
11408
  ...textMetrics === "monospace" ? { textMetrics: "monospace" } : {},
11179
11409
  // 種類も配色と同じく空の目印を出さない。 遮断をここに揃えると手書きの図にも効く
11180
11410
  ...diag.type ? { type: diag.type } : {},
11411
+ ...legendLayout ? { legend: legendLayout.legend } : {},
11181
11412
  bboxes,
11182
11413
  collisions,
11183
11414
  nearCollisions,
@@ -11366,6 +11597,11 @@ function validate(diag) {
11366
11597
  \u2192 \u4FEE\u6B63\u4F8B: .lane("${n.lane}", { x: 0, width: 400 }) \u3092 node \u5BA3\u8A00\u524D\u306B\u8FFD\u52A0`
11367
11598
  );
11368
11599
  }
11600
+ if (n.leaderTo !== void 0 && !nodeIds.has(n.leaderTo)) {
11601
+ errors.push(
11602
+ `[cdl] unknown-ref: node "${n.id}" \u306E leaderTo "${n.leaderTo}" \u304C node \u306B\u5B58\u5728\u3057\u307E\u305B\u3093${suggest(n.leaderTo, nodeIds, "node")}`
11603
+ );
11604
+ }
11369
11605
  }
11370
11606
  for (const e of diag.edges) {
11371
11607
  if (!nodeIds.has(e.from)) {
@@ -11984,6 +12220,10 @@ function uniformLifelineEndY(lanes, nodes, starts = lifelineStartYs(nodes)) {
11984
12220
 
11985
12221
  // src/render/edge-head.ts
11986
12222
  var EDGE_HEAD_MUTED_MARKER_ID = "cdl-arrow-muted";
12223
+ var METRO_RETURN_ARROW_D = "M 21 0 L 4 9.5 L 4 -9.5 Z";
12224
+ function metroReturnArrowMarkerId(tone) {
12225
+ return `cdl-metro-return-arrow-${tone}`;
12226
+ }
11987
12227
  var EDGE_HEAD_SHAPE = {
11988
12228
  // 何も描かない端。 marker 自体を出さないので `d` は読まれない
11989
12229
  none: { d: "", w: 10, h: 10, refX: 0, filled: false },
@@ -12038,6 +12278,7 @@ function \u7B49\u5E45\u306E\u5B57(text, fontSize) {
12038
12278
  });
12039
12279
  return out;
12040
12280
  }
12281
+ var METRO_PENDING_ATTRIBUTE = "data-cdl-pending";
12041
12282
  function defaultEdgeWidth(active) {
12042
12283
  return active ? 2.5 : 1.25;
12043
12284
  }
@@ -12056,12 +12297,14 @@ function CdlEdgeView({
12056
12297
  drawing,
12057
12298
  progress,
12058
12299
  stateValues,
12059
- flowing = false
12300
+ flowing = false,
12301
+ pending = false
12060
12302
  }) {
12061
12303
  const color = edgeColor(edge);
12062
- const marker = edgeHeadMarkerName(edge.tone, edge.head, !active, edge.headFill);
12063
- const tailMarker = edge.tailHead === void 0 ? void 0 : edgeHeadMarkerName(edge.tone, edge.tailHead, !active, edge.tailHeadFill);
12064
12304
  const style = edge.style ?? "solid";
12305
+ const isMetroReturn = edge.metroReturn === true;
12306
+ const marker = isMetroReturn ? metroReturnArrowMarkerId(edge.tone) : edgeHeadMarkerName(edge.tone, edge.head, !active, edge.headFill);
12307
+ const tailMarker = edge.tailHead === void 0 ? void 0 : edgeHeadMarkerName(edge.tone, edge.tailHead, !active, edge.tailHeadFill);
12065
12308
  const isDotted = style === "dotted-flow";
12066
12309
  const isPassThrough = isDotted && Boolean(edge.dThrough);
12067
12310
  const lineDotsD = isDotted ? shrinkPathEnd(edge.d, 12) : edge.d;
@@ -12071,6 +12314,7 @@ function CdlEdgeView({
12071
12314
  return /* @__PURE__ */ jsxs(
12072
12315
  "g",
12073
12316
  {
12317
+ ...pending ? { [METRO_PENDING_ATTRIBUTE]: "true" } : {},
12074
12318
  "data-cdl-edge": edge.id,
12075
12319
  "data-cdl-routing": edge.routing === "metro" ? "metro" : void 0,
12076
12320
  "data-cdl-from": edge.from,
@@ -12096,11 +12340,12 @@ function CdlEdgeView({
12096
12340
  fill: "none",
12097
12341
  stroke: color,
12098
12342
  strokeOpacity: active ? 0.95 : 0.9,
12099
- strokeWidth: active ? 5 : 4,
12343
+ strokeWidth: edge.routing === "metro" ? METRO_LINE_WIDTH : active ? 5 : 4,
12100
12344
  strokeDasharray: "0 18",
12101
12345
  strokeLinecap: "round",
12102
12346
  style: {
12103
- transition: "stroke 280ms ease, stroke-opacity 280ms ease, stroke-width 280ms ease"
12347
+ transition: "stroke 280ms ease, stroke-opacity 280ms ease, stroke-width 280ms ease",
12348
+ strokeWidth: edge.routing === "metro" ? METRO_LINE_WIDTH_CSS : void 0
12104
12349
  }
12105
12350
  }
12106
12351
  ),
@@ -12155,7 +12400,9 @@ function SolidEdgePath({
12155
12400
  const boundWidth = edge.widthBind && stateValues ? resolveNumericAttr(edge.widthBind, stateValues, defaultWidth) : defaultWidth;
12156
12401
  const boundStroke = edge.strokeBind && stateValues ? interpolate(edge.strokeBind, stateValues) : color;
12157
12402
  const boundDashOffset = edge.dashOffsetBind && stateValues ? resolveNumericAttr(edge.dashOffsetBind, stateValues, 0) : void 0;
12158
- const dashArray = dashed ? "10 8" : active ? boundDashOffset !== void 0 ? "8 6" : void 0 : "4 5";
12403
+ const strokeWidth = edge.routing === "metro" && edge.widthBind === void 0 ? edge.metroReturn ? 6 : METRO_LINE_WIDTH : boundWidth;
12404
+ const strokeWidthCss = edge.routing === "metro" && !edge.metroReturn ? edge.widthBind === void 0 ? METRO_LINE_WIDTH_CSS : `var(--cdl-metro-line-width, ${strokeWidth}px)` : void 0;
12405
+ const dashArray = dashed ? edge.metroReturn ? "0 9.6" : "10 8" : active ? boundDashOffset !== void 0 ? "8 6" : void 0 : "4 5";
12159
12406
  return /* @__PURE__ */ jsx(
12160
12407
  "path",
12161
12408
  {
@@ -12164,14 +12411,15 @@ function SolidEdgePath({
12164
12411
  fill: "none",
12165
12412
  stroke: boundStroke,
12166
12413
  strokeOpacity: active ? 0.95 : 0.9,
12167
- strokeWidth: boundWidth,
12414
+ strokeWidth,
12168
12415
  strokeDasharray: dashArray,
12169
12416
  strokeDashoffset: boundDashOffset,
12170
12417
  strokeLinecap: "round",
12171
12418
  markerEnd: marker === void 0 ? void 0 : `url(#${marker})`,
12172
12419
  markerStart: tailMarker === void 0 ? void 0 : `url(#${tailMarker})`,
12173
12420
  style: {
12174
- transition: "stroke 280ms ease, stroke-opacity 280ms ease, stroke-width 280ms ease"
12421
+ transition: "stroke 280ms ease, stroke-opacity 280ms ease, stroke-width 280ms ease",
12422
+ strokeWidth: strokeWidthCss
12175
12423
  }
12176
12424
  }
12177
12425
  );
@@ -12468,7 +12716,11 @@ function \u5272\u308B\u5019\u88DC(text) {
12468
12716
  function \u5165\u3063\u3066\u3044\u308B([\u5DE6, \u53F3]) {
12469
12717
  return \u5DE6 !== "" && \u53F3 !== "";
12470
12718
  }
12471
- function CardNode({ node, active }) {
12719
+ function CardNode({
12720
+ node,
12721
+ active,
12722
+ compactTitleInset
12723
+ }) {
12472
12724
  const accent = nodeAccent(node, "var(--cdl-tone-accent, #2d6a8f)");
12473
12725
  const x = node.cx - node.w / 2;
12474
12726
  const y = node.cy - node.h / 2;
@@ -12486,12 +12738,12 @@ function CardNode({ node, active }) {
12486
12738
  });
12487
12739
  const \u305A\u3089\u3057 = \u7E70\u308A\u4E0A\u3052 - \u4F59\u308A;
12488
12740
  const titleY = compact ? \u5C0F\u578B\u306B\u547C\u3073\u540D ? node.h / 2 - 2 : node.h / 2 + 8 : 86 - \u305A\u3089\u3057;
12489
- const titleX = compact ? node.w / 2 : 26;
12490
- const paddingX = compact ? 16 : 52;
12741
+ const titleX = compact ? compactTitleInset ?? node.w / 2 : 26;
12742
+ const paddingX = compact ? (compactTitleInset ?? 8) * 2 : 52;
12491
12743
  const availableW = node.w - paddingX;
12492
12744
  const initialTitleSize = compact ? 20 : 24;
12493
12745
  const titleSize = fitTextSize(node.title, availableW, initialTitleSize, 11);
12494
- const textAnchor = compact ? "middle" : "start";
12746
+ const textAnchor = compact && compactTitleInset === void 0 ? "middle" : "start";
12495
12747
  const subtitleY = 124 - \u305A\u3089\u3057;
12496
12748
  const \u6700\u5927\u306E\u884C\u9001\u308A = SUBTITLE_SIZE * \u884C\u9001\u308A\u306E\u500D\u7387;
12497
12749
  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;
@@ -13251,7 +13503,9 @@ function GenericNode({
13251
13503
  value,
13252
13504
  stateValues,
13253
13505
  \u5024\u306E\u540D\u524D,
13254
- textMetrics = "proportional"
13506
+ textMetrics = "proportional",
13507
+ showKindIcon = true,
13508
+ metroDecision = false
13255
13509
  }) {
13256
13510
  const base = STYLES[node.kind] ?? FALLBACK;
13257
13511
  const toned = toneColor3(node.tone);
@@ -13282,14 +13536,14 @@ function GenericNode({
13282
13536
  if (shape === "diamond") {
13283
13537
  const cx = w / 2;
13284
13538
  const cy = h / 2;
13285
- const pad = 8;
13539
+ const pad = metroDecision ? 0 : 8;
13286
13540
  return /* @__PURE__ */ jsx(
13287
13541
  "path",
13288
13542
  {
13289
13543
  d: `M ${cx} ${pad} L ${w - pad} ${cy} L ${cx} ${h - pad} L ${pad} ${cy} Z`,
13290
13544
  fill: "var(--cdl-node-fill, #ffffff)",
13291
13545
  stroke: active ? style.accent : "var(--cdl-divider, #d4d4d8)",
13292
- strokeWidth: active ? 3 : 1.25
13546
+ strokeWidth: metroDecision ? 1.5 : active ? 3 : 1.25
13293
13547
  }
13294
13548
  );
13295
13549
  }
@@ -13353,11 +13607,11 @@ function GenericNode({
13353
13607
  const \u305A\u3089\u3057 = \u7E70\u308A\u4E0A\u3052 - \u4F59\u308A;
13354
13608
  const \u7D75\u306E\u82AF = centered && STYLES[node.kind]?.shape === "cloud" ? h * CLOUD_ART_CENTER_RISE : 0;
13355
13609
  const eyebrowY = centered ? h / 2 - 26 - \u7D75\u306E\u82AF : 32 + cylinderShift + \u4F59\u308A;
13356
- const titleY = genericTitleBaselineY(node.kind, h, hasRows) - \u305A\u3089\u3057 - \u7D75\u306E\u82AF;
13610
+ const titleY = metroDecision ? h / 2 + 9 : genericTitleBaselineY(node.kind, h, hasRows) - \u305A\u3089\u3057 - \u7D75\u306E\u82AF;
13357
13611
  const subtitleY = centered ? h / 2 + 28 - \u7D75\u306E\u82AF : 96 + cylinderShift - \u305A\u3089\u3057;
13358
13612
  const iconX = centered ? w / 2 + 40 : w - 48;
13359
13613
  const iconY = centered ? h / 2 - 44 - \u7D75\u306E\u82AF : 18 + cylinderShift;
13360
- const dominantBaseline = centered ? "middle" : void 0;
13614
+ const dominantBaseline = centered && !metroDecision ? "middle" : void 0;
13361
13615
  return /* @__PURE__ */ jsxs(
13362
13616
  "g",
13363
13617
  {
@@ -13366,12 +13620,12 @@ function GenericNode({
13366
13620
  "data-cdl-look": style.look,
13367
13621
  children: [
13368
13622
  shapeEl,
13369
- /* @__PURE__ */ jsxs("g", { transform: `translate(${iconX} ${iconY}) scale(1)`, "data-cdl-role": "node-kind-icon", children: [
13623
+ showKindIcon && /* @__PURE__ */ jsxs("g", { transform: `translate(${iconX} ${iconY}) scale(1)`, "data-cdl-role": "node-kind-icon", children: [
13370
13624
  /* @__PURE__ */ jsx("rect", { width: 24, height: 24, fill: "transparent" }),
13371
13625
  /* @__PURE__ */ jsx("path", { d: style.icon, fill: style.eyebrowColor, opacity: 0.7, transform: "scale(1)" })
13372
13626
  ] }),
13373
13627
  node.eyebrow && /* @__PURE__ */ 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 }),
13374
- /* @__PURE__ */ 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 }),
13628
+ /* @__PURE__ */ 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 }),
13375
13629
  node.subtitle && /* @__PURE__ */ 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) }),
13376
13630
  resolvedValue && /* @__PURE__ */ jsx(
13377
13631
  "text",
@@ -13420,50 +13674,176 @@ function GenericNode({
13420
13674
  var MARK_INNER_RATIO = 0.56;
13421
13675
  var RING_WIDTH_ACTIVE = 6;
13422
13676
  var RING_WIDTH_IDLE = 4;
13677
+ var METRO_START_RADIUS = 17;
13678
+ var METRO_END_RADIUS = 19;
13679
+ var METRO_END_RING_WIDTH = 3.5;
13680
+ var METRO_END_INNER_RADIUS = 10;
13423
13681
  var MARK_DEFAULT = "var(--cdl-tone-accent, #2d6a8f)";
13424
13682
  function markRadius(node) {
13425
13683
  return Math.min(node.w, node.h) / 2;
13426
13684
  }
13427
- function MarkStartNode({ node }) {
13685
+ function MarkStartNode({
13686
+ node,
13687
+ metro = false
13688
+ }) {
13428
13689
  const \u8272 = nodeAccent(node, MARK_DEFAULT);
13429
- return /* @__PURE__ */ jsx("g", { transform: `translate(${node.cx - node.w / 2} ${node.cy - node.h / 2})`, children: /* @__PURE__ */ jsx(
13430
- "circle",
13690
+ return /* @__PURE__ */ jsx(
13691
+ "g",
13431
13692
  {
13432
- "data-cdl-role": "node-body",
13433
- "data-cdl-mark": "start",
13434
- cx: node.w / 2,
13435
- cy: node.h / 2,
13436
- r: markRadius(node),
13437
- fill: \u8272
13693
+ "data-cdl-role": metro ? "mark-start" : void 0,
13694
+ transform: `translate(${node.cx - node.w / 2} ${node.cy - node.h / 2})`,
13695
+ children: /* @__PURE__ */ jsx(
13696
+ "circle",
13697
+ {
13698
+ "data-cdl-role": "node-body",
13699
+ "data-cdl-mark": "start",
13700
+ cx: node.w / 2,
13701
+ cy: node.h / 2,
13702
+ r: metro ? METRO_START_RADIUS : markRadius(node),
13703
+ fill: \u8272
13704
+ }
13705
+ )
13438
13706
  }
13439
- ) });
13707
+ );
13440
13708
  }
13441
- function MarkEndNode({ node, active }) {
13709
+ function MarkEndNode({
13710
+ node,
13711
+ active,
13712
+ metro = false
13713
+ }) {
13442
13714
  const \u8272 = nodeAccent(node, MARK_DEFAULT);
13443
- const r = markRadius(node);
13444
- const \u8F2A\u306E\u592A\u3055 = active ? RING_WIDTH_ACTIVE : RING_WIDTH_IDLE;
13445
- return /* @__PURE__ */ jsxs("g", { transform: `translate(${node.cx - node.w / 2} ${node.cy - node.h / 2})`, children: [
13715
+ const r = metro ? METRO_END_RADIUS : markRadius(node);
13716
+ const \u8F2A\u306E\u592A\u3055 = metro ? METRO_END_RING_WIDTH : active ? RING_WIDTH_ACTIVE : RING_WIDTH_IDLE;
13717
+ return /* @__PURE__ */ jsxs(
13718
+ "g",
13719
+ {
13720
+ "data-cdl-role": metro ? "mark-end" : void 0,
13721
+ transform: `translate(${node.cx - node.w / 2} ${node.cy - node.h / 2})`,
13722
+ children: [
13723
+ /* @__PURE__ */ jsx(
13724
+ "circle",
13725
+ {
13726
+ "data-cdl-role": "node-body",
13727
+ "data-cdl-mark": "end",
13728
+ cx: node.w / 2,
13729
+ cy: node.h / 2,
13730
+ r: metro ? r : Math.max(0, r - \u8F2A\u306E\u592A\u3055 / 2),
13731
+ fill: "var(--cdl-node-fill, #ffffff)",
13732
+ stroke: \u8272,
13733
+ strokeWidth: \u8F2A\u306E\u592A\u3055
13734
+ }
13735
+ ),
13736
+ /* @__PURE__ */ jsx(
13737
+ "circle",
13738
+ {
13739
+ "data-cdl-role": "node-inner",
13740
+ cx: node.w / 2,
13741
+ cy: node.h / 2,
13742
+ r: metro ? METRO_END_INNER_RADIUS : Math.max(0, r * MARK_INNER_RATIO),
13743
+ fill: \u8272
13744
+ }
13745
+ )
13746
+ ]
13747
+ }
13748
+ );
13749
+ }
13750
+ var METRO_STATION_RADIUS = 16;
13751
+ var METRO_STATION_STROKE_WIDTH = 6;
13752
+ var METRO_STATION_RADIUS_CSS = `var(--cdl-metro-station-radius, ${METRO_STATION_RADIUS}px)`;
13753
+ var METRO_STATION_STROKE_WIDTH_CSS = `var(--cdl-metro-station-stroke-width, ${METRO_STATION_STROKE_WIDTH}px)`;
13754
+ var STATION_DEFAULT = "var(--cdl-tone-accent, #2d6a8f)";
13755
+ var STATION_NAME_FONT_SIZE = 25;
13756
+ var STATION_NAME_GAP = 18;
13757
+ var STATION_NAME_RIGHT_GAP = 32;
13758
+ var STATION_NAME_RIGHT_BASELINE = 9;
13759
+ var STATION_SUBTITLE_FONT_SIZE = 18;
13760
+ var STATION_SUBTITLE_GAP = 22;
13761
+ function StationNode({
13762
+ node,
13763
+ nameRight = false
13764
+ }) {
13765
+ const name = node.title.trim();
13766
+ const subtitle = node.subtitle?.trim() ?? "";
13767
+ return /* @__PURE__ */ jsxs("g", { children: [
13446
13768
  /* @__PURE__ */ jsx(
13447
13769
  "circle",
13448
13770
  {
13449
13771
  "data-cdl-role": "node-body",
13450
- "data-cdl-mark": "end",
13451
- cx: node.w / 2,
13452
- cy: node.h / 2,
13453
- r: Math.max(0, r - \u8F2A\u306E\u592A\u3055 / 2),
13772
+ "data-cdl-mark": "station",
13773
+ cx: node.cx,
13774
+ cy: node.cy,
13775
+ r: METRO_STATION_RADIUS,
13454
13776
  fill: "var(--cdl-node-fill, #ffffff)",
13455
- stroke: \u8272,
13456
- strokeWidth: \u8F2A\u306E\u592A\u3055
13777
+ stroke: nodeAccent(node, STATION_DEFAULT),
13778
+ strokeWidth: METRO_STATION_STROKE_WIDTH,
13779
+ style: { r: METRO_STATION_RADIUS_CSS, strokeWidth: METRO_STATION_STROKE_WIDTH_CSS }
13780
+ }
13781
+ ),
13782
+ name !== "" && /* @__PURE__ */ jsx(
13783
+ "text",
13784
+ {
13785
+ "data-cdl-role": "shape-name",
13786
+ x: nameRight ? node.cx + STATION_NAME_RIGHT_GAP : node.cx,
13787
+ y: nameRight ? node.cy + STATION_NAME_RIGHT_BASELINE : node.cy - METRO_STATION_RADIUS - STATION_NAME_GAP,
13788
+ textAnchor: nameRight ? "start" : "middle",
13789
+ fontSize: STATION_NAME_FONT_SIZE,
13790
+ fontWeight: 700,
13791
+ fill: "var(--cdl-text, #23201b)",
13792
+ stroke: "var(--cdl-node-fill, #ffffff)",
13793
+ strokeWidth: 8,
13794
+ strokeLinejoin: "round",
13795
+ paintOrder: "stroke",
13796
+ children: name
13457
13797
  }
13458
13798
  ),
13799
+ subtitle !== "" && /* @__PURE__ */ jsx(
13800
+ "text",
13801
+ {
13802
+ "data-cdl-role": "shape-value",
13803
+ x: node.cx,
13804
+ y: node.cy + METRO_STATION_RADIUS + STATION_SUBTITLE_GAP,
13805
+ textAnchor: "middle",
13806
+ fontSize: STATION_SUBTITLE_FONT_SIZE,
13807
+ fill: "var(--cdl-text-dim, #5a6270)",
13808
+ children: subtitle
13809
+ }
13810
+ )
13811
+ ] });
13812
+ }
13813
+ function TimelineNumberNode({ node }) {
13814
+ const radius = Math.max(0, Math.min(node.w, node.h) / 2 - TIMELINE_NUMBER_STROKE / 2);
13815
+ const fontSize = fitTextSize(
13816
+ node.title,
13817
+ titleUsableWidth(node.kind, node.w, node.h),
13818
+ 18,
13819
+ MIN_TEXT_SIZE
13820
+ );
13821
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
13459
13822
  /* @__PURE__ */ jsx(
13460
13823
  "circle",
13461
13824
  {
13462
- "data-cdl-role": "node-inner",
13463
- cx: node.w / 2,
13464
- cy: node.h / 2,
13465
- r: Math.max(0, r * MARK_INNER_RATIO),
13466
- fill: \u8272
13825
+ "data-cdl-role": "node-body",
13826
+ "data-cdl-mark": "timeline-number",
13827
+ cx: node.cx,
13828
+ cy: node.cy,
13829
+ r: radius,
13830
+ fill: "var(--cdl-node-fill, #ffffff)",
13831
+ stroke: nodeAccent(node, STATION_DEFAULT),
13832
+ strokeWidth: TIMELINE_NUMBER_STROKE
13833
+ }
13834
+ ),
13835
+ /* @__PURE__ */ jsx(
13836
+ "text",
13837
+ {
13838
+ "data-cdl-role": "node-label",
13839
+ x: node.cx,
13840
+ y: node.cy,
13841
+ textAnchor: "middle",
13842
+ dominantBaseline: "central",
13843
+ fontSize,
13844
+ fontWeight: 700,
13845
+ fill: "var(--cdl-text, #1a1f2a)",
13846
+ children: node.title
13467
13847
  }
13468
13848
  )
13469
13849
  ] });
@@ -13795,27 +14175,6 @@ function interpolateColor(template, stateValues, fallback) {
13795
14175
  if (!template) return fallback;
13796
14176
  return interpolate(template, stateValues);
13797
14177
  }
13798
- var STATION_STROKE_ACTIVE = 4;
13799
- var STATION_STROKE_IDLE = 3;
13800
- var STATION_DEFAULT = "var(--cdl-tone-accent, #2d6a8f)";
13801
- function StationNode({ node, active }) {
13802
- const strokeWidth = active ? STATION_STROKE_ACTIVE : STATION_STROKE_IDLE;
13803
- const radius = Math.max(0, Math.min(node.w, node.h) / 2 - strokeWidth / 2);
13804
- const \u5F62 = /* @__PURE__ */ jsx(
13805
- "circle",
13806
- {
13807
- "data-cdl-role": "node-body",
13808
- "data-cdl-mark": "station",
13809
- cx: node.cx,
13810
- cy: node.cy,
13811
- r: radius,
13812
- fill: "var(--cdl-node-fill, #ffffff)",
13813
- stroke: nodeAccent(node, STATION_DEFAULT),
13814
- strokeWidth
13815
- }
13816
- );
13817
- return renderShapeWithOutsideLabels(\u5F62, node, "middle");
13818
- }
13819
14178
 
13820
14179
  // src/kinds/label-stack.ts
13821
14180
  function \u540D\u672D\u306E\u6BB5\u3092\u307B\u3069\u304F(\u771F\u306Ey, \u9593\u9694, \u4E0A\u9650, \u4E0B\u9650) {
@@ -20977,10 +21336,14 @@ function CdlNodeView({
20977
21336
  stateValues,
20978
21337
  currentPhaseId,
20979
21338
  chartLineScale,
21339
+ compactTitleInset,
20980
21340
  \u95A2\u4FC2\u306E\u53D7\u3051\u53E3,
20981
21341
  lang,
20982
21342
  textMetrics,
20983
- \u5024\u306E\u540D\u524D
21343
+ \u5024\u306E\u540D\u524D,
21344
+ metroDiagram = false,
21345
+ metroDecision = false,
21346
+ stationNameRight = false
20984
21347
  }) {
20985
21348
  const value = node.value ? interpolate(node.value, stateValues, \u5024\u306E\u540D\u524D) : void 0;
20986
21349
  const rows = (node.rows ?? []).map((r) => interpolate(r, stateValues, \u5024\u306E\u540D\u524D));
@@ -21021,7 +21384,7 @@ function CdlNodeView({
21021
21384
  case "event":
21022
21385
  return /* @__PURE__ */ jsx(EventNode, { node: resolvedNode, active, progress });
21023
21386
  case "card":
21024
- return /* @__PURE__ */ jsx(CardNode, { node: resolvedNode, active });
21387
+ return /* @__PURE__ */ jsx(CardNode, { node: resolvedNode, active, compactTitleInset });
21025
21388
  // 時系列のやり取り (#578)。 1 つの箱が図を丸ごと描く = 縦が時間で、参加者は箱を持たない
21026
21389
  case "sequence-board":
21027
21390
  return /* @__PURE__ */ jsx(
@@ -21308,11 +21671,13 @@ function CdlNodeView({
21308
21671
  case "shape-customer-service":
21309
21672
  return /* @__PURE__ */ jsx(ShapeCustomerServiceNode, { node: resolvedNode, active });
21310
21673
  case "mark-start":
21311
- return /* @__PURE__ */ jsx(MarkStartNode, { node: resolvedNode, active });
21674
+ return /* @__PURE__ */ jsx(MarkStartNode, { node: resolvedNode, active, metro: metroDiagram });
21312
21675
  case "mark-end":
21313
- return /* @__PURE__ */ jsx(MarkEndNode, { node: resolvedNode, active });
21676
+ return /* @__PURE__ */ jsx(MarkEndNode, { node: resolvedNode, active, metro: metroDiagram });
21314
21677
  case "station":
21315
- return /* @__PURE__ */ jsx(StationNode, { node: resolvedNode, active });
21678
+ return /* @__PURE__ */ jsx(StationNode, { node: resolvedNode, active, nameRight: stationNameRight });
21679
+ case "timeline-number":
21680
+ return /* @__PURE__ */ jsx(TimelineNumberNode, { node: resolvedNode });
21316
21681
  default:
21317
21682
  return /* @__PURE__ */ jsx(
21318
21683
  GenericNode,
@@ -21322,7 +21687,9 @@ function CdlNodeView({
21322
21687
  value: node.value,
21323
21688
  stateValues,
21324
21689
  \u5024\u306E\u540D\u524D,
21325
- textMetrics
21690
+ textMetrics,
21691
+ showKindIcon: !(metroDiagram && node.kind === "decision"),
21692
+ metroDecision
21326
21693
  }
21327
21694
  );
21328
21695
  }
@@ -21436,6 +21803,112 @@ function \u7BB1\u306E\u4E0A\u306E\u8A9E(node, textMetrics = "proportional") {
21436
21803
  )
21437
21804
  ] });
21438
21805
  }
21806
+ var ink = "var(--cdl-text-accent, #143a52)";
21807
+ var paper = "var(--cdl-stage-bg, #f8fafc)";
21808
+ var LEGEND_MARK_RENDERERS = {
21809
+ diamond: () => /* @__PURE__ */ jsx("path", { d: "M 23 -26 L 46 -8 L 23 10 L 0 -8 Z", fill: "none", stroke: ink, strokeWidth: 2 }),
21810
+ "filled-circle": () => /* @__PURE__ */ jsx("circle", { cx: 23, cy: -8, r: 11, fill: ink }),
21811
+ "double-circle": () => /* @__PURE__ */ jsxs(Fragment, { children: [
21812
+ /* @__PURE__ */ jsx("circle", { cx: 23, cy: -8, r: 12, fill: "none", stroke: ink, strokeWidth: 2.5 }),
21813
+ /* @__PURE__ */ jsx("circle", { cx: 23, cy: -8, r: 6, fill: ink })
21814
+ ] }),
21815
+ "dotted-line": () => /* @__PURE__ */ jsx(
21816
+ "path",
21817
+ {
21818
+ d: `M 0 -8 H ${LEGEND_MARK_REFERENCE_WIDTH}`,
21819
+ fill: "none",
21820
+ stroke: ink,
21821
+ strokeWidth: 6,
21822
+ strokeLinecap: "round",
21823
+ strokeDasharray: "0 9.6"
21824
+ }
21825
+ ),
21826
+ "solid-line": () => /* @__PURE__ */ jsx(
21827
+ "path",
21828
+ {
21829
+ d: `M 0 -8 H ${LEGEND_MARK_REFERENCE_WIDTH}`,
21830
+ fill: "none",
21831
+ stroke: ink,
21832
+ strokeWidth: 4,
21833
+ strokeLinecap: "round"
21834
+ }
21835
+ ),
21836
+ "curved-line": () => /* @__PURE__ */ jsx(
21837
+ "path",
21838
+ {
21839
+ d: `M 0 6 C 12 -22, 34 -22, ${LEGEND_MARK_REFERENCE_WIDTH} 6`,
21840
+ fill: "none",
21841
+ stroke: ink,
21842
+ strokeWidth: 4,
21843
+ strokeLinecap: "round"
21844
+ }
21845
+ ),
21846
+ "rounded-label": () => /* @__PURE__ */ jsx(
21847
+ "rect",
21848
+ {
21849
+ x: 0,
21850
+ y: -23,
21851
+ width: LEGEND_MARK_REFERENCE_WIDTH,
21852
+ height: 30,
21853
+ rx: 9,
21854
+ fill: paper,
21855
+ stroke: ink,
21856
+ strokeWidth: 2
21857
+ }
21858
+ ),
21859
+ "numbered-circle": () => /* @__PURE__ */ jsxs(Fragment, { children: [
21860
+ /* @__PURE__ */ jsx("circle", { cx: 23, cy: -8, r: 13, fill: paper, stroke: ink, strokeWidth: 2.5 }),
21861
+ /* @__PURE__ */ jsx("text", { x: 23, y: -2, textAnchor: "middle", fontSize: 17, fontWeight: 700, fill: ink, children: "1" })
21862
+ ] }),
21863
+ station: () => /* @__PURE__ */ jsx("circle", { cx: 23, cy: -8, r: 11, fill: paper, stroke: ink, strokeWidth: 4 }),
21864
+ arrow: () => /* @__PURE__ */ jsx(
21865
+ "path",
21866
+ {
21867
+ d: `M 0 -8 H 42 M 32 -18 L ${LEGEND_MARK_REFERENCE_WIDTH} -8 L 32 2`,
21868
+ fill: "none",
21869
+ stroke: ink,
21870
+ strokeWidth: 3,
21871
+ strokeLinecap: "round",
21872
+ strokeLinejoin: "round"
21873
+ }
21874
+ )
21875
+ };
21876
+ function CdlLegend({ items }) {
21877
+ return /* @__PURE__ */ jsx("g", { "data-cdl-role": "legend", children: items.map((item, index) => {
21878
+ const Mark = LEGEND_MARK_RENDERERS[item.mark];
21879
+ return /* @__PURE__ */ jsxs(
21880
+ "g",
21881
+ {
21882
+ "data-cdl-role": "legend-item",
21883
+ "data-cdl-y": item.y,
21884
+ transform: `translate(${item.x} ${item.y})`,
21885
+ children: [
21886
+ /* @__PURE__ */ jsx(
21887
+ "g",
21888
+ {
21889
+ "data-cdl-role": "legend-mark",
21890
+ "data-cdl-legend-mark": item.mark,
21891
+ transform: `scale(${LEGEND_MARK_SCALE})`,
21892
+ children: /* @__PURE__ */ jsx(Mark, {})
21893
+ }
21894
+ ),
21895
+ /* @__PURE__ */ jsx(
21896
+ "text",
21897
+ {
21898
+ "data-cdl-role": "legend-text",
21899
+ x: LEGEND_MARK_WIDTH + LEGEND_MARK_TEXT_GAP,
21900
+ y: 0,
21901
+ fontSize: LEGEND_FONT_SIZE,
21902
+ fill: "var(--cdl-text-mute, #4d7187)",
21903
+ children: item.text
21904
+ }
21905
+ )
21906
+ ]
21907
+ },
21908
+ `${item.mark}-${index}`
21909
+ );
21910
+ }) });
21911
+ }
21439
21912
 
21440
21913
  // src/render/relation-focus.ts
21441
21914
  function relatedFocusSet(edges, nodeId) {
@@ -21448,10 +21921,17 @@ function relatedFocusSet(edges, nodeId) {
21448
21921
  }
21449
21922
  return \u96C6\u5408;
21450
21923
  }
21924
+ var METRO_LANE_BADGE_WIDTH = 200;
21925
+ var METRO_LANE_BADGE_HEIGHT = 88;
21926
+ var METRO_LANE_BADGE_INSET = 24;
21927
+ var METRO_LANE_GUIDE_GAP = 24;
21451
21928
  function \u63CF\u304F\u3082\u306E\u304C\u3042\u308B(laid) {
21929
+ if (laid.legend && laid.legend.length > 0) return true;
21452
21930
  if (laid.edges.length > 0) return true;
21453
21931
  if (laid.nodes.some((n) => isRenderedNode(n))) return true;
21454
- return laid.lanes.some((l) => l.contain === true || l.lifeline === true || Boolean(l.label));
21932
+ return laid.lanes.some(
21933
+ (l) => l.contain === true || l.lifeline === true || Boolean(l.label) || l.stage !== void 0 || l.metro !== void 0
21934
+ );
21455
21935
  }
21456
21936
  function CdlStage({
21457
21937
  laid,
@@ -21492,6 +21972,7 @@ function CdlStage({
21492
21972
  [\u95A2\u4FC2\u3092\u5149\u3089\u305B\u308B, \u7126\u70B9, laid.edges]
21493
21973
  );
21494
21974
  const activeSet = \u7126\u70B9\u306E\u96C6\u5408 ?? new Set(currentPhase?.activate ?? []);
21975
+ const \u8DEF\u7DDA\u56F3 = laid.edges.some((edge) => edge.routing === "metro") && laid.nodes.some((node) => node.kind === "station");
21495
21976
  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;
21496
21977
  const \u77E2\u5370\u306E\u51FA\u756A = useMemo(() => {
21497
21978
  const \u521D\u51FA = /* @__PURE__ */ new Map();
@@ -21520,9 +22001,15 @@ function CdlStage({
21520
22001
  const first = \u521D\u51FA.get(edgeId);
21521
22002
  if (first === void 0) return false;
21522
22003
  return \u4ECA === first;
22004
+ },
22005
+ /** 初出の段より前にある区間だけを、通過前として選べるようにする。 */
22006
+ \u307E\u3060\u304B: (edgeId) => {
22007
+ const first = \u521D\u51FA.get(edgeId);
22008
+ return first !== void 0 && \u4ECA >= 0 && \u4ECA < first;
21523
22009
  }
21524
22010
  };
21525
22011
  }, [laid.phases, currentPhase, laid.edgeReveal]);
22012
+ const \u7DDA\u3092\u63CF\u304F = (edge) => edge.routing === "metro" || \u77E2\u5370\u306E\u51FA\u756A.\u51FA\u308B\u304B(edge.id);
21526
22013
  const drawSet = new Set(currentPhase?.draw ?? []);
21527
22014
  const chartLineScales = useMemo(() => {
21528
22015
  const nodes = laid.nodes.filter((node) => node.kind === "chart-line");
@@ -21610,7 +22097,53 @@ function CdlStage({
21610
22097
  )
21611
22098
  );
21612
22099
  }, [laid]);
22100
+ const metroReturnArrowMarkers = useMemo(() => {
22101
+ const tones = new Set(
22102
+ laid.edges.filter((edge) => edge.metroReturn === true).map((edge) => edge.tone)
22103
+ );
22104
+ return [...tones].map((tone) => /* @__PURE__ */ jsx(
22105
+ "marker",
22106
+ {
22107
+ id: metroReturnArrowMarkerId(tone),
22108
+ viewBox: "0 -9.5 21 19",
22109
+ refX: "0",
22110
+ refY: "0",
22111
+ markerWidth: "21",
22112
+ markerHeight: "19",
22113
+ markerUnits: "userSpaceOnUse",
22114
+ orient: "auto",
22115
+ children: /* @__PURE__ */ jsx(
22116
+ "path",
22117
+ {
22118
+ "data-cdl-role": "edge-arrowhead",
22119
+ "data-cdl-mark": "metro-return-arrow",
22120
+ d: METRO_RETURN_ARROW_D,
22121
+ fill: TONE[tone]
22122
+ }
22123
+ )
22124
+ },
22125
+ tone
22126
+ ));
22127
+ }, [laid.edges]);
21613
22128
  const diagramScale = laid.diagramScale ?? 1;
22129
+ const stageLaneIds = new Set(
22130
+ laid.lanes.filter((lane) => lane.stage !== void 0).map((lane) => lane.id)
22131
+ );
22132
+ const stageNoteNodes = laid.nodes.filter(
22133
+ (node) => node.kind === "card" && (stageLaneIds.has(node.lane) || node.leaderTo !== void 0) && isRenderedNode(node) && Boolean(node.subtitle)
22134
+ );
22135
+ const stageNoteNodeIds = new Set(stageNoteNodes.map((node) => node.id));
22136
+ const nodesById = new Map(laid.nodes.map((node) => [node.id, node]));
22137
+ const metroDecisionIds = /* @__PURE__ */ new Set();
22138
+ const stationNamesRight = /* @__PURE__ */ new Set();
22139
+ for (const edge of laid.edges) {
22140
+ if (edge.routing !== "metro") continue;
22141
+ if (nodesById.get(edge.from)?.kind === "decision") metroDecisionIds.add(edge.from);
22142
+ if (nodesById.get(edge.to)?.kind === "decision") metroDecisionIds.add(edge.to);
22143
+ if (edge.toSide === "top" && nodesById.get(edge.to)?.kind === "station") {
22144
+ stationNamesRight.add(edge.to);
22145
+ }
22146
+ }
21614
22147
  if (!\u63CF\u304F\u3082\u306E\u304C\u3042\u308B(laid)) return null;
21615
22148
  return /* @__PURE__ */ jsxs("div", { className: compact ? "px-3 py-3" : "px-6 py-6", style: compact ? void 0 : { minHeight: 460 }, children: [
21616
22149
  /* @__PURE__ */ jsxs(
@@ -21630,6 +22163,7 @@ function CdlStage({
21630
22163
  children: [
21631
22164
  /* @__PURE__ */ jsxs("defs", { children: [
21632
22165
  edgeHeadMarkers,
22166
+ metroReturnArrowMarkers,
21633
22167
  /* @__PURE__ */ jsx(
21634
22168
  "marker",
21635
22169
  {
@@ -21666,7 +22200,61 @@ function CdlStage({
21666
22200
  pointerEvents: "none"
21667
22201
  }
21668
22202
  ),
21669
- lane.contain && /* @__PURE__ */ jsx(
22203
+ lane.stage && /* @__PURE__ */ jsxs(Fragment, { children: [
22204
+ /* @__PURE__ */ jsx(
22205
+ "rect",
22206
+ {
22207
+ "data-cdl-role": "stage-column",
22208
+ x: lane.x + STAGE_COLUMN_GAP / 2,
22209
+ y: lane.y,
22210
+ width: lane.width - STAGE_COLUMN_GAP,
22211
+ height: lane.height,
22212
+ rx: 16,
22213
+ fill: "var(--cdl-stage-bg, #fafafa)",
22214
+ fillOpacity: 0.5,
22215
+ stroke: "var(--cdl-text-mute, #8a8678)",
22216
+ strokeWidth: 1.5
22217
+ }
22218
+ ),
22219
+ /* @__PURE__ */ jsx(
22220
+ "text",
22221
+ {
22222
+ "data-cdl-role": "stage-number",
22223
+ x: lane.x + STAGE_COLUMN_GAP / 2 + 20,
22224
+ y: lane.y + 24,
22225
+ fontSize: 14,
22226
+ fontWeight: 500,
22227
+ letterSpacing: 1.5,
22228
+ fill: "var(--cdl-text-mute, #8a8678)",
22229
+ children: `\u6BB5\u968E ${lane.stage.number}`
22230
+ }
22231
+ ),
22232
+ /* @__PURE__ */ jsx(
22233
+ "text",
22234
+ {
22235
+ "data-cdl-role": "stage-name",
22236
+ x: lane.x + STAGE_COLUMN_GAP / 2 + 20,
22237
+ y: lane.y + 55,
22238
+ fontSize: 22,
22239
+ fontWeight: 700,
22240
+ fill: "var(--cdl-text, #1a1f2a)",
22241
+ children: lane.stage.name
22242
+ }
22243
+ ),
22244
+ /* @__PURE__ */ jsx(
22245
+ "line",
22246
+ {
22247
+ "data-cdl-role": "stage-divider",
22248
+ x1: lane.x + STAGE_COLUMN_GAP / 2,
22249
+ y1: lane.y + 72,
22250
+ x2: lane.x + lane.width - STAGE_COLUMN_GAP / 2,
22251
+ y2: lane.y + 72,
22252
+ stroke: "var(--cdl-divider, #d4d4d8)",
22253
+ strokeWidth: 1.5
22254
+ }
22255
+ )
22256
+ ] }),
22257
+ lane.contain && lane.stage === void 0 && /* @__PURE__ */ jsx(
21670
22258
  "rect",
21671
22259
  {
21672
22260
  "data-cdl-role": "lane-container",
@@ -21702,7 +22290,85 @@ function CdlStage({
21702
22290
  }
21703
22291
  );
21704
22292
  })(),
21705
- lane.label && /* @__PURE__ */ jsx(
22293
+ lane.timelineAxis && (() => {
22294
+ const ys = laid.nodes.filter((node) => node.lane === lane.id && isRenderedNode(node)).map((node) => node.cy);
22295
+ if (ys.length < 2) return null;
22296
+ const y1 = Math.min(...ys);
22297
+ const y2 = Math.max(...ys);
22298
+ if (y2 <= y1) return null;
22299
+ return /* @__PURE__ */ jsx(
22300
+ "line",
22301
+ {
22302
+ "data-cdl-role": "timeline-axis",
22303
+ x1: lane.x + lane.width / 2,
22304
+ y1,
22305
+ x2: lane.x + lane.width / 2,
22306
+ y2,
22307
+ stroke: "var(--cdl-tone-accent, #2d6a8f)",
22308
+ strokeWidth: TIMELINE_NUMBER_STROKE
22309
+ }
22310
+ );
22311
+ })(),
22312
+ lane.metro !== void 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
22313
+ /* @__PURE__ */ jsx(
22314
+ "line",
22315
+ {
22316
+ "data-cdl-role": "metro-lane-guide",
22317
+ x1: lane.x + METRO_LANE_BADGE_INSET + METRO_LANE_BADGE_WIDTH + METRO_LANE_GUIDE_GAP,
22318
+ y1: lane.y + lane.height / 2,
22319
+ x2: lane.x + lane.width - METRO_LANE_BADGE_INSET,
22320
+ y2: lane.y + lane.height / 2,
22321
+ stroke: "var(--cdl-line-mute, #a89d8f)",
22322
+ strokeWidth: 2,
22323
+ strokeDasharray: "22 7 5 7",
22324
+ opacity: 0.45
22325
+ }
22326
+ ),
22327
+ /* @__PURE__ */ jsxs(
22328
+ "g",
22329
+ {
22330
+ "data-cdl-role": "metro-lane-badge",
22331
+ transform: `translate(${lane.x + METRO_LANE_BADGE_INSET} ${lane.y + lane.height / 2 - METRO_LANE_BADGE_HEIGHT / 2})`,
22332
+ children: [
22333
+ /* @__PURE__ */ jsx(
22334
+ "rect",
22335
+ {
22336
+ width: METRO_LANE_BADGE_WIDTH,
22337
+ height: METRO_LANE_BADGE_HEIGHT,
22338
+ rx: 12,
22339
+ fill: "var(--cdl-node-fill, #ffffff)",
22340
+ stroke: "var(--cdl-tone-accent, #2d6a8f)",
22341
+ strokeWidth: 1.25
22342
+ }
22343
+ ),
22344
+ /* @__PURE__ */ jsx(
22345
+ "text",
22346
+ {
22347
+ x: METRO_LANE_BADGE_WIDTH / 2,
22348
+ y: lane.metro.subtitle ? 37 : 53,
22349
+ fontSize: 26,
22350
+ fontWeight: 700,
22351
+ fill: "var(--cdl-text, #1a1f2a)",
22352
+ textAnchor: "middle",
22353
+ children: lane.label
22354
+ }
22355
+ ),
22356
+ lane.metro.subtitle && /* @__PURE__ */ jsx(
22357
+ "text",
22358
+ {
22359
+ x: METRO_LANE_BADGE_WIDTH / 2,
22360
+ y: 67,
22361
+ fontSize: 18,
22362
+ fill: "var(--cdl-text-dim, #5a6270)",
22363
+ textAnchor: "middle",
22364
+ children: lane.metro.subtitle
22365
+ }
22366
+ )
22367
+ ]
22368
+ }
22369
+ )
22370
+ ] }),
22371
+ lane.label && lane.metro === void 0 && /* @__PURE__ */ jsx(
21706
22372
  "text",
21707
22373
  {
21708
22374
  "data-cdl-role": "lane-label",
@@ -21719,7 +22385,34 @@ function CdlStage({
21719
22385
  },
21720
22386
  lane.id
21721
22387
  )),
21722
- laid.edges.filter((edge) => \u77E2\u5370\u306E\u51FA\u756A.\u51FA\u308B\u304B(edge.id)).map((edge) => /* @__PURE__ */ jsx(
22388
+ laid.nodes.map((node) => {
22389
+ if (node.leaderTo === void 0) return null;
22390
+ const target = nodesById.get(node.leaderTo);
22391
+ if (target === void 0) return null;
22392
+ if (!isRenderedNode(node) || !isRenderedNode(target)) return null;
22393
+ const targetIsRight = target.cx >= node.cx;
22394
+ const x1 = node.cx + (targetIsRight ? node.w / 2 : -node.w / 2);
22395
+ const x2 = target.cx + (targetIsRight ? -target.w / 2 : target.w / 2);
22396
+ if (targetIsRight ? x2 <= x1 : x1 <= x2) return null;
22397
+ return /* @__PURE__ */ jsx(
22398
+ "line",
22399
+ {
22400
+ "data-cdl-role": "timeline-leader",
22401
+ "data-cdl-leader-from": node.id,
22402
+ "data-cdl-leader-to": node.leaderTo,
22403
+ x1,
22404
+ y1: node.cy,
22405
+ x2,
22406
+ y2: target.cy,
22407
+ stroke: "var(--cdl-line-mute, #a89d8f)",
22408
+ strokeWidth: 1.5,
22409
+ strokeDasharray: "2 4",
22410
+ strokeLinecap: "round"
22411
+ },
22412
+ `timeline-leader-${node.id}`
22413
+ );
22414
+ }),
22415
+ laid.edges.filter(\u7DDA\u3092\u63CF\u304F).map((edge) => /* @__PURE__ */ jsx(
21723
22416
  CdlEdgeView,
21724
22417
  {
21725
22418
  edge,
@@ -21727,16 +22420,19 @@ function CdlStage({
21727
22420
  flowing: \u7126\u70B9\u306E\u96C6\u5408?.has(edge.id) === true && !\u52D5\u304D\u3092\u63A7\u3048\u308B,
21728
22421
  drawing: \u77E2\u5370\u306E\u51FA\u756A.\u4F38\u3070\u3059\u304B(edge.id),
21729
22422
  progress: \u77E2\u5370\u306E\u51FA\u756A.\u4F38\u3070\u3059\u304B(edge.id) ? \u63CF\u304F\u9032\u307F : progress,
21730
- stateValues
22423
+ stateValues,
22424
+ pending: edge.routing === "metro" && \u77E2\u5370\u306E\u51FA\u756A.\u307E\u3060\u304B(edge.id)
21731
22425
  },
21732
22426
  edge.id
21733
22427
  )),
21734
22428
  laid.nodes.map((node) => {
21735
22429
  const \u63CF\u304F = drawSet.has(node.id);
22430
+ const \u6BB5\u968E\u306E\u88DC\u8DB3 = stageNoteNodeIds.has(node.id);
21736
22431
  const view = /* @__PURE__ */ jsx(
21737
22432
  CdlNodeView,
21738
22433
  {
21739
- node,
22434
+ node: \u6BB5\u968E\u306E\u88DC\u8DB3 ? { ...node, subtitle: void 0 } : node,
22435
+ compactTitleInset: \u6BB5\u968E\u306E\u88DC\u8DB3 ? STAGE_CARD_TEXT_INSET : void 0,
21740
22436
  active: activeSet.has(node.id),
21741
22437
  lang: laid.lang,
21742
22438
  textMetrics: laid.textMetrics,
@@ -21746,13 +22442,32 @@ function CdlStage({
21746
22442
  stateValues,
21747
22443
  \u5024\u306E\u540D\u524D,
21748
22444
  currentPhaseId: currentPhase?.id,
21749
- chartLineScale: chartLineScales.get(node.id)
22445
+ chartLineScale: chartLineScales.get(node.id),
22446
+ metroDiagram: \u8DEF\u7DDA\u56F3,
22447
+ metroDecision: metroDecisionIds.has(node.id),
22448
+ stationNameRight: stationNamesRight.has(node.id)
21750
22449
  }
21751
22450
  );
21752
22451
  return /* @__PURE__ */ jsx("g", { children: view }, node.id);
21753
22452
  }),
21754
- laid.edges.filter((edge) => Boolean(edge.dThrough) && \u77E2\u5370\u306E\u51FA\u756A.\u51FA\u308B\u304B(edge.id)).map((edge) => /* @__PURE__ */ jsx(CdlEdgeGlowView, { edge, active: activeSet.has(edge.id) }, `glow-${edge.id}`)),
21755
- laid.edges.filter((edge) => \u77E2\u5370\u306E\u51FA\u756A.\u51FA\u308B\u304B(edge.id)).map((edge) => /* @__PURE__ */ jsx(
22453
+ stageNoteNodes.map((node) => /* @__PURE__ */ jsx(
22454
+ "text",
22455
+ {
22456
+ "data-cdl-role": "stage-note",
22457
+ "data-cdl-stage-node": node.id,
22458
+ x: node.cx + node.w / 2 - STAGE_CARD_TEXT_INSET,
22459
+ y: node.cy + 1,
22460
+ fontSize: 14,
22461
+ fontWeight: 500,
22462
+ textAnchor: "end",
22463
+ dominantBaseline: "middle",
22464
+ fill: "var(--cdl-text-mute, #8a8678)",
22465
+ children: interpolate(node.subtitle, stateValues, \u5024\u306E\u540D\u524D)
22466
+ },
22467
+ `stage-note-${node.id}`
22468
+ )),
22469
+ laid.edges.filter((edge) => Boolean(edge.dThrough) && \u7DDA\u3092\u63CF\u304F(edge)).map((edge) => /* @__PURE__ */ jsx(CdlEdgeGlowView, { edge, active: activeSet.has(edge.id) }, `glow-${edge.id}`)),
22470
+ laid.edges.filter(\u7DDA\u3092\u63CF\u304F).map((edge) => /* @__PURE__ */ jsx(
21756
22471
  CdlEdgeLabelView,
21757
22472
  {
21758
22473
  edge,
@@ -21761,7 +22476,7 @@ function CdlStage({
21761
22476
  },
21762
22477
  `label-${edge.id}`
21763
22478
  )),
21764
- laid.edges.filter((edge) => (edge.headLabel || edge.tailLabel) && \u77E2\u5370\u306E\u51FA\u756A.\u51FA\u308B\u304B(edge.id)).flatMap(
22479
+ laid.edges.filter((edge) => (edge.headLabel || edge.tailLabel) && \u7DDA\u3092\u63CF\u304F(edge)).flatMap(
21765
22480
  (edge) => ["head", "tail"].map((\u7AEF) => /* @__PURE__ */ jsx(
21766
22481
  CdlEdgeEndLabelView,
21767
22482
  {
@@ -21773,6 +22488,7 @@ function CdlStage({
21773
22488
  `${\u7AEF}-label-${edge.id}`
21774
22489
  ))
21775
22490
  ),
22491
+ laid.legend && /* @__PURE__ */ jsx(CdlLegend, { items: laid.legend }),
21776
22492
  debug && /* @__PURE__ */ jsxs("g", { pointerEvents: "none", children: [
21777
22493
  laid.bboxes.map((bbox, idx) => {
21778
22494
  const color = bbox.kind === "node" ? "#3b82f6" : bbox.kind === "lane-label" ? "#a855f7" : bbox.kind === "edge-label" ? "#10b981" : "#f59e0b";