@cardenelabs/cdl 0.121.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/index.cjs CHANGED
@@ -12390,6 +12390,7 @@ var KINDS_DRAWING_TITLE_OUTSIDE = /* @__PURE__ */ new Set([
12390
12390
  "dyn-arc",
12391
12391
  "dyn-wave",
12392
12392
  "dyn-polygon",
12393
+ "station",
12393
12394
  ...KINDS_DRAWING_OWN_PAYLOAD
12394
12395
  ]);
12395
12396
  function drawsTitleOutsideBox(kind) {
@@ -12397,7 +12398,7 @@ function drawsTitleOutsideBox(kind) {
12397
12398
  }
12398
12399
  var TITLE_FONT_SIZE_DEFAULT = 18;
12399
12400
  var TITLE_FONT_SIZE = new Map([
12400
- ...["dyn-rect", "dyn-circle", "dyn-arc", "dyn-wave", "dyn-polygon"].map(
12401
+ ...["dyn-rect", "dyn-circle", "dyn-arc", "dyn-wave", "dyn-polygon", "station"].map(
12401
12402
  (k) => [k, 13]
12402
12403
  ),
12403
12404
  ...[
@@ -12448,6 +12449,10 @@ var TITLE_FONT_SIZE = new Map([
12448
12449
  function titleFontSize(kind) {
12449
12450
  return TITLE_FONT_SIZE.get(kind ?? "") ?? TITLE_FONT_SIZE_DEFAULT;
12450
12451
  }
12452
+ var KINDS_WITH_TITLE_WIDTH_OUTSIDE_NODE = /* @__PURE__ */ new Set(["station"]);
12453
+ function skipsNodeTitleWidthCheck(kind) {
12454
+ return KINDS_WITH_TITLE_WIDTH_OUTSIDE_NODE.has(kind ?? "");
12455
+ }
12451
12456
  var TITLE_BOX_PADDING = 52;
12452
12457
  var KINDS_SCALING_TITLE_WHEN_LOW = /* @__PURE__ */ new Set(["actor", "event", "function", "storage"]);
12453
12458
  var KINDS_SCALING_TITLE_ALWAYS = /* @__PURE__ */ new Set([
@@ -12530,7 +12535,8 @@ var KINDS_DRAWING_ONLY_SHAPE = [
12530
12535
  "dyn-circle",
12531
12536
  "dyn-arc",
12532
12537
  "dyn-wave",
12533
- "dyn-polygon"
12538
+ "dyn-polygon",
12539
+ "station"
12534
12540
  ];
12535
12541
  var KINDS_WITHOUT_EYEBROW_TEXT = /* @__PURE__ */ new Set([
12536
12542
  ...KINDS_DRAWING_ONLY_SHAPE,
@@ -13310,13 +13316,13 @@ function detectCollisions(bboxes, edges) {
13310
13316
  }
13311
13317
  function extractPathSegments(d) {
13312
13318
  const segs = [];
13313
- const commandRegex = /([MLCQZmlcqz])([^MLCQZmlcqz]*)/g;
13319
+ const commandRegex = /([MLCQAZmlcqaz])([^MLCQAZmlcqaz]*)/g;
13314
13320
  let cur = { x: 0, y: 0 };
13315
13321
  let m;
13316
13322
  while ((m = commandRegex.exec(d)) !== null) {
13317
13323
  const cmd = m[1];
13318
13324
  const args = (m[2] ?? "").trim().split(/[\s,]+/).filter(Boolean).map(parseFloat);
13319
- const need = cmd === "M" || cmd === "L" ? 2 : cmd === "Q" ? 4 : cmd === "C" ? 6 : 0;
13325
+ const need = cmd === "M" || cmd === "L" ? 2 : cmd === "Q" ? 4 : cmd === "C" ? 6 : cmd === "A" ? 7 : 0;
13320
13326
  if (need > 0 && (args.length < need || !args.slice(0, need).every(Number.isFinite))) continue;
13321
13327
  if (cmd === "M") {
13322
13328
  cur = { x: args[0], y: args[1] };
@@ -13346,6 +13352,10 @@ function extractPathSegments(d) {
13346
13352
  ctrl: [{ x: args[0], y: args[1] }, { x: args[2], y: args[3] }]
13347
13353
  });
13348
13354
  cur = next;
13355
+ } else if (cmd === "A") {
13356
+ const next = { x: args[5], y: args[6] };
13357
+ segs.push({ x1: cur.x, y1: cur.y, x2: next.x, y2: next.y, cmd: "A" });
13358
+ cur = next;
13349
13359
  }
13350
13360
  }
13351
13361
  return segs;
@@ -14548,6 +14558,232 @@ function applyEdgeLabelAndPathShifts(edges, labelShiftMap, edgePathShiftMap) {
14548
14558
  });
14549
14559
  }
14550
14560
 
14561
+ // src/layout/curve-path.ts
14562
+ var SAME_HEIGHT_THRESHOLD = 1;
14563
+ function curvePath(from, to, edge, labelMarginExtra = 0) {
14564
+ const fromLeft = from.cx - from.w / 2;
14565
+ const fromRight = from.cx + from.w / 2;
14566
+ const toLeft = to.cx - to.w / 2;
14567
+ const toRight = to.cx + to.w / 2;
14568
+ let fromSide;
14569
+ let toSide;
14570
+ let x1;
14571
+ let x2;
14572
+ if (toLeft >= fromRight) {
14573
+ fromSide = "right";
14574
+ toSide = "left";
14575
+ x1 = fromRight;
14576
+ x2 = toLeft;
14577
+ } else if (toRight <= fromLeft) {
14578
+ fromSide = "left";
14579
+ toSide = "right";
14580
+ x1 = fromLeft;
14581
+ x2 = toRight;
14582
+ } else {
14583
+ fromSide = "right";
14584
+ toSide = "right";
14585
+ x1 = fromRight;
14586
+ x2 = toRight;
14587
+ }
14588
+ const y1 = from.cy;
14589
+ const y2 = to.cy;
14590
+ const sameHeight = Math.abs(y2 - y1) < SAME_HEIGHT_THRESHOLD;
14591
+ let d;
14592
+ let midX;
14593
+ let midY;
14594
+ let tangentX;
14595
+ let tangentY;
14596
+ if (sameHeight) {
14597
+ d = `M ${x1} ${y1} L ${x2} ${y2}`;
14598
+ midX = (x1 + x2) / 2;
14599
+ midY = (y1 + y2) / 2;
14600
+ tangentX = x2 - x1;
14601
+ tangentY = y2 - y1;
14602
+ } else {
14603
+ let c1x;
14604
+ let c2x;
14605
+ if (fromSide !== toSide) {
14606
+ const direction = x2 > x1 ? 1 : -1;
14607
+ const bulge = Math.max(Math.abs(x2 - x1) / 2, EDGE_STUB_OUT);
14608
+ c1x = x1 + direction * bulge;
14609
+ c2x = x2 - direction * bulge;
14610
+ } else {
14611
+ const bulgeX = Math.max(x1, x2) + EDGE_STUB_OUT;
14612
+ c1x = bulgeX;
14613
+ c2x = bulgeX;
14614
+ }
14615
+ d = `M ${x1} ${y1} C ${c1x} ${y1}, ${c2x} ${y2}, ${x2} ${y2}`;
14616
+ midX = (x1 + 3 * c1x + 3 * c2x + x2) / 8;
14617
+ midY = (y1 + 3 * y1 + 3 * y2 + y2) / 8;
14618
+ tangentX = x2 + c2x - c1x - x1;
14619
+ tangentY = y2 + y2 - y1 - y1;
14620
+ }
14621
+ if (edge?.overlay) {
14622
+ return { d, fromSide, toSide, labelX: midX, labelY: midY, labelAnchor: "middle" };
14623
+ }
14624
+ const tangentLength = Math.hypot(tangentX, tangentY);
14625
+ let normalX = tangentY / tangentLength;
14626
+ let normalY = -tangentX / tangentLength;
14627
+ if (normalY > Number.EPSILON || Math.abs(normalY) <= Number.EPSILON && normalX < 0) {
14628
+ normalX *= -1;
14629
+ normalY *= -1;
14630
+ }
14631
+ const labelW = computeLabelBoxW(edge?.label, edge?.sub);
14632
+ const labelH = labelPillH(edge?.sub);
14633
+ const distance = labelPathClearanceHorizontal(edge) + (Math.abs(normalX) * labelW + Math.abs(normalY) * labelH) / 2 + labelMarginExtra;
14634
+ return {
14635
+ d,
14636
+ fromSide,
14637
+ toSide,
14638
+ labelX: midX + normalX * distance,
14639
+ labelY: midY + normalY * distance,
14640
+ labelAnchor: "middle"
14641
+ };
14642
+ }
14643
+
14644
+ // src/layout/metro-path.ts
14645
+ var METRO_CORNER_RADIUS = 24;
14646
+ var T = Math.SQRT2 - 1;
14647
+ function round2(v) {
14648
+ return Math.round(v * 100) / 100;
14649
+ }
14650
+ function point(p) {
14651
+ return `${round2(p.x)} ${round2(p.y)}`;
14652
+ }
14653
+ function add(p, v, scale) {
14654
+ return { x: p.x + v.x * scale, y: p.y + v.y * scale };
14655
+ }
14656
+ function polyline(points) {
14657
+ const distinct = points.filter((p, i) => {
14658
+ const previous = points[i - 1];
14659
+ return previous === void 0 || p.x !== previous.x || p.y !== previous.y;
14660
+ });
14661
+ return distinct.map((p, i) => `${i === 0 ? "M" : "L"} ${point(p)}`).join(" ");
14662
+ }
14663
+ function metroPath(from, to, edge, labelMarginExtra = 0) {
14664
+ const fromLeft = from.cx - from.w / 2;
14665
+ const fromRight = from.cx + from.w / 2;
14666
+ const toLeft = to.cx - to.w / 2;
14667
+ const toRight = to.cx + to.w / 2;
14668
+ let fromSide;
14669
+ let toSide;
14670
+ let x1;
14671
+ let x2;
14672
+ let dir;
14673
+ if (toLeft >= fromRight) {
14674
+ fromSide = "right";
14675
+ toSide = "left";
14676
+ x1 = fromRight;
14677
+ x2 = toLeft;
14678
+ dir = 1;
14679
+ } else if (toRight <= fromLeft) {
14680
+ fromSide = "left";
14681
+ toSide = "right";
14682
+ x1 = fromLeft;
14683
+ x2 = toRight;
14684
+ dir = -1;
14685
+ } else {
14686
+ fromSide = "right";
14687
+ toSide = "right";
14688
+ x1 = fromRight;
14689
+ x2 = toRight;
14690
+ dir = 1;
14691
+ }
14692
+ const y1 = from.cy;
14693
+ const y2 = to.cy;
14694
+ const dy = y2 - y1;
14695
+ const sgn = Math.sign(dy);
14696
+ let d;
14697
+ let mid;
14698
+ let tangent;
14699
+ if (Math.abs(dy) < SAME_HEIGHT_THRESHOLD) {
14700
+ d = `M ${point({ x: x1, y: y1 })} L ${point({ x: x2, y: y2 })}`;
14701
+ mid = { x: (x1 + x2) / 2, y: (y1 + y2) / 2 };
14702
+ tangent = { x: x2 - x1, y: y2 - y1 };
14703
+ } else if (fromSide !== toSide && Math.abs(x2 - x1) >= Math.abs(dy)) {
14704
+ const horizontal = Math.abs(x2 - x1);
14705
+ const s = (horizontal - Math.abs(dy)) / 2;
14706
+ const p0 = { x: x1, y: y1 };
14707
+ const p1 = { x: x1 + dir * s, y: y1 };
14708
+ const p2 = { x: x2 - dir * s, y: y2 };
14709
+ const p3 = { x: x2, y: y2 };
14710
+ const u = { x: dir, y: 0 };
14711
+ const w = { x: dir / Math.SQRT2, y: sgn / Math.SQRT2 };
14712
+ const r = Math.min(
14713
+ METRO_CORNER_RADIUS,
14714
+ s / T,
14715
+ Math.SQRT2 * Math.abs(dy) / (2 * T)
14716
+ );
14717
+ const t = r * T;
14718
+ if (r >= 0.5) {
14719
+ const f1 = dir * sgn > 0 ? 1 : 0;
14720
+ d = [
14721
+ `M ${point(p0)}`,
14722
+ `L ${point(add(p1, u, -t))}`,
14723
+ `A ${round2(r)} ${round2(r)} 0 0 ${f1} ${point(add(p1, w, t))}`,
14724
+ `L ${point(add(p2, w, -t))}`,
14725
+ `A ${round2(r)} ${round2(r)} 0 0 ${1 - f1} ${point(add(p2, u, t))}`,
14726
+ `L ${point(p3)}`
14727
+ ].join(" ");
14728
+ } else {
14729
+ d = polyline([p0, p1, p2, p3]);
14730
+ }
14731
+ mid = { x: (x1 + x2) / 2, y: (y1 + y2) / 2 };
14732
+ tangent = w;
14733
+ } else {
14734
+ fromSide = "right";
14735
+ toSide = "right";
14736
+ x1 = fromRight;
14737
+ x2 = toRight;
14738
+ const xa = Math.max(x1, x2) + EDGE_STUB_OUT;
14739
+ const h = Math.abs(dy) / 2;
14740
+ const p0 = { x: x1, y: y1 };
14741
+ const p1 = { x: xa, y: y1 };
14742
+ const p2 = { x: xa + h, y: y1 + sgn * h };
14743
+ const p3 = { x: xa, y: y2 };
14744
+ const p4 = { x: x2, y: y2 };
14745
+ const u = { x: 1, y: 0 };
14746
+ const w1 = { x: 1 / Math.SQRT2, y: sgn / Math.SQRT2 };
14747
+ const w2 = { x: -1 / Math.SQRT2, y: sgn / Math.SQRT2 };
14748
+ const r = Math.min(METRO_CORNER_RADIUS, h);
14749
+ const t = r * T;
14750
+ const sweep = sgn > 0 ? 1 : 0;
14751
+ d = [
14752
+ `M ${point(p0)}`,
14753
+ `L ${point(add(p1, u, -t))}`,
14754
+ `A ${round2(r)} ${round2(r)} 0 0 ${sweep} ${point(add(p1, w1, t))}`,
14755
+ `L ${point(add(p2, w1, -r))}`,
14756
+ `A ${round2(r)} ${round2(r)} 0 0 ${sweep} ${point(add(p2, w2, r))}`,
14757
+ `L ${point(add(p3, w2, -t))}`,
14758
+ `A ${round2(r)} ${round2(r)} 0 0 ${sweep} ${point(add(p3, u, -t))}`,
14759
+ `L ${point(p4)}`
14760
+ ].join(" ");
14761
+ mid = { x: p2.x - T * r, y: p2.y };
14762
+ tangent = { x: 0, y: sgn };
14763
+ }
14764
+ if (edge?.overlay) {
14765
+ return { d, fromSide, toSide, labelX: mid.x, labelY: mid.y, labelAnchor: "middle" };
14766
+ }
14767
+ const tangentLength = Math.hypot(tangent.x, tangent.y);
14768
+ let normalX = tangent.y / tangentLength;
14769
+ let normalY = -tangent.x / tangentLength;
14770
+ if (normalY > Number.EPSILON || Math.abs(normalY) <= Number.EPSILON && normalX < 0) {
14771
+ normalX *= -1;
14772
+ normalY *= -1;
14773
+ }
14774
+ const labelW = computeLabelBoxW(edge?.label, edge?.sub);
14775
+ const labelH = labelPillH(edge?.sub);
14776
+ const distance = labelPathClearanceHorizontal(edge) + (Math.abs(normalX) * labelW + Math.abs(normalY) * labelH) / 2 + labelMarginExtra;
14777
+ return {
14778
+ d,
14779
+ fromSide,
14780
+ toSide,
14781
+ labelX: mid.x + normalX * distance,
14782
+ labelY: mid.y + normalY * distance,
14783
+ labelAnchor: "middle"
14784
+ };
14785
+ }
14786
+
14551
14787
  // src/types.ts
14552
14788
  var TONES = ["accent", "teal", "success", "error", "warning", "info"];
14553
14789
  var NODE_KINDS = [
@@ -14675,6 +14911,8 @@ var NODE_KINDS = [
14675
14911
  // 状態遷移図が使う。 印そのものが意味を持つので文字は描かない
14676
14912
  "mark-start",
14677
14913
  "mark-end",
14914
+ // 路線図の駅 (#996) ... 線路上の点を小さな丸で示し、名前と説明は丸の外に置く
14915
+ "station",
14678
14916
  // 時系列のやり取り (#578) ... 1 つの箱が図を丸ごと描く。 縦が時間で、参加者は箱を持たない
14679
14917
  "sequence-board"
14680
14918
  ];
@@ -17386,6 +17624,7 @@ function runAxes(laid, diag, violations, counts, push, profile, skippedAxes, \u5
17386
17624
  const LIFELINE_W_THRESHOLD = 8;
17387
17625
  for (const n of laid.nodes) {
17388
17626
  if (n.w < LIFELINE_W_THRESHOLD) continue;
17627
+ if (n.kind === "station") continue;
17389
17628
  if (n.w < MIN_NODE_W || n.h < MIN_NODE_H) {
17390
17629
  push(
17391
17630
  "node-visibility",
@@ -17407,6 +17646,7 @@ function runAxes(laid, diag, violations, counts, push, profile, skippedAxes, \u5
17407
17646
  if (n.id.endsWith("-spacer")) continue;
17408
17647
  if (!rendersTitleText(n.kind)) continue;
17409
17648
  if (fitsOwnTitleWidth(n.kind)) continue;
17649
+ if (skipsNodeTitleWidthCheck(n.kind)) continue;
17410
17650
  if (!isRenderedNode(n)) continue;
17411
17651
  const titleTrimmed = n.title?.trim() ?? "";
17412
17652
  if (!titleTrimmed) continue;
@@ -18011,6 +18251,9 @@ function runAxes(laid, diag, violations, counts, push, profile, skippedAxes, \u5
18011
18251
  for (let i = 0; i + 1 < sorted.length; i++) {
18012
18252
  const upper = sorted[i];
18013
18253
  const lower = sorted[i + 1];
18254
+ if (upper.cx + upper.w / 2 <= lower.cx - lower.w / 2 || lower.cx + lower.w / 2 <= upper.cx - upper.w / 2) {
18255
+ continue;
18256
+ }
18014
18257
  const upperBottom = upper.cy + upper.h / 2;
18015
18258
  const lowerTop = lower.cy - lower.h / 2;
18016
18259
  const gap = lowerTop - upperBottom;
@@ -18642,11 +18885,18 @@ function runAxes(laid, diag, violations, counts, push, profile, skippedAxes, \u5
18642
18885
  {
18643
18886
  const crestSpans = (segs, peakY) => {
18644
18887
  const out = [];
18888
+ let \u5186\u5F27\u306E\u59CB\u70B9 = null;
18645
18889
  for (const s of segs) {
18646
- if (Math.abs(s.y1 - s.y2) > 1) continue;
18647
- if (Math.abs(s.y1 - peakY) > 1) continue;
18890
+ if (s.cmd === "A") {
18891
+ \u5186\u5F27\u306E\u59CB\u70B9 ??= { x: s.x1, y: s.y1 };
18892
+ continue;
18893
+ }
18894
+ const \u59CB\u70B9 = \u5186\u5F27\u306E\u59CB\u70B9 ?? { x: s.x1, y: s.y1 };
18895
+ \u5186\u5F27\u306E\u59CB\u70B9 = null;
18896
+ if (Math.abs(\u59CB\u70B9.y - s.y2) > 1) continue;
18897
+ if (Math.abs(\u59CB\u70B9.y - peakY) > 1) continue;
18648
18898
  if (s.cmd !== "L" && !(s.ctrl ?? []).every((c) => Math.abs(c.y - peakY) <= 1)) continue;
18649
- out.push({ minX: Math.min(s.x1, s.x2), maxX: Math.max(s.x1, s.x2) });
18899
+ out.push({ minX: Math.min(\u59CB\u70B9.x, s.x2), maxX: Math.max(\u59CB\u70B9.x, s.x2) });
18650
18900
  }
18651
18901
  return out;
18652
18902
  };
@@ -19337,6 +19587,8 @@ var TOKENS = {
19337
19587
  // 箱としての最小 (80x40) より大きく、名前を持つ箱 (card 320x150) よりは明らかに小さい
19338
19588
  "mark-start": { w: 96, h: 96 },
19339
19589
  "mark-end": { w: 96, h: 96 },
19590
+ // 路線図の駅 (#996) ... 正方形にして小さな丸が縦横どちらにも歪まないようにする。
19591
+ station: { w: 24, h: 24 },
19340
19592
  // 時系列のやり取り (#578)。 実寸は中身から決まる (`sequenceBoardMetrics`) ので、
19341
19593
  // ここは書かなかった時の受け皿
19342
19594
  "sequence-board": { w: 960, h: 640 }
@@ -19361,6 +19613,7 @@ var LANE_PAD_MIN = 25;
19361
19613
  // src/layout/edges.ts
19362
19614
  var VERTICAL_DETOUR_GAP = 50;
19363
19615
  var VERTICAL_DETOUR_CORNER = 14;
19616
+ var OVERLAY_LABEL_ON_PATH_TOLERANCE = 1;
19364
19617
  function verticalCornerDir(from, to) {
19365
19618
  return to < from ? -1 : 1;
19366
19619
  }
@@ -19444,6 +19697,36 @@ function layoutEdges(diag, nodes, lanes = []) {
19444
19697
  });
19445
19698
  } else if (isSelfLoop(e)) {
19446
19699
  selfLoopEdges.push(e);
19700
+ } else if (e.routing === "curve") {
19701
+ const from = nmap.get(e.from);
19702
+ const to = nmap.get(e.to);
19703
+ if (!from) throw new Error(`cdl layout: edge "${e.id}" \u306E from "${e.from}" \u304C node \u306B\u7121\u3044`);
19704
+ if (!to) throw new Error(`cdl layout: edge "${e.id}" \u306E to "${e.to}" \u304C node \u306B\u7121\u3044`);
19705
+ const c = curvePath(from, to, e, labelMarginExtra);
19706
+ positionedEdges.push({
19707
+ ...e,
19708
+ d: c.d,
19709
+ fromSide: c.fromSide,
19710
+ toSide: c.toSide,
19711
+ labelX: c.labelX + (e.labelOffsetX ?? 0),
19712
+ labelY: c.labelY + (e.labelOffsetY ?? 0),
19713
+ labelAnchor: c.labelAnchor
19714
+ });
19715
+ } else if (e.routing === "metro") {
19716
+ const from = nmap.get(e.from);
19717
+ const to = nmap.get(e.to);
19718
+ if (!from) throw new Error(`cdl layout: edge "${e.id}" \u306E from "${e.from}" \u304C node \u306B\u7121\u3044`);
19719
+ if (!to) throw new Error(`cdl layout: edge "${e.id}" \u306E to "${e.to}" \u304C node \u306B\u7121\u3044`);
19720
+ const m = metroPath(from, to, e, labelMarginExtra);
19721
+ positionedEdges.push({
19722
+ ...e,
19723
+ d: m.d,
19724
+ fromSide: m.fromSide,
19725
+ toSide: m.toSide,
19726
+ labelX: m.labelX + (e.labelOffsetX ?? 0),
19727
+ labelY: m.labelY + (e.labelOffsetY ?? 0),
19728
+ labelAnchor: m.labelAnchor
19729
+ });
19447
19730
  } else {
19448
19731
  autoEdges.push(e);
19449
19732
  }
@@ -20948,9 +21231,11 @@ function hopEdgeCrossings(edges) {
20948
21231
  ends.set(e.id, pathEnds(segs));
20949
21232
  }
20950
21233
  for (const e of edges) {
21234
+ if (e.routing === "metro") continue;
20951
21235
  if ((e.d.match(/M/g) ?? []).length !== 1) continue;
20952
21236
  const own = extractPathSegments(e.d);
20953
21237
  if (own.length === 0) continue;
21238
+ if (own.some((s) => s.cmd === "A")) continue;
20954
21239
  const myEnds = ends.get(e.id) ?? [];
20955
21240
  const hops = /* @__PURE__ */ new Map();
20956
21241
  for (let i = 0; i < own.length; i++) {
@@ -21038,7 +21323,7 @@ function pathVertices(d) {
21038
21323
  const out = [{ x: first.x1, y: first.y1 }];
21039
21324
  for (let i = 0; i < segs.length; i++) {
21040
21325
  const s = segs[i];
21041
- if (s.cmd === "C") return null;
21326
+ if (s.cmd === "C" || s.cmd === "A") return null;
21042
21327
  if (s.cmd === "Q") {
21043
21328
  const c = s.ctrl?.[0];
21044
21329
  if (!c) return null;
@@ -21101,7 +21386,7 @@ function separateSharedStraightRuns(edges, nodes) {
21101
21386
  const v = pathVertices(e.d);
21102
21387
  if (v && v.length >= 2) verts.set(e.id, v);
21103
21388
  }
21104
- if (verts.size < 2) return;
21389
+ if (verts.size === 0) return;
21105
21390
  const moved = /* @__PURE__ */ new Set();
21106
21391
  const fits = (e, pts, k, ax, delta) => {
21107
21392
  const \u8A66\u3057 = pts.map(
@@ -21119,33 +21404,40 @@ function separateSharedStraightRuns(edges, nodes) {
21119
21404
  for (let j = i + 1; j < edges.length; j++) {
21120
21405
  const \u524D = edges[i];
21121
21406
  const \u5F8C = edges[j];
21122
- const A = verts.get(\u524D.id);
21123
- const B = verts.get(\u5F8C.id);
21124
- if (!A || !B) continue;
21125
- if (\u524D.from === \u5F8C.from) continue;
21407
+ if (\u524D.routing === "metro" && \u5F8C.routing === "metro") continue;
21408
+ const \u56FA\u5B9A = \u5F8C.routing === "metro" ? \u5F8C : \u524D;
21409
+ const \u52D5\u304B\u3059 = \u5F8C.routing === "metro" ? \u524D : \u5F8C;
21410
+ const B = verts.get(\u52D5\u304B\u3059.id);
21411
+ if (!B) continue;
21412
+ const \u56FA\u5B9A\u306E\u89D2 = verts.get(\u56FA\u5B9A.id);
21413
+ 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 }]) : [];
21414
+ if (\u56FA\u5B9A\u306E\u7D4C\u8DEF.length === 0) continue;
21415
+ if (\u56FA\u5B9A.from === \u52D5\u304B\u3059.from) continue;
21126
21416
  for (const ax of [0, 1]) {
21127
- for (let ka = 0; ka + 1 < A.length; ka++) {
21128
- if (!runIsAligned(A, ka, ax)) continue;
21129
- for (let kb = 0; kb + 1 < B.length; kb++) {
21130
- if (!runIsAligned(B, kb, ax)) continue;
21131
- if (kb === 0 || kb + 1 === B.length - 1) continue;
21132
- const \u5EA7\u6A19A = ax === 0 ? A[ka].x : A[ka].y;
21133
- const \u5EA7\u6A19B = ax === 0 ? B[kb].x : B[kb].y;
21134
- if (Math.abs(\u5EA7\u6A19A - \u5EA7\u6A19B) > SHARED_RUN_EPS) continue;
21135
- const sa = runSpan(A, ka, ax);
21136
- const sb = runSpan(B, kb, ax);
21137
- if (Math.min(sa.hi, sb.hi) - Math.max(sa.lo, sb.lo) < SHARED_RUN_MIN) continue;
21138
- for (const delta of [SHARED_RUN_SEPARATION, -SHARED_RUN_SEPARATION]) {
21139
- if (!fits(\u5F8C, B, kb, ax, delta)) continue;
21140
- if (ax === 0) {
21141
- B[kb].x += delta;
21142
- B[kb + 1].x += delta;
21143
- } else {
21144
- B[kb].y += delta;
21145
- B[kb + 1].y += delta;
21417
+ for (const A of \u56FA\u5B9A\u306E\u7D4C\u8DEF) {
21418
+ for (let ka = 0; ka + 1 < A.length; ka++) {
21419
+ if (!runIsAligned(A, ka, ax)) continue;
21420
+ for (let kb = 0; kb + 1 < B.length; kb++) {
21421
+ if (!runIsAligned(B, kb, ax)) continue;
21422
+ if (kb === 0 || kb + 1 === B.length - 1) continue;
21423
+ const \u5EA7\u6A19A = ax === 0 ? A[ka].x : A[ka].y;
21424
+ const \u5EA7\u6A19B = ax === 0 ? B[kb].x : B[kb].y;
21425
+ if (Math.abs(\u5EA7\u6A19A - \u5EA7\u6A19B) > SHARED_RUN_EPS) continue;
21426
+ const sa = runSpan(A, ka, ax);
21427
+ const sb = runSpan(B, kb, ax);
21428
+ if (Math.min(sa.hi, sb.hi) - Math.max(sa.lo, sb.lo) < SHARED_RUN_MIN) continue;
21429
+ for (const delta of [SHARED_RUN_SEPARATION, -SHARED_RUN_SEPARATION]) {
21430
+ if (!fits(\u52D5\u304B\u3059, B, kb, ax, delta)) continue;
21431
+ if (ax === 0) {
21432
+ B[kb].x += delta;
21433
+ B[kb + 1].x += delta;
21434
+ } else {
21435
+ B[kb].y += delta;
21436
+ B[kb + 1].y += delta;
21437
+ }
21438
+ moved.add(\u52D5\u304B\u3059.id);
21439
+ break;
21146
21440
  }
21147
- moved.add(\u5F8C.id);
21148
- break;
21149
21441
  }
21150
21442
  }
21151
21443
  }
@@ -21172,12 +21464,19 @@ function pullOutgoingFirstCorner(edges, nodes) {
21172
21464
  let \u672D\u3092\u65E7\u7D4C\u8DEF\u3078\u8FFD\u5F93\u6E08\u307F = false;
21173
21465
  for (const list of \u675F.values()) {
21174
21466
  if (list.length < 2) continue;
21175
- const \u4E26\u3073 = list.flatMap((e) => {
21467
+ const \u4E26\u3073 = list.map((e) => {
21176
21468
  const pts = pathVertices(e.d);
21177
- if (!pts || pts.length < 2) return [];
21178
- return [{ e, pts, \u6700\u521D\u306E\u9577\u3055: Math.hypot(pts[1].x - pts[0].x, pts[1].y - pts[0].y) }];
21179
- }).sort((a, b) => b.\u6700\u521D\u306E\u9577\u3055 - a.\u6700\u521D\u306E\u9577\u3055);
21469
+ if (pts && pts.length >= 2) {
21470
+ return { e, pts, \u6700\u521D\u306E\u9577\u3055: Math.hypot(pts[1].x - pts[0].x, pts[1].y - pts[0].y) };
21471
+ }
21472
+ if (e.routing !== "metro") return null;
21473
+ const first = extractPathSegments(e.d)[0];
21474
+ if (!first) return null;
21475
+ return { e, pts: null, \u6700\u521D\u306E\u9577\u3055: Math.hypot(first.x2 - first.x1, first.y2 - first.y1) };
21476
+ }).filter((x) => x !== null).sort((a, b) => b.\u6700\u521D\u306E\u9577\u3055 - a.\u6700\u521D\u306E\u9577\u3055);
21180
21477
  for (const { e, pts, \u6700\u521D\u306E\u9577\u3055 } of \u4E26\u3073.slice(1)) {
21478
+ if (e.routing === "metro") continue;
21479
+ if (!pts) continue;
21181
21480
  if ((\u540C\u3058\u7D44\u306E\u6570.get(`${e.from}->${e.to}`) ?? 0) > 1) continue;
21182
21481
  if (pts.length < 3 || \u6700\u521D\u306E\u9577\u3055 <= EDGE_STUB_OUT + VERTICAL_DETOUR_CORNER) continue;
21183
21482
  const \u6A2A = e.fromSide === "left" || e.fromSide === "right";
@@ -21231,7 +21530,8 @@ var ENDPOINT_FAN_MIN = 20;
21231
21530
  var ENDPOINT_FAN_RUNUP = 40;
21232
21531
  var MIXED_ROOT_OVERLAP_MIN = 24;
21233
21532
  function fanIncomingEndpoints(edges, nodes) {
21234
- if (edges.length < 2) return;
21533
+ const \u7D42\u70B9\u306E\u79FB\u52D5 = /* @__PURE__ */ new Map();
21534
+ if (edges.length < 2) return \u7D42\u70B9\u306E\u79FB\u52D5;
21235
21535
  const nmap = new Map(nodes.map((n) => [n.id, n]));
21236
21536
  const \u5E73\u5766\u306A\u533A\u9593 = new Map(edges.map((e) => [e.id, flattenPathSegments(extractPathSegments(e.d))]));
21237
21537
  const \u8EF8\u306B\u5E73\u884C\u306A\u91CD\u306A\u308A\u9577\u3055 = (\u4E00\u65B9, \u4ED6\u65B9) => {
@@ -21301,6 +21601,7 @@ function fanIncomingEndpoints(edges, nodes) {
21301
21601
  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;
21302
21602
  if (Math.abs(d) < 1e-3) continue;
21303
21603
  const e = \u4E26\u3073[k].e;
21604
+ if (e.routing === "metro") continue;
21304
21605
  const \u524D = e.d;
21305
21606
  const \u524D\u306E\u4EA4\u5DEE = countEdgeCrossings(edges).count;
21306
21607
  const \u9577\u3044\u52A9\u8D70 = \u4E26\u3073[k].\u9577\u3044\u52A9\u8D70;
@@ -21312,6 +21613,68 @@ function fanIncomingEndpoints(edges, nodes) {
21312
21613
  if (countEdgeCrossings(edges).count <= \u524D\u306E\u4EA4\u5DEE) break;
21313
21614
  e.d = \u524D;
21314
21615
  }
21616
+ if (e.d !== \u524D) \u7D42\u70B9\u306E\u79FB\u52D5.set(e.id, \u6A2A ? { dx: 0, dy: d } : { dx: d, dy: 0 });
21617
+ }
21618
+ }
21619
+ return \u7D42\u70B9\u306E\u79FB\u52D5;
21620
+ }
21621
+ function followLabelsToFannedTails(edges, obstacles, shifts) {
21622
+ if (shifts.size === 0) return;
21623
+ const \u6E2C\u308A\u65B9 = \u7DDA\u306E\u6E2C\u308A\u65B9\u3092\u4F5C\u308B();
21624
+ const labels = edges.filter(hasRenderedLabel).map((e) => ({
21625
+ kind: "edge-label",
21626
+ id: e.id,
21627
+ ...computeLabelBBoxWorld(e)
21628
+ }));
21629
+ const labelIndexByEdgeId = new Map(labels.map((label, index) => [label.id, index]));
21630
+ const edgeById = new Map(edges.map((e) => [e.id, e]));
21631
+ const \u5468\u308A = {
21632
+ \u7BB1\u305F\u3061: obstacles.filter((o) => o.kind === "node" || o.kind === "lane-label"),
21633
+ labels,
21634
+ edges,
21635
+ edgeById,
21636
+ \u6E2C\u308A\u65B9
21637
+ };
21638
+ for (const e of edges) {
21639
+ const \u79FB\u52D5 = shifts.get(e.id);
21640
+ if (!\u79FB\u52D5 || !hasRenderedLabel(e)) continue;
21641
+ const labelIndex = labelIndexByEdgeId.get(e.id);
21642
+ if (labelIndex === void 0) continue;
21643
+ const \u5143\u306E\u672D = labels[labelIndex];
21644
+ const \u81EA\u5206\u306E\u533A\u9593 = \u6E2C\u308A\u65B9.\u533A\u9593(e.id, e.d);
21645
+ if (e.overlay && measurePathLabelGapSegs(\u81EA\u5206\u306E\u533A\u9593, {
21646
+ x: \u5143\u306E\u672D.x + \u5143\u306E\u672D.w / 2,
21647
+ y: \u5143\u306E\u672D.y + \u5143\u306E\u672D.h / 2,
21648
+ w: 0,
21649
+ h: 0
21650
+ }) <= OVERLAY_LABEL_ON_PATH_TOLERANCE) continue;
21651
+ const \u6700\u5F8C = \u81EA\u5206\u306E\u533A\u9593.at(-1);
21652
+ if (!\u6700\u5F8C) continue;
21653
+ if (!segmentsCrossRect([\u6700\u5F8C], \u5143\u306E\u672D)) continue;
21654
+ const \u5019\u88DCa = {
21655
+ labelX: e.labelX + \u79FB\u52D5.dx,
21656
+ labelY: e.labelY + \u79FB\u52D5.dy,
21657
+ labelAnchor: e.labelAnchor
21658
+ };
21659
+ const \u5019\u88DCa\u306E\u7BB1 = computeLabelBBoxWorld({ ...e, ...\u5019\u88DCa });
21660
+ const \u6700\u5F8C\u306F\u7E26 = Math.abs(\u6700\u5F8C.x2 - \u6700\u5F8C.x1) <= Math.abs(\u6700\u5F8C.y2 - \u6700\u5F8C.y1);
21661
+ const \u5019\u88DC = [
21662
+ \u5019\u88DCa,
21663
+ {
21664
+ 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,
21665
+ 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),
21666
+ labelAnchor: "middle"
21667
+ }
21668
+ ];
21669
+ for (const c of \u5019\u88DC) {
21670
+ const \u672D = { kind: "edge-label", id: e.id, ...computeLabelBBoxWorld({ ...e, ...c }) };
21671
+ if (segmentsCrossRect([\u6700\u5F8C], \u672D)) continue;
21672
+ if (\u672D\u306E\u79FB\u52D5\u3092\u585E\u3050\u76F8\u624B\u3092\u63A2\u3059(\u5468\u308A, \u5143\u306E\u672D, \u672D) !== null) continue;
21673
+ e.labelX = c.labelX;
21674
+ e.labelY = c.labelY;
21675
+ e.labelAnchor = c.labelAnchor;
21676
+ labels[labelIndex] = \u672D;
21677
+ break;
21315
21678
  }
21316
21679
  }
21317
21680
  }
@@ -21328,7 +21691,7 @@ function \u672B\u5C3E\u3092\u8FBA\u306B\u6CBF\u3063\u3066\u305A\u3089\u3059(e, \
21328
21691
  if ((e.d.match(/M/g) ?? []).length !== 1) return null;
21329
21692
  const segs = extractPathSegments(e.d);
21330
21693
  if (segs.length === 0) return null;
21331
- if (segs.some((s) => s.cmd === "C")) return null;
21694
+ if (segs.some((s) => s.cmd === "C" || s.cmd === "A")) return null;
21332
21695
  const last = segs[segs.length - 1];
21333
21696
  if (last.cmd !== "L") return null;
21334
21697
  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;
@@ -21619,13 +21982,16 @@ function \u5411\u304D\u3092\u53D6\u308B(end, \u5019\u88DC) {
21619
21982
  }
21620
21983
  return null;
21621
21984
  }
21622
- function \u7AEF\u3067\u306E\u5411\u304D(d, \u7AEF) {
21985
+ function \u7DDA\u306E\u7AEF\u306E\u5411\u304D(d, \u7AEF) {
21623
21986
  const segs = extractPathSegments(d);
21624
21987
  if (segs.length === 0) return null;
21625
21988
  if (\u7AEF === "head") {
21626
21989
  const last = segs[segs.length - 1];
21627
21990
  const end2 = { x: last.x2, y: last.y2 };
21628
- 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 }]);
21991
+ const \u5019\u88DC2 = segs.slice().reverse().flatMap((s) => {
21992
+ const \u5236\u5FA1\u70B9 = s.cmd === "C" ? [s.ctrl?.[1], s.ctrl?.[0]] : s.cmd === "Q" ? [s.ctrl?.[0]] : [];
21993
+ return [...\u5236\u5FA1\u70B9, { x: s.x1, y: s.y1 }];
21994
+ });
21629
21995
  return \u5411\u304D\u3092\u53D6\u308B(end2, \u5019\u88DC2);
21630
21996
  }
21631
21997
  const first = segs[0];
@@ -21636,7 +22002,7 @@ function \u7AEF\u3067\u306E\u5411\u304D(d, \u7AEF) {
21636
22002
  function \u7AEF\u306E\u5B57\u3092\u7F6E\u304F(edge, \u7AEF = "head") {
21637
22003
  const text = (\u7AEF === "head" ? edge.headLabel : edge.tailLabel)?.trim();
21638
22004
  if (!text) return null;
21639
- const \u5411\u304D = \u7AEF\u3067\u306E\u5411\u304D(edge.d, \u7AEF);
22005
+ const \u5411\u304D = \u7DDA\u306E\u7AEF\u306E\u5411\u304D(edge.d, \u7AEF);
21640
22006
  if (!\u5411\u304D) return null;
21641
22007
  const { end, ux, uy } = \u5411\u304D;
21642
22008
  const bx = end.x - ux * \u7AEF\u306E\u5B57\u3092\u623B\u3059\u8DDD\u96E2;
@@ -21964,22 +22330,23 @@ function layout(diag) {
21964
22330
  }
21965
22331
  }
21966
22332
  const positionedEdgeIds = new Set(
21967
- diag.edges.filter((e) => e.posX1 !== void 0 && e.posY1 !== void 0 && e.posX2 !== void 0 && e.posY2 !== void 0).map((e) => e.id)
22333
+ diag.edges.filter(
22334
+ (e) => e.routing === "curve" || e.routing === "metro" || e.posX1 !== void 0 && e.posY1 !== void 0 && e.posX2 !== void 0 && e.posY2 !== void 0
22335
+ ).map((e) => e.id)
21968
22336
  );
21969
22337
  bowSharedStraightPaths(edges, nodesFinal, positionedEdgeIds);
21970
22338
  pullOutgoingFirstCorner(edges, nodesFinal);
21971
- fanIncomingEndpoints(edges, nodesFinal);
22339
+ const fannedTailShifts = fanIncomingEndpoints(edges, nodesFinal);
21972
22340
  separateSharedStraightRuns(edges, nodesFinal);
21973
22341
  hopEdgeCrossings(edges);
22342
+ const labelClearanceObstacles = collectBBoxes(containedLanesFinal, nodesFinal, []).filter(
22343
+ (b) => b.kind === "node" || b.kind === "lane-label"
22344
+ );
22345
+ followLabelsToFannedTails(edges, labelClearanceObstacles, fannedTailShifts);
21974
22346
  repositionParallelLabels(edges, nodesFinal);
21975
22347
  slideLabelsAlongOwnPath(edges, nodesFinal);
21976
22348
  if (!isChainEdgeLabelDisabled()) {
21977
- edges = resolveLabelClearanceAfterPathPasses(
21978
- edges,
21979
- collectBBoxes(containedLanesFinal, nodesFinal, []).filter(
21980
- (b) => b.kind === "node" || b.kind === "lane-label"
21981
- )
21982
- );
22349
+ edges = resolveLabelClearanceAfterPathPasses(edges, labelClearanceObstacles);
21983
22350
  }
21984
22351
  const bboxes = collectBBoxes(containedLanesFinal, nodesFinal, edges);
21985
22352
  const collisions = detectCollisions(bboxes, edges);
@@ -22588,11 +22955,13 @@ function applyHeuristics(diag, laid, violations, applied) {
22588
22955
  violations.filter((v) => v.axis === "edge-node-cross").map((v) => extractEdgeId(v.detail)).filter((id) => id !== null)
22589
22956
  );
22590
22957
  if (h3Edges.size > 0) {
22591
- const originalEdges = edges;
22592
- edges = edges.map(
22593
- (e) => h3Edges.has(e.id) && e.routing !== "back-detour" ? { ...e, routing: "back-detour" } : e
22958
+ const eligibleEdges = new Set(
22959
+ edges.filter((e) => h3Edges.has(e.id) && (e.routing === void 0 || e.routing === "default")).map((e) => e.id)
22594
22960
  );
22595
- if (edges !== originalEdges) {
22961
+ if (eligibleEdges.size > 0) {
22962
+ edges = edges.map(
22963
+ (e) => eligibleEdges.has(e.id) ? { ...e, routing: "back-detour" } : e
22964
+ );
22596
22965
  applied.push("h3-edge-back-detour");
22597
22966
  mutated = true;
22598
22967
  }
@@ -22912,15 +23281,15 @@ function extractPathEndpoints(d) {
22912
23281
  if (!start || !end) return null;
22913
23282
  return { start, end };
22914
23283
  }
22915
- function isPointOnNodeBoundary(point, node, tol) {
23284
+ function isPointOnNodeBoundary(point2, node, tol) {
22916
23285
  const left = node.cx - node.w / 2;
22917
23286
  const right = node.cx + node.w / 2;
22918
23287
  const top = node.cy - node.h / 2;
22919
23288
  const bot = node.cy + node.h / 2;
22920
- if (Math.abs(point.x - left) <= tol && point.y >= top - tol && point.y <= bot + tol) return true;
22921
- if (Math.abs(point.x - right) <= tol && point.y >= top - tol && point.y <= bot + tol) return true;
22922
- if (Math.abs(point.y - top) <= tol && point.x >= left - tol && point.x <= right + tol) return true;
22923
- if (Math.abs(point.y - bot) <= tol && point.x >= left - tol && point.x <= right + tol) return true;
23289
+ if (Math.abs(point2.x - left) <= tol && point2.y >= top - tol && point2.y <= bot + tol) return true;
23290
+ if (Math.abs(point2.x - right) <= tol && point2.y >= top - tol && point2.y <= bot + tol) return true;
23291
+ if (Math.abs(point2.y - top) <= tol && point2.x >= left - tol && point2.x <= right + tol) return true;
23292
+ if (Math.abs(point2.y - bot) <= tol && point2.x >= left - tol && point2.x <= right + tol) return true;
22924
23293
  return false;
22925
23294
  }
22926
23295
  async function verifyBoundingBoxes(page, laid, options) {
@@ -24132,6 +24501,7 @@ function CdlEdgeView({
24132
24501
  "g",
24133
24502
  {
24134
24503
  "data-cdl-edge": edge.id,
24504
+ "data-cdl-routing": edge.routing === "metro" ? "metro" : void 0,
24135
24505
  "data-cdl-from": edge.from,
24136
24506
  "data-cdl-to": edge.to,
24137
24507
  "data-cdl-active": active ? "true" : "false",
@@ -25563,17 +25933,22 @@ function DynShapeNode({
25563
25933
  return renderPolygon(shape, cx, cy, stateValues, fillColor, strokeColor, activeOpacity);
25564
25934
  }
25565
25935
  })();
25936
+ const \u63C3\u3048 = \u7BB1\u3044\u3063\u3071\u3044\u306B\u63CF\u304F2(shape.kind) ? "start" : "middle";
25937
+ return renderShapeWithOutsideLabels(\u5F62, node, \u63C3\u3048);
25938
+ }
25939
+ function renderShapeWithOutsideLabels(\u5F62, node, \u63C3\u3048) {
25940
+ const cx = node.cx;
25941
+ const y = node.cy - node.h / 2;
25566
25942
  const \u540D\u524D = (node.title ?? "").trim();
25567
25943
  const \u5024 = (node.subtitle ?? "").trim();
25568
25944
  if (\u540D\u524D === "" && \u5024 === "") return \u5F62;
25569
- const \u63C3\u3048 = \u7BB1\u3044\u3063\u3071\u3044\u306B\u63CF\u304F2(shape.kind) ? "start" : "middle";
25570
25945
  return /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
25571
25946
  \u5F62,
25572
25947
  \u540D\u524D !== "" && /* @__PURE__ */ jsxRuntime.jsx(
25573
25948
  "text",
25574
25949
  {
25575
25950
  "data-cdl-role": "shape-name",
25576
- x: \u63C3\u3048 === "start" ? x : cx,
25951
+ x: \u63C3\u3048 === "start" ? node.cx - node.w / 2 : cx,
25577
25952
  y: y - NAME_GAP,
25578
25953
  textAnchor: \u63C3\u3048,
25579
25954
  fontSize: 13,
@@ -25587,7 +25962,7 @@ function DynShapeNode({
25587
25962
  {
25588
25963
  "data-cdl-role": "shape-value",
25589
25964
  x: cx,
25590
- y: y + h + VALUE_GAP,
25965
+ y: y + node.h + VALUE_GAP,
25591
25966
  textAnchor: "middle",
25592
25967
  fontSize: 12,
25593
25968
  fill: "var(--cdl-text-dim, #5a6270)",
@@ -25837,6 +26212,27 @@ function interpolateColor(template, stateValues, fallback) {
25837
26212
  if (!template) return fallback;
25838
26213
  return interpolate(template, stateValues);
25839
26214
  }
26215
+ var STATION_STROKE_ACTIVE = 4;
26216
+ var STATION_STROKE_IDLE = 3;
26217
+ var STATION_DEFAULT = "var(--cdl-tone-accent, #2d6a8f)";
26218
+ function StationNode({ node, active }) {
26219
+ const strokeWidth = active ? STATION_STROKE_ACTIVE : STATION_STROKE_IDLE;
26220
+ const radius = Math.max(0, Math.min(node.w, node.h) / 2 - strokeWidth / 2);
26221
+ const \u5F62 = /* @__PURE__ */ jsxRuntime.jsx(
26222
+ "circle",
26223
+ {
26224
+ "data-cdl-role": "node-body",
26225
+ "data-cdl-mark": "station",
26226
+ cx: node.cx,
26227
+ cy: node.cy,
26228
+ r: radius,
26229
+ fill: "var(--cdl-node-fill, #ffffff)",
26230
+ stroke: nodeAccent(node, STATION_DEFAULT),
26231
+ strokeWidth
26232
+ }
26233
+ );
26234
+ return renderShapeWithOutsideLabels(\u5F62, node, "middle");
26235
+ }
25840
26236
 
25841
26237
  // src/kinds/label-stack.ts
25842
26238
  function \u540D\u672D\u306E\u6BB5\u3092\u307B\u3069\u304F(\u771F\u306Ey, \u9593\u9694, \u4E0A\u9650, \u4E0B\u9650) {
@@ -33065,6 +33461,8 @@ function CdlNodeView({
33065
33461
  return /* @__PURE__ */ jsxRuntime.jsx(MarkStartNode, { node: resolvedNode, active });
33066
33462
  case "mark-end":
33067
33463
  return /* @__PURE__ */ jsxRuntime.jsx(MarkEndNode, { node: resolvedNode, active });
33464
+ case "station":
33465
+ return /* @__PURE__ */ jsxRuntime.jsx(StationNode, { node: resolvedNode, active });
33068
33466
  default:
33069
33467
  return /* @__PURE__ */ jsxRuntime.jsx(GenericNode, { node: resolvedNode, active, value: node.value, stateValues, \u5024\u306E\u540D\u524D });
33070
33468
  }
@@ -33297,15 +33695,15 @@ function CdlStage({
33297
33695
  }, [laid]);
33298
33696
  const edgeHeadMarkers = react.useMemo(() => {
33299
33697
  const referenced = /* @__PURE__ */ new Set();
33300
- const add = (tone, head, fill) => {
33698
+ const add2 = (tone, head, fill) => {
33301
33699
  for (const small of [false, true]) {
33302
33700
  const id = edgeHeadMarkerName(tone, head, small, fill);
33303
33701
  if (id !== void 0) referenced.add(id);
33304
33702
  }
33305
33703
  };
33306
33704
  for (const edge of laid.edges) {
33307
- add(edge.tone, edge.head, edge.headFill);
33308
- if (edge.tailHead !== void 0) add(edge.tone, edge.tailHead, edge.tailHeadFill);
33705
+ add2(edge.tone, edge.head, edge.headFill);
33706
+ if (edge.tailHead !== void 0) add2(edge.tone, edge.tailHead, edge.tailHeadFill);
33309
33707
  }
33310
33708
  return Object.entries(TONE).flatMap(
33311
33709
  ([tone, color]) => [