@cardenelabs/cdl 0.13.1 → 0.15.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 +250 -1
- package/README.md +20 -1
- package/dist/index.cjs +2912 -1723
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -6
- package/dist/index.d.ts +25 -6
- package/dist/index.js +2911 -1724
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +1395 -230
- 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 +1395 -230
- package/dist/react.js.map +1 -1
- package/dist/{render-B5JHcUrb.d.cts → render-BS1E6Mlo.d.cts} +55 -2
- package/dist/{render-B5JHcUrb.d.ts → render-BS1E6Mlo.d.ts} +55 -2
- package/package.json +1 -1
- package/src/builder.ts +24 -0
- package/src/index.ts +2 -1
- package/src/kinds/chart-gauge.tsx +241 -0
- package/src/kinds/chart-pie.tsx +214 -53
- package/src/kinds/chart-radial.tsx +210 -0
- package/src/kinds/chart-stacked-bar.tsx +272 -0
- package/src/kinds/chart-stat.tsx +231 -0
- package/src/kinds/chart-waffle.tsx +197 -0
- package/src/kinds/draw-ratio.ts +44 -0
- package/src/kinds/dyn-shape.tsx +3 -6
- package/src/kinds/generic.tsx +18 -6
- package/src/kinds/mind-map.tsx +52 -22
- package/src/kinds/terminal-mark.tsx +96 -0
- package/src/kinds/tree.tsx +51 -21
- package/src/layout/edges.ts +30 -1
- package/src/layout/self-loop.ts +217 -0
- package/src/layout/spec.ts +81 -4
- package/src/layout/tokens.ts +15 -0
- package/src/presets.ts +33 -3
- package/src/render/edge-head.ts +45 -0
- package/src/render/edges.tsx +4 -2
- package/src/render/interactive-panel.tsx +2 -2
- package/src/render/nodes.tsx +25 -0
- package/src/render/payload-binding.ts +14 -3
- package/src/render/stage.tsx +41 -18
- package/src/render/tone.ts +52 -0
- package/src/types.ts +66 -2
- package/src/validate.ts +13 -5
- package/src/visual-validate.ts +137 -4
package/src/render/stage.tsx
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { useMemo, type JSX } from "react";
|
|
2
|
-
import type
|
|
2
|
+
import { EDGE_HEADS, type CdlPhase, type LaidDiagram } from "../types";
|
|
3
3
|
import { CdlEdgeView, CdlEdgeGlowView, CdlEdgeLabelView } from "./edges";
|
|
4
4
|
import { CdlNodeView } from "./nodes";
|
|
5
5
|
import { TONE, TONE_HEX } from "./tone";
|
|
6
|
+
import { EDGE_HEAD_SHAPE, edgeHeadMarkerId } from "./edge-head";
|
|
6
7
|
import { buildDiagramAltText } from "../layout/spec";
|
|
7
8
|
import { lifelineStartYs, uniformLifelineEndY } from "../layout/lifeline";
|
|
9
|
+
import { 描く進みに直す } from "../kinds/draw-ratio";
|
|
8
10
|
|
|
9
11
|
export function CdlStage({
|
|
10
12
|
laid,
|
|
@@ -28,6 +30,9 @@ export function CdlStage({
|
|
|
28
30
|
// この phase で左の起点から描き始める node (cdl#512)。 `activate` とは別集合で持つ =
|
|
29
31
|
// 焦点が当たり続ける図で毎 phase 引き直すことになるため、 光っているかを引き金にしない
|
|
30
32
|
const drawSet = new Set(currentPhase?.draw ?? []);
|
|
33
|
+
// 描く対象から見た進み (cdl#570)。 `drawRatio` を書かない phase では `progress` と同値で、
|
|
34
|
+
// 描く速さを phase の長さから切り離すためだけに使う
|
|
35
|
+
const 描く進み = 描く進みに直す(progress, currentPhase?.drawRatio);
|
|
31
36
|
// 縦線の終点は全 lane で 1 つに揃える (等長保証)。 値の決め方と、 1 lane の異常が図全体から
|
|
32
37
|
// 縦線を消していた形の対処は `uniformLifelineEndY` の説明。
|
|
33
38
|
//
|
|
@@ -97,21 +102,35 @@ export function CdlStage({
|
|
|
97
102
|
光っていない edge も tone の色で描くが、 矢頭の大きさは従来通り小さいまま保つ
|
|
98
103
|
(Issue の制約 = 色だけを足す)。 */}
|
|
99
104
|
{Object.entries(TONE).flatMap(([tone, color]) =>
|
|
100
|
-
([["", 14], ["-sm", 10]] as const).
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
105
|
+
([["", 14], ["-sm", 10]] as const).flatMap(([suffix, size]) =>
|
|
106
|
+
EDGE_HEADS.map((head) => {
|
|
107
|
+
const 形 = EDGE_HEAD_SHAPE[head];
|
|
108
|
+
return (
|
|
109
|
+
<marker
|
|
110
|
+
key={`${tone}${suffix}-${head}`}
|
|
111
|
+
id={edgeHeadMarkerId(tone, head, suffix === "-sm")}
|
|
112
|
+
viewBox="0 0 10 10"
|
|
113
|
+
refX={形.refX}
|
|
114
|
+
refY="5"
|
|
115
|
+
markerWidth={size}
|
|
116
|
+
markerHeight={size}
|
|
117
|
+
markerUnits="userSpaceOnUse"
|
|
118
|
+
orient="auto-start-reverse"
|
|
119
|
+
>
|
|
120
|
+
<path
|
|
121
|
+
data-cdl-role="edge-arrowhead"
|
|
122
|
+
data-cdl-edge-head={head}
|
|
123
|
+
d={形.d}
|
|
124
|
+
fill={形.filled ? color : "none"}
|
|
125
|
+
stroke={形.filled ? "none" : color}
|
|
126
|
+
strokeWidth={形.filled ? undefined : 1.6}
|
|
127
|
+
strokeLinecap={形.filled ? undefined : "round"}
|
|
128
|
+
strokeLinejoin={形.filled ? undefined : "round"}
|
|
129
|
+
/>
|
|
130
|
+
</marker>
|
|
131
|
+
);
|
|
132
|
+
}),
|
|
133
|
+
),
|
|
115
134
|
)}
|
|
116
135
|
{/* tone が TONE 定義に無い edge の受け皿。 visual-validate の tone-marker-ref 軸が
|
|
117
136
|
dead ref を検知するが、 検知するまでの間に矢頭が消えないようにする。 */}
|
|
@@ -207,12 +226,16 @@ export function CdlStage({
|
|
|
207
226
|
|
|
208
227
|
{/* nodes (2nd pass) */}
|
|
209
228
|
{laid.nodes.map((node) => {
|
|
229
|
+
// 描く対象だけ進みを引き伸ばす (cdl#570)。 同じ phase の他の node と edge、
|
|
230
|
+
// および `tweens` は元の進みのまま = 「線はゆっくり、 値は短く」 が 1 つの phase で
|
|
231
|
+
// 両立する。 対象外にも掛けると値の移りまで速くなる
|
|
232
|
+
const 描く = drawSet.has(node.id);
|
|
210
233
|
const view = (
|
|
211
234
|
<CdlNodeView
|
|
212
235
|
node={node}
|
|
213
236
|
active={activeSet.has(node.id)}
|
|
214
|
-
drawing={
|
|
215
|
-
progress={progress}
|
|
237
|
+
drawing={描く}
|
|
238
|
+
progress={描く ? 描く進み : progress}
|
|
216
239
|
stateValues={stateValues}
|
|
217
240
|
currentPhaseId={currentPhase?.id}
|
|
218
241
|
/>
|
package/src/render/tone.ts
CHANGED
|
@@ -30,6 +30,58 @@ export const TONE_HEX: Record<Tone, string> = {
|
|
|
30
30
|
info: "#5a8ec1",
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* 図表の系列の色 (cdl#546)。
|
|
35
|
+
*
|
|
36
|
+
* `TONE` は **意味付きの色** で、箱や矢印が「これは異常」 「これは成功」 を示すために使う。
|
|
37
|
+
* 図表の系列に要るのは意味ではなく **並べて見分けられること** で、選び方の基準が違う。
|
|
38
|
+
*
|
|
39
|
+
* 使い回していた間、2 つの困りが出ていた。 明るい画面の `warning` (`#a67a2e`) は彩度が低く
|
|
40
|
+
* 青と青緑の隣で沈み、色相の離れた暖色を置こうとすると `error` (赤) しか無いので装飾の
|
|
41
|
+
* つもりの色に「異常」 の意味が乗った。
|
|
42
|
+
*
|
|
43
|
+
* ## 選び方
|
|
44
|
+
*
|
|
45
|
+
* **色相を離す**。 6 色を色相環におおよそ等間隔で置き、隣り合う 2 色が近くならない順に
|
|
46
|
+
* 並べた。 彩度は `TONE` より高く取る = 図表は面で見せるので、線や字より彩度を上げても
|
|
47
|
+
* うるさくならない。
|
|
48
|
+
*
|
|
49
|
+
* ## 差し替え
|
|
50
|
+
*
|
|
51
|
+
* `TONE` と同じく CSS 変数にする。 dragon 側で `--cdl-chart-N` を定義すれば、明暗で別の値を
|
|
52
|
+
* 持てる。 定義しなければここの既定値が出る。
|
|
53
|
+
*
|
|
54
|
+
* datum に `tone` を明示した場合は `TONE` が優先される (意味を示したい図はそのまま書ける)。
|
|
55
|
+
*/
|
|
56
|
+
export const CHART_SERIES: readonly string[] = [
|
|
57
|
+
"var(--cdl-chart-1, #4338ca)",
|
|
58
|
+
"var(--cdl-chart-2, #0d9488)",
|
|
59
|
+
"var(--cdl-chart-3, #f59e0b)",
|
|
60
|
+
"var(--cdl-chart-4, #be185d)",
|
|
61
|
+
"var(--cdl-chart-5, #4d7c0f)",
|
|
62
|
+
"var(--cdl-chart-6, #0369a1)",
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 系列の色の hex のみ。 CSS 変数を評価できない環境 (SSR / 検査) で色を比べる時に使う。
|
|
67
|
+
*
|
|
68
|
+
* `CHART_SERIES` と **同じ並び / 同じ長さ** であることは検査が見る。 片方だけ足すと、
|
|
69
|
+
* 実物と計算が別の色を指す。
|
|
70
|
+
*/
|
|
71
|
+
export const CHART_SERIES_HEX: readonly string[] = [
|
|
72
|
+
"#4338ca",
|
|
73
|
+
"#0d9488",
|
|
74
|
+
"#f59e0b",
|
|
75
|
+
"#be185d",
|
|
76
|
+
"#4d7c0f",
|
|
77
|
+
"#0369a1",
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
/** 系列の色を順に配る。 6 色を超えたら先頭へ戻る */
|
|
81
|
+
export function chartSeriesColor(idx: number): string {
|
|
82
|
+
return CHART_SERIES[((idx % CHART_SERIES.length) + CHART_SERIES.length) % CHART_SERIES.length]!;
|
|
83
|
+
}
|
|
84
|
+
|
|
33
85
|
/** TONE に無い tone が来た時に使う色。 */
|
|
34
86
|
export const TONE_FALLBACK = "#2d6a8f";
|
|
35
87
|
|
package/src/types.ts
CHANGED
|
@@ -58,8 +58,13 @@ export const NODE_KINDS = [
|
|
|
58
58
|
"service", "api", "frontend", "backend", "webhook", "microservice",
|
|
59
59
|
// データ / 判定 (4)
|
|
60
60
|
"signer", "oracle", "merkle-tree", "decision",
|
|
61
|
-
// Chart 系 (
|
|
62
|
-
|
|
61
|
+
// Chart 系 (5) ... 1 node に datum 配列を持ち、 kind 内で全 datum を SVG 描画。
|
|
62
|
+
// chart-gauge は合計を主役に置く形 (cdl#549)、 chart-radial は弧の長さで比べる形 (cdl#550)、
|
|
63
|
+
// chart-stat は数値そのものを主役に置く形 (cdl#553)
|
|
64
|
+
// chart-waffle は 100 個の印で割合を数えて確かめる形 (cdl#552)
|
|
65
|
+
"chart-pie", "chart-line", "chart-bar", "chart-gauge", "chart-radial", "chart-stat",
|
|
66
|
+
// chart-stacked-bar は帯を割合で区切り時点を並べる形 (cdl#551)
|
|
67
|
+
"chart-waffle", "chart-stacked-bar",
|
|
63
68
|
// Timeline / Mind 系 (2、 CAR-994 Phase B) ... 1 node に payload 配列、 SVG geometry render
|
|
64
69
|
"gantt-timeline", "mind-map",
|
|
65
70
|
// Analytic 系 (4、 CAR-994 Phase C) ... 1 node に payload 配列、 SVG geometry render
|
|
@@ -89,6 +94,9 @@ export const NODE_KINDS = [
|
|
|
89
94
|
// Shape-driven people 6 (CAR-1111 Phase 2-D) ... 職種別 person 型 (監査 / 規制 / 公証 / 弁護 / トレーダ / CS)
|
|
90
95
|
"shape-auditor", "shape-regulator", "shape-notary",
|
|
91
96
|
"shape-lawyer", "shape-trader", "shape-customer-service",
|
|
97
|
+
// 流れの始まりと終わりの印 2 種 (#560) ... 塗った丸と輪で囲んだ丸。
|
|
98
|
+
// 状態遷移図が使う。 印そのものが意味を持つので文字は描かない
|
|
99
|
+
"mark-start", "mark-end",
|
|
92
100
|
] as const;
|
|
93
101
|
|
|
94
102
|
export type NodeKind = (typeof NODE_KINDS)[number];
|
|
@@ -103,6 +111,26 @@ export type Side = "top" | "right" | "bottom" | "left";
|
|
|
103
111
|
*/
|
|
104
112
|
export type EdgeStyle = "solid" | "dotted-flow";
|
|
105
113
|
|
|
114
|
+
/**
|
|
115
|
+
* 関係の **端の形** (#560)。 線の種類 (`EdgeStyle`) とは別の軸で、線の先端に何を置くかを決める。
|
|
116
|
+
*
|
|
117
|
+
* - `"triangle"` ... 塗った三角。 **既定**。 書かなければこれになる
|
|
118
|
+
* - `"diamond"` ... 塗った菱。 「持っている」 側を指す
|
|
119
|
+
* - `"open"` ... 閉じない 2 本の腕。 向きだけを示し、待たない
|
|
120
|
+
* - `"crow"` ... 三又。 「多」 を示す (ER の鳥の足)
|
|
121
|
+
*
|
|
122
|
+
* 線の種類と分けて持つのは、両者が独立に組み合わさるため。 実線 × 三角 (継ぐ) と
|
|
123
|
+
* 破線 × 三角 (満たす) は線種で分かれ、実線 × 三角 (継ぐ) と実線 × 菱 (持つ) は端で分かれる。
|
|
124
|
+
* 1 つの欄に畳むと 2 軸の積を名前で列挙することになり、組合せが増えるたびに語が増える。
|
|
125
|
+
*/
|
|
126
|
+
export type EdgeHead = "triangle" | "diamond" | "open" | "crow";
|
|
127
|
+
|
|
128
|
+
/** 書かなかった時の端の形。 既存の図はすべてこれで描かれている */
|
|
129
|
+
export const EDGE_HEAD_DEFAULT: EdgeHead = "triangle";
|
|
130
|
+
|
|
131
|
+
/** 端の形の全種。 marker の定義と検査の走査がこの並びを引く (書き写すとずれる) */
|
|
132
|
+
export const EDGE_HEADS: readonly EdgeHead[] = ["triangle", "diamond", "open", "crow"];
|
|
133
|
+
|
|
106
134
|
export type CdlLane = {
|
|
107
135
|
id: string;
|
|
108
136
|
/** lane の x 座標 (optional、 未指定なら engine が前 lane 右端 + gap で auto 計算) */
|
|
@@ -335,6 +363,16 @@ export type ChartDatumPayload = {
|
|
|
335
363
|
label: string;
|
|
336
364
|
/** 棒 / 折れ線 / 扇の大きさ。 `{signal}` で状態に追随する */
|
|
337
365
|
value: BoundNumber;
|
|
366
|
+
/**
|
|
367
|
+
* **前の時点の値** (cdl#551)。 書くと `chart-stacked-bar` が帯を 2 本描き、
|
|
368
|
+
* 内訳の変化がそのまま見える。 書かない図は 1 本のまま。
|
|
369
|
+
*
|
|
370
|
+
* 節を時点ごとに置く案は採らない = 図表は「1 節に datum 配列を持ち、種別が全 datum を
|
|
371
|
+
* 描く」 構造で揃っており、節を増やすと配置と組み立て器の両方が別経路になる。
|
|
372
|
+
*
|
|
373
|
+
* 3 時点以上は持たない (cdl#551 で対象外)。 足す時は欄の形ごと決め直す。
|
|
374
|
+
*/
|
|
375
|
+
previous?: BoundNumber;
|
|
338
376
|
tone?: Tone;
|
|
339
377
|
};
|
|
340
378
|
|
|
@@ -452,6 +490,8 @@ export type CdlEdge = {
|
|
|
452
490
|
side?: Side;
|
|
453
491
|
/** visual style、 default "solid" */
|
|
454
492
|
style?: EdgeStyle;
|
|
493
|
+
/** 端の形、 default "triangle" (#560)。 線の種類とは別の軸 */
|
|
494
|
+
head?: EdgeHead;
|
|
455
495
|
/**
|
|
456
496
|
* FSM transition の guard 条件 (例 "if validated")。
|
|
457
497
|
* stateMachine preset では sub に併合される、 text-dsl v0.5 では guard 単独で渡せる。
|
|
@@ -2158,6 +2198,30 @@ export type CdlPhase = {
|
|
|
2158
2198
|
* 書かない phase では従来どおり全長で描く (DOM に dash 属性も付かない)。
|
|
2159
2199
|
*/
|
|
2160
2200
|
draw?: string[];
|
|
2201
|
+
/**
|
|
2202
|
+
* `draw` の対象を描き終えるまでに、 phase の何割を使うか (cdl#570)。
|
|
2203
|
+
*
|
|
2204
|
+
* 0 より大きく 1 以下の割合で書く。 `0.4` なら phase の進みが 0.4 に達した時点で
|
|
2205
|
+
* 描き終わり、 残りの 0.6 は描き終わった図のまま値だけが動く。 既定 (書かない) は 1 で、
|
|
2206
|
+
* 従来どおり phase の終わりに描き終わる。
|
|
2207
|
+
*
|
|
2208
|
+
* **描く速さと phase の長さを切り離すための欄**。 進みは phase の進みそのものなので、
|
|
2209
|
+
* これが無いと「線はゆっくり引きたいが値の移りは短くしたい」 が書けず、 描く速さのために
|
|
2210
|
+
* phase の長さを動かすことになる。 phase の長さを動かすと同じ phase の `tweens` まで
|
|
2211
|
+
* 遅くなり、 値が移るだけの phase を長くする形の回避が利用側で要る。
|
|
2212
|
+
*
|
|
2213
|
+
* **割合で持ち、 秒数では持たない**。 割合なら phase に収まるので、 再生速度の切替
|
|
2214
|
+
* (0.5x / 2x) がそのまま効き、 phase をはみ出した時の扱いを決めずに済む。 秒数だと
|
|
2215
|
+
* 切替との掛かり方とはみ出しの扱いを新たに決めることになる。
|
|
2216
|
+
*
|
|
2217
|
+
* 掛かる相手は `draw` に載せた node だけ。 同じ phase の他の node と edge、 および
|
|
2218
|
+
* `tweens` は元の進みのまま動く。
|
|
2219
|
+
*
|
|
2220
|
+
* 範囲外 (0 以下 / 1 超 / NaN / Infinity) は **1 として扱う**。 描く動きは見え方であって
|
|
2221
|
+
* 図の意味ではないため、 書き間違いで図が出なくなる方が損が大きい。 書き間違いは
|
|
2222
|
+
* `validate` が warning で知らせる。
|
|
2223
|
+
*/
|
|
2224
|
+
drawRatio?: number;
|
|
2161
2225
|
/** 数値 state の線形補間 (write phase で 100→90 等) */
|
|
2162
2226
|
tweens: Array<{ stateId: string; from: number; to: number }>;
|
|
2163
2227
|
/** 任意型 state の即時切替 (この phase に到達したら値を上書き、 lerp なし) */
|
package/src/validate.ts
CHANGED
|
@@ -168,11 +168,6 @@ export function validate(diag: CdlDiagram): void {
|
|
|
168
168
|
`[cdl] unknown-ref: edge "${e.id}" の to "${e.to}" が node に存在しません${suggest(e.to, nodeIds, "node")}\n → 修正例: .node("${e.to}", { lane: "...", stack: 0, kind: "...", title: "..." }) を edge 宣言前に追加`,
|
|
169
169
|
);
|
|
170
170
|
}
|
|
171
|
-
if (e.from === e.to) {
|
|
172
|
-
errors.push(
|
|
173
|
-
`[cdl] self-loop: edge "${e.id}" の from と to が同じ node "${e.from}" です (self-loop は現状未対応)\n → 別 node を作って 2 段に分けるか、 後続 PR の self-loop support を待ってください`,
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
171
|
}
|
|
177
172
|
|
|
178
173
|
// ───────────────────────────────────────────────────────────
|
|
@@ -200,6 +195,19 @@ export function validate(diag: CdlDiagram): void {
|
|
|
200
195
|
);
|
|
201
196
|
}
|
|
202
197
|
}
|
|
198
|
+
// 描く速さの割合 (cdl#570)。 範囲外は描画側が 1 に落とすので図は出るが、 書いた指定は
|
|
199
|
+
// 何もしない。 黙って捨てると「書いたのに速さが変わらない」 が手掛かりなしで起きる
|
|
200
|
+
if (p.drawRatio !== undefined) {
|
|
201
|
+
if (!Number.isFinite(p.drawRatio) || p.drawRatio <= 0 || p.drawRatio > 1) {
|
|
202
|
+
warnings.push(
|
|
203
|
+
`phase "${p.id}" の drawRatio ${p.drawRatio} は範囲外 (0 より大きく 1 以下)。 1 として扱う`,
|
|
204
|
+
);
|
|
205
|
+
} else if ((p.draw ?? []).length === 0) {
|
|
206
|
+
warnings.push(
|
|
207
|
+
`phase "${p.id}" は drawRatio を持つが draw が空。 描く対象が無いので何も変わらない`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
203
211
|
for (const t of p.tweens) {
|
|
204
212
|
if (!stateIds.has(t.stateId)) {
|
|
205
213
|
errors.push(`[cdl] unknown-ref: phase "${p.id}" tween 対象 state "${t.stateId}" 未定義`);
|
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
|
|
80
|
+
import { EDGE_HEADS, type CdlDiagram, type LaidDiagram, type LaidNode, type 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,
|
|
@@ -128,7 +134,9 @@ import {
|
|
|
128
134
|
computeLabelBBoxWorld,
|
|
129
135
|
hasRenderedLabel,
|
|
130
136
|
isRenderedNode,
|
|
137
|
+
genericTitleBaselineY,
|
|
131
138
|
rendersRows,
|
|
139
|
+
TITLE_ROW_MIN_GAP,
|
|
132
140
|
rendersTitleText,
|
|
133
141
|
fitsOwnTitleWidth,
|
|
134
142
|
rowBaselineY,
|
|
@@ -232,6 +240,11 @@ export const GRID_ALIGNED_KINDS: ReadonlySet<NodeKind> = new Set([
|
|
|
232
240
|
"chart-pie",
|
|
233
241
|
"chart-line",
|
|
234
242
|
"chart-bar",
|
|
243
|
+
"chart-gauge",
|
|
244
|
+
"chart-radial",
|
|
245
|
+
"chart-stat",
|
|
246
|
+
"chart-waffle",
|
|
247
|
+
"chart-stacked-bar",
|
|
235
248
|
"funnel-stages",
|
|
236
249
|
"quadrant-matrix",
|
|
237
250
|
]);
|
|
@@ -331,6 +344,8 @@ export type VisualAxis =
|
|
|
331
344
|
| "node-inside-viewbox"
|
|
332
345
|
// Axis 67 (`#492` 追加) = 箱が狭くて書いた補いの行が描かれない node を書き手に伝える。
|
|
333
346
|
| "shape-label-dropped"
|
|
347
|
+
// Axis 68 (`#538` 追加) = 木と放射の中の箱が低くて、 書いた補足が落ちたことを書き手に伝える。
|
|
348
|
+
| "box-line-dropped"
|
|
334
349
|
// Axis 54 (2026-07-03 追加) = node bbox が所属 lane 内に収まっているか。
|
|
335
350
|
// Axis 15 (lane-cx-consistency) は中心 x のみ、 本 axis は node 4 辺と lane 4 辺の包含関係を検知。
|
|
336
351
|
| "node-inside-lane"
|
|
@@ -366,6 +381,7 @@ export type VisualAxis =
|
|
|
366
381
|
| "column-gap-uniform"
|
|
367
382
|
// Axis 68 (#387) = 行を描かない種別に rows が書かれている (書いた内容が画面に出ない)。
|
|
368
383
|
| "rows-not-rendered"
|
|
384
|
+
| "title-row-overlap"
|
|
369
385
|
// #393 = 検査に渡された図の形自体が壊れている (検査できなかったことを結果として出す)。
|
|
370
386
|
| "malformed-input"
|
|
371
387
|
// #393 = 検査が例外で中断したが、 原因が入力の破綻か検査側の欠陥か **決められない**。
|
|
@@ -476,6 +492,7 @@ function emptyCounts(): Record<VisualAxis, number> {
|
|
|
476
492
|
"node-inside-viewbox": 0,
|
|
477
493
|
"node-inside-lane": 0,
|
|
478
494
|
"shape-label-dropped": 0,
|
|
495
|
+
"box-line-dropped": 0,
|
|
479
496
|
"edge-inside-viewbox": 0,
|
|
480
497
|
"lane-label-inside-viewbox": 0,
|
|
481
498
|
"lane-label-overlap": 0,
|
|
@@ -489,13 +506,36 @@ function emptyCounts(): Record<VisualAxis, number> {
|
|
|
489
506
|
"row-gap-uniform": 0,
|
|
490
507
|
"column-gap-uniform": 0,
|
|
491
508
|
"rows-not-rendered": 0,
|
|
509
|
+
"title-row-overlap": 0,
|
|
492
510
|
"malformed-input": 0,
|
|
493
511
|
"validation-interrupted": 0,
|
|
494
512
|
};
|
|
495
513
|
}
|
|
496
514
|
|
|
515
|
+
/** 箱に行を積む種別の、 中の 1 箱。 */
|
|
516
|
+
type 積む箱 = { 行: 積む行[]; w: number; h: number };
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* 木と放射の中の箱を、 **描く時と同じ手順で** 集める (`#538`)。
|
|
520
|
+
*
|
|
521
|
+
* 箱の並べ方と積む行を kind 側の 1 箇所 (`木の中身` / `木の行` / `放射の中身` / `放射の行`) から
|
|
522
|
+
* 取る。 ここで余白や字の大きさを書き写すと、 描画側を変えた時に検査だけが古い箱を見る。
|
|
523
|
+
*
|
|
524
|
+
* 状態の値は渡さない。 `箱に積む` が行を落とすかどうかは **字の大きさと行数だけ** で決まり、
|
|
525
|
+
* 文字の中身に依らない。 状態を読む名前 (`{x}`) も解決の前後で行数が変わらないため、
|
|
526
|
+
* 落ちる / 落ちないの判定は状態に依らず同じになる。
|
|
527
|
+
*
|
|
528
|
+
* 行を積まない種別では空を返す。
|
|
529
|
+
*/
|
|
530
|
+
function 積む箱を集める(n: LaidNode): 積む箱[] {
|
|
531
|
+
const 箱 = (b: { w: number; h: number }, 行: 積む行[]): 積む箱 => ({ 行, w: b.w, h: b.h });
|
|
532
|
+
if (n.kind === "tree-hierarchy") return 木の中身(n).nodes.map((b) => 箱(b, 木の行(b)));
|
|
533
|
+
if (n.kind === "mind-map") return (放射の中身(n)?.nodes ?? []).map((b) => 箱(b, 放射の行(b)));
|
|
534
|
+
return [];
|
|
535
|
+
}
|
|
536
|
+
|
|
497
537
|
/**
|
|
498
|
-
* LaidDiagram
|
|
538
|
+
* LaidDiagram を直接受け取って全ての軸で視覚検証する低 layer API (v0.9+ 追加)。
|
|
499
539
|
*
|
|
500
540
|
* visualValidate() は layout 経由で LaidDiagram を得た後、 本関数を呼び出す thin wrapper。
|
|
501
541
|
* fixture test で layout を bypass して LaidDiagram を直接構築、 各 axis の real defect を
|
|
@@ -2262,6 +2302,59 @@ function runAxes(
|
|
|
2262
2302
|
);
|
|
2263
2303
|
}
|
|
2264
2304
|
|
|
2305
|
+
// ───────────────────────────────────────────────────────────
|
|
2306
|
+
// Axis 68: box-line-dropped (箱が低くて、 書いた補足が落ちる、 #538)
|
|
2307
|
+
//
|
|
2308
|
+
// 木 (`tree-hierarchy`) と放射 (`mind-map`) は中の箱に名前と補足を積む (`箱に積む`、 #471)。
|
|
2309
|
+
// 箱の高さは段の間隔から出るため、 段や枝が増えると 2 行が入らなくなる。 入らない行は
|
|
2310
|
+
// **後ろから落ちる** = 書いた補足が知らせも印も無く図から消える。
|
|
2311
|
+
//
|
|
2312
|
+
// 落ち方は段階的でない。 箱の高さは図の中で揃っているので、 1 つ入らなくなると同じ図の補足が
|
|
2313
|
+
// 全部消える (実測 = 1 本道の木は深さ 8 で 9 件すべて出て、 深さ 9 で 10 件すべて消える)。
|
|
2314
|
+
//
|
|
2315
|
+
// 幅の側は切った印 (`…`) が図に出る (#526) が、 高さの側は **箱に描く余地が無いから落ちている**
|
|
2316
|
+
// ので印を出せない。 検査で伝えるのが唯一の出口になる。
|
|
2317
|
+
//
|
|
2318
|
+
// 判定は描く時と同じ関数を同じ引数で呼ぶ。 描いた結果と突き合わせる形は採らない = 描画と判定で
|
|
2319
|
+
// 別の経路を通ると、 片方だけ直したずれに気付けない。
|
|
2320
|
+
//
|
|
2321
|
+
// Axis 67 (`shape-label-dropped`) との境界は **どの箱を見るか**。 あちらは `shape-` の箱が絵に
|
|
2322
|
+
// 帯を譲って補いの行を落とす形 (`labelPlan`)、 こちらは木と放射の中の箱が高さで落とす形。
|
|
2323
|
+
//
|
|
2324
|
+
// 中心の箱 (放射の高さ 52 固定) も対象に含める。 今のところ落ちないが、 高さは実装が決める値
|
|
2325
|
+
// なので、 対象から外すと変えた時に落ちても見えない。
|
|
2326
|
+
// ───────────────────────────────────────────────────────────
|
|
2327
|
+
for (const n of laid.nodes) {
|
|
2328
|
+
const 箱 = 積む箱を集める(n);
|
|
2329
|
+
if (箱.length === 0) continue;
|
|
2330
|
+
/** 補足を書いた箱の数。 書いていない箱は元から描かれないので数えない。 */
|
|
2331
|
+
let 対象 = 0;
|
|
2332
|
+
const 落ちた: { title: string; h: number }[] = [];
|
|
2333
|
+
for (const b of 箱) {
|
|
2334
|
+
// 先頭 (名前) は `箱に積む` が必ず残すので、 落ちうるのは 2 行目以降だけ。
|
|
2335
|
+
const 宣言 = b.行.slice(1).filter((r) => r.text.trim() !== "");
|
|
2336
|
+
if (宣言.length === 0) continue;
|
|
2337
|
+
対象 += 1;
|
|
2338
|
+
const 残った = new Set(箱に積む(b.行, b.w, b.h).map((r) => r.役割));
|
|
2339
|
+
if (宣言.every((r) => 残った.has(r.役割))) continue;
|
|
2340
|
+
落ちた.push({ title: b.行[0]?.text ?? "", h: b.h });
|
|
2341
|
+
}
|
|
2342
|
+
if (落ちた.length === 0) continue;
|
|
2343
|
+
|
|
2344
|
+
// 名前は先頭 5 件まで出し、 **残りの件数を必ず添える**。 黙って打ち切ると、 出ている名前が
|
|
2345
|
+
// 全部だと読める。
|
|
2346
|
+
const 名前 = 落ちた.map((d) => `"${d.title}"`);
|
|
2347
|
+
const 見せる = 名前.slice(0, 5).join(" / ");
|
|
2348
|
+
const 残り = 名前.length > 5 ? ` 他 ${名前.length - 5} 件` : "";
|
|
2349
|
+
const 低 = Math.min(...落ちた.map((d) => d.h)).toFixed(1);
|
|
2350
|
+
const 高 = Math.max(...落ちた.map((d) => d.h)).toFixed(1);
|
|
2351
|
+
push(
|
|
2352
|
+
"box-line-dropped",
|
|
2353
|
+
`node "${n.id}" (${n.kind}) は箱が低く、 書いた補足が ${落ちた.length}/${対象} 箱で描かれない (${見せる}${残り}、 箱の高さ ${低 === 高 ? 低 : `${低}〜${高}`})。 図を高くするか段を減らすか、 補足を消す`,
|
|
2354
|
+
"warn",
|
|
2355
|
+
);
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2265
2358
|
// ───────────────────────────────────────────────────────────
|
|
2266
2359
|
// Axis 55: edge-inside-viewbox (edge path 全 segment 端点が viewBox 内に収まる)
|
|
2267
2360
|
//
|
|
@@ -2652,6 +2745,36 @@ function runAxes(
|
|
|
2652
2745
|
);
|
|
2653
2746
|
}
|
|
2654
2747
|
|
|
2748
|
+
// ───────────────────────────────────────────────────────────
|
|
2749
|
+
// Axis 69: title-row-overlap (題の字と 1 行目の字が重なる)
|
|
2750
|
+
//
|
|
2751
|
+
// 判定 = 題の baseline から下へ伸びる字形と、 1 行目の baseline から上へ伸びる字形が
|
|
2752
|
+
// 触れるか。 位置は `spec.ts` の 2 関数 (`genericTitleBaselineY` / `rowBaselineY`) から
|
|
2753
|
+
// 取るので、 描画側を変えれば本軸も一緒に動く。
|
|
2754
|
+
//
|
|
2755
|
+
// 題と行の縦位置は形ごとに別々の式で決まっていた時期があり、 円柱と中央寄せの 3 種で
|
|
2756
|
+
// 字が重なっていた (#558)。 軸 4 (`row-format`) と軸 16 (`row-vertical-spacing`) は
|
|
2757
|
+
// 行どうしと枠との関係しか見ないため、 題との関係は誰も見ていなかった。
|
|
2758
|
+
//
|
|
2759
|
+
// `storage` は専用の表組みで題と列の間に区切り線を引いており、 位置の出所が別なので対象外。
|
|
2760
|
+
// ───────────────────────────────────────────────────────────
|
|
2761
|
+
for (const n of diag.nodes) {
|
|
2762
|
+
if (!n.rows || n.rows.length === 0) continue;
|
|
2763
|
+
if (!rendersRows(n.kind)) continue;
|
|
2764
|
+
if (n.kind === "storage") continue;
|
|
2765
|
+
// 高さを取れない node は測らない。 0 に潰すと中央寄せの題が上端に来て誤って重なる判定になる
|
|
2766
|
+
if (typeof n.h !== "number") continue;
|
|
2767
|
+
const 題 = genericTitleBaselineY(n.kind, n.h, true);
|
|
2768
|
+
const 行 = rowBaselineY(n.kind, 0);
|
|
2769
|
+
if (行 === null) continue;
|
|
2770
|
+
const 間 = 行 - 題;
|
|
2771
|
+
if (間 >= TITLE_ROW_MIN_GAP) continue;
|
|
2772
|
+
push(
|
|
2773
|
+
"title-row-overlap",
|
|
2774
|
+
`node "${n.id}" (kind=${n.kind ?? "未指定"}) の題 baseline y=${題.toFixed(0)} と 1 行目 baseline y=${行.toFixed(0)} の間が ${間.toFixed(0)} world で、 見込む字形の高さ ${TITLE_ROW_MIN_GAP.toFixed(0)} に届かない (字が重なる)`,
|
|
2775
|
+
);
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2655
2778
|
// ───────────────────────────────────────────────────────────
|
|
2656
2779
|
// Axis 17: group-boundary-clearance (topology group の boundary padding + 外部 clearance)
|
|
2657
2780
|
//
|
|
@@ -3223,6 +3346,14 @@ function runAxes(
|
|
|
3223
3346
|
`edge "${e.id}" tone "${e.tone}" が TONE_COLORS 定義に存在しない、 marker "cdl-arrow-${e.tone}" dead ref`,
|
|
3224
3347
|
);
|
|
3225
3348
|
}
|
|
3349
|
+
// 端の形も同じ理由で dead ref になる (#560)。 marker は EDGE_HEADS の分しか定義しないので、
|
|
3350
|
+
// 定義外の値が来ると "cdl-arrow-<tone>-<未定義>" を引いて矢頭が消える。
|
|
3351
|
+
if (e.head !== undefined && !EDGE_HEADS.includes(e.head)) {
|
|
3352
|
+
push(
|
|
3353
|
+
"marker-gradient-def-integrity",
|
|
3354
|
+
`edge "${e.id}" head "${e.head}" が EDGE_HEADS 定義に存在しない、 marker "cdl-arrow-${e.tone}-${e.head}" dead ref`,
|
|
3355
|
+
);
|
|
3356
|
+
}
|
|
3226
3357
|
}
|
|
3227
3358
|
|
|
3228
3359
|
// ───────────────────────────────────────────────────────────
|
|
@@ -4296,6 +4427,7 @@ function validateMetaAxes(
|
|
|
4296
4427
|
"node-inside-viewbox",
|
|
4297
4428
|
"node-inside-lane",
|
|
4298
4429
|
"shape-label-dropped",
|
|
4430
|
+
"box-line-dropped",
|
|
4299
4431
|
"edge-inside-viewbox",
|
|
4300
4432
|
"lane-label-inside-viewbox",
|
|
4301
4433
|
"lane-label-overlap",
|
|
@@ -4365,6 +4497,7 @@ function validateMetaAxes(
|
|
|
4365
4497
|
"node-inside-viewbox",
|
|
4366
4498
|
"node-inside-lane",
|
|
4367
4499
|
"shape-label-dropped",
|
|
4500
|
+
"box-line-dropped",
|
|
4368
4501
|
"edge-inside-viewbox",
|
|
4369
4502
|
"lane-label-inside-viewbox",
|
|
4370
4503
|
"lane-label-overlap",
|
|
@@ -4400,7 +4533,7 @@ function validateMetaAxes(
|
|
|
4400
4533
|
//
|
|
4401
4534
|
// 削除済 = engine-meta 51 (mermaid-parity、 #366) / 軸 29 (svg-filter-integrity) /
|
|
4402
4535
|
// 軸 30 (neumorphism-shadow-budget、 どちらも #426 で主題の廃止に伴い削除)。
|
|
4403
|
-
const EXPECTED_AXIS_COUNT =
|
|
4536
|
+
const EXPECTED_AXIS_COUNT = 68;
|
|
4404
4537
|
if (allAxes.length !== EXPECTED_AXIS_COUNT) {
|
|
4405
4538
|
out.push({
|
|
4406
4539
|
axis: "axis-documentation-completeness",
|