@cardenelabs/cdl 0.49.0 → 0.51.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/CHANGELOG.md CHANGED
@@ -5,6 +5,36 @@ CDL (Chainome Diagram Language) の主要変更履歴。
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.51.0] - 2026-09-11
9
+
10
+ ### Changed
11
+
12
+ - **クラス図の多重度を、線の中ほどの札から行き先の端へ移した** (#821)
13
+
14
+ クラス図の関係に書いた多重度 (`cardinality: "1..*"`) は、札の語の下の行 (`sub`) に入っていた。
15
+ 語と続けて読まれ (英字の頃は `HAS` と `1..*` が「HAS1」)、 どちらの端の数かも読めなかった。
16
+ UML と同じく、行き先の端の近くへ等幅の字で添える。
17
+
18
+ 置き場所は線の最後の区間から決まる。 端から 22 world 戻った点の、札と反対の側に置く
19
+ (札を持たない線は、横向きなら下、縦向きなら左)。 札と同じ側に置くと、見本帳のクラス図で
20
+ 1 本が札と重なり、もう 1 本が札と 2 world まで寄った。
21
+
22
+ 線に「行き先の端に添える字」 の欄 `headLabel` を足した。 クラス図の組み立て器は多重度を
23
+ `sub` ではなく `headLabel` に渡す。 ER 図の多重度の出し方は変えない (端の印で示す)。
24
+
25
+ ## [0.50.0] - 2026-09-11
26
+
27
+ ### Fixed
28
+
29
+ - **等幅の文字で `[` と `]` が隣り合うと四角 1 つに見えるのを直した** (#817)
30
+
31
+ 見本帳のクラス図で、型の `string[]` が画面では `string□` と読めた。 等幅の書体 (`JetBrains Mono`) の
32
+ `[` と `]` は横棒が字の枠いっぱいまで伸びており、図を画面の幅に合わせて縮めると (画面上 14px 前後)
33
+ 2 本の横棒がつながる。 合字ではない (合字を止めても変わらない)。
34
+
35
+ engine が等幅の書体で描く全ての文字で、 `[]` の間にだけ字の大きさの 0.08 倍の隙間を足す。
36
+ 隙間は `<tspan dx>` で足すので、 文字の中身 (`textContent`) は `string[]` のまま変わらない。
37
+
8
38
  ## [0.49.0] - 2026-09-11
9
39
 
10
40
  ### Added
@@ -2405,7 +2435,9 @@ Initial OSS release.
2405
2435
  使わない = annotated tag を push しても GitHub Release は作られず、 Release を作っていない版で
2406
2436
  行き止まりになる。
2407
2437
 
2408
- [Unreleased]: https://github.com/cardene777/cdl/compare/v0.49.0...HEAD
2438
+ [Unreleased]: https://github.com/cardene777/cdl/compare/v0.51.0...HEAD
2439
+ [0.51.0]: https://github.com/cardene777/cdl/compare/v0.50.0...v0.51.0
2440
+ [0.50.0]: https://github.com/cardene777/cdl/compare/v0.49.0...v0.50.0
2409
2441
  [0.49.0]: https://github.com/cardene777/cdl/compare/v0.48.0...v0.49.0
2410
2442
  [0.48.0]: https://github.com/cardene777/cdl/compare/v0.47.0...v0.48.0
2411
2443
  [0.47.0]: https://github.com/cardene777/cdl/compare/v0.46.0...v0.47.0
package/dist/index.cjs CHANGED
@@ -9501,6 +9501,7 @@ function diagram(id, options) {
9501
9501
  tailHeadFill: opts.tailHeadFill,
9502
9502
  guard: opts.guard,
9503
9503
  cardinality: opts.cardinality,
9504
+ headLabel: opts.headLabel,
9504
9505
  labelOffsetX: opts.labelOffsetX,
9505
9506
  labelOffsetY: opts.labelOffsetY,
9506
9507
  routing: opts.routing,
@@ -9627,6 +9628,7 @@ function diagram(id, options) {
9627
9628
  tailHeadFill: d.tailHeadFill,
9628
9629
  guard: d.guard,
9629
9630
  cardinality: d.cardinality,
9631
+ headLabel: d.headLabel,
9630
9632
  labelOffsetX: d.labelOffsetX,
9631
9633
  labelOffsetY: d.labelOffsetY,
9632
9634
  routing: d.routing,
@@ -14105,10 +14107,10 @@ function \u66F8\u8A18\u7D20\u306B\u5206\u3051\u308B(text) {
14105
14107
  if (typeof \u533A\u5207\u308A !== "function") return [...text];
14106
14108
  return [...new \u533A\u5207\u308A(void 0, { granularity: "grapheme" }).segment(text)].map((g) => g.segment);
14107
14109
  }
14108
- function \u7BB1\u306B\u7A4D\u3080(\u884C, \u7BB1\u306E\u5E45, \u7BB1\u306E\u9AD8\u30552) {
14110
+ function \u7BB1\u306B\u7A4D\u3080(\u884C, \u7BB1\u306E\u5E45, \u7BB1\u306E\u9AD8\u30553) {
14109
14111
  const \u5019\u88DC = \u884C.filter((r, i) => i === 0 || r.text.trim() !== "");
14110
14112
  if (\u5019\u88DC.length === 0) return [];
14111
- const \u4F7F\u3048\u308B\u9AD8\u3055 = \u7BB1\u306E\u9AD8\u30552 - \u4E0A\u4E0B\u306E\u4F59\u767D * 2;
14113
+ const \u4F7F\u3048\u308B\u9AD8\u3055 = \u7BB1\u306E\u9AD8\u30553 - \u4E0A\u4E0B\u306E\u4F59\u767D * 2;
14112
14114
  const \u9AD8\u3055 = (rs) => rs.reduce((a, r) => a + r.fontSize * \u884C\u9001\u308A\u306E\u6BD4, 0);
14113
14115
  const \u6B8B\u3059 = [...\u5019\u88DC];
14114
14116
  while (\u6B8B\u3059.length > 1 && \u9AD8\u3055(\u6B8B\u3059) > \u4F7F\u3048\u308B\u9AD8\u3055) \u6B8B\u3059.pop();
@@ -14835,7 +14837,7 @@ function \u30D9\u30B8\u30A8\u306E\u7D2F\u7A4D\u5272\u5408(\u70B9, \u5236\u5FA1\u
14835
14837
  function \u7BB1\u306E\u6587\u5B57({
14836
14838
  \u884C,
14837
14839
  x,
14838
- \u7BB1\u306E\u9AD8\u3055: \u7BB1\u306E\u9AD8\u30552,
14840
+ \u7BB1\u306E\u9AD8\u3055: \u7BB1\u306E\u9AD8\u30553,
14839
14841
  fill,
14840
14842
  \u540D\u524D\u306E\u592A\u3055
14841
14843
  }) {
@@ -14844,7 +14846,7 @@ function \u7BB1\u306E\u6587\u5B57({
14844
14846
  {
14845
14847
  "data-cdl-role": r.\u5F79\u5272 === "subtitle" ? "tree-node-subtitle" : void 0,
14846
14848
  x,
14847
- y: \u7BB1\u306E\u9AD8\u30552 / 2 + r.dy,
14849
+ y: \u7BB1\u306E\u9AD8\u30553 / 2 + r.dy,
14848
14850
  fontSize: r.fontSize,
14849
14851
  fontWeight: r.\u5F79\u5272 === "title" ? \u540D\u524D\u306E\u592A\u3055 : 500,
14850
14852
  textAnchor: "middle",
@@ -22920,6 +22922,84 @@ function edgeHeadPaint(head, fill, color) {
22920
22922
  }
22921
22923
  return { fill: color, stroke: "none" };
22922
22924
  }
22925
+ var \u89D2\u62EC\u5F27\u306E\u9699\u9593 = 0.08;
22926
+ function \u7B49\u5E45\u306E\u5B57(text, fontSize) {
22927
+ if (typeof text !== "string" || !text.includes("[]")) return text;
22928
+ const \u584A = text.split("[]");
22929
+ const out = [];
22930
+ \u584A.forEach((s, i) => {
22931
+ if (i > 0) out.push("[", react.createElement("tspan", { key: i, dx: fontSize * \u89D2\u62EC\u5F27\u306E\u9699\u9593 }, "]"));
22932
+ if (s !== "") out.push(s);
22933
+ });
22934
+ return out;
22935
+ }
22936
+
22937
+ // src/layout/head-label.ts
22938
+ var \u7AEF\u306E\u5B57\u3092\u623B\u3059\u8DDD\u96E2 = 22;
22939
+ var \u7AEF\u306E\u5B57\u3068\u7DDA\u306E\u9699\u9593 = 8;
22940
+ var \u7AEF\u306E\u5B57\u306E\u5927\u304D\u3055 = EDGE_LABEL_TEXT.sub.fontSize;
22941
+ var \u7BB1\u306E\u9AD8\u30552 = Math.round(\u7AEF\u306E\u5B57\u306E\u5927\u304D\u3055 * 1.2);
22942
+ var BASELINE_RATIO = 0.8;
22943
+ function \u6700\u5F8C\u306E\u5411\u304D(d) {
22944
+ const segs = extractPathSegments(d);
22945
+ const last = segs[segs.length - 1];
22946
+ if (!last) return null;
22947
+ const end = { x: last.x2, y: last.y2 };
22948
+ for (let i = segs.length - 1; i >= 0; i--) {
22949
+ const s = segs[i];
22950
+ const \u5236\u5FA1\u70B9 = s.cmd === "Q" ? s.ctrl?.[0] : s.cmd === "C" ? s.ctrl?.[1] : void 0;
22951
+ for (const p of [\u5236\u5FA1\u70B9, { x: s.x1, y: s.y1 }]) {
22952
+ if (!p) continue;
22953
+ const dx = end.x - p.x;
22954
+ const dy = end.y - p.y;
22955
+ const len = Math.hypot(dx, dy);
22956
+ if (len > 1e-6) return { end, ux: dx / len, uy: dy / len };
22957
+ }
22958
+ }
22959
+ return null;
22960
+ }
22961
+ function \u7AEF\u306E\u5B57\u3092\u7F6E\u304F(edge) {
22962
+ const text = edge.headLabel?.trim();
22963
+ if (!text) return null;
22964
+ const \u5411\u304D = \u6700\u5F8C\u306E\u5411\u304D(edge.d);
22965
+ if (!\u5411\u304D) return null;
22966
+ const { end, ux, uy } = \u5411\u304D;
22967
+ const bx = end.x - ux * \u7AEF\u306E\u5B57\u3092\u623B\u3059\u8DDD\u96E2;
22968
+ const by = end.y - uy * \u7AEF\u306E\u5B57\u3092\u623B\u3059\u8DDD\u96E2;
22969
+ const w = measureTextWidth(text, {
22970
+ fontSize: \u7AEF\u306E\u5B57\u306E\u5927\u304D\u3055,
22971
+ fontWeight: EDGE_LABEL_TEXT.sub.fontWeight,
22972
+ fontFamily: "mono"
22973
+ });
22974
+ const h = \u7BB1\u306E\u9AD8\u30552;
22975
+ const \u672D = hasRenderedLabel(edge) && edge.labelX !== void 0 && edge.labelY !== void 0 ? { x: edge.labelX, y: edge.labelY } : null;
22976
+ let anchor;
22977
+ let box;
22978
+ if (Math.abs(ux) >= Math.abs(uy)) {
22979
+ const \u53F3\u3078\u9032\u3080 = ux > 0;
22980
+ const \u4E0A\u306B\u7F6E\u304F = \u672D !== null && \u672D.y > by;
22981
+ anchor = \u53F3\u3078\u9032\u3080 ? "end" : "start";
22982
+ box = {
22983
+ x: \u53F3\u3078\u9032\u3080 ? bx - w : bx,
22984
+ y: \u4E0A\u306B\u7F6E\u304F ? by - \u7AEF\u306E\u5B57\u3068\u7DDA\u306E\u9699\u9593 - h : by + \u7AEF\u306E\u5B57\u3068\u7DDA\u306E\u9699\u9593,
22985
+ w,
22986
+ h
22987
+ };
22988
+ } else {
22989
+ const \u4E0B\u3078\u9032\u3080 = uy > 0;
22990
+ const \u53F3\u306B\u7F6E\u304F = \u672D !== null && \u672D.x < bx;
22991
+ anchor = \u53F3\u306B\u7F6E\u304F ? "start" : "end";
22992
+ box = {
22993
+ x: \u53F3\u306B\u7F6E\u304F ? bx + \u7AEF\u306E\u5B57\u3068\u7DDA\u306E\u9699\u9593 : bx - \u7AEF\u306E\u5B57\u3068\u7DDA\u306E\u9699\u9593 - w,
22994
+ y: \u4E0B\u3078\u9032\u3080 ? by - h : by,
22995
+ w,
22996
+ h
22997
+ };
22998
+ }
22999
+ const x = anchor === "start" ? box.x : box.x + box.w;
23000
+ const y = box.y + (h - \u7AEF\u306E\u5B57\u306E\u5927\u304D\u3055) / 2 + \u7AEF\u306E\u5B57\u306E\u5927\u304D\u3055 * BASELINE_RATIO;
23001
+ return { x, y, anchor, box };
23002
+ }
22923
23003
  function defaultEdgeWidth(active) {
22924
23004
  return active ? 2.5 : 1.25;
22925
23005
  }
@@ -23175,13 +23255,40 @@ function CdlEdgeLabelView({
23175
23255
  fill: active ? color : "var(--cdl-text-dim, #5a6270)",
23176
23256
  opacity: labelTextOpacity("sub"),
23177
23257
  style: { fontFamily: fontFamilyAttr("sub") },
23178
- children: edge.sub
23258
+ children: \u7B49\u5E45\u306E\u5B57(edge.sub, EDGE_LABEL_TEXT.sub.fontSize)
23179
23259
  }
23180
23260
  )
23181
23261
  ]
23182
23262
  }
23183
23263
  );
23184
23264
  }
23265
+ function CdlEdgeHeadLabelView({
23266
+ edge,
23267
+ active
23268
+ }) {
23269
+ const \u7F6E\u304D\u5834\u6240 = \u7AEF\u306E\u5B57\u3092\u7F6E\u304F(edge);
23270
+ if (!\u7F6E\u304D\u5834\u6240) return null;
23271
+ const color = edgeColor(edge);
23272
+ return /* @__PURE__ */ jsxRuntime.jsx(
23273
+ "text",
23274
+ {
23275
+ "data-cdl-role": "edge-end-label",
23276
+ "data-cdl-edge-end-label-for": edge.id,
23277
+ x: \u7F6E\u304D\u5834\u6240.x,
23278
+ y: \u7F6E\u304D\u5834\u6240.y,
23279
+ fontSize: \u7AEF\u306E\u5B57\u306E\u5927\u304D\u3055,
23280
+ fontWeight: EDGE_LABEL_TEXT.sub.fontWeight,
23281
+ textAnchor: \u7F6E\u304D\u5834\u6240.anchor,
23282
+ fill: active ? color : "var(--cdl-text-dim, #5a6270)",
23283
+ stroke: "var(--cdl-label-bg, #ffffff)",
23284
+ strokeWidth: 4,
23285
+ strokeLinejoin: "round",
23286
+ paintOrder: "stroke",
23287
+ style: { fontFamily: fontFamilyAttr("sub") },
23288
+ children: \u7B49\u5E45\u306E\u5B57(edge.headLabel, \u7AEF\u306E\u5B57\u306E\u5927\u304D\u3055)
23289
+ }
23290
+ );
23291
+ }
23185
23292
 
23186
23293
  // src/kinds/node-tone.ts
23187
23294
  function nodeAccent(node, fallback) {
@@ -23255,7 +23362,7 @@ function ActorNode({ node, active, value }) {
23255
23362
  textAnchor: "end",
23256
23363
  fill: "var(--cdl-text, #1a1f2a)",
23257
23364
  style: { fontFamily: "'JetBrains Mono', monospace" },
23258
- children: value
23365
+ children: \u7B49\u5E45\u306E\u5B57(value, 42)
23259
23366
  }
23260
23367
  )
23261
23368
  ] });
@@ -23431,7 +23538,7 @@ function SequenceBoardNode({
23431
23538
  textAnchor: "middle",
23432
23539
  fill: "var(--cdl-text, #1a1f2a)",
23433
23540
  style: { fontFamily: "'JetBrains Mono', monospace" },
23434
- children: a.name
23541
+ children: \u7B49\u5E45\u306E\u5B57(a.name, SEQ_NAME)
23435
23542
  }
23436
23543
  ),
23437
23544
  a.subtitle && /* @__PURE__ */ jsxRuntime.jsx(
@@ -23621,7 +23728,7 @@ function EventNode({
23621
23728
  fill: "var(--cdl-text, #1a1f2a)",
23622
23729
  textAnchor: \u540D\u524D.anchor,
23623
23730
  style: { fontFamily: "'JetBrains Mono', monospace" },
23624
- children: node.title
23731
+ children: \u7B49\u5E45\u306E\u5B57(node.title, \u540D\u524D.size)
23625
23732
  }
23626
23733
  ),
23627
23734
  node.subtitle && /* @__PURE__ */ jsxRuntime.jsx(
@@ -23632,7 +23739,7 @@ function EventNode({
23632
23739
  fontSize: 21,
23633
23740
  fill: "var(--cdl-text-dim, #5a6270)",
23634
23741
  style: { fontFamily: "'JetBrains Mono', monospace" },
23635
- children: node.subtitle
23742
+ children: \u7B49\u5E45\u306E\u5B57(node.subtitle, 21)
23636
23743
  }
23637
23744
  )
23638
23745
  ] })
@@ -23681,7 +23788,7 @@ function FunctionNode({ node, active }) {
23681
23788
  fill: "var(--cdl-text, #1a1f2a)",
23682
23789
  textAnchor: \u540D\u524D.anchor,
23683
23790
  style: { fontFamily: "'JetBrains Mono', monospace" },
23684
- children: node.title
23791
+ children: \u7B49\u5E45\u306E\u5B57(node.title, \u540D\u524D.size)
23685
23792
  }
23686
23793
  ),
23687
23794
  node.subtitle && /* @__PURE__ */ jsxRuntime.jsx("text", { x: 26, y: 126 - \u305A\u3089\u3057, fontSize: 20, fill: "var(--cdl-text-dim, #5a6270)", children: node.subtitle })
@@ -23794,7 +23901,7 @@ function StorageNode({
23794
23901
  fill: "var(--cdl-text, #1a1f2a)",
23795
23902
  textAnchor: \u540D\u524D.anchor,
23796
23903
  style: { fontFamily: "'JetBrains Mono', monospace" },
23797
- children: node.title
23904
+ children: \u7B49\u5E45\u306E\u5B57(node.title, \u540D\u524D.size)
23798
23905
  }
23799
23906
  ),
23800
23907
  rows.length > 0 && !\u5370\u3067\u5206\u3051\u308B && /* @__PURE__ */ jsxRuntime.jsx(
@@ -23855,7 +23962,7 @@ function StorageNode({
23855
23962
  fontWeight: \u5370\u3067\u5206\u3051\u308B ? 500 : 600,
23856
23963
  fill: "var(--cdl-text, #1a1f2a)",
23857
23964
  style: { fontFamily: "'JetBrains Mono', monospace" },
23858
- children: s.left
23965
+ children: \u7B49\u5E45\u306E\u5B57(s.left, 26)
23859
23966
  }
23860
23967
  ),
23861
23968
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -23869,7 +23976,7 @@ function StorageNode({
23869
23976
  textAnchor: \u5370\u3067\u5206\u3051\u308B ? "end" : "start",
23870
23977
  fill: \u5370\u3067\u5206\u3051\u308B ? "var(--cdl-text-dim, #5a6270)" : highlightRows ? accent : "var(--cdl-text, #1a1f2a)",
23871
23978
  style: { fontFamily: "'JetBrains Mono', monospace" },
23872
- children: s.right
23979
+ children: \u7B49\u5E45\u306E\u5B57(s.right, \u5370\u3067\u5206\u3051\u308B ? ROW_TYPE_FONT : 26)
23873
23980
  }
23874
23981
  )
23875
23982
  ] }, idx);
