@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 +33 -1
- package/dist/index.cjs +135 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -3
- package/dist/index.d.ts +7 -3
- package/dist/index.js +136 -19
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +130 -17
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +131 -18
- package/dist/react.js.map +1 -1
- package/dist/{render-C53-PeJZ.d.cts → render-BoBEyCu-.d.cts} +7 -0
- package/dist/{render-C53-PeJZ.d.ts → render-BoBEyCu-.d.ts} +7 -0
- package/package.json +1 -1
- package/src/builder.ts +6 -0
- package/src/kinds/actor.tsx +2 -1
- package/src/kinds/event.tsx +3 -2
- package/src/kinds/function.tsx +2 -1
- package/src/kinds/generic.tsx +5 -4
- package/src/kinds/sequence-board.tsx +2 -1
- package/src/kinds/storage.tsx +4 -3
- package/src/layout/head-label.ts +136 -0
- package/src/presets.ts +4 -2
- package/src/render/edges.tsx +40 -1
- package/src/render/mono-text.ts +44 -0
- package/src/render/stage.tsx +12 -1
- package/src/types.ts +7 -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、 + で下、 - で上) */
|
|
@@ -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.
|
|
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,
|
package/src/kinds/actor.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { 等幅の字 } from "../render/mono-text";
|
|
1
2
|
import type { JSX } from "react";
|
|
2
3
|
import type { LaidNode } from "../types";
|
|
3
4
|
import { nodeAccent } from "./node-tone";
|
|
@@ -84,7 +85,7 @@ export function ActorNode({ node, active, value }: { node: LaidNode; active: boo
|
|
|
84
85
|
fill="var(--cdl-text, #1a1f2a)"
|
|
85
86
|
style={{ fontFamily: "'JetBrains Mono', monospace" }}
|
|
86
87
|
>
|
|
87
|
-
{value}
|
|
88
|
+
{等幅の字(value, 42)}
|
|
88
89
|
</text>
|
|
89
90
|
)}
|
|
90
91
|
</g>
|
package/src/kinds/event.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { 等幅の字 } from "../render/mono-text";
|
|
1
2
|
import type { JSX } from "react";
|
|
2
3
|
import type { LaidNode } from "../types";
|
|
3
4
|
import { nodeAccent } from "./node-tone";
|
|
@@ -63,7 +64,7 @@ export function EventNode({
|
|
|
63
64
|
textAnchor={名前.anchor}
|
|
64
65
|
style={{ fontFamily: "'JetBrains Mono', monospace" }}
|
|
65
66
|
>
|
|
66
|
-
{node.title}
|
|
67
|
+
{等幅の字(node.title, 名前.size)}
|
|
67
68
|
</text>
|
|
68
69
|
{node.subtitle && (
|
|
69
70
|
<text
|
|
@@ -73,7 +74,7 @@ export function EventNode({
|
|
|
73
74
|
fill="var(--cdl-text-dim, #5a6270)"
|
|
74
75
|
style={{ fontFamily: "'JetBrains Mono', monospace" }}
|
|
75
76
|
>
|
|
76
|
-
{node.subtitle}
|
|
77
|
+
{等幅の字(node.subtitle, 21)}
|
|
77
78
|
</text>
|
|
78
79
|
)}
|
|
79
80
|
</g>
|
package/src/kinds/function.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { 等幅の字 } from "../render/mono-text";
|
|
1
2
|
import type { JSX } from "react";
|
|
2
3
|
import type { LaidNode } from "../types";
|
|
3
4
|
import { nodeAccent } from "./node-tone";
|
|
@@ -53,7 +54,7 @@ export function FunctionNode({ node, active }: { node: LaidNode; active: boolean
|
|
|
53
54
|
textAnchor={名前.anchor}
|
|
54
55
|
style={{ fontFamily: "'JetBrains Mono', monospace" }}
|
|
55
56
|
>
|
|
56
|
-
{node.title}
|
|
57
|
+
{等幅の字(node.title, 名前.size)}
|
|
57
58
|
</text>
|
|
58
59
|
{node.subtitle && (
|
|
59
60
|
<text x={26} y={126 - ずらし} fontSize={20} fill="var(--cdl-text-dim, #5a6270)">
|
package/src/kinds/generic.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { 等幅の字 } from "../render/mono-text";
|
|
1
2
|
import type { JSX } from "react";
|
|
2
3
|
import type { LaidNode, NodeKind } from "../types";
|
|
3
4
|
import { toneColor } from "./node-tone";
|
|
@@ -423,11 +424,11 @@ export function GenericNode({
|
|
|
423
424
|
</text>
|
|
424
425
|
)}
|
|
425
426
|
<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={textAnchor} dominantBaseline={dominantBaseline} style={node.kind === "function" || node.kind === "api" ? { fontFamily: "'JetBrains Mono', monospace" } : undefined}>
|
|
426
|
-
{node.title}
|
|
427
|
+
{node.kind === "function" || node.kind === "api" ? 等幅の字(node.title, 20) : node.title}
|
|
427
428
|
</text>
|
|
428
429
|
{node.subtitle && (
|
|
429
430
|
<text x={textX} y={subtitleY} fontSize={17} fill={paint.onSolid ? "var(--cdl-node-fill, #ffffff)" : "var(--cdl-text-dim, #5a6270)"} textAnchor={textAnchor} dominantBaseline={dominantBaseline} style={{ fontFamily: "'JetBrains Mono', monospace" }}>
|
|
430
|
-
{node.subtitle}
|
|
431
|
+
{等幅の字(node.subtitle, 17)}
|
|
431
432
|
</text>
|
|
432
433
|
)}
|
|
433
434
|
{resolvedValue && (
|
|
@@ -440,7 +441,7 @@ export function GenericNode({
|
|
|
440
441
|
fill="var(--cdl-text, #1a1f2a)"
|
|
441
442
|
style={{ fontFamily: "'JetBrains Mono', monospace" }}
|
|
442
443
|
>
|
|
443
|
-
{resolvedValue}
|
|
444
|
+
{等幅の字(resolvedValue, 32)}
|
|
444
445
|
</text>
|
|
445
446
|
)}
|
|
446
447
|
{node.rows && node.rows.length > 0 && (() => {
|
|
@@ -473,7 +474,7 @@ export function GenericNode({
|
|
|
473
474
|
fill="var(--cdl-text, #1a1f2a)"
|
|
474
475
|
style={{ fontFamily: "'JetBrains Mono', monospace" }}
|
|
475
476
|
>
|
|
476
|
-
{s.right}
|
|
477
|
+
{等幅の字(s.right, 21)}
|
|
477
478
|
</text>
|
|
478
479
|
</g>
|
|
479
480
|
))}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { 等幅の字 } from "../render/mono-text";
|
|
1
2
|
import type { JSX } from "react";
|
|
2
3
|
import type { LaidNode } from "../types";
|
|
3
4
|
import { interpolate } from "../render/utils";
|
|
@@ -144,7 +145,7 @@ export function SequenceBoardNode({
|
|
|
144
145
|
fill="var(--cdl-text, #1a1f2a)"
|
|
145
146
|
style={{ fontFamily: "'JetBrains Mono', monospace" }}
|
|
146
147
|
>
|
|
147
|
-
{a.name}
|
|
148
|
+
{等幅の字(a.name, SEQ_NAME)}
|
|
148
149
|
</text>
|
|
149
150
|
{a.subtitle && (
|
|
150
151
|
<text
|
package/src/kinds/storage.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { 等幅の字 } from "../render/mono-text";
|
|
1
2
|
import type { JSX } from "react";
|
|
2
3
|
import type { LaidNode, RowMark } from "../types";
|
|
3
4
|
import { nodeAccent } from "./node-tone";
|
|
@@ -233,7 +234,7 @@ export function StorageNode({
|
|
|
233
234
|
textAnchor={名前.anchor}
|
|
234
235
|
style={{ fontFamily: "'JetBrains Mono', monospace" }}
|
|
235
236
|
>
|
|
236
|
-
{node.title}
|
|
237
|
+
{等幅の字(node.title, 名前.size)}
|
|
237
238
|
</text>
|
|
238
239
|
{/*
|
|
239
240
|
表の区切り線 2 本 (`#419`)。
|
|
@@ -323,7 +324,7 @@ export function StorageNode({
|
|
|
323
324
|
fill="var(--cdl-text, #1a1f2a)"
|
|
324
325
|
style={{ fontFamily: "'JetBrains Mono', monospace" }}
|
|
325
326
|
>
|
|
326
|
-
{s.left}
|
|
327
|
+
{等幅の字(s.left, 26)}
|
|
327
328
|
</text>
|
|
328
329
|
{/*
|
|
329
330
|
型は **読み手が探すものではない**ので、右端で揃えて薄く引く。 名前の右に続けて
|
|
@@ -347,7 +348,7 @@ export function StorageNode({
|
|
|
347
348
|
}
|
|
348
349
|
style={{ fontFamily: "'JetBrains Mono', monospace" }}
|
|
349
350
|
>
|
|
350
|
-
{s.right}
|
|
351
|
+
{等幅の字(s.right, 印で分ける ? ROW_TYPE_FONT : 26)}
|
|
351
352
|
</text>
|
|
352
353
|
</g>
|
|
353
354
|
);
|
|
@@ -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
|
-
|
|
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,
|
package/src/render/edges.tsx
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { 等幅の字 } from "./mono-text";
|
|
1
2
|
import type { JSX } from "react";
|
|
2
3
|
import { computeLabelBBoxWorld, hasRenderedLabel, LABEL_PILL_PAD_X } from "../layout/spec";
|
|
4
|
+
import { 端の字の大きさ, 端の字を置く } from "../layout/head-label";
|
|
3
5
|
import type { EdgeRole, LaidEdge, Tone } from "../types";
|
|
4
6
|
import { edgeHeadMarkerName } from "./edge-head";
|
|
5
7
|
import { toneColor } from "./tone";
|
|
@@ -387,9 +389,46 @@ export function CdlEdgeLabelView({
|
|
|
387
389
|
opacity={labelTextOpacity("sub")}
|
|
388
390
|
style={{ fontFamily: fontFamilyAttr("sub") }}
|
|
389
391
|
>
|
|
390
|
-
{edge.sub}
|
|
392
|
+
{等幅の字(edge.sub, EDGE_LABEL_TEXT.sub.fontSize)}
|
|
391
393
|
</text>
|
|
392
394
|
)}
|
|
393
395
|
</g>
|
|
394
396
|
);
|
|
395
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
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 等幅の書体で描く文字の、 `[` と `]` の間にだけ隙間を足す (#817)。
|
|
3
|
+
*
|
|
4
|
+
* `JetBrains Mono` の `[` と `]` は横棒が字の枠いっぱいまで伸びている。 図を画面の幅に合わせて
|
|
5
|
+
* 縮めると (画面上 14px 前後)、 隣り合った 2 本の横棒がつながって `[]` が四角 1 つに見える
|
|
6
|
+
* (`string[]` が `string□` と読める)。 合字ではない = 合字を止めても見え方は変わらない。
|
|
7
|
+
*
|
|
8
|
+
* 隙間は `<tspan dx>` で足す。 文字は増やさないので `textContent` は元のまま (`string[]`) で、
|
|
9
|
+
* 文字を読む側 (検索 / 読み上げ / 写し取り) は変わらない。 `dx` は文字の塊を新しく始めないので、
|
|
10
|
+
* 右寄せ (`text-anchor: end`) も隙間を含めた全体に効く。
|
|
11
|
+
*
|
|
12
|
+
* `][` は横棒が外を向いていてつながらないので、 `[]` の組ごとに足せば足りる。
|
|
13
|
+
*
|
|
14
|
+
* **engine が等幅の書体で描く文字は全てここを通す**。 通していない箇所はソースの走査で落とす
|
|
15
|
+
* (`mono-bracket-gap.test.tsx`)。
|
|
16
|
+
*/
|
|
17
|
+
import { createElement, type ReactNode } from "react";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 隙間の幅 (字の大きさに対する比)。
|
|
21
|
+
*
|
|
22
|
+
* 等倍で撮り比べて、 全ての字の間を 0.06 空けると 2 本の横棒が分かれて読めた。 図はそれより小さく
|
|
23
|
+
* 縮めて描かれることもあるので、 少し余裕を持たせる。 1 組あたり字幅の 1 割に満たず、 字の送り幅の
|
|
24
|
+
* 計算 (等幅を前提に文字数から桁の位置を決める) には入れない。
|
|
25
|
+
*/
|
|
26
|
+
export const 角括弧の隙間 = 0.08;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 文字列の `[]` を `[` と、 前に隙間を持つ `]` に分けて返す。 `[]` を含まない文字と、 文字列でない
|
|
30
|
+
* 値 (数値 / 要素) はそのまま返す。
|
|
31
|
+
*
|
|
32
|
+
* @param text 描く文字
|
|
33
|
+
* @param fontSize その文字の大きさ (隙間はこれに比例する)
|
|
34
|
+
*/
|
|
35
|
+
export function 等幅の字(text: ReactNode, fontSize: number): ReactNode {
|
|
36
|
+
if (typeof text !== "string" || !text.includes("[]")) return text;
|
|
37
|
+
const 塊 = text.split("[]");
|
|
38
|
+
const out: ReactNode[] = [];
|
|
39
|
+
塊.forEach((s, i) => {
|
|
40
|
+
if (i > 0) out.push("[", createElement("tspan", { key: i, dx: fontSize * 角括弧の隙間 }, "]"));
|
|
41
|
+
if (s !== "") out.push(s);
|
|
42
|
+
});
|
|
43
|
+
return out;
|
|
44
|
+
}
|
package/src/render/stage.tsx
CHANGED
|
@@ -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、 + で下、 - で上) */
|