@cardenelabs/cdl 0.50.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.
@@ -686,6 +686,13 @@ type CdlEdge = {
686
686
  * compileEr 経路で edge label に "(1:N)" 形式で併記、 他 preset では透過。
687
687
  */
688
688
  cardinality?: string;
689
+ /**
690
+ * 行き先の端に添える短い字 (#821)。 クラス図の多重度 (`1..*`) に使う。
691
+ *
692
+ * 線の中ほどの札 (`label` / `sub`) に入れると、どちらの端の数かが読めない。
693
+ * 置き場所は線の最後の区間から決まる (`layout/head-label.ts`)。
694
+ */
695
+ headLabel?: string;
689
696
  /** label の x 方向 fine offset (default 0)、 著者が細かく位置調整するための逃げ道 */
690
697
  labelOffsetX?: number;
691
698
  /** label の y 方向 fine offset (default 0、 + で下、 - で上) */
@@ -686,6 +686,13 @@ type CdlEdge = {
686
686
  * compileEr 経路で edge label に "(1:N)" 形式で併記、 他 preset では透過。
687
687
  */
688
688
  cardinality?: string;
689
+ /**
690
+ * 行き先の端に添える短い字 (#821)。 クラス図の多重度 (`1..*`) に使う。
691
+ *
692
+ * 線の中ほどの札 (`label` / `sub`) に入れると、どちらの端の数かが読めない。
693
+ * 置き場所は線の最後の区間から決まる (`layout/head-label.ts`)。
694
+ */
695
+ headLabel?: string;
689
696
  /** label の x 方向 fine offset (default 0)、 著者が細かく位置調整するための逃げ道 */
690
697
  labelOffsetX?: number;
691
698
  /** label の y 方向 fine offset (default 0、 + で下、 - で上) */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cardenelabs/cdl",
3
- "version": "0.50.0",
3
+ "version": "0.51.0",
4
4
  "description": "CDL (Chainome Diagram Language). Mermaid-like declarative DSL that compiles to animated SVG diagrams. Built for blockchain / Solidity flows, generic enough for sequence / flow / state / ER / topology diagrams.",
5
5
  "license": "MIT",
6
6
  "author": "cardene777",
package/src/builder.ts CHANGED
@@ -1452,6 +1452,8 @@ export type DiagramBuilder = {
1452
1452
  guard?: string;
1453
1453
  /** ER 関係 cardinality、 text-dsl v0.5 inline option (`cardinality: "1:N"`) を CdlEdge.cardinality へ透過 */
1454
1454
  cardinality?: string;
1455
+ /** 行き先の端に添える字 (#821)。 クラス図の多重度に使う */
1456
+ headLabel?: string;
1455
1457
  labelOffsetX?: number;
1456
1458
  labelOffsetY?: number;
1457
1459
  routing?: "default" | "back-detour";
@@ -1560,6 +1562,8 @@ export type DiagramBuilder = {
1560
1562
  tailHeadFill?: EdgeHeadFill;
1561
1563
  guard?: string;
1562
1564
  cardinality?: string;
1565
+ /** 行き先の端に添える字 (#821)。 クラス図の多重度に使う */
1566
+ headLabel?: string;
1563
1567
  labelOffsetX?: number;
1564
1568
  labelOffsetY?: number;
1565
1569
  routing?: "default" | "back-detour";
@@ -1776,6 +1780,7 @@ export function diagram(
1776
1780
  tailHeadFill: opts.tailHeadFill,
1777
1781
  guard: opts.guard,
1778
1782
  cardinality: opts.cardinality,
1783
+ headLabel: opts.headLabel,
1779
1784
  labelOffsetX: opts.labelOffsetX,
1780
1785
  labelOffsetY: opts.labelOffsetY,
1781
1786
  routing: opts.routing,
@@ -1910,6 +1915,7 @@ export function diagram(
1910
1915
  tailHeadFill: d.tailHeadFill,
1911
1916
  guard: d.guard,
1912
1917
  cardinality: d.cardinality,
1918
+ headLabel: d.headLabel,
1913
1919
  labelOffsetX: d.labelOffsetX,
1914
1920
  labelOffsetY: d.labelOffsetY,
1915
1921
  routing: d.routing,
@@ -0,0 +1,136 @@
1
+ /**
2
+ * 行き先の端に添える字 (`headLabel`) の置き場所 (#821)。
3
+ *
4
+ * 描く側 (`render/edges.tsx`) と検査が同じ値を使うため、計算をここに 1 つだけ置く。
5
+ *
6
+ * 線の最後の区間の向きで決める。 端から `端の字を戻す距離` だけ線を戻った点を基準に、
7
+ * 線の脇へ置く。 字は端に近い側の縁を基準点に揃える = 字の長さが変わっても、端から字までの
8
+ * 隙間は変わらない。
9
+ *
10
+ * **線の札の反対側に置く**。 札と同じ側に置くと、札の語と多重度が続けて読まれる (「持つ」 の
11
+ * 隣に `1..*`、 英字の頃は「HAS1」)。 実測で、札と同じ側 (横向きなら上、縦向きなら右) に
12
+ * 置く形は見本帳のクラス図で 1 本が札と重なり、 もう 1 本が札と 2 world まで寄った。
13
+ * 札を持たない線は、横向きなら下、縦向きなら左に置く (engine が札を置く既定の側の反対)。
14
+ *
15
+ * 斜めの線と曲線は、向きの大きいほうの軸で横向き / 縦向きに振り分ける。 曲線の向きは
16
+ * 終点の接線 (最後の制御点から終点へ) を使う。 弦の向きを使うと、大きく膨らむ曲線で
17
+ * 線が端へ入ってくる向きと別の向きが出る。
18
+ */
19
+ import { EDGE_LABEL_TEXT } from "../label-text";
20
+ import { extractPathSegments } from "./collisions";
21
+ import { hasRenderedLabel } from "./spec";
22
+ import { measureTextWidth } from "./text-width";
23
+
24
+ /**
25
+ * 端から字までの、線に沿った距離 (world)。
26
+ *
27
+ * 端の印の長さの最大 (光っている時の 14、 `render/stage.tsx` の marker の大きさ) に隙間 8 を足す。
28
+ * 印を持たない端 (菱が出どころ側にある「持つ」 「抱える」) でも同じ距離にする = 端ごとに
29
+ * 距離が変わると、図の中で多重度の位置が揃わない。
30
+ */
31
+ export const 端の字を戻す距離 = 22;
32
+
33
+ /** 線と字の間の隙間 (world) */
34
+ export const 端の字と線の隙間 = 8;
35
+
36
+ /** 端の字の大きさ。 札の下の行と同じ等幅の字 = 多重度を札に入れていた頃と同じ見え方 */
37
+ export const 端の字の大きさ = EDGE_LABEL_TEXT.sub.fontSize;
38
+
39
+ /** 字の箱の高さ。 字の大きさに上下の余白を足す */
40
+ const 箱の高さ = Math.round(端の字の大きさ * 1.2);
41
+
42
+ /** 字の大きさに対する baseline の深さ。 等幅の字の ascent が字の大きさの 8 割ほど */
43
+ const BASELINE_RATIO = 0.8;
44
+
45
+ export interface 端の字の置き場所 {
46
+ /** `text` の x。 どちらの縁かは `anchor` が決める */
47
+ x: number;
48
+ /** `text` の baseline の y */
49
+ y: number;
50
+ anchor: "start" | "end";
51
+ /** 字が占める箱 (world)。 重なりの検査に使う */
52
+ box: { x: number; y: number; w: number; h: number };
53
+ }
54
+
55
+ /**
56
+ * 線の終点と、線が終点へ入ってくる向き (単位ベクトル)。
57
+ *
58
+ * 最後の区間の長さが 0 の時は 1 つ前の区間へ戻る。 向きが取れない (区間が無い / 全て長さ 0)
59
+ * 時は `null`。
60
+ */
61
+ function 最後の向き(d: string): { end: { x: number; y: number }; ux: number; uy: number } | null {
62
+ const segs = extractPathSegments(d);
63
+ const last = segs[segs.length - 1];
64
+ if (!last) return null;
65
+ const end = { x: last.x2, y: last.y2 };
66
+ for (let i = segs.length - 1; i >= 0; i--) {
67
+ const s = segs[i]!;
68
+ const 制御点 = s.cmd === "Q" ? s.ctrl?.[0] : s.cmd === "C" ? s.ctrl?.[1] : undefined;
69
+ for (const p of [制御点, { x: s.x1, y: s.y1 }]) {
70
+ if (!p) continue;
71
+ const dx = end.x - p.x;
72
+ const dy = end.y - p.y;
73
+ const len = Math.hypot(dx, dy);
74
+ if (len > 1e-6) return { end, ux: dx / len, uy: dy / len };
75
+ }
76
+ }
77
+ return null;
78
+ }
79
+
80
+ /** `headLabel` を描く位置。 字が空か、線の向きが取れない時は `null` (描かない) */
81
+ export function 端の字を置く(edge: {
82
+ d: string;
83
+ headLabel?: string;
84
+ label?: string;
85
+ sub?: string;
86
+ labelX?: number;
87
+ labelY?: number;
88
+ }): 端の字の置き場所 | null {
89
+ const text = edge.headLabel?.trim();
90
+ if (!text) return null;
91
+ const 向き = 最後の向き(edge.d);
92
+ if (!向き) return null;
93
+ const { end, ux, uy } = 向き;
94
+ const bx = end.x - ux * 端の字を戻す距離;
95
+ const by = end.y - uy * 端の字を戻す距離;
96
+ const w = measureTextWidth(text, {
97
+ fontSize: 端の字の大きさ,
98
+ fontWeight: EDGE_LABEL_TEXT.sub.fontWeight,
99
+ fontFamily: "mono",
100
+ });
101
+ const h = 箱の高さ;
102
+ // 札の中心。 札を描かない線と、位置がまだ無い線は `null`
103
+ const 札 =
104
+ hasRenderedLabel(edge) && edge.labelX !== undefined && edge.labelY !== undefined
105
+ ? { x: edge.labelX, y: edge.labelY }
106
+ : null;
107
+
108
+ let anchor: "start" | "end";
109
+ let box: { x: number; y: number; w: number; h: number };
110
+ if (Math.abs(ux) >= Math.abs(uy)) {
111
+ // 横向き。 右へ進む線は字が端の手前 (左) に伸びるので右の縁を揃える
112
+ const 右へ進む = ux > 0;
113
+ const 上に置く = 札 !== null && 札.y > by;
114
+ anchor = 右へ進む ? "end" : "start";
115
+ box = {
116
+ x: 右へ進む ? bx - w : bx,
117
+ y: 上に置く ? by - 端の字と線の隙間 - h : by + 端の字と線の隙間,
118
+ w,
119
+ h,
120
+ };
121
+ } else {
122
+ // 縦向き。 下へ進む線は字が端の手前 (上) に来るので下の辺を揃える
123
+ const 下へ進む = uy > 0;
124
+ const 右に置く = 札 !== null && 札.x < bx;
125
+ anchor = 右に置く ? "start" : "end";
126
+ box = {
127
+ x: 右に置く ? bx + 端の字と線の隙間 : bx - 端の字と線の隙間 - w,
128
+ y: 下へ進む ? by - h : by,
129
+ w,
130
+ h,
131
+ };
132
+ }
133
+ const x = anchor === "start" ? box.x : box.x + box.w;
134
+ const y = box.y + (h - 端の字の大きさ) / 2 + 端の字の大きさ * BASELINE_RATIO;
135
+ return { x, y, anchor, box };
136
+ }
package/src/presets.ts CHANGED
@@ -1430,7 +1430,7 @@ export type UmlRelation = {
1430
1430
  type: ClassRelationType;
1431
1431
  /** 関係 label (default は type 名) */
1432
1432
  label?: string;
1433
- /** 多重度 (例 "1..*" / "0..1") */
1433
+ /** 多重度 (例 "1..*" / "0..1")。 行き先 (`to`) の端に添える (#821) */
1434
1434
  cardinality?: string;
1435
1435
  tone?: Tone;
1436
1436
  /**
@@ -1573,7 +1573,9 @@ export function classDiagram(preset: ClassDiagramPreset): ClassDiagramBuilder {
1573
1573
  b.edge(r.from, r.to, {
1574
1574
  id: edgeId,
1575
1575
  label: r.label ?? r.type,
1576
- ...(r.cardinality ? { sub: r.cardinality } : {}),
1576
+ // 多重度は行き先の端に添える (#821)。 札の下の行に入れると語と続けて読まれ
1577
+ // (英字の頃は「HAS 1..*」 が「HAS1」)、 どちらの端の数かも読めない
1578
+ ...(r.cardinality ? { headLabel: r.cardinality } : {}),
1577
1579
  tone: r.tone ?? 図の既定 ?? CLASS_RELATION_TONE[r.type],
1578
1580
  // 関係の種類から線と端の形を決める (cdl#574 / #578)。 書いた値が勝つ
1579
1581
  style: r.style ?? 見た目.style,
@@ -1,6 +1,7 @@
1
1
  import { 等幅の字 } from "./mono-text";
2
2
  import type { JSX } from "react";
3
3
  import { computeLabelBBoxWorld, hasRenderedLabel, LABEL_PILL_PAD_X } from "../layout/spec";
4
+ import { 端の字の大きさ, 端の字を置く } from "../layout/head-label";
4
5
  import type { EdgeRole, LaidEdge, Tone } from "../types";
5
6
  import { edgeHeadMarkerName } from "./edge-head";
6
7
  import { toneColor } from "./tone";
@@ -394,3 +395,40 @@ export function CdlEdgeLabelView({
394
395
  </g>
395
396
  );
396
397
  }
398
+
399
+ /**
400
+ * 行き先の端に添える字 (#821)。 札と同じく最前面の pass で描く。
401
+ *
402
+ * 下地の四角は敷かず、字の周りに下地色の縁取りを付ける (`paint-order: stroke`)。 字は線の脇、
403
+ * 箱の角の近くに置くので、四角い下地だと箱の角や線に被って小さな箱に見える。
404
+ */
405
+ export function CdlEdgeHeadLabelView({
406
+ edge,
407
+ active,
408
+ }: {
409
+ edge: LaidEdge;
410
+ active: boolean;
411
+ }): JSX.Element | null {
412
+ const 置き場所 = 端の字を置く(edge);
413
+ if (!置き場所) return null;
414
+ const color = edgeColor(edge);
415
+ return (
416
+ <text
417
+ data-cdl-role="edge-end-label"
418
+ data-cdl-edge-end-label-for={edge.id}
419
+ x={置き場所.x}
420
+ y={置き場所.y}
421
+ fontSize={端の字の大きさ}
422
+ fontWeight={EDGE_LABEL_TEXT.sub.fontWeight}
423
+ textAnchor={置き場所.anchor}
424
+ fill={active ? color : "var(--cdl-text-dim, #5a6270)"}
425
+ stroke="var(--cdl-label-bg, #ffffff)"
426
+ strokeWidth={4}
427
+ strokeLinejoin="round"
428
+ paintOrder="stroke"
429
+ style={{ fontFamily: fontFamilyAttr("sub") }}
430
+ >
431
+ {等幅の字(edge.headLabel, 端の字の大きさ)}
432
+ </text>
433
+ );
434
+ }
@@ -20,7 +20,7 @@ import {
20
20
  edgeHeadMarkerName,
21
21
  edgeHeadPaint,
22
22
  } from "./edge-head";
23
- import { CdlEdgeView, CdlEdgeGlowView, CdlEdgeLabelView } from "./edges";
23
+ import { CdlEdgeView, CdlEdgeGlowView, CdlEdgeHeadLabelView, CdlEdgeLabelView } from "./edges";
24
24
  import { CdlNodeView } from "./nodes";
25
25
  import { resolveChartData } from "./payload-binding";
26
26
  import { relatedFocusSet } from "./relation-focus";
@@ -453,6 +453,17 @@ export function CdlStage({
453
453
  />
454
454
  ))}
455
455
 
456
+ {/* 行き先の端に添える字 (#821)。 札と同じ最前面の pass */}
457
+ {laid.edges
458
+ .filter((edge) => edge.headLabel && 矢印の出番.出るか(edge.id))
459
+ .map((edge) => (
460
+ <CdlEdgeHeadLabelView
461
+ key={`head-label-${edge.id}`}
462
+ edge={edge}
463
+ active={activeSet.has(edge.id)}
464
+ />
465
+ ))}
466
+
456
467
  {/* debug overlay = engine が把握している bbox + collision を半透明で重ね描き */}
457
468
  {debug && (
458
469
  <g pointerEvents="none">
package/src/types.ts CHANGED
@@ -758,6 +758,13 @@ export type CdlEdge = {
758
758
  * compileEr 経路で edge label に "(1:N)" 形式で併記、 他 preset では透過。
759
759
  */
760
760
  cardinality?: string;
761
+ /**
762
+ * 行き先の端に添える短い字 (#821)。 クラス図の多重度 (`1..*`) に使う。
763
+ *
764
+ * 線の中ほどの札 (`label` / `sub`) に入れると、どちらの端の数かが読めない。
765
+ * 置き場所は線の最後の区間から決まる (`layout/head-label.ts`)。
766
+ */
767
+ headLabel?: string;
761
768
  /** label の x 方向 fine offset (default 0)、 著者が細かく位置調整するための逃げ道 */
762
769
  labelOffsetX?: number;
763
770
  /** label の y 方向 fine offset (default 0、 + で下、 - で上) */