@@ -24143,8 +24250,8 @@ function GenericNode({
24143
24250
  /* @__PURE__ */ jsxRuntime.jsx("path", { d: style.icon, fill: style.eyebrowColor, opacity: 0.7, transform: "scale(1)" })
24144
24251
  ] }),
24145
24252
  node.eyebrow && /* @__PURE__ */ jsxRuntime.jsx("text", { x: textX, y: eyebrowY, fontSize: 17, fontWeight: 700, letterSpacing: 1.5, fill: paint.onSolid ? "var(--cdl-node-fill, #ffffff)" : style.eyebrowColor, textAnchor, dominantBaseline, children: node.eyebrow }),
24146
- /* @__PURE__ */ jsxRuntime.jsx("text", { "data-cdl-role": "node-label", x: textX, y: titleY, fontSize: 20, fontWeight: 700, fill: paint.onSolid ? "var(--cdl-node-fill, #ffffff)" : "var(--cdl-text, #1a1f2a)", textAnchor, dominantBaseline, style: node.kind === "function" || node.kind === "api" ? { fontFamily: "'JetBrains Mono', monospace" } : void 0, children: node.title }),
24147
- node.subtitle && /* @__PURE__ */ jsxRuntime.jsx("text", { x: textX, y: subtitleY, fontSize: 17, fill: paint.onSolid ? "var(--cdl-node-fill, #ffffff)" : "var(--cdl-text-dim, #5a6270)", textAnchor, dominantBaseline, style: { fontFamily: "'JetBrains Mono', monospace" }, children: node.subtitle }),
24253
+ /* @__PURE__ */ jsxRuntime.jsx("text", { "data-cdl-role": "node-label", x: textX, y: titleY, fontSize: 20, fontWeight: 700, fill: paint.onSolid ? "var(--cdl-node-fill, #ffffff)" : "var(--cdl-text, #1a1f2a)", textAnchor, dominantBaseline, style: node.kind === "function" || node.kind === "api" ? { fontFamily: "'JetBrains Mono', monospace" } : void 0, children: node.kind === "function" || node.kind === "api" ? \u7B49\u5E45\u306E\u5B57(node.title, 20) : node.title }),
24254
+ node.subtitle && /* @__PURE__ */ jsxRuntime.jsx("text", { x: textX, y: subtitleY, fontSize: 17, fill: paint.onSolid ? "var(--cdl-node-fill, #ffffff)" : "var(--cdl-text-dim, #5a6270)", textAnchor, dominantBaseline, style: { fontFamily: "'JetBrains Mono', monospace" }, children: \u7B49\u5E45\u306E\u5B57(node.subtitle, 17) }),
24148
24255
  resolvedValue && /* @__PURE__ */ jsxRuntime.jsx(
24149
24256
  "text",
24150
24257
  {
@@ -24155,7 +24262,7 @@ function GenericNode({
24155
24262
  textAnchor: "end",
24156
24263
  fill: "var(--cdl-text, #1a1f2a)",
24157
24264
  style: { fontFamily: "'JetBrains Mono', monospace" },
24158
- children: resolvedValue
24265
+ children: \u7B49\u5E45\u306E\u5B57(resolvedValue, 32)
24159
24266
  }
24160
24267
  ),
24161
24268
  node.rows && node.rows.length > 0 && (() => {
@@ -24180,7 +24287,7 @@ function GenericNode({
24180
24287
  textAnchor: "start",
24181
24288
  fill: "var(--cdl-text, #1a1f2a)",
24182
24289
  style: { fontFamily: "'JetBrains Mono', monospace" },
24183
- children: s.right
24290
+ children: \u7B49\u5E45\u306E\u5B57(s.right, 21)
24184
24291
  }
24185
24292
  )
24186
24293
  ] }, i)) });
