@cardenelabs/cdl 0.13.0 → 0.14.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 +49 -1
- package/dist/index.cjs +6249 -6192
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -2
- package/dist/index.d.ts +5 -2
- package/dist/index.js +6249 -6192
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +225 -170
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +225 -170
- package/dist/react.js.map +1 -1
- package/package.json +1 -1
- package/src/kinds/box-lines.ts +45 -4
- package/src/kinds/compact-title.ts +6 -6
- package/src/kinds/mind-map.tsx +59 -24
- package/src/kinds/tree.tsx +60 -25
- package/src/visual-validate.ts +90 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cardenelabs/cdl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.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/kinds/box-lines.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { textWidth } from "./text-width";
|
|
2
|
+
import { MIN_TEXT_SIZE } from "./compact-title";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* 箱の中に文字の行を積む (#471)。
|
|
@@ -84,6 +85,40 @@ export function 幅で切る(
|
|
|
84
85
|
return `${出}${省略}`;
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
/**
|
|
89
|
+
* 字を小さくする下限。
|
|
90
|
+
*
|
|
91
|
+
* `compact-title.ts` の `MIN_TEXT_SIZE` を共有する。 小型の箱の名前が既にこの値まで縮んでおり、
|
|
92
|
+
* 読める下限として使われている。 同じ値を参照すれば、同じ図の中で名前の下限が場所によって
|
|
93
|
+
* 変わらない。
|
|
94
|
+
*/
|
|
95
|
+
export const 縮めの下限 = MIN_TEXT_SIZE;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 幅に入るまで字を小さくし、下限まで縮めても入らなければ切る (#534)。
|
|
99
|
+
*
|
|
100
|
+
* **切るのは最後の手段にする**。 字の大きさを保ったまま切ると、全角 7 文字の名前
|
|
101
|
+
* (`新プロジェクト`) が `新プロジェ…` になって何の箱か読めない。 1 段小さくすれば
|
|
102
|
+
* 入る名前を切っていた。
|
|
103
|
+
*
|
|
104
|
+
* 縮めるのは **入るところまで** で、一律に下限へ落とさない。 落とすと 1 段で足りる名前まで
|
|
105
|
+
* 最小になり、同じ図の中で名前の大きさがばらつく。
|
|
106
|
+
*
|
|
107
|
+
* 元から下限以下の字は縮めない (補足の 9px がここに当たる)。 巻き込むと補足が読めなくなる。
|
|
108
|
+
*
|
|
109
|
+
* 箱の大きさは変えない。 高さの都合は呼出側 (`箱に積む`) が見る。
|
|
110
|
+
*/
|
|
111
|
+
export function 幅に収める(
|
|
112
|
+
text: string,
|
|
113
|
+
fontSize: number,
|
|
114
|
+
使える幅: number,
|
|
115
|
+
測る: (s: string, size: number) => number = textWidth,
|
|
116
|
+
): { text: string; fontSize: number } {
|
|
117
|
+
let size = fontSize;
|
|
118
|
+
while (size > 縮めの下限 && 測る(text, size) > 使える幅) size -= 1;
|
|
119
|
+
return { text: 幅で切る(text, size, 使える幅, 測る), fontSize: size };
|
|
120
|
+
}
|
|
121
|
+
|
|
87
122
|
/**
|
|
88
123
|
* 見た目の 1 字 (書記素クラスタ) に分ける。
|
|
89
124
|
*
|
|
@@ -115,16 +150,22 @@ export function 箱に積む(行: 積む行[], 箱の幅: number, 箱の高さ:
|
|
|
115
150
|
const 使える高さ = 箱の高さ - 上下の余白 * 2;
|
|
116
151
|
const 高さ = (rs: 積む行[]): number => rs.reduce((a, r) => a + r.fontSize * 行送りの比, 0);
|
|
117
152
|
|
|
118
|
-
// 入るまで後ろから落とす。
|
|
153
|
+
// 入るまで後ろから落とす。 先頭は落とさない。
|
|
154
|
+
// **判定は縮める前の大きさで行う** (#534)。 縮めた後で決めると、名前が長い箱でだけ
|
|
155
|
+
// 補足が復活する = 同じ高さの箱でも中身の長さで行数が変わり、挙動が読みにくくなる
|
|
119
156
|
const 残す = [...候補];
|
|
120
157
|
while (残す.length > 1 && 高さ(残す) > 使える高さ) 残す.pop();
|
|
121
158
|
|
|
122
159
|
const 幅 = 使える幅(箱の幅);
|
|
123
|
-
|
|
160
|
+
// **幅に収めるのを先に済ませる** (#534)。 縮めると行の高さも変わるため、後から縮めると
|
|
161
|
+
// `dy` が縮める前の大きさで出て、行の塊が箱の上下の中央からずれる
|
|
162
|
+
const 収めた = 残す.map((r) => ({ ...r, ...幅に収める(r.text, r.fontSize, 幅) }));
|
|
163
|
+
|
|
164
|
+
const 総高 = 高さ(収めた);
|
|
124
165
|
let 積み = -総高 / 2;
|
|
125
|
-
return
|
|
166
|
+
return 収めた.map((r) => {
|
|
126
167
|
const dy = 積み + ((行送りの比 + 字の高さの比) / 2) * r.fontSize;
|
|
127
168
|
積み += r.fontSize * 行送りの比;
|
|
128
|
-
return { ...r,
|
|
169
|
+
return { ...r, dy };
|
|
129
170
|
});
|
|
130
171
|
}
|
|
@@ -54,8 +54,8 @@ export const COMPACT_MAX_H = 100;
|
|
|
54
54
|
/** 小型で名前の左右に空ける幅の合計。 `card` の compact と同じ。 */
|
|
55
55
|
const COMPACT_PADDING_X = 16;
|
|
56
56
|
|
|
57
|
-
/**
|
|
58
|
-
const
|
|
57
|
+
/** 小型で名前を縮める共通の下限。 `card` と tree / mind-map で同じ値を使う。 */
|
|
58
|
+
export const MIN_TEXT_SIZE = 11;
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
61
|
* 字が baseline から上下へどれだけ張り出すかの見積り。
|
|
@@ -69,10 +69,10 @@ const DESCENT = 0.25;
|
|
|
69
69
|
/**
|
|
70
70
|
* 名前が収まる最小の帯の高さ。 これより低い帯では **収めきれない**。
|
|
71
71
|
*
|
|
72
|
-
* 読める下限 (`
|
|
72
|
+
* 読める下限 (`MIN_TEXT_SIZE`) を割ってまで縮めない。 その場合の張り出しは上下へ均等に出す =
|
|
73
73
|
* 下だけに落ちる形にはしない。
|
|
74
74
|
*/
|
|
75
|
-
export const MIN_BAND_H =
|
|
75
|
+
export const MIN_BAND_H = MIN_TEXT_SIZE * (ASCENT + DESCENT);
|
|
76
76
|
|
|
77
77
|
/** 目印 (`eyebrow`) の baseline と大きさ。 4 種で同じ値を使っている。 */
|
|
78
78
|
const EYEBROW_BASELINE = 38;
|
|
@@ -144,10 +144,10 @@ export function titlePlacement(
|
|
|
144
144
|
node.title,
|
|
145
145
|
Math.max(0, node.w - COMPACT_PADDING_X),
|
|
146
146
|
通常.size,
|
|
147
|
-
|
|
147
|
+
MIN_TEXT_SIZE,
|
|
148
148
|
);
|
|
149
149
|
const 帯で = Math.floor(帯 / (ASCENT + DESCENT));
|
|
150
|
-
const size = Math.max(
|
|
150
|
+
const size = Math.max(MIN_TEXT_SIZE, Math.min(幅で, 帯で));
|
|
151
151
|
const 字高 = size * (ASCENT + DESCENT);
|
|
152
152
|
|
|
153
153
|
// 収まるなら帯の中央。 収まらない時の逃がし方は目印の有無で変える
|
package/src/kinds/mind-map.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { JSX } from "react";
|
|
2
|
-
import { 使える幅,
|
|
2
|
+
import { 使える幅, 幅に収める, 箱に積む, type 積む行 } from "./box-lines";
|
|
3
3
|
import type { LaidNode, Tone, MindBranchNode, MindBranchPayload } from "../types";
|
|
4
4
|
import { TONE } from "../render/tone";
|
|
5
5
|
import { resolveMindData } from "../render/payload-binding";
|
|
@@ -28,6 +28,49 @@ export const MIND_BOX_GAP = 24;
|
|
|
28
28
|
* 深い階層は parent 方向へ扇状展開。 depth 別 tone を rotate。
|
|
29
29
|
* curved edge = bezier で親子を接続、 node は rounded rect で読みやすさ優先。
|
|
30
30
|
*/
|
|
31
|
+
/**
|
|
32
|
+
* 箱の内側に取る余白。 図の外枠と放射の描画領域の間。
|
|
33
|
+
*
|
|
34
|
+
* **module に置く** (#538)。 描く側の中に閉じていた間、 検査側は同じ値を書き写すしかなく、
|
|
35
|
+
* 片方を変えた時にもう片方が古いまま残る。
|
|
36
|
+
*/
|
|
37
|
+
const MIND_PAD = 40;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 放射の中の箱と枝を並べる (#538)。
|
|
41
|
+
*
|
|
42
|
+
* 描く側 (`MindMapNode`) と検査側 (`visual-validate.ts` の `box-line-dropped`) の**唯一の入口**。
|
|
43
|
+
* 名前を解決できない (`mindData` を持たない) 節では `undefined` を返す = 描く側はそこで
|
|
44
|
+
* 空の `<g>` を返しており、 検査側も見る箱を持たない。
|
|
45
|
+
*/
|
|
46
|
+
export function 放射の中身(
|
|
47
|
+
node: LaidNode,
|
|
48
|
+
stateValues: Record<string, string> = {},
|
|
49
|
+
): { nodes: LayoutNode[]; edges: LayoutEdge[] } | undefined {
|
|
50
|
+
// 名前は状態を読める (#467)。 中心と枝の両方を解決層に通す
|
|
51
|
+
const mindData = node.mindData ? resolveMindData(node.mindData, stateValues) : undefined;
|
|
52
|
+
if (!mindData) return undefined;
|
|
53
|
+
return computeMindMapLayout(
|
|
54
|
+
mindData,
|
|
55
|
+
node.w / 2,
|
|
56
|
+
node.h / 2,
|
|
57
|
+
node.w - MIND_PAD * 2,
|
|
58
|
+
node.h - MIND_PAD * 2,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 放射の 1 箱に積む行 (名前 + 補足)。
|
|
64
|
+
*
|
|
65
|
+
* 字の大きさをここに集める (#538)。 理由は `木の行` と同じ。
|
|
66
|
+
*/
|
|
67
|
+
export function 放射の行(n: LayoutNode): 積む行[] {
|
|
68
|
+
return [
|
|
69
|
+
{ 役割: "title", text: n.title, fontSize: n.isRoot ? 15 : 13 },
|
|
70
|
+
{ 役割: "subtitle", text: n.subtitle ?? "", fontSize: 9 },
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
|
|
31
74
|
export function MindMapNode({
|
|
32
75
|
node,
|
|
33
76
|
active,
|
|
@@ -45,17 +88,8 @@ export function MindMapNode({
|
|
|
45
88
|
}): JSX.Element {
|
|
46
89
|
const x0 = node.cx - node.w / 2;
|
|
47
90
|
const y0 = node.cy - node.h / 2;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (!mindData) return <g transform={`translate(${x0} ${y0})`} />;
|
|
51
|
-
|
|
52
|
-
const PAD = 40;
|
|
53
|
-
const canvasCx = node.w / 2;
|
|
54
|
-
const canvasCy = node.h / 2;
|
|
55
|
-
const canvasW = node.w - PAD * 2;
|
|
56
|
-
const canvasH = node.h - PAD * 2;
|
|
57
|
-
|
|
58
|
-
const layout = computeMindMapLayout(mindData, canvasCx, canvasCy, canvasW, canvasH);
|
|
91
|
+
const layout = 放射の中身(node, stateValues);
|
|
92
|
+
if (!layout) return <g transform={`translate(${x0} ${y0})`} />;
|
|
59
93
|
|
|
60
94
|
/**
|
|
61
95
|
* 枝がどこまで伸びたか (0..1、 cdl#520)。 描く指定が無ければ常に 1 = 全部出ている。
|
|
@@ -105,16 +139,16 @@ export function MindMapNode({
|
|
|
105
139
|
// 枝が届いてから箱を出す。 届く前に出すと、繋がっていない箱が宙に浮く
|
|
106
140
|
if (!箱が出る(n.depth)) return null;
|
|
107
141
|
// 補足は箱の中の 2 行目に出す (#471)。 箱の高さは変えないため、 入らない時は落ちる
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
{ 役割: "title" as const, text: n.title, fontSize: n.isRoot ? 15 : 13 },
|
|
111
|
-
{ 役割: "subtitle" as const, text: n.subtitle ?? "", fontSize: 9 },
|
|
112
|
-
],
|
|
113
|
-
n.w,
|
|
114
|
-
n.h,
|
|
115
|
-
);
|
|
142
|
+
const 材料 = 放射の行(n);
|
|
143
|
+
const 行 = 箱に積む(材料, n.w, n.h);
|
|
116
144
|
// 判定は行数でなく「補足を渡したか」 で行う (理由は tree.tsx と同じ、 #471 Round 1)
|
|
117
145
|
const 補足あり = (n.subtitle ?? "").trim() !== "";
|
|
146
|
+
// 補足ありでは `箱に積む` が既に同じ処理を済ませているため、1 行の経路だけ計算する。
|
|
147
|
+
// 名前の大きさは `放射の行` から取る (#538、 理由は tree.tsx と同じ)
|
|
148
|
+
const 名前 = 材料[0]!;
|
|
149
|
+
const 収めた名前 = 補足あり
|
|
150
|
+
? undefined
|
|
151
|
+
: 幅に収める(名前.text, 名前.fontSize, 使える幅(n.w));
|
|
118
152
|
const 文字の色 = n.isRoot
|
|
119
153
|
? "var(--cdl-text-on-tone, #ffffff)"
|
|
120
154
|
: "var(--cdl-chip-text, #1a1f2a)";
|
|
@@ -130,7 +164,7 @@ export function MindMapNode({
|
|
|
130
164
|
stroke={n.isRoot ? toneColor(n.tone ?? "accent") : toneColor(n.tone)}
|
|
131
165
|
strokeWidth={n.isRoot ? 2 : 1.75}
|
|
132
166
|
/>
|
|
133
|
-
{
|
|
167
|
+
{収めた名前 === undefined ? (
|
|
134
168
|
行.map((r) => (
|
|
135
169
|
<text
|
|
136
170
|
key={r.役割}
|
|
@@ -149,16 +183,17 @@ export function MindMapNode({
|
|
|
149
183
|
) : (
|
|
150
184
|
// 補足が無くても **幅に収める** (#526)。 `箱に積む` を通らないこの経路だけが
|
|
151
185
|
// 幅を見ておらず、長い名前がそのまま箱の外へ出て枝の線と重なっていた
|
|
152
|
-
// (実測 = 中心の箱で文字 163px に対し箱 120px)
|
|
186
|
+
// (実測 = 中心の箱で文字 163px に対し箱 120px)。
|
|
187
|
+
// 収める手段は縮めるのが先で、切るのは下限まで縮めても入らない時だけ (#534)
|
|
153
188
|
<text
|
|
154
189
|
x={n.x}
|
|
155
190
|
y={n.y + 5}
|
|
156
|
-
fontSize={
|
|
191
|
+
fontSize={収めた名前.fontSize}
|
|
157
192
|
fontWeight={n.isRoot ? 700 : 600}
|
|
158
193
|
textAnchor="middle"
|
|
159
194
|
fill={文字の色}
|
|
160
195
|
>
|
|
161
|
-
{
|
|
196
|
+
{収めた名前.text}
|
|
162
197
|
</text>
|
|
163
198
|
)}
|
|
164
199
|
</g>
|
package/src/kinds/tree.tsx
CHANGED
|
@@ -2,7 +2,7 @@ import type { JSX } from "react";
|
|
|
2
2
|
import type { LaidNode, TreeNodePayload } from "../types";
|
|
3
3
|
import { TONE } from "../render/tone";
|
|
4
4
|
import { resolveTreeData } from "../render/payload-binding";
|
|
5
|
-
import { 使える幅,
|
|
5
|
+
import { 使える幅, 幅に収める, 箱に積む, type 積む行, type 積んだ行 } from "../kinds/box-lines";
|
|
6
6
|
import { 進みを畳む, 伸ばすdash, 段の伸び } from "./draw-progress";
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -50,6 +50,49 @@ function 箱の文字({
|
|
|
50
50
|
));
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* 箱の内側に取る余白。 図の外枠と木の描画領域の間。
|
|
55
|
+
*
|
|
56
|
+
* **module に置く** (#538)。 描く側の中に閉じていた間、 検査側は同じ値を書き写すしかなく、
|
|
57
|
+
* 片方を変えた時にもう片方が古いまま残る。
|
|
58
|
+
*/
|
|
59
|
+
const PAD_TOP = 40;
|
|
60
|
+
const PAD_RIGHT = 24;
|
|
61
|
+
const PAD_BOTTOM = 40;
|
|
62
|
+
const PAD_LEFT = 24;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 木の中の箱と枝を並べる (#538)。
|
|
66
|
+
*
|
|
67
|
+
* 描く側 (`TreeNode`) と検査側 (`visual-validate.ts` の `box-line-dropped`) の**唯一の入口**。
|
|
68
|
+
* 余白と canvas の出し方を 2 箇所に書くと、 余白を変えた時に検査だけが古い箱を見る。
|
|
69
|
+
*/
|
|
70
|
+
export function 木の中身(
|
|
71
|
+
node: LaidNode,
|
|
72
|
+
stateValues: Record<string, string> = {},
|
|
73
|
+
): { nodes: LayoutNode[]; edges: LayoutEdge[] } {
|
|
74
|
+
// 名前は状態を読める (#467)。 解決できない名前は書いたまま残して印を付ける
|
|
75
|
+
const treeNodes = resolveTreeData(node.treeData ?? [], stateValues);
|
|
76
|
+
return computeTreeLayout(
|
|
77
|
+
treeNodes,
|
|
78
|
+
node.w - PAD_LEFT - PAD_RIGHT,
|
|
79
|
+
node.h - PAD_TOP - PAD_BOTTOM,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 木の 1 箱に積む行 (名前 + 補足)。
|
|
85
|
+
*
|
|
86
|
+
* 字の大きさをここに集める (#538)。 描く側に直書きすると、 検査側が同じ数を書き写すことになり、
|
|
87
|
+
* 大きさを変えた時に「入るか」 の判定だけが古い値で動く。
|
|
88
|
+
*/
|
|
89
|
+
export function 木の行(n: LayoutNode): 積む行[] {
|
|
90
|
+
return [
|
|
91
|
+
{ 役割: "title", text: n.title, fontSize: n.depth === 0 ? 13 : 12 },
|
|
92
|
+
{ 役割: "subtitle", text: n.subtitle ?? "", fontSize: 9 },
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
|
|
53
96
|
export function TreeNode({
|
|
54
97
|
node,
|
|
55
98
|
active,
|
|
@@ -67,18 +110,9 @@ export function TreeNode({
|
|
|
67
110
|
}): JSX.Element {
|
|
68
111
|
const x0 = node.cx - node.w / 2;
|
|
69
112
|
const y0 = node.cy - node.h / 2;
|
|
70
|
-
// 名前は状態を読める (#467)。 解決できない名前は書いたまま残して印を付ける
|
|
71
|
-
const treeNodes = resolveTreeData(node.treeData ?? [], stateValues);
|
|
72
113
|
const gradientId = `tree-root-grad-${node.id}`;
|
|
73
114
|
|
|
74
|
-
const
|
|
75
|
-
const PAD_RIGHT = 24;
|
|
76
|
-
const PAD_BOTTOM = 40;
|
|
77
|
-
const PAD_LEFT = 24;
|
|
78
|
-
const canvasW = node.w - PAD_LEFT - PAD_RIGHT;
|
|
79
|
-
const canvasH = node.h - PAD_TOP - PAD_BOTTOM;
|
|
80
|
-
|
|
81
|
-
const layout = computeTreeLayout(treeNodes, canvasW, canvasH);
|
|
115
|
+
const layout = 木の中身(node, stateValues);
|
|
82
116
|
|
|
83
117
|
/**
|
|
84
118
|
* 枝がどこまで伸びたか (0..1、 cdl#520)。 描く指定が無ければ常に 1 = 全部出ている。
|
|
@@ -153,17 +187,18 @@ export function TreeNode({
|
|
|
153
187
|
const accent = depthColors[Math.min(n.depth, depthColors.length - 1)] ?? depthColors[0]!;
|
|
154
188
|
const isRoot = n.depth === 0;
|
|
155
189
|
// 補足は箱の中の 2 行目に出す (#471)。 箱の高さは変えないため、 入らない時は落ちる
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
{ 役割: "title" as const, text: n.title, fontSize: isRoot ? 13 : 12 },
|
|
159
|
-
{ 役割: "subtitle" as const, text: n.subtitle ?? "", fontSize: 9 },
|
|
160
|
-
],
|
|
161
|
-
n.w,
|
|
162
|
-
n.h,
|
|
163
|
-
);
|
|
190
|
+
const 材料 = 木の行(n);
|
|
191
|
+
const 行 = 箱に積む(材料, n.w, n.h);
|
|
164
192
|
// **判定は行数でなく「補足を渡したか」 で行う**。 行数で見ると、 箱が低くて補足が落ちた時に
|
|
165
193
|
// 従来の 1 行の経路へ戻り、 幅で切っていない名前が箱から横にはみ出す (#471 Round 1)
|
|
166
194
|
const 補足あり = (n.subtitle ?? "").trim() !== "";
|
|
195
|
+
// 補足ありでは `箱に積む` が既に同じ処理を済ませているため、1 行の経路だけ計算する。
|
|
196
|
+
// 収める手段は縮めるのが先で、切るのは下限まで縮めても入らない時だけ (#534)。
|
|
197
|
+
// 名前の大きさは `木の行` から取る = ここに書き写すと 2 経路で別の大きさになる (#538)
|
|
198
|
+
const 名前 = 材料[0]!;
|
|
199
|
+
const 収めた名前 = 補足あり
|
|
200
|
+
? undefined
|
|
201
|
+
: 幅に収める(名前.text, 名前.fontSize, 使える幅(n.w));
|
|
167
202
|
return (
|
|
168
203
|
<g
|
|
169
204
|
key={`n-${n.id}`}
|
|
@@ -181,7 +216,7 @@ export function TreeNode({
|
|
|
181
216
|
rx={12}
|
|
182
217
|
fill={`url(#${gradientId})`}
|
|
183
218
|
/>
|
|
184
|
-
{
|
|
219
|
+
{収めた名前 === undefined ? (
|
|
185
220
|
箱の文字({
|
|
186
221
|
行,
|
|
187
222
|
x: n.w / 2,
|
|
@@ -195,12 +230,12 @@ export function TreeNode({
|
|
|
195
230
|
<text
|
|
196
231
|
x={n.w / 2}
|
|
197
232
|
y={n.h / 2 + 5}
|
|
198
|
-
fontSize={
|
|
233
|
+
fontSize={収めた名前.fontSize}
|
|
199
234
|
fontWeight={700}
|
|
200
235
|
textAnchor="middle"
|
|
201
236
|
fill="var(--cdl-text-on-tone, #ffffff)"
|
|
202
237
|
>
|
|
203
|
-
{
|
|
238
|
+
{収めた名前.text}
|
|
204
239
|
</text>
|
|
205
240
|
)}
|
|
206
241
|
</>
|
|
@@ -218,7 +253,7 @@ export function TreeNode({
|
|
|
218
253
|
strokeWidth={1.5}
|
|
219
254
|
/>
|
|
220
255
|
<rect x={0} y={0} width={4} height={n.h} rx={2} fill={accent} />
|
|
221
|
-
{
|
|
256
|
+
{収めた名前 === undefined ? (
|
|
222
257
|
箱の文字({
|
|
223
258
|
行,
|
|
224
259
|
x: n.w / 2 + 2,
|
|
@@ -230,12 +265,12 @@ export function TreeNode({
|
|
|
230
265
|
<text
|
|
231
266
|
x={n.w / 2 + 2}
|
|
232
267
|
y={n.h / 2 + 4}
|
|
233
|
-
fontSize={
|
|
268
|
+
fontSize={収めた名前.fontSize}
|
|
234
269
|
fontWeight={600}
|
|
235
270
|
textAnchor="middle"
|
|
236
271
|
fill="var(--cdl-chip-text, #1a1f2a)"
|
|
237
272
|
>
|
|
238
|
-
{
|
|
273
|
+
{収めた名前.text}
|
|
239
274
|
</text>
|
|
240
275
|
)}
|
|
241
276
|
</>
|
package/src/visual-validate.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Visual validator ... layout 出力 (LaidDiagram)
|
|
2
|
+
* Visual validator ... layout 出力 (LaidDiagram) を全ての軸で検証する。
|
|
3
|
+
*
|
|
4
|
+
* **軸の数をここに書き写さない** (#538)。 数は `EXPECTED_AXIS_COUNT` が SSOT で、 写すと軸を
|
|
5
|
+
* 足すたびに片方が古くなる (実測 = 本行が 67 のまま、 下の低 layer API の説明が 56 のままだった)。
|
|
3
6
|
*
|
|
4
7
|
* cdl 著者が「component が render される」 までで完成と思いがちな問題に対し、
|
|
5
8
|
* 「視覚的に正しい」 を engine 層で機械判定する。
|
|
@@ -74,7 +77,7 @@
|
|
|
74
77
|
* test が無いと、 次に種別を足した時に同じ状態へ戻る。
|
|
75
78
|
* ───────────────────────────────────────────────────────────
|
|
76
79
|
*/
|
|
77
|
-
import type { CdlDiagram, LaidDiagram, NodeKind } from "./types";
|
|
80
|
+
import type { CdlDiagram, LaidDiagram, LaidNode, NodeKind } from "./types";
|
|
78
81
|
import { CLEARANCE_LANE_LABEL, DIST_LABEL_PATH_MAX } from "./layout/clearance-constants";
|
|
79
82
|
import { layout } from "./layout";
|
|
80
83
|
import {
|
|
@@ -88,6 +91,9 @@ import {
|
|
|
88
91
|
} from "./layout/collisions";
|
|
89
92
|
import { measureTextWidth } from "./layout/text-width";
|
|
90
93
|
import { droppedLineIsHidden, labelPlan } from "./kinds/shape-region";
|
|
94
|
+
import { 箱に積む, type 積む行 } from "./kinds/box-lines";
|
|
95
|
+
import { 木の中身, 木の行 } from "./kinds/tree";
|
|
96
|
+
import { 放射の中身, 放射の行 } from "./kinds/mind-map";
|
|
91
97
|
import { templateRefIds } from "./render/utils";
|
|
92
98
|
import {
|
|
93
99
|
bboxOverlaps,
|
|
@@ -331,6 +337,8 @@ export type VisualAxis =
|
|
|
331
337
|
| "node-inside-viewbox"
|
|
332
338
|
// Axis 67 (`#492` 追加) = 箱が狭くて書いた補いの行が描かれない node を書き手に伝える。
|
|
333
339
|
| "shape-label-dropped"
|
|
340
|
+
// Axis 68 (`#538` 追加) = 木と放射の中の箱が低くて、 書いた補足が落ちたことを書き手に伝える。
|
|
341
|
+
| "box-line-dropped"
|
|
334
342
|
// Axis 54 (2026-07-03 追加) = node bbox が所属 lane 内に収まっているか。
|
|
335
343
|
// Axis 15 (lane-cx-consistency) は中心 x のみ、 本 axis は node 4 辺と lane 4 辺の包含関係を検知。
|
|
336
344
|
| "node-inside-lane"
|
|
@@ -476,6 +484,7 @@ function emptyCounts(): Record<VisualAxis, number> {
|
|
|
476
484
|
"node-inside-viewbox": 0,
|
|
477
485
|
"node-inside-lane": 0,
|
|
478
486
|
"shape-label-dropped": 0,
|
|
487
|
+
"box-line-dropped": 0,
|
|
479
488
|
"edge-inside-viewbox": 0,
|
|
480
489
|
"lane-label-inside-viewbox": 0,
|
|
481
490
|
"lane-label-overlap": 0,
|
|
@@ -494,8 +503,30 @@ function emptyCounts(): Record<VisualAxis, number> {
|
|
|
494
503
|
};
|
|
495
504
|
}
|
|
496
505
|
|
|
506
|
+
/** 箱に行を積む種別の、 中の 1 箱。 */
|
|
507
|
+
type 積む箱 = { 行: 積む行[]; w: number; h: number };
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* 木と放射の中の箱を、 **描く時と同じ手順で** 集める (`#538`)。
|
|
511
|
+
*
|
|
512
|
+
* 箱の並べ方と積む行を kind 側の 1 箇所 (`木の中身` / `木の行` / `放射の中身` / `放射の行`) から
|
|
513
|
+
* 取る。 ここで余白や字の大きさを書き写すと、 描画側を変えた時に検査だけが古い箱を見る。
|
|
514
|
+
*
|
|
515
|
+
* 状態の値は渡さない。 `箱に積む` が行を落とすかどうかは **字の大きさと行数だけ** で決まり、
|
|
516
|
+
* 文字の中身に依らない。 状態を読む名前 (`{x}`) も解決の前後で行数が変わらないため、
|
|
517
|
+
* 落ちる / 落ちないの判定は状態に依らず同じになる。
|
|
518
|
+
*
|
|
519
|
+
* 行を積まない種別では空を返す。
|
|
520
|
+
*/
|
|
521
|
+
function 積む箱を集める(n: LaidNode): 積む箱[] {
|
|
522
|
+
const 箱 = (b: { w: number; h: number }, 行: 積む行[]): 積む箱 => ({ 行, w: b.w, h: b.h });
|
|
523
|
+
if (n.kind === "tree-hierarchy") return 木の中身(n).nodes.map((b) => 箱(b, 木の行(b)));
|
|
524
|
+
if (n.kind === "mind-map") return (放射の中身(n)?.nodes ?? []).map((b) => 箱(b, 放射の行(b)));
|
|
525
|
+
return [];
|
|
526
|
+
}
|
|
527
|
+
|
|
497
528
|
/**
|
|
498
|
-
* LaidDiagram
|
|
529
|
+
* LaidDiagram を直接受け取って全ての軸で視覚検証する低 layer API (v0.9+ 追加)。
|
|
499
530
|
*
|
|
500
531
|
* visualValidate() は layout 経由で LaidDiagram を得た後、 本関数を呼び出す thin wrapper。
|
|
501
532
|
* fixture test で layout を bypass して LaidDiagram を直接構築、 各 axis の real defect を
|
|
@@ -2262,6 +2293,59 @@ function runAxes(
|
|
|
2262
2293
|
);
|
|
2263
2294
|
}
|
|
2264
2295
|
|
|
2296
|
+
// ───────────────────────────────────────────────────────────
|
|
2297
|
+
// Axis 68: box-line-dropped (箱が低くて、 書いた補足が落ちる、 #538)
|
|
2298
|
+
//
|
|
2299
|
+
// 木 (`tree-hierarchy`) と放射 (`mind-map`) は中の箱に名前と補足を積む (`箱に積む`、 #471)。
|
|
2300
|
+
// 箱の高さは段の間隔から出るため、 段や枝が増えると 2 行が入らなくなる。 入らない行は
|
|
2301
|
+
// **後ろから落ちる** = 書いた補足が知らせも印も無く図から消える。
|
|
2302
|
+
//
|
|
2303
|
+
// 落ち方は段階的でない。 箱の高さは図の中で揃っているので、 1 つ入らなくなると同じ図の補足が
|
|
2304
|
+
// 全部消える (実測 = 1 本道の木は深さ 8 で 9 件すべて出て、 深さ 9 で 10 件すべて消える)。
|
|
2305
|
+
//
|
|
2306
|
+
// 幅の側は切った印 (`…`) が図に出る (#526) が、 高さの側は **箱に描く余地が無いから落ちている**
|
|
2307
|
+
// ので印を出せない。 検査で伝えるのが唯一の出口になる。
|
|
2308
|
+
//
|
|
2309
|
+
// 判定は描く時と同じ関数を同じ引数で呼ぶ。 描いた結果と突き合わせる形は採らない = 描画と判定で
|
|
2310
|
+
// 別の経路を通ると、 片方だけ直したずれに気付けない。
|
|
2311
|
+
//
|
|
2312
|
+
// Axis 67 (`shape-label-dropped`) との境界は **どの箱を見るか**。 あちらは `shape-` の箱が絵に
|
|
2313
|
+
// 帯を譲って補いの行を落とす形 (`labelPlan`)、 こちらは木と放射の中の箱が高さで落とす形。
|
|
2314
|
+
//
|
|
2315
|
+
// 中心の箱 (放射の高さ 52 固定) も対象に含める。 今のところ落ちないが、 高さは実装が決める値
|
|
2316
|
+
// なので、 対象から外すと変えた時に落ちても見えない。
|
|
2317
|
+
// ───────────────────────────────────────────────────────────
|
|
2318
|
+
for (const n of laid.nodes) {
|
|
2319
|
+
const 箱 = 積む箱を集める(n);
|
|
2320
|
+
if (箱.length === 0) continue;
|
|
2321
|
+
/** 補足を書いた箱の数。 書いていない箱は元から描かれないので数えない。 */
|
|
2322
|
+
let 対象 = 0;
|
|
2323
|
+
const 落ちた: { title: string; h: number }[] = [];
|
|
2324
|
+
for (const b of 箱) {
|
|
2325
|
+
// 先頭 (名前) は `箱に積む` が必ず残すので、 落ちうるのは 2 行目以降だけ。
|
|
2326
|
+
const 宣言 = b.行.slice(1).filter((r) => r.text.trim() !== "");
|
|
2327
|
+
if (宣言.length === 0) continue;
|
|
2328
|
+
対象 += 1;
|
|
2329
|
+
const 残った = new Set(箱に積む(b.行, b.w, b.h).map((r) => r.役割));
|
|
2330
|
+
if (宣言.every((r) => 残った.has(r.役割))) continue;
|
|
2331
|
+
落ちた.push({ title: b.行[0]?.text ?? "", h: b.h });
|
|
2332
|
+
}
|
|
2333
|
+
if (落ちた.length === 0) continue;
|
|
2334
|
+
|
|
2335
|
+
// 名前は先頭 5 件まで出し、 **残りの件数を必ず添える**。 黙って打ち切ると、 出ている名前が
|
|
2336
|
+
// 全部だと読める。
|
|
2337
|
+
const 名前 = 落ちた.map((d) => `"${d.title}"`);
|
|
2338
|
+
const 見せる = 名前.slice(0, 5).join(" / ");
|
|
2339
|
+
const 残り = 名前.length > 5 ? ` 他 ${名前.length - 5} 件` : "";
|
|
2340
|
+
const 低 = Math.min(...落ちた.map((d) => d.h)).toFixed(1);
|
|
2341
|
+
const 高 = Math.max(...落ちた.map((d) => d.h)).toFixed(1);
|
|
2342
|
+
push(
|
|
2343
|
+
"box-line-dropped",
|
|
2344
|
+
`node "${n.id}" (${n.kind}) は箱が低く、 書いた補足が ${落ちた.length}/${対象} 箱で描かれない (${見せる}${残り}、 箱の高さ ${低 === 高 ? 低 : `${低}〜${高}`})。 図を高くするか段を減らすか、 補足を消す`,
|
|
2345
|
+
"warn",
|
|
2346
|
+
);
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2265
2349
|
// ───────────────────────────────────────────────────────────
|
|
2266
2350
|
// Axis 55: edge-inside-viewbox (edge path 全 segment 端点が viewBox 内に収まる)
|
|
2267
2351
|
//
|
|
@@ -4296,6 +4380,7 @@ function validateMetaAxes(
|
|
|
4296
4380
|
"node-inside-viewbox",
|
|
4297
4381
|
"node-inside-lane",
|
|
4298
4382
|
"shape-label-dropped",
|
|
4383
|
+
"box-line-dropped",
|
|
4299
4384
|
"edge-inside-viewbox",
|
|
4300
4385
|
"lane-label-inside-viewbox",
|
|
4301
4386
|
"lane-label-overlap",
|
|
@@ -4365,6 +4450,7 @@ function validateMetaAxes(
|
|
|
4365
4450
|
"node-inside-viewbox",
|
|
4366
4451
|
"node-inside-lane",
|
|
4367
4452
|
"shape-label-dropped",
|
|
4453
|
+
"box-line-dropped",
|
|
4368
4454
|
"edge-inside-viewbox",
|
|
4369
4455
|
"lane-label-inside-viewbox",
|
|
4370
4456
|
"lane-label-overlap",
|
|
@@ -4400,7 +4486,7 @@ function validateMetaAxes(
|
|
|
4400
4486
|
//
|
|
4401
4487
|
// 削除済 = engine-meta 51 (mermaid-parity、 #366) / 軸 29 (svg-filter-integrity) /
|
|
4402
4488
|
// 軸 30 (neumorphism-shadow-budget、 どちらも #426 で主題の廃止に伴い削除)。
|
|
4403
|
-
const EXPECTED_AXIS_COUNT =
|
|
4489
|
+
const EXPECTED_AXIS_COUNT = 68;
|
|
4404
4490
|
if (allAxes.length !== EXPECTED_AXIS_COUNT) {
|
|
4405
4491
|
out.push({
|
|
4406
4492
|
axis: "axis-documentation-completeness",
|