@cardenelabs/cdl 0.120.0 → 0.122.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
@@ -986,6 +986,7 @@ var KINDS_DRAWING_TITLE_OUTSIDE = /* @__PURE__ */ new Set([
986
986
  "dyn-arc",
987
987
  "dyn-wave",
988
988
  "dyn-polygon",
989
+ "station",
989
990
  ...KINDS_DRAWING_OWN_PAYLOAD
990
991
  ]);
991
992
  function drawsTitleOutsideBox(kind) {
@@ -993,7 +994,7 @@ function drawsTitleOutsideBox(kind) {
993
994
  }
994
995
  var TITLE_FONT_SIZE_DEFAULT = 18;
995
996
  var TITLE_FONT_SIZE = new Map([
996
- ...["dyn-rect", "dyn-circle", "dyn-arc", "dyn-wave", "dyn-polygon"].map(
997
+ ...["dyn-rect", "dyn-circle", "dyn-arc", "dyn-wave", "dyn-polygon", "station"].map(
997
998
  (k) => [k, 13]
998
999
  ),
999
1000
  ...[
@@ -1044,6 +1045,10 @@ var TITLE_FONT_SIZE = new Map([
1044
1045
  function titleFontSize(kind) {
1045
1046
  return TITLE_FONT_SIZE.get(kind ?? "") ?? TITLE_FONT_SIZE_DEFAULT;
1046
1047
  }
1048
+ var KINDS_WITH_TITLE_WIDTH_OUTSIDE_NODE = /* @__PURE__ */ new Set(["station"]);
1049
+ function skipsNodeTitleWidthCheck(kind) {
1050
+ return KINDS_WITH_TITLE_WIDTH_OUTSIDE_NODE.has(kind ?? "");
1051
+ }
1047
1052
  var TITLE_BOX_PADDING = 52;
1048
1053
  var KINDS_SCALING_TITLE_WHEN_LOW = /* @__PURE__ */ new Set(["actor", "event", "function", "storage"]);
1049
1054
  var KINDS_SCALING_TITLE_ALWAYS = /* @__PURE__ */ new Set([
@@ -1126,7 +1131,8 @@ var KINDS_DRAWING_ONLY_SHAPE = [
1126
1131
  "dyn-circle",
1127
1132
  "dyn-arc",
1128
1133
  "dyn-wave",
1129
- "dyn-polygon"
1134
+ "dyn-polygon",
1135
+ "station"
1130
1136
  ];
1131
1137
  var KINDS_WITHOUT_EYEBROW_TEXT = /* @__PURE__ */ new Set([
1132
1138
  ...KINDS_DRAWING_ONLY_SHAPE,
@@ -1829,13 +1835,13 @@ function detectCollisions(bboxes, edges) {
1829
1835
  }
1830
1836
  function extractPathSegments(d) {
1831
1837
  const segs = [];
1832
- const commandRegex = /([MLCQZmlcqz])([^MLCQZmlcqz]*)/g;
1838
+ const commandRegex = /([MLCQAZmlcqaz])([^MLCQAZmlcqaz]*)/g;
1833
1839
  let cur = { x: 0, y: 0 };
1834
1840
  let m;
1835
1841
  while ((m = commandRegex.exec(d)) !== null) {
1836
1842
  const cmd = m[1];
1837
1843
  const args = (m[2] ?? "").trim().split(/[\s,]+/).filter(Boolean).map(parseFloat);
1838
- const need = cmd === "M" || cmd === "L" ? 2 : cmd === "Q" ? 4 : cmd === "C" ? 6 : 0;
1844
+ const need = cmd === "M" || cmd === "L" ? 2 : cmd === "Q" ? 4 : cmd === "C" ? 6 : cmd === "A" ? 7 : 0;
1839
1845
  if (need > 0 && (args.length < need || !args.slice(0, need).every(Number.isFinite))) continue;
1840
1846
  if (cmd === "M") {
1841
1847
  cur = { x: args[0], y: args[1] };
@@ -1865,6 +1871,10 @@ function extractPathSegments(d) {
1865
1871
  ctrl: [{ x: args[0], y: args[1] }, { x: args[2], y: args[3] }]
1866
1872
  });
1867
1873
  cur = next;
1874
+ } else if (cmd === "A") {
1875
+ const next = { x: args[5], y: args[6] };
1876
+ segs.push({ x1: cur.x, y1: cur.y, x2: next.x, y2: next.y, cmd: "A" });
1877
+ cur = next;
1868
1878
  }
1869
1879
  }
1870
1880
  return segs;
@@ -2971,6 +2981,232 @@ function applyEdgeLabelAndPathShifts(edges, labelShiftMap, edgePathShiftMap) {
2971
2981
  });
2972
2982
  }
2973
2983
 
2984
+ // src/layout/curve-path.ts
2985
+ var SAME_HEIGHT_THRESHOLD = 1;
2986
+ function curvePath(from, to, edge, labelMarginExtra = 0) {
2987
+ const fromLeft = from.cx - from.w / 2;
2988
+ const fromRight = from.cx + from.w / 2;
2989
+ const toLeft = to.cx - to.w / 2;
2990
+ const toRight = to.cx + to.w / 2;
2991
+ let fromSide;
2992
+ let toSide;
2993
+ let x1;
2994
+ let x2;
2995
+ if (toLeft >= fromRight) {
2996
+ fromSide = "right";
2997
+ toSide = "left";
2998
+ x1 = fromRight;
2999
+ x2 = toLeft;
3000
+ } else if (toRight <= fromLeft) {
3001
+ fromSide = "left";
3002
+ toSide = "right";
3003
+ x1 = fromLeft;
3004
+ x2 = toRight;
3005
+ } else {
3006
+ fromSide = "right";
3007
+ toSide = "right";
3008
+ x1 = fromRight;
3009
+ x2 = toRight;
3010
+ }
3011
+ const y1 = from.cy;
3012
+ const y2 = to.cy;
3013
+ const sameHeight = Math.abs(y2 - y1) < SAME_HEIGHT_THRESHOLD;
3014
+ let d;
3015
+ let midX;
3016
+ let midY;
3017
+ let tangentX;
3018
+ let tangentY;
3019
+ if (sameHeight) {
3020
+ d = `M ${x1} ${y1} L ${x2} ${y2}`;
3021
+ midX = (x1 + x2) / 2;
3022
+ midY = (y1 + y2) / 2;
3023
+ tangentX = x2 - x1;
3024
+ tangentY = y2 - y1;
3025
+ } else {
3026
+ let c1x;
3027
+ let c2x;
3028
+ if (fromSide !== toSide) {
3029
+ const direction = x2 > x1 ? 1 : -1;
3030
+ const bulge = Math.max(Math.abs(x2 - x1) / 2, EDGE_STUB_OUT);
3031
+ c1x = x1 + direction * bulge;
3032
+ c2x = x2 - direction * bulge;
3033
+ } else {
3034
+ const bulgeX = Math.max(x1, x2) + EDGE_STUB_OUT;
3035
+ c1x = bulgeX;
3036
+ c2x = bulgeX;
3037
+ }
3038
+ d = `M ${x1} ${y1} C ${c1x} ${y1}, ${c2x} ${y2}, ${x2} ${y2}`;
3039
+ midX = (x1 + 3 * c1x + 3 * c2x + x2) / 8;
3040
+ midY = (y1 + 3 * y1 + 3 * y2 + y2) / 8;
3041
+ tangentX = x2 + c2x - c1x - x1;
3042
+ tangentY = y2 + y2 - y1 - y1;
3043
+ }
3044
+ if (edge?.overlay) {
3045
+ return { d, fromSide, toSide, labelX: midX, labelY: midY, labelAnchor: "middle" };
3046
+ }
3047
+ const tangentLength = Math.hypot(tangentX, tangentY);
3048
+ let normalX = tangentY / tangentLength;
3049
+ let normalY = -tangentX / tangentLength;
3050
+ if (normalY > Number.EPSILON || Math.abs(normalY) <= Number.EPSILON && normalX < 0) {
3051
+ normalX *= -1;
3052
+ normalY *= -1;
3053
+ }
3054
+ const labelW = computeLabelBoxW(edge?.label, edge?.sub);
3055
+ const labelH = labelPillH(edge?.sub);
3056
+ const distance = labelPathClearanceHorizontal(edge) + (Math.abs(normalX) * labelW + Math.abs(normalY) * labelH) / 2 + labelMarginExtra;
3057
+ return {
3058
+ d,
3059
+ fromSide,
3060
+ toSide,
3061
+ labelX: midX + normalX * distance,
3062
+ labelY: midY + normalY * distance,
3063
+ labelAnchor: "middle"
3064
+ };
3065
+ }
3066
+
3067
+ // src/layout/metro-path.ts
3068
+ var METRO_CORNER_RADIUS = 24;
3069
+ var T = Math.SQRT2 - 1;
3070
+ function round2(v) {
3071
+ return Math.round(v * 100) / 100;
3072
+ }
3073
+ function point(p) {
3074
+ return `${round2(p.x)} ${round2(p.y)}`;
3075
+ }
3076
+ function add(p, v, scale) {
3077
+ return { x: p.x + v.x * scale, y: p.y + v.y * scale };
3078
+ }
3079
+ function polyline(points) {
3080
+ const distinct = points.filter((p, i) => {
3081
+ const previous = points[i - 1];
3082
+ return previous === void 0 || p.x !== previous.x || p.y !== previous.y;
3083
+ });
3084
+ return distinct.map((p, i) => `${i === 0 ? "M" : "L"} ${point(p)}`).join(" ");
3085
+ }
3086
+ function metroPath(from, to, edge, labelMarginExtra = 0) {
3087
+ const fromLeft = from.cx - from.w / 2;
3088
+ const fromRight = from.cx + from.w / 2;
3089
+ const toLeft = to.cx - to.w / 2;
3090
+ const toRight = to.cx + to.w / 2;
3091
+ let fromSide;
3092
+ let toSide;
3093
+ let x1;
3094
+ let x2;
3095
+ let dir;
3096
+ if (toLeft >= fromRight) {
3097
+ fromSide = "right";
3098
+ toSide = "left";
3099
+ x1 = fromRight;
3100
+ x2 = toLeft;
3101
+ dir = 1;
3102
+ } else if (toRight <= fromLeft) {
3103
+ fromSide = "left";
3104
+ toSide = "right";
3105
+ x1 = fromLeft;
3106
+ x2 = toRight;
3107
+ dir = -1;
3108
+ } else {
3109
+ fromSide = "right";
3110
+ toSide = "right";
3111
+ x1 = fromRight;
3112
+ x2 = toRight;
3113
+ dir = 1;
3114
+ }
3115
+ const y1 = from.cy;
3116
+ const y2 = to.cy;
3117
+ const dy = y2 - y1;
3118
+ const sgn = Math.sign(dy);
3119
+ let d;
3120
+ let mid;
3121
+ let tangent;
3122
+ if (Math.abs(dy) < SAME_HEIGHT_THRESHOLD) {
3123
+ d = `M ${point({ x: x1, y: y1 })} L ${point({ x: x2, y: y2 })}`;
3124
+ mid = { x: (x1 + x2) / 2, y: (y1 + y2) / 2 };
3125
+ tangent = { x: x2 - x1, y: y2 - y1 };
3126
+ } else if (fromSide !== toSide && Math.abs(x2 - x1) >= Math.abs(dy)) {
3127
+ const horizontal = Math.abs(x2 - x1);
3128
+ const s = (horizontal - Math.abs(dy)) / 2;
3129
+ const p0 = { x: x1, y: y1 };
3130
+ const p1 = { x: x1 + dir * s, y: y1 };
3131
+ const p2 = { x: x2 - dir * s, y: y2 };
3132
+ const p3 = { x: x2, y: y2 };
3133
+ const u = { x: dir, y: 0 };
3134
+ const w = { x: dir / Math.SQRT2, y: sgn / Math.SQRT2 };
3135
+ const r = Math.min(
3136
+ METRO_CORNER_RADIUS,
3137
+ s / T,
3138
+ Math.SQRT2 * Math.abs(dy) / (2 * T)
3139
+ );
3140
+ const t = r * T;
3141
+ if (r >= 0.5) {
3142
+ const f1 = dir * sgn > 0 ? 1 : 0;
3143
+ d = [
3144
+ `M ${point(p0)}`,
3145
+ `L ${point(add(p1, u, -t))}`,
3146
+ `A ${round2(r)} ${round2(r)} 0 0 ${f1} ${point(add(p1, w, t))}`,
3147
+ `L ${point(add(p2, w, -t))}`,
3148
+ `A ${round2(r)} ${round2(r)} 0 0 ${1 - f1} ${point(add(p2, u, t))}`,
3149
+ `L ${point(p3)}`
3150
+ ].join(" ");
3151
+ } else {
3152
+ d = polyline([p0, p1, p2, p3]);
3153
+ }
3154
+ mid = { x: (x1 + x2) / 2, y: (y1 + y2) / 2 };
3155
+ tangent = w;
3156
+ } else {
3157
+ fromSide = "right";
3158
+ toSide = "right";
3159
+ x1 = fromRight;
3160
+ x2 = toRight;
3161
+ const xa = Math.max(x1, x2) + EDGE_STUB_OUT;
3162
+ const h = Math.abs(dy) / 2;
3163
+ const p0 = { x: x1, y: y1 };
3164
+ const p1 = { x: xa, y: y1 };
3165
+ const p2 = { x: xa + h, y: y1 + sgn * h };
3166
+ const p3 = { x: xa, y: y2 };
3167
+ const p4 = { x: x2, y: y2 };
3168
+ const u = { x: 1, y: 0 };
3169
+ const w1 = { x: 1 / Math.SQRT2, y: sgn / Math.SQRT2 };
3170
+ const w2 = { x: -1 / Math.SQRT2, y: sgn / Math.SQRT2 };
3171
+ const r = Math.min(METRO_CORNER_RADIUS, h);
3172
+ const t = r * T;
3173
+ const sweep = sgn > 0 ? 1 : 0;
3174
+ d = [
3175
+ `M ${point(p0)}`,
3176
+ `L ${point(add(p1, u, -t))}`,
3177
+ `A ${round2(r)} ${round2(r)} 0 0 ${sweep} ${point(add(p1, w1, t))}`,
3178
+ `L ${point(add(p2, w1, -r))}`,
3179
+ `A ${round2(r)} ${round2(r)} 0 0 ${sweep} ${point(add(p2, w2, r))}`,
3180
+ `L ${point(add(p3, w2, -t))}`,
3181
+ `A ${round2(r)} ${round2(r)} 0 0 ${sweep} ${point(add(p3, u, -t))}`,
3182
+ `L ${point(p4)}`
3183
+ ].join(" ");
3184
+ mid = { x: p2.x - T * r, y: p2.y };
3185
+ tangent = { x: 0, y: sgn };
3186
+ }
3187
+ if (edge?.overlay) {
3188
+ return { d, fromSide, toSide, labelX: mid.x, labelY: mid.y, labelAnchor: "middle" };
3189
+ }
3190
+ const tangentLength = Math.hypot(tangent.x, tangent.y);
3191
+ let normalX = tangent.y / tangentLength;
3192
+ let normalY = -tangent.x / tangentLength;
3193
+ if (normalY > Number.EPSILON || Math.abs(normalY) <= Number.EPSILON && normalX < 0) {
3194
+ normalX *= -1;
3195
+ normalY *= -1;
3196
+ }
3197
+ const labelW = computeLabelBoxW(edge?.label, edge?.sub);
3198
+ const labelH = labelPillH(edge?.sub);
3199
+ const distance = labelPathClearanceHorizontal(edge) + (Math.abs(normalX) * labelW + Math.abs(normalY) * labelH) / 2 + labelMarginExtra;
3200
+ return {
3201
+ d,
3202
+ fromSide,
3203
+ toSide,
3204
+ labelX: mid.x + normalX * distance,
3205
+ labelY: mid.y + normalY * distance,
3206
+ labelAnchor: "middle"
3207
+ };
3208
+ }
3209
+
2974
3210
  // src/types.ts
2975
3211
  var TONES = ["accent", "teal", "success", "error", "warning", "info"];
2976
3212
  var NODE_KINDS = [
@@ -3098,6 +3334,8 @@ var NODE_KINDS = [
3098
3334
  // 状態遷移図が使う。 印そのものが意味を持つので文字は描かない
3099
3335
  "mark-start",
3100
3336
  "mark-end",
3337
+ // 路線図の駅 (#996) ... 線路上の点を小さな丸で示し、名前と説明は丸の外に置く
3338
+ "station",
3101
3339
  // 時系列のやり取り (#578) ... 1 つの箱が図を丸ごと描く。 縦が時間で、参加者は箱を持たない
3102
3340
  "sequence-board"
3103
3341
  ];
@@ -6257,6 +6495,7 @@ function runAxes(laid, diag, violations, counts, push, profile, skippedAxes, \u5
6257
6495
  const LIFELINE_W_THRESHOLD = 8;
6258
6496
  for (const n of laid.nodes) {
6259
6497
  if (n.w < LIFELINE_W_THRESHOLD) continue;
6498
+ if (n.kind === "station") continue;
6260
6499
  if (n.w < MIN_NODE_W || n.h < MIN_NODE_H) {
6261
6500
  push(
6262
6501
  "node-visibility",
@@ -6278,6 +6517,7 @@ function runAxes(laid, diag, violations, counts, push, profile, skippedAxes, \u5
6278
6517
  if (n.id.endsWith("-spacer")) continue;
6279
6518
  if (!rendersTitleText(n.kind)) continue;
6280
6519
  if (fitsOwnTitleWidth(n.kind)) continue;
6520
+ if (skipsNodeTitleWidthCheck(n.kind)) continue;
6281
6521
  if (!isRenderedNode(n)) continue;
6282
6522
  const titleTrimmed = n.title?.trim() ?? "";
6283
6523
  if (!titleTrimmed) continue;
@@ -6882,6 +7122,9 @@ function runAxes(laid, diag, violations, counts, push, profile, skippedAxes, \u5
6882
7122
  for (let i = 0; i + 1 < sorted.length; i++) {
6883
7123
  const upper = sorted[i];
6884
7124
  const lower = sorted[i + 1];
7125
+ if (upper.cx + upper.w / 2 <= lower.cx - lower.w / 2 || lower.cx + lower.w / 2 <= upper.cx - upper.w / 2) {
7126
+ continue;
7127
+ }
6885
7128
  const upperBottom = upper.cy + upper.h / 2;
6886
7129
  const lowerTop = lower.cy - lower.h / 2;
6887
7130
  const gap = lowerTop - upperBottom;
@@ -7513,11 +7756,18 @@ function runAxes(laid, diag, violations, counts, push, profile, skippedAxes, \u5
7513
7756
  {
7514
7757
  const crestSpans = (segs, peakY) => {
7515
7758
  const out = [];
7759
+ let \u5186\u5F27\u306E\u59CB\u70B9 = null;
7516
7760
  for (const s of segs) {
7517
- if (Math.abs(s.y1 - s.y2) > 1) continue;
7518
- if (Math.abs(s.y1 - peakY) > 1) continue;
7761
+ if (s.cmd === "A") {
7762
+ \u5186\u5F27\u306E\u59CB\u70B9 ??= { x: s.x1, y: s.y1 };
7763
+ continue;
7764
+ }
7765
+ const \u59CB\u70B9 = \u5186\u5F27\u306E\u59CB\u70B9 ?? { x: s.x1, y: s.y1 };
7766
+ \u5186\u5F27\u306E\u59CB\u70B9 = null;
7767
+ if (Math.abs(\u59CB\u70B9.y - s.y2) > 1) continue;
7768
+ if (Math.abs(\u59CB\u70B9.y - peakY) > 1) continue;
7519
7769
  if (s.cmd !== "L" && !(s.ctrl ?? []).every((c) => Math.abs(c.y - peakY) <= 1)) continue;
7520
- out.push({ minX: Math.min(s.x1, s.x2), maxX: Math.max(s.x1, s.x2) });
7770
+ out.push({ minX: Math.min(\u59CB\u70B9.x, s.x2), maxX: Math.max(\u59CB\u70B9.x, s.x2) });
7521
7771
  }
7522
7772
  return out;
7523
7773
  };
@@ -7954,6 +8204,8 @@ var TOKENS = {
7954
8204
  // 箱としての最小 (80x40) より大きく、名前を持つ箱 (card 320x150) よりは明らかに小さい
7955
8205
  "mark-start": { w: 96, h: 96 },
7956
8206
  "mark-end": { w: 96, h: 96 },
8207
+ // 路線図の駅 (#996) ... 正方形にして小さな丸が縦横どちらにも歪まないようにする。
8208
+ station: { w: 24, h: 24 },
7957
8209
  // 時系列のやり取り (#578)。 実寸は中身から決まる (`sequenceBoardMetrics`) ので、
7958
8210
  // ここは書かなかった時の受け皿
7959
8211
  "sequence-board": { w: 960, h: 640 }
@@ -7978,6 +8230,7 @@ var LANE_PAD_MIN = 25;
7978
8230
  // src/layout/edges.ts
7979
8231
  var VERTICAL_DETOUR_GAP = 50;
7980
8232
  var VERTICAL_DETOUR_CORNER = 14;
8233
+ var OVERLAY_LABEL_ON_PATH_TOLERANCE = 1;
7981
8234
  function verticalCornerDir(from, to) {
7982
8235
  return to < from ? -1 : 1;
7983
8236
  }
@@ -8061,6 +8314,36 @@ function layoutEdges(diag, nodes, lanes = []) {
8061
8314
  });
8062
8315
  } else if (isSelfLoop(e)) {
8063
8316
  selfLoopEdges.push(e);
8317
+ } else if (e.routing === "curve") {
8318
+ const from = nmap.get(e.from);
8319
+ const to = nmap.get(e.to);
8320
+ if (!from) throw new Error(`cdl layout: edge "${e.id}" \u306E from "${e.from}" \u304C node \u306B\u7121\u3044`);
8321
+ if (!to) throw new Error(`cdl layout: edge "${e.id}" \u306E to "${e.to}" \u304C node \u306B\u7121\u3044`);
8322
+ const c = curvePath(from, to, e, labelMarginExtra);
8323
+ positionedEdges.push({
8324
+ ...e,
8325
+ d: c.d,
8326
+ fromSide: c.fromSide,
8327
+ toSide: c.toSide,
8328
+ labelX: c.labelX + (e.labelOffsetX ?? 0),
8329
+ labelY: c.labelY + (e.labelOffsetY ?? 0),
8330
+ labelAnchor: c.labelAnchor
8331
+ });
8332
+ } else if (e.routing === "metro") {
8333
+ const from = nmap.get(e.from);
8334
+ const to = nmap.get(e.to);
8335
+ if (!from) throw new Error(`cdl layout: edge "${e.id}" \u306E from "${e.from}" \u304C node \u306B\u7121\u3044`);
8336
+ if (!to) throw new Error(`cdl layout: edge "${e.id}" \u306E to "${e.to}" \u304C node \u306B\u7121\u3044`);
8337
+ const m = metroPath(from, to, e, labelMarginExtra);
8338
+ positionedEdges.push({
8339
+ ...e,
8340
+ d: m.d,
8341
+ fromSide: m.fromSide,
8342
+ toSide: m.toSide,
8343
+ labelX: m.labelX + (e.labelOffsetX ?? 0),
8344
+ labelY: m.labelY + (e.labelOffsetY ?? 0),
8345
+ labelAnchor: m.labelAnchor
8346
+ });
8064
8347
  } else {
8065
8348
  autoEdges.push(e);
8066
8349
  }
@@ -9565,9 +9848,11 @@ function hopEdgeCrossings(edges) {
9565
9848
  ends.set(e.id, pathEnds(segs));
9566
9849
  }
9567
9850
  for (const e of edges) {
9851
+ if (e.routing === "metro") continue;
9568
9852
  if ((e.d.match(/M/g) ?? []).length !== 1) continue;
9569
9853
  const own = extractPathSegments(e.d);
9570
9854
  if (own.length === 0) continue;
9855
+ if (own.some((s) => s.cmd === "A")) continue;
9571
9856
  const myEnds = ends.get(e.id) ?? [];
9572
9857
  const hops = /* @__PURE__ */ new Map();
9573
9858
  for (let i = 0; i < own.length; i++) {
@@ -9655,7 +9940,7 @@ function pathVertices(d) {
9655
9940
  const out = [{ x: first.x1, y: first.y1 }];
9656
9941
  for (let i = 0; i < segs.length; i++) {
9657
9942
  const s = segs[i];
9658
- if (s.cmd === "C") return null;
9943
+ if (s.cmd === "C" || s.cmd === "A") return null;
9659
9944
  if (s.cmd === "Q") {
9660
9945
  const c = s.ctrl?.[0];
9661
9946
  if (!c) return null;
@@ -9718,7 +10003,7 @@ function separateSharedStraightRuns(edges, nodes) {
9718
10003
  const v = pathVertices(e.d);
9719
10004
  if (v && v.length >= 2) verts.set(e.id, v);
9720
10005
  }
9721
- if (verts.size < 2) return;
10006
+ if (verts.size === 0) return;
9722
10007
  const moved = /* @__PURE__ */ new Set();
9723
10008
  const fits = (e, pts, k, ax, delta) => {
9724
10009
  const \u8A66\u3057 = pts.map(
@@ -9736,33 +10021,40 @@ function separateSharedStraightRuns(edges, nodes) {
9736
10021
  for (let j = i + 1; j < edges.length; j++) {
9737
10022
  const \u524D = edges[i];
9738
10023
  const \u5F8C = edges[j];
9739
- const A = verts.get(\u524D.id);
9740
- const B = verts.get(\u5F8C.id);
9741
- if (!A || !B) continue;
9742
- if (\u524D.from === \u5F8C.from) continue;
10024
+ if (\u524D.routing === "metro" && \u5F8C.routing === "metro") continue;
10025
+ const \u56FA\u5B9A = \u5F8C.routing === "metro" ? \u5F8C : \u524D;
10026
+ const \u52D5\u304B\u3059 = \u5F8C.routing === "metro" ? \u524D : \u5F8C;
10027
+ const B = verts.get(\u52D5\u304B\u3059.id);
10028
+ if (!B) continue;
10029
+ const \u56FA\u5B9A\u306E\u89D2 = verts.get(\u56FA\u5B9A.id);
10030
+ const \u56FA\u5B9A\u306E\u7D4C\u8DEF = \u56FA\u5B9A\u306E\u89D2 ? [\u56FA\u5B9A\u306E\u89D2] : \u56FA\u5B9A.routing === "metro" ? extractPathSegments(\u56FA\u5B9A.d).filter((s) => s.cmd === "L").map((s) => [{ x: s.x1, y: s.y1 }, { x: s.x2, y: s.y2 }]) : [];
10031
+ if (\u56FA\u5B9A\u306E\u7D4C\u8DEF.length === 0) continue;
10032
+ if (\u56FA\u5B9A.from === \u52D5\u304B\u3059.from) continue;
9743
10033
  for (const ax of [0, 1]) {
9744
- for (let ka = 0; ka + 1 < A.length; ka++) {
9745
- if (!runIsAligned(A, ka, ax)) continue;
9746
- for (let kb = 0; kb + 1 < B.length; kb++) {
9747
- if (!runIsAligned(B, kb, ax)) continue;
9748
- if (kb === 0 || kb + 1 === B.length - 1) continue;
9749
- const \u5EA7\u6A19A = ax === 0 ? A[ka].x : A[ka].y;
9750
- const \u5EA7\u6A19B = ax === 0 ? B[kb].x : B[kb].y;
9751
- if (Math.abs(\u5EA7\u6A19A - \u5EA7\u6A19B) > SHARED_RUN_EPS) continue;
9752
- const sa = runSpan(A, ka, ax);
9753
- const sb = runSpan(B, kb, ax);
9754
- if (Math.min(sa.hi, sb.hi) - Math.max(sa.lo, sb.lo) < SHARED_RUN_MIN) continue;
9755
- for (const delta of [SHARED_RUN_SEPARATION, -SHARED_RUN_SEPARATION]) {
9756
- if (!fits(\u5F8C, B, kb, ax, delta)) continue;
9757
- if (ax === 0) {
9758
- B[kb].x += delta;
9759
- B[kb + 1].x += delta;
9760
- } else {
9761
- B[kb].y += delta;
9762
- B[kb + 1].y += delta;
10034
+ for (const A of \u56FA\u5B9A\u306E\u7D4C\u8DEF) {
10035
+ for (let ka = 0; ka + 1 < A.length; ka++) {
10036
+ if (!runIsAligned(A, ka, ax)) continue;
10037
+ for (let kb = 0; kb + 1 < B.length; kb++) {
10038
+ if (!runIsAligned(B, kb, ax)) continue;
10039
+ if (kb === 0 || kb + 1 === B.length - 1) continue;
10040
+ const \u5EA7\u6A19A = ax === 0 ? A[ka].x : A[ka].y;
10041
+ const \u5EA7\u6A19B = ax === 0 ? B[kb].x : B[kb].y;
10042
+ if (Math.abs(\u5EA7\u6A19A - \u5EA7\u6A19B) > SHARED_RUN_EPS) continue;
10043
+ const sa = runSpan(A, ka, ax);
10044
+ const sb = runSpan(B, kb, ax);
10045
+ if (Math.min(sa.hi, sb.hi) - Math.max(sa.lo, sb.lo) < SHARED_RUN_MIN) continue;
10046
+ for (const delta of [SHARED_RUN_SEPARATION, -SHARED_RUN_SEPARATION]) {
10047
+ if (!fits(\u52D5\u304B\u3059, B, kb, ax, delta)) continue;
10048
+ if (ax === 0) {
10049
+ B[kb].x += delta;
10050
+ B[kb + 1].x += delta;
10051
+ } else {
10052
+ B[kb].y += delta;
10053
+ B[kb + 1].y += delta;
10054
+ }
10055
+ moved.add(\u52D5\u304B\u3059.id);
10056
+ break;
9763
10057
  }
9764
- moved.add(\u5F8C.id);
9765
- break;
9766
10058
  }
9767
10059
  }
9768
10060
  }
@@ -9789,12 +10081,19 @@ function pullOutgoingFirstCorner(edges, nodes) {
9789
10081
  let \u672D\u3092\u65E7\u7D4C\u8DEF\u3078\u8FFD\u5F93\u6E08\u307F = false;
9790
10082
  for (const list of \u675F.values()) {
9791
10083
  if (list.length < 2) continue;
9792
- const \u4E26\u3073 = list.flatMap((e) => {
10084
+ const \u4E26\u3073 = list.map((e) => {
9793
10085
  const pts = pathVertices(e.d);
9794
- if (!pts || pts.length < 2) return [];
9795
- return [{ e, pts, \u6700\u521D\u306E\u9577\u3055: Math.hypot(pts[1].x - pts[0].x, pts[1].y - pts[0].y) }];
9796
- }).sort((a, b) => b.\u6700\u521D\u306E\u9577\u3055 - a.\u6700\u521D\u306E\u9577\u3055);
10086
+ if (pts && pts.length >= 2) {
10087
+ return { e, pts, \u6700\u521D\u306E\u9577\u3055: Math.hypot(pts[1].x - pts[0].x, pts[1].y - pts[0].y) };
10088
+ }
10089
+ if (e.routing !== "metro") return null;
10090
+ const first = extractPathSegments(e.d)[0];
10091
+ if (!first) return null;
10092
+ return { e, pts: null, \u6700\u521D\u306E\u9577\u3055: Math.hypot(first.x2 - first.x1, first.y2 - first.y1) };
10093
+ }).filter((x) => x !== null).sort((a, b) => b.\u6700\u521D\u306E\u9577\u3055 - a.\u6700\u521D\u306E\u9577\u3055);
9797
10094
  for (const { e, pts, \u6700\u521D\u306E\u9577\u3055 } of \u4E26\u3073.slice(1)) {
10095
+ if (e.routing === "metro") continue;
10096
+ if (!pts) continue;
9798
10097
  if ((\u540C\u3058\u7D44\u306E\u6570.get(`${e.from}->${e.to}`) ?? 0) > 1) continue;
9799
10098
  if (pts.length < 3 || \u6700\u521D\u306E\u9577\u3055 <= EDGE_STUB_OUT + VERTICAL_DETOUR_CORNER) continue;
9800
10099
  const \u6A2A = e.fromSide === "left" || e.fromSide === "right";
@@ -9848,7 +10147,8 @@ var ENDPOINT_FAN_MIN = 20;
9848
10147
  var ENDPOINT_FAN_RUNUP = 40;
9849
10148
  var MIXED_ROOT_OVERLAP_MIN = 24;
9850
10149
  function fanIncomingEndpoints(edges, nodes) {
9851
- if (edges.length < 2) return;
10150
+ const \u7D42\u70B9\u306E\u79FB\u52D5 = /* @__PURE__ */ new Map();
10151
+ if (edges.length < 2) return \u7D42\u70B9\u306E\u79FB\u52D5;
9852
10152
  const nmap = new Map(nodes.map((n) => [n.id, n]));
9853
10153
  const \u5E73\u5766\u306A\u533A\u9593 = new Map(edges.map((e) => [e.id, flattenPathSegments(extractPathSegments(e.d))]));
9854
10154
  const \u8EF8\u306B\u5E73\u884C\u306A\u91CD\u306A\u308A\u9577\u3055 = (\u4E00\u65B9, \u4ED6\u65B9) => {
@@ -9918,6 +10218,7 @@ function fanIncomingEndpoints(edges, nodes) {
9918
10218
  const d = \u4E2D\u592E\u304C\u585E\u304C\u3063\u3066\u3044\u308B ? \u4E26\u3073[k].\u6DF1\u3055 < \u8FBA\u306E\u4E2D\u5FC3 ? -(++\u8CA0\u5074\u306E\u5E2D * \u9593\u9694) : ++\u6B63\u5074\u306E\u5E2D * \u9593\u9694 : k * \u9593\u9694 - \u4E2D\u5FC3;
9919
10219
  if (Math.abs(d) < 1e-3) continue;
9920
10220
  const e = \u4E26\u3073[k].e;
10221
+ if (e.routing === "metro") continue;
9921
10222
  const \u524D = e.d;
9922
10223
  const \u524D\u306E\u4EA4\u5DEE = countEdgeCrossings(edges).count;
9923
10224
  const \u9577\u3044\u52A9\u8D70 = \u4E26\u3073[k].\u9577\u3044\u52A9\u8D70;
@@ -9929,6 +10230,68 @@ function fanIncomingEndpoints(edges, nodes) {
9929
10230
  if (countEdgeCrossings(edges).count <= \u524D\u306E\u4EA4\u5DEE) break;
9930
10231
  e.d = \u524D;
9931
10232
  }
10233
+ if (e.d !== \u524D) \u7D42\u70B9\u306E\u79FB\u52D5.set(e.id, \u6A2A ? { dx: 0, dy: d } : { dx: d, dy: 0 });
10234
+ }
10235
+ }
10236
+ return \u7D42\u70B9\u306E\u79FB\u52D5;
10237
+ }
10238
+ function followLabelsToFannedTails(edges, obstacles, shifts) {
10239
+ if (shifts.size === 0) return;
10240
+ const \u6E2C\u308A\u65B9 = \u7DDA\u306E\u6E2C\u308A\u65B9\u3092\u4F5C\u308B();
10241
+ const labels = edges.filter(hasRenderedLabel).map((e) => ({
10242
+ kind: "edge-label",
10243
+ id: e.id,
10244
+ ...computeLabelBBoxWorld(e)
10245
+ }));
10246
+ const labelIndexByEdgeId = new Map(labels.map((label, index) => [label.id, index]));
10247
+ const edgeById = new Map(edges.map((e) => [e.id, e]));
10248
+ const \u5468\u308A = {
10249
+ \u7BB1\u305F\u3061: obstacles.filter((o) => o.kind === "node" || o.kind === "lane-label"),
10250
+ labels,
10251
+ edges,
10252
+ edgeById,
10253
+ \u6E2C\u308A\u65B9
10254
+ };
10255
+ for (const e of edges) {
10256
+ const \u79FB\u52D5 = shifts.get(e.id);
10257
+ if (!\u79FB\u52D5 || !hasRenderedLabel(e)) continue;
10258
+ const labelIndex = labelIndexByEdgeId.get(e.id);
10259
+ if (labelIndex === void 0) continue;
10260
+ const \u5143\u306E\u672D = labels[labelIndex];
10261
+ const \u81EA\u5206\u306E\u533A\u9593 = \u6E2C\u308A\u65B9.\u533A\u9593(e.id, e.d);
10262
+ if (e.overlay && measurePathLabelGapSegs(\u81EA\u5206\u306E\u533A\u9593, {
10263
+ x: \u5143\u306E\u672D.x + \u5143\u306E\u672D.w / 2,
10264
+ y: \u5143\u306E\u672D.y + \u5143\u306E\u672D.h / 2,
10265
+ w: 0,
10266
+ h: 0
10267
+ }) <= OVERLAY_LABEL_ON_PATH_TOLERANCE) continue;
10268
+ const \u6700\u5F8C = \u81EA\u5206\u306E\u533A\u9593.at(-1);
10269
+ if (!\u6700\u5F8C) continue;
10270
+ if (!segmentsCrossRect([\u6700\u5F8C], \u5143\u306E\u672D)) continue;
10271
+ const \u5019\u88DCa = {
10272
+ labelX: e.labelX + \u79FB\u52D5.dx,
10273
+ labelY: e.labelY + \u79FB\u52D5.dy,
10274
+ labelAnchor: e.labelAnchor
10275
+ };
10276
+ const \u5019\u88DCa\u306E\u7BB1 = computeLabelBBoxWorld({ ...e, ...\u5019\u88DCa });
10277
+ const \u6700\u5F8C\u306F\u7E26 = Math.abs(\u6700\u5F8C.x2 - \u6700\u5F8C.x1) <= Math.abs(\u6700\u5F8C.y2 - \u6700\u5F8C.y1);
10278
+ const \u5019\u88DC = [
10279
+ \u5019\u88DCa,
10280
+ {
10281
+ labelX: \u6700\u5F8C\u306F\u7E26 ? \u6700\u5F8C.x1 * 2 - (\u5019\u88DCa\u306E\u7BB1.x + \u5019\u88DCa\u306E\u7BB1.w / 2) : \u5019\u88DCa\u306E\u7BB1.x + \u5019\u88DCa\u306E\u7BB1.w / 2,
10282
+ labelY: \u6700\u5F8C\u306F\u7E26 ? \u5019\u88DCa\u306E\u7BB1.y + \u5019\u88DCa\u306E\u7BB1.h / 2 : \u6700\u5F8C.y1 * 2 - (\u5019\u88DCa\u306E\u7BB1.y + \u5019\u88DCa\u306E\u7BB1.h / 2),
10283
+ labelAnchor: "middle"
10284
+ }
10285
+ ];
10286
+ for (const c of \u5019\u88DC) {
10287
+ const \u672D = { kind: "edge-label", id: e.id, ...computeLabelBBoxWorld({ ...e, ...c }) };
10288
+ if (segmentsCrossRect([\u6700\u5F8C], \u672D)) continue;
10289
+ if (\u672D\u306E\u79FB\u52D5\u3092\u585E\u3050\u76F8\u624B\u3092\u63A2\u3059(\u5468\u308A, \u5143\u306E\u672D, \u672D) !== null) continue;
10290
+ e.labelX = c.labelX;
10291
+ e.labelY = c.labelY;
10292
+ e.labelAnchor = c.labelAnchor;
10293
+ labels[labelIndex] = \u672D;
10294
+ break;
9932
10295
  }
9933
10296
  }
9934
10297
  }
@@ -9945,7 +10308,7 @@ function \u672B\u5C3E\u3092\u8FBA\u306B\u6CBF\u3063\u3066\u305A\u3089\u3059(e, \
9945
10308
  if ((e.d.match(/M/g) ?? []).length !== 1) return null;
9946
10309
  const segs = extractPathSegments(e.d);
9947
10310
  if (segs.length === 0) return null;
9948
- if (segs.some((s) => s.cmd === "C")) return null;
10311
+ if (segs.some((s) => s.cmd === "C" || s.cmd === "A")) return null;
9949
10312
  const last = segs[segs.length - 1];
9950
10313
  if (last.cmd !== "L") return null;
9951
10314
  const \u5782\u76F4\u306B\u5165\u308B = \u6A2A ? Math.abs(last.y2 - last.y1) < 0.5 && Math.abs(last.x2 - last.x1) > 0.5 : Math.abs(last.x2 - last.x1) < 0.5 && Math.abs(last.y2 - last.y1) > 0.5;
@@ -10211,9 +10574,9 @@ function recomputeContainedLanes(lanes, nodes) {
10211
10574
  }
10212
10575
 
10213
10576
  // src/layout/head-label.ts
10214
- var \u7AEF\u306E\u5B57\u306E\u5927\u304D\u3055 = EDGE_LABEL_TEXT.sub.fontSize;
10577
+ var \u7AEF\u306E\u5B57\u306E\u5927\u304D\u3055 = 26;
10215
10578
  var \u7BB1\u306E\u9AD8\u3055 = Math.round(\u7AEF\u306E\u5B57\u306E\u5927\u304D\u3055 * 1.2);
10216
- var \u7AEF\u306E\u5B57\u3068\u5370\u306E\u4F59\u767D = Math.round(\u7BB1\u306E\u9AD8\u3055 / 2);
10579
+ var \u7AEF\u306E\u5B57\u3068\u5370\u306E\u4F59\u767D = 12;
10217
10580
  var \u7AEF\u306E\u5370\u306E\u5F35\u308A\u51FA\u3057 = EDGE_HEAD_MARKER_SIZE.\u65E2\u5B9A / 2;
10218
10581
  var \u7AEF\u306E\u5B57\u3092\u623B\u3059\u8DDD\u96E2 = EDGE_HEAD_MARKER_SIZE.\u65E2\u5B9A + \u7AEF\u306E\u5B57\u3068\u5370\u306E\u4F59\u767D;
10219
10582
  var \u7AEF\u306E\u5B57\u3068\u7DDA\u306E\u9699\u9593 = \u7AEF\u306E\u5370\u306E\u5F35\u308A\u51FA\u3057 + \u7AEF\u306E\u5B57\u3068\u5370\u306E\u4F59\u767D;
@@ -10236,13 +10599,16 @@ function \u5411\u304D\u3092\u53D6\u308B(end, \u5019\u88DC) {
10236
10599
  }
10237
10600
  return null;
10238
10601
  }
10239
- function \u7AEF\u3067\u306E\u5411\u304D(d, \u7AEF) {
10602
+ function \u7DDA\u306E\u7AEF\u306E\u5411\u304D(d, \u7AEF) {
10240
10603
  const segs = extractPathSegments(d);
10241
10604
  if (segs.length === 0) return null;
10242
10605
  if (\u7AEF === "head") {
10243
10606
  const last = segs[segs.length - 1];
10244
10607
  const end2 = { x: last.x2, y: last.y2 };
10245
- const \u5019\u88DC2 = segs.slice().reverse().flatMap((s) => [s.cmd === "Q" ? s.ctrl?.[0] : s.cmd === "C" ? s.ctrl?.[1] : void 0, { x: s.x1, y: s.y1 }]);
10608
+ const \u5019\u88DC2 = segs.slice().reverse().flatMap((s) => {
10609
+ const \u5236\u5FA1\u70B9 = s.cmd === "C" ? [s.ctrl?.[1], s.ctrl?.[0]] : s.cmd === "Q" ? [s.ctrl?.[0]] : [];
10610
+ return [...\u5236\u5FA1\u70B9, { x: s.x1, y: s.y1 }];
10611
+ });
10246
10612
  return \u5411\u304D\u3092\u53D6\u308B(end2, \u5019\u88DC2);
10247
10613
  }
10248
10614
  const first = segs[0];
@@ -10253,7 +10619,7 @@ function \u7AEF\u3067\u306E\u5411\u304D(d, \u7AEF) {
10253
10619
  function \u7AEF\u306E\u5B57\u3092\u7F6E\u304F(edge, \u7AEF = "head") {
10254
10620
  const text = (\u7AEF === "head" ? edge.headLabel : edge.tailLabel)?.trim();
10255
10621
  if (!text) return null;
10256
- const \u5411\u304D = \u7AEF\u3067\u306E\u5411\u304D(edge.d, \u7AEF);
10622
+ const \u5411\u304D = \u7DDA\u306E\u7AEF\u306E\u5411\u304D(edge.d, \u7AEF);
10257
10623
  if (!\u5411\u304D) return null;
10258
10624
  const { end, ux, uy } = \u5411\u304D;
10259
10625
  const bx = end.x - ux * \u7AEF\u306E\u5B57\u3092\u623B\u3059\u8DDD\u96E2;
@@ -10581,22 +10947,23 @@ function layout(diag) {
10581
10947
  }
10582
10948
  }
10583
10949
  const positionedEdgeIds = new Set(
10584
- diag.edges.filter((e) => e.posX1 !== void 0 && e.posY1 !== void 0 && e.posX2 !== void 0 && e.posY2 !== void 0).map((e) => e.id)
10950
+ diag.edges.filter(
10951
+ (e) => e.routing === "curve" || e.routing === "metro" || e.posX1 !== void 0 && e.posY1 !== void 0 && e.posX2 !== void 0 && e.posY2 !== void 0
10952
+ ).map((e) => e.id)
10585
10953
  );
10586
10954
  bowSharedStraightPaths(edges, nodesFinal, positionedEdgeIds);
10587
10955
  pullOutgoingFirstCorner(edges, nodesFinal);
10588
- fanIncomingEndpoints(edges, nodesFinal);
10956
+ const fannedTailShifts = fanIncomingEndpoints(edges, nodesFinal);
10589
10957
  separateSharedStraightRuns(edges, nodesFinal);
10590
10958
  hopEdgeCrossings(edges);
10959
+ const labelClearanceObstacles = collectBBoxes(containedLanesFinal, nodesFinal, []).filter(
10960
+ (b) => b.kind === "node" || b.kind === "lane-label"
10961
+ );
10962
+ followLabelsToFannedTails(edges, labelClearanceObstacles, fannedTailShifts);
10591
10963
  repositionParallelLabels(edges, nodesFinal);
10592
10964
  slideLabelsAlongOwnPath(edges, nodesFinal);
10593
10965
  if (!isChainEdgeLabelDisabled()) {
10594
- edges = resolveLabelClearanceAfterPathPasses(
10595
- edges,
10596
- collectBBoxes(containedLanesFinal, nodesFinal, []).filter(
10597
- (b) => b.kind === "node" || b.kind === "lane-label"
10598
- )
10599
- );
10966
+ edges = resolveLabelClearanceAfterPathPasses(edges, labelClearanceObstacles);
10600
10967
  }
10601
10968
  const bboxes = collectBBoxes(containedLanesFinal, nodesFinal, edges);
10602
10969
  const collisions = detectCollisions(bboxes, edges);
@@ -11528,12 +11895,13 @@ function CdlEdgeView({
11528
11895
  const isPassThrough = isDotted && Boolean(edge.dThrough);
11529
11896
  const lineDotsD = isDotted ? shrinkPathEnd(edge.d, 12) : edge.d;
11530
11897
  const glowPathId = `cdl-edge-${edge.id}`;
11531
- const \u901A\u308A\u9053\u3092\u7E2E\u3081\u308B = EDGE_HEAD_MARKER_SIZE.\u65E2\u5B9A + \u8D70\u308B\u4E38\u306E\u534A\u5F84.\u82AF;
11898
+ const \u901A\u308A\u9053\u3092\u7E2E\u3081\u308B = EDGE_HEAD_MARKER_SIZE.\u65E2\u5B9A + \u8D70\u308B\u4E38\u306E\u5916\u5074;
11532
11899
  const glowPathD = shrinkPathStart(shrinkPathEnd(edge.d, \u901A\u308A\u9053\u3092\u7E2E\u3081\u308B), \u901A\u308A\u9053\u3092\u7E2E\u3081\u308B);
11533
11900
  return /* @__PURE__ */ jsxs(
11534
11901
  "g",
11535
11902
  {
11536
11903
  "data-cdl-edge": edge.id,
11904
+ "data-cdl-routing": edge.routing === "metro" ? "metro" : void 0,
11537
11905
  "data-cdl-from": edge.from,
11538
11906
  "data-cdl-to": edge.to,
11539
11907
  "data-cdl-active": active ? "true" : "false",
@@ -11636,7 +12004,9 @@ function SolidEdgePath({
11636
12004
  }
11637
12005
  );
11638
12006
  }
11639
- var \u8D70\u308B\u4E38\u306E\u534A\u5F84 = { \u5916: 28, \u4E2D: 18, \u82AF: 11 };
12007
+ var \u8D70\u308B\u4E38\u306E\u534A\u5F84 = { \u5916: 28, \u4E2D: 18, \u82AF: 12 };
12008
+ var \u8D70\u308B\u4E38\u306E\u7E01\u306E\u898B\u8FBC\u307F = 2;
12009
+ var \u8D70\u308B\u4E38\u306E\u5916\u5074 = \u8D70\u308B\u4E38\u306E\u534A\u5F84.\u82AF + \u8D70\u308B\u4E38\u306E\u7E01\u306E\u898B\u8FBC\u307F;
11640
12010
  function EdgeGlowCircles({ color, edgeId }) {
11641
12011
  const ref = `#cdl-edge-${edgeId}`;
11642
12012
  return /* @__PURE__ */ jsxs("g", { "data-cdl-particle": edgeId, "data-cdl-particle-path-ref": ref, children: [
@@ -12960,17 +13330,22 @@ function DynShapeNode({
12960
13330
  return renderPolygon(shape, cx, cy, stateValues, fillColor, strokeColor, activeOpacity);
12961
13331
  }
12962
13332
  })();
13333
+ const \u63C3\u3048 = \u7BB1\u3044\u3063\u3071\u3044\u306B\u63CF\u304F2(shape.kind) ? "start" : "middle";
13334
+ return renderShapeWithOutsideLabels(\u5F62, node, \u63C3\u3048);
13335
+ }
13336
+ function renderShapeWithOutsideLabels(\u5F62, node, \u63C3\u3048) {
13337
+ const cx = node.cx;
13338
+ const y = node.cy - node.h / 2;
12963
13339
  const \u540D\u524D = (node.title ?? "").trim();
12964
13340
  const \u5024 = (node.subtitle ?? "").trim();
12965
13341
  if (\u540D\u524D === "" && \u5024 === "") return \u5F62;
12966
- const \u63C3\u3048 = \u7BB1\u3044\u3063\u3071\u3044\u306B\u63CF\u304F2(shape.kind) ? "start" : "middle";
12967
13342
  return /* @__PURE__ */ jsxs("g", { children: [
12968
13343
  \u5F62,
12969
13344
  \u540D\u524D !== "" && /* @__PURE__ */ jsx(
12970
13345
  "text",
12971
13346
  {
12972
13347
  "data-cdl-role": "shape-name",
12973
- x: \u63C3\u3048 === "start" ? x : cx,
13348
+ x: \u63C3\u3048 === "start" ? node.cx - node.w / 2 : cx,
12974
13349
  y: y - NAME_GAP,
12975
13350
  textAnchor: \u63C3\u3048,
12976
13351
  fontSize: 13,
@@ -12984,7 +13359,7 @@ function DynShapeNode({
12984
13359
  {
12985
13360
  "data-cdl-role": "shape-value",
12986
13361
  x: cx,
12987
- y: y + h + VALUE_GAP,
13362
+ y: y + node.h + VALUE_GAP,
12988
13363
  textAnchor: "middle",
12989
13364
  fontSize: 12,
12990
13365
  fill: "var(--cdl-text-dim, #5a6270)",
@@ -13234,6 +13609,27 @@ function interpolateColor(template, stateValues, fallback) {
13234
13609
  if (!template) return fallback;
13235
13610
  return interpolate(template, stateValues);
13236
13611
  }
13612
+ var STATION_STROKE_ACTIVE = 4;
13613
+ var STATION_STROKE_IDLE = 3;
13614
+ var STATION_DEFAULT = "var(--cdl-tone-accent, #2d6a8f)";
13615
+ function StationNode({ node, active }) {
13616
+ const strokeWidth = active ? STATION_STROKE_ACTIVE : STATION_STROKE_IDLE;
13617
+ const radius = Math.max(0, Math.min(node.w, node.h) / 2 - strokeWidth / 2);
13618
+ const \u5F62 = /* @__PURE__ */ jsx(
13619
+ "circle",
13620
+ {
13621
+ "data-cdl-role": "node-body",
13622
+ "data-cdl-mark": "station",
13623
+ cx: node.cx,
13624
+ cy: node.cy,
13625
+ r: radius,
13626
+ fill: "var(--cdl-node-fill, #ffffff)",
13627
+ stroke: nodeAccent(node, STATION_DEFAULT),
13628
+ strokeWidth
13629
+ }
13630
+ );
13631
+ return renderShapeWithOutsideLabels(\u5F62, node, "middle");
13632
+ }
13237
13633
 
13238
13634
  // src/kinds/label-stack.ts
13239
13635
  function \u540D\u672D\u306E\u6BB5\u3092\u307B\u3069\u304F(\u771F\u306Ey, \u9593\u9694, \u4E0A\u9650, \u4E0B\u9650) {
@@ -20462,6 +20858,8 @@ function CdlNodeView({
20462
20858
  return /* @__PURE__ */ jsx(MarkStartNode, { node: resolvedNode, active });
20463
20859
  case "mark-end":
20464
20860
  return /* @__PURE__ */ jsx(MarkEndNode, { node: resolvedNode, active });
20861
+ case "station":
20862
+ return /* @__PURE__ */ jsx(StationNode, { node: resolvedNode, active });
20465
20863
  default:
20466
20864
  return /* @__PURE__ */ jsx(GenericNode, { node: resolvedNode, active, value: node.value, stateValues, \u5024\u306E\u540D\u524D });
20467
20865
  }
@@ -20694,15 +21092,15 @@ function CdlStage({
20694
21092
  }, [laid]);
20695
21093
  const edgeHeadMarkers = useMemo(() => {
20696
21094
  const referenced = /* @__PURE__ */ new Set();
20697
- const add = (tone, head, fill) => {
21095
+ const add2 = (tone, head, fill) => {
20698
21096
  for (const small of [false, true]) {
20699
21097
  const id = edgeHeadMarkerName(tone, head, small, fill);
20700
21098
  if (id !== void 0) referenced.add(id);
20701
21099
  }
20702
21100
  };
20703
21101
  for (const edge of laid.edges) {
20704
- add(edge.tone, edge.head, edge.headFill);
20705
- if (edge.tailHead !== void 0) add(edge.tone, edge.tailHead, edge.tailHeadFill);
21102
+ add2(edge.tone, edge.head, edge.headFill);
21103
+ if (edge.tailHead !== void 0) add2(edge.tone, edge.tailHead, edge.tailHeadFill);
20706
21104
  }
20707
21105
  return Object.entries(TONE).flatMap(
20708
21106
  ([tone, color]) => [