@@ -31466,6 +31573,14 @@ function CdlStage({
31466
31573
  },
31467
31574
  `label-${edge.id}`
31468
31575
  )),
31576
+ laid.edges.filter((edge) => edge.headLabel && \u77E2\u5370\u306E\u51FA\u756A.\u51FA\u308B\u304B(edge.id)).map((edge) => /* @__PURE__ */ jsxRuntime.jsx(
31577
+ CdlEdgeHeadLabelView,
31578
+ {
31579
+ edge,
31580
+ active: activeSet.has(edge.id)
31581
+ },
31582
+ `head-label-${edge.id}`
31583
+ )),
31469
31584
  debug && /* @__PURE__ */ jsxRuntime.jsxs("g", { pointerEvents: "none", children: [
31470
31585
  laid.bboxes.map((bbox, idx) => {
31471
31586
  const color = bbox.kind === "node" ? "#3b82f6" : bbox.kind === "lane-label" ? "#a855f7" : bbox.kind === "edge-label" ? "#10b981" : "#f59e0b";
@@ -32510,7 +32625,9 @@ function classDiagram(preset) {
32510
32625
  b.edge(r.from, r.to, {
32511
32626
  id: edgeId,
32512
32627
  label: r.label ?? r.type,
32513
- ...r.cardinality ? { sub: r.cardinality } : {},
32628
+ // 多重度は行き先の端に添える (#821)。 札の下の行に入れると語と続けて読まれ
32629
+ // (英字の頃は「HAS 1..*」 が「HAS1」)、 どちらの端の数かも読めない
32630
+ ...r.cardinality ? { headLabel: r.cardinality } : {},
32514
32631
  tone: r.tone ?? \u56F3\u306E\u65E2\u5B9A ?? CLASS_RELATION_TONE[r.type],
32515
32632
  // 関係の種類から線と端の形を決める (cdl#574 / #578)。 書いた値が勝つ
32516
32633
  style: r.style ?? \u898B\u305F\u76EE.style,