@cardenelabs/cdl 0.7.0 → 0.10.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 +446 -0
- package/SPEC.md +42 -18
- package/dist/index.cjs +793 -347
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +73 -14
- package/dist/index.d.ts +73 -14
- package/dist/index.js +793 -347
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +658 -236
- 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 +658 -236
- package/dist/react.js.map +1 -1
- package/dist/{render-DquCvgOB.d.cts → render-CH1Qn3Jv.d.cts} +47 -2
- package/dist/{render-DquCvgOB.d.ts → render-CH1Qn3Jv.d.ts} +47 -2
- package/package.json +6 -3
- package/src/builder.ts +13 -1
- package/src/kinds/box-lines.ts +120 -0
- package/src/kinds/chart-line.tsx +94 -25
- package/src/kinds/gantt.tsx +152 -24
- package/src/kinds/mind-map.tsx +50 -12
- package/src/kinds/quadrant.tsx +28 -2
- package/src/kinds/shape-blockchain.tsx +65 -12
- package/src/kinds/shape-region.tsx +152 -34
- package/src/kinds/tree.tsx +92 -20
- package/src/layout/collisions.ts +10 -2
- package/src/layout/footer-shape.ts +86 -24
- package/src/layout/lifeline.ts +80 -0
- package/src/layout/spec.ts +21 -2
- package/src/layout/text-width.ts +92 -4
- package/src/layout/viewbox.ts +0 -8
- package/src/layout.ts +8 -1
- package/src/presets.ts +120 -22
- package/src/render/header.tsx +3 -1
- package/src/render/nodes.tsx +15 -1
- package/src/render/payload-binding.ts +31 -8
- package/src/render/stage.tsx +21 -34
- package/src/render/template-fields.ts +212 -0
- package/src/render/utils.ts +32 -2
- package/src/render.tsx +2 -9
- package/src/types.ts +47 -2
- package/src/validate.ts +45 -13
- package/src/visual-validate.ts +34 -1
- package/src/layout/label-shift.ts +0 -28
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import type { CdlNode } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* node のうち `{名前}` を読む欄の SSOT (#455)。
|
|
5
|
+
*
|
|
6
|
+
* 検査側 (`validate.ts`) は以前 `value` と `rows` の 2 つしか見ていなかった。 描画側は他の欄でも
|
|
7
|
+
* `{名前}` を読むため、 **書いた名前が未定義でも黙って通り**、 逆にその名前が「どこでも参照されて
|
|
8
|
+
* いない」 と報告されていた (実測)。
|
|
9
|
+
*
|
|
10
|
+
* 欄を列挙する形は足し忘れる。 そこで **列挙が `CdlNode` の全 key を覆うことを型で固定する**
|
|
11
|
+
* (末尾の `_覆えている` / `_余りがない`)。 欄を足して両方の list に入れ忘れると型検査が落ちる。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** `{名前}` を読む文字の欄 */
|
|
15
|
+
const TEXT_FIELDS = ["title", "eyebrow", "subtitle", "value", "wBind", "hBind", "visibleIf"] as const;
|
|
16
|
+
|
|
17
|
+
/** `{名前}` を読む文字の並びの欄 */
|
|
18
|
+
const LIST_FIELDS = ["rows"] as const;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 数を直接書くか `{名前}` を書く欄。
|
|
22
|
+
*
|
|
23
|
+
* 数で書かれている時は読む名前が無いので、 文字の時だけ見る。
|
|
24
|
+
*/
|
|
25
|
+
const NUMERIC_FIELDS = ["opacity", "renderOffsetX", "renderOffsetY"] as const;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* `{名前}` を読まない欄。
|
|
29
|
+
*
|
|
30
|
+
* **理由を持たせるために、 読む欄と同じく明示的に列挙する**。 ここに載っていない欄が増えると
|
|
31
|
+
* 型検査が落ちるので、 「読むのか読まないのか」 を必ず判断することになる。
|
|
32
|
+
*
|
|
33
|
+
* | 欄 | 読まない理由 |
|
|
34
|
+
* |---|---|
|
|
35
|
+
* | `id` / `lane` / `kind` / `role` / `tone` | 名前や区分で、 描画側が置換に掛けない |
|
|
36
|
+
* | `stack` / `w` / `h` / `pos*` / `ganttAxisMax` | 数のみ。 型が `number` で文字を受けない |
|
|
37
|
+
* | `shape` | 入れ子の構造。 下の `shapeTexts` が中の文字を全部拾う |
|
|
38
|
+
* | `*Data` | 図表 payload。 入れ子の構造で、 下の `payloadTexts` が中の文字を拾う |
|
|
39
|
+
*/
|
|
40
|
+
type 読まない欄 =
|
|
41
|
+
| "id"
|
|
42
|
+
| "lane"
|
|
43
|
+
| "stack"
|
|
44
|
+
| "kind"
|
|
45
|
+
| "w"
|
|
46
|
+
| "h"
|
|
47
|
+
| "posX"
|
|
48
|
+
| "posY"
|
|
49
|
+
| "posW"
|
|
50
|
+
| "posH"
|
|
51
|
+
| "role"
|
|
52
|
+
| "tone"
|
|
53
|
+
| "shape"
|
|
54
|
+
| "chartData"
|
|
55
|
+
| "ganttData"
|
|
56
|
+
| "ganttAxisMax"
|
|
57
|
+
| "mindData"
|
|
58
|
+
| "funnelData"
|
|
59
|
+
| "quadrantData"
|
|
60
|
+
| "treeData"
|
|
61
|
+
| "journeyData";
|
|
62
|
+
|
|
63
|
+
type 列挙した欄 =
|
|
64
|
+
| (typeof TEXT_FIELDS)[number]
|
|
65
|
+
| (typeof LIST_FIELDS)[number]
|
|
66
|
+
| (typeof NUMERIC_FIELDS)[number]
|
|
67
|
+
| 読まない欄;
|
|
68
|
+
|
|
69
|
+
/** 引数が `never` でなければ型検査が落ちる */
|
|
70
|
+
type 空であること<T extends never> = T;
|
|
71
|
+
|
|
72
|
+
/** `CdlNode` に列挙し漏れた欄があると落ちる */
|
|
73
|
+
export type _覆えている = 空であること<Exclude<keyof CdlNode, 列挙した欄>>;
|
|
74
|
+
|
|
75
|
+
/** `CdlNode` に無い欄を列挙していると落ちる */
|
|
76
|
+
export type _余りがない = 空であること<Exclude<列挙した欄, keyof CdlNode>>;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 読む欄と読まない欄が重なっていると落ちる。
|
|
80
|
+
*
|
|
81
|
+
* 覆えているかだけを見ると、 同じ欄を両方に入れても union で 1 つに畳まれて通る = 「読むのか
|
|
82
|
+
* 読まないのか」 を一意に決めた形にならない。
|
|
83
|
+
*/
|
|
84
|
+
export type _重なっていない = 空であること<
|
|
85
|
+
Extract<
|
|
86
|
+
| (typeof TEXT_FIELDS)[number]
|
|
87
|
+
| (typeof LIST_FIELDS)[number]
|
|
88
|
+
| (typeof NUMERIC_FIELDS)[number],
|
|
89
|
+
読まない欄
|
|
90
|
+
>
|
|
91
|
+
>;
|
|
92
|
+
|
|
93
|
+
/** `shape` のうち `{名前}` を読む欄 */
|
|
94
|
+
const SHAPE_TEMPLATE_FIELDS = ["source", "radius", "fillProgress", "angle", "level", "rotation", "fill"] as const;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* `shape` のうち `{名前}` を読まない欄。
|
|
98
|
+
*
|
|
99
|
+
* `stroke` はここに入る。 `kinds/dyn-shape.tsx` は `shape.stroke ?? 既定` をそのまま SVG に渡す
|
|
100
|
+
* (`fill` だけが `interpolateColor` を通る)。 実測で `stroke: "{名前}"` は画面にも書いたまま
|
|
101
|
+
* 残った。 読む欄に入れると **画面には出ないのに検査だけが通る / 落ちる** ずれが生まれる。
|
|
102
|
+
*
|
|
103
|
+
* 残りは数だけを受ける欄 (`fillMax` / `innerRadius` 等) と種類の名前 (`kind`)。
|
|
104
|
+
*/
|
|
105
|
+
type 形の読まない欄 =
|
|
106
|
+
| "kind"
|
|
107
|
+
| "fillMax"
|
|
108
|
+
| "orient"
|
|
109
|
+
| "stroke"
|
|
110
|
+
| "innerRadius"
|
|
111
|
+
| "outerRadius"
|
|
112
|
+
| "startAngle"
|
|
113
|
+
| "sweepMax"
|
|
114
|
+
| "amplitude"
|
|
115
|
+
| "frequency"
|
|
116
|
+
| "waveHeight"
|
|
117
|
+
| "sides";
|
|
118
|
+
|
|
119
|
+
/** union の全ての枝の key を集める (`keyof` は共通の key しか返さない) */
|
|
120
|
+
type 全ての枝の欄<T> = T extends unknown ? keyof T : never;
|
|
121
|
+
|
|
122
|
+
type 形の欄 = 全ての枝の欄<NonNullable<CdlNode["shape"]>>;
|
|
123
|
+
type 列挙した形の欄 = (typeof SHAPE_TEMPLATE_FIELDS)[number] | 形の読まない欄;
|
|
124
|
+
|
|
125
|
+
/** `CdlDynShape` の枝に列挙し漏れた欄があると落ちる */
|
|
126
|
+
export type _形を覆えている = 空であること<Exclude<形の欄, 列挙した形の欄>>;
|
|
127
|
+
|
|
128
|
+
/** `CdlDynShape` に無い欄を列挙していると落ちる */
|
|
129
|
+
export type _形に余りがない = 空であること<Exclude<列挙した形の欄, 形の欄>>;
|
|
130
|
+
|
|
131
|
+
/** 読む欄と読まない欄が重なっていると落ちる */
|
|
132
|
+
export type _形が重なっていない = 空であること<
|
|
133
|
+
Extract<(typeof SHAPE_TEMPLATE_FIELDS)[number], 形の読まない欄>
|
|
134
|
+
>;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* `shape` のうち `{名前}` を読む欄の文字を拾う。
|
|
138
|
+
*
|
|
139
|
+
* 枝ごとに欄が違うが、 欄の名前は枝をまたいで重ならない (`radius` は `rect` では数だけを
|
|
140
|
+
* 受けるので、 文字の時だけ拾う形で問題にならない)。 列挙の覆えているかは上の型が固定する。
|
|
141
|
+
*/
|
|
142
|
+
function shapeTexts(shape: CdlNode["shape"]): string[] {
|
|
143
|
+
if (!shape) return [];
|
|
144
|
+
const out: string[] = [];
|
|
145
|
+
const 中身 = shape as Record<string, unknown>;
|
|
146
|
+
for (const f of SHAPE_TEMPLATE_FIELDS) {
|
|
147
|
+
const v = 中身[f];
|
|
148
|
+
if (typeof v === "string" && v) out.push(v);
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 図表 payload のうち `{名前}` を読む欄の文字を拾う。
|
|
155
|
+
*
|
|
156
|
+
* 解決するのは `render/payload-binding.ts` で node の欄とは別経路だが、 **未定義の判定と
|
|
157
|
+
* unused の判定は同じ** = 別扱いにすると、 payload だけで読んでいる状態が「どこでも参照されて
|
|
158
|
+
* いない」 と報告される (実測)。
|
|
159
|
+
*
|
|
160
|
+
* 数と語の欄 (`value` / `count` / `startIdx` / `quadrant` / `emotion` 等) は数や決まった語を
|
|
161
|
+
* 直接書くこともあるため、 文字の時だけ拾う。
|
|
162
|
+
*/
|
|
163
|
+
function payloadTexts(node: CdlNode): string[] {
|
|
164
|
+
const out: string[] = [];
|
|
165
|
+
const 足す = (v: unknown): void => {
|
|
166
|
+
if (typeof v === "string" && v) out.push(v);
|
|
167
|
+
};
|
|
168
|
+
for (const d of node.chartData ?? []) 足す(d.value);
|
|
169
|
+
for (const s of node.funnelData ?? []) 足す(s.count);
|
|
170
|
+
for (const t of node.ganttData ?? []) {
|
|
171
|
+
足す(t.startIdx);
|
|
172
|
+
足す(t.endIdx);
|
|
173
|
+
}
|
|
174
|
+
for (const i of node.quadrantData?.items ?? []) 足す(i.quadrant);
|
|
175
|
+
for (const s of node.journeyData ?? []) 足す(s.emotion);
|
|
176
|
+
for (const n of node.treeData ?? []) {
|
|
177
|
+
足す(n.title);
|
|
178
|
+
足す(n.subtitle);
|
|
179
|
+
}
|
|
180
|
+
if (node.mindData) {
|
|
181
|
+
足す(node.mindData.rootTitle);
|
|
182
|
+
for (const b of node.mindData.branches) {
|
|
183
|
+
足す(b.title);
|
|
184
|
+
足す(b.subtitle);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* その node が `{名前}` を読みうる文字を全部返す。
|
|
192
|
+
*
|
|
193
|
+
* 返すのは文字だけで、 名前の切り出しは呼ぶ側が `templateRefIds` で行う。 切り出しの式を
|
|
194
|
+
* 1 箇所に保つため (描画側と別に書くと字種がずれ、 置換されうる名前が検証を素通りする)。
|
|
195
|
+
*/
|
|
196
|
+
export function nodeTemplateTexts(node: CdlNode): string[] {
|
|
197
|
+
const out: string[] = [];
|
|
198
|
+
for (const f of TEXT_FIELDS) {
|
|
199
|
+
const v = node[f];
|
|
200
|
+
if (typeof v === "string" && v) out.push(v);
|
|
201
|
+
}
|
|
202
|
+
for (const f of LIST_FIELDS) {
|
|
203
|
+
for (const v of node[f] ?? []) if (v) out.push(v);
|
|
204
|
+
}
|
|
205
|
+
for (const f of NUMERIC_FIELDS) {
|
|
206
|
+
const v = node[f];
|
|
207
|
+
if (typeof v === "string" && v) out.push(v);
|
|
208
|
+
}
|
|
209
|
+
out.push(...shapeTexts(node.shape));
|
|
210
|
+
out.push(...payloadTexts(node));
|
|
211
|
+
return out;
|
|
212
|
+
}
|
package/src/render/utils.ts
CHANGED
|
@@ -50,7 +50,15 @@ export function computeStateValues(
|
|
|
50
50
|
phaseIdx: number,
|
|
51
51
|
progress: number,
|
|
52
52
|
): Record<string, string> {
|
|
53
|
-
|
|
53
|
+
// 継承を持たない入れ物に集める (#456)。 通常の object だと 2 つ壊れる。
|
|
54
|
+
// `__proto__` という名前の状態は代入が黙って捨てられて値を入れても読めず、
|
|
55
|
+
// `constructor` / `toString` は値を入れていなくても「値がある」 と読まれて
|
|
56
|
+
// prototype の中身が図に出る (実測 = `{constructor}` が
|
|
57
|
+
// `function Object() { [native code] }` として描かれた)。
|
|
58
|
+
//
|
|
59
|
+
// 読み手側の `Object.hasOwn` は残す = この入れ物は spread や `JSON.parse` を
|
|
60
|
+
// 通ると通常の object に戻るため、 入れ物だけでは塞ぎ切れない。
|
|
61
|
+
const values: Record<string, number | string> = Object.create(null);
|
|
54
62
|
for (const s of laid.states) values[s.id] = s.initial;
|
|
55
63
|
|
|
56
64
|
for (let i = 0; i < laid.phases.length; i++) {
|
|
@@ -69,11 +77,33 @@ export function computeStateValues(
|
|
|
69
77
|
}
|
|
70
78
|
}
|
|
71
79
|
}
|
|
72
|
-
const out: Record<string, string> =
|
|
80
|
+
const out: Record<string, string> = Object.create(null);
|
|
73
81
|
for (const [k, v] of Object.entries(values)) out[k] = String(v);
|
|
74
82
|
return withDerivedValues(out, laid.derived);
|
|
75
83
|
}
|
|
76
84
|
|
|
85
|
+
/**
|
|
86
|
+
* 段の値に、 入力欄 / 式 / 巻き取りの値を重ねる (interactive 経路)。
|
|
87
|
+
*
|
|
88
|
+
* 継承を持たない入れ物のまま返す (#456)。 spread (`{ ...base }`) で受けると通常の object に
|
|
89
|
+
* 戻り、 `constructor` / `toString` が「値がある」 と読まれる状態に戻る。
|
|
90
|
+
*
|
|
91
|
+
* 重ねるのは数 / 文字列 / 真偽の 3 種だけ。 それ以外は状態の値として文字にできないため落とす。
|
|
92
|
+
*/
|
|
93
|
+
export function mergeStateValues(
|
|
94
|
+
base: Record<string, string>,
|
|
95
|
+
overrides: Record<string, unknown>,
|
|
96
|
+
): Record<string, string> {
|
|
97
|
+
const merged: Record<string, string> = Object.assign(Object.create(null), base);
|
|
98
|
+
for (const key of Object.keys(overrides)) {
|
|
99
|
+
const v = overrides[key];
|
|
100
|
+
if (typeof v === "number" || typeof v === "string" || typeof v === "boolean") {
|
|
101
|
+
merged[key] = String(v);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return merged;
|
|
105
|
+
}
|
|
106
|
+
|
|
77
107
|
/**
|
|
78
108
|
* template literal `{stateId}` を values map で置換 (未解決は `{id}` のまま)。
|
|
79
109
|
* array signal 拡張 (SSOT): 値が JSON array (例 `"[1,2,3]"`) の signal に対しては、
|
package/src/render.tsx
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
useInteractiveIntegration,
|
|
10
10
|
type InteractiveHandlerMap,
|
|
11
11
|
} from "./render/interactive-panel";
|
|
12
|
-
import { computeStateValues } from "./render/utils";
|
|
12
|
+
import { computeStateValues, mergeStateValues } from "./render/utils";
|
|
13
13
|
import { visualValidateLaid } from "./visual-validate";
|
|
14
14
|
import type { CdlDiagram, LaidDiagram } from "./types";
|
|
15
15
|
|
|
@@ -153,14 +153,7 @@ export function CdlDiagramView({ diagram, laid: laidProp, hideHeader = false, hi
|
|
|
153
153
|
// 既存 phase-driven state と衝突しないよう state.id が signal / formula / scroll id と同じ場合のみ上書き。
|
|
154
154
|
const stateValues = useMemo(() => {
|
|
155
155
|
if (!isInteractive) return baseStateValues;
|
|
156
|
-
|
|
157
|
-
for (const key of Object.keys(interactive.stateOverrides)) {
|
|
158
|
-
const v = interactive.stateOverrides[key];
|
|
159
|
-
if (typeof v === "number" || typeof v === "string" || typeof v === "boolean") {
|
|
160
|
-
merged[key] = String(v);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
return merged;
|
|
156
|
+
return mergeStateValues(baseStateValues, interactive.stateOverrides);
|
|
164
157
|
}, [isInteractive, baseStateValues, interactive.stateOverrides]);
|
|
165
158
|
|
|
166
159
|
return (
|
package/src/types.ts
CHANGED
|
@@ -209,6 +209,17 @@ export type CdlNode = {
|
|
|
209
209
|
*/
|
|
210
210
|
posW?: number;
|
|
211
211
|
posH?: number;
|
|
212
|
+
/**
|
|
213
|
+
* 図の中での役割。 位置ではなく **組み立てた側が付ける目印** で、 描画側の分岐に使う。
|
|
214
|
+
*
|
|
215
|
+
* `lifeline-footer` = 順序図で lane の末尾に置く下端の名札 (`presets.ts` の `build()` が付ける)。
|
|
216
|
+
* 一部の `shape-` は絵を箱の下端から上へ積むため、 箱が低いと絵が縦線と重なる。 その回避で
|
|
217
|
+
* 絵を下げる対象がこの名札に限られる。
|
|
218
|
+
*
|
|
219
|
+
* **位置で推測してはいけない** = 手で組んだ図で一番下が名札でない `shape-` の node だと、
|
|
220
|
+
* その絵が下がってしまう (`#458`)。
|
|
221
|
+
*/
|
|
222
|
+
role?: "lifeline-footer";
|
|
212
223
|
/**
|
|
213
224
|
* node の主色。 未指定なら kind ごとの既定色。
|
|
214
225
|
*
|
|
@@ -269,6 +280,19 @@ export type CdlNode = {
|
|
|
269
280
|
* kinds/gantt.tsx が縦軸 task lane + 横軸日付 rect bar で SVG 描画する。
|
|
270
281
|
*/
|
|
271
282
|
ganttData?: GanttTaskPayload[];
|
|
283
|
+
/**
|
|
284
|
+
* gantt-timeline の横軸を何目盛り分にするか (`#483`)。
|
|
285
|
+
*
|
|
286
|
+
* 書かない図では今までどおり全 task の最大値から自動で決まる。 書くと尺がその値で固定され、
|
|
287
|
+
* 状態で動く task があっても他の task の帯の位置と幅が変わらない。
|
|
288
|
+
*
|
|
289
|
+
* 位置は 0 から `ganttAxisMax - 1` までで、 そこから外れる task の帯は右端の目盛りに寄せて
|
|
290
|
+
* 描く (図の外へ出さない、 `kinds/gantt.tsx` SSOT)。
|
|
291
|
+
*
|
|
292
|
+
* 有限の正の数でなければ書かなかったものとして扱う = 尺が壊れて帯が図の外へ出るより、
|
|
293
|
+
* 自動に落ちる方が読める図になる。
|
|
294
|
+
*/
|
|
295
|
+
ganttAxisMax?: number;
|
|
272
296
|
/**
|
|
273
297
|
* mind-map 専用の branch payload。 tree 構造を 1 node に格納し、
|
|
274
298
|
* kinds/mind-map.tsx が中心 + 放射で SVG 描画する。
|
|
@@ -381,13 +405,20 @@ export type QuadrantItemPayload = {
|
|
|
381
405
|
subtitle?: string;
|
|
382
406
|
};
|
|
383
407
|
|
|
384
|
-
/**
|
|
408
|
+
/**
|
|
409
|
+
* tree-hierarchy kind の 1 node payload。
|
|
410
|
+
*
|
|
411
|
+
* **上の小ラベル (`eyebrow`) は受けない** (#471)。 箱の高さは段の間隔から出ており
|
|
412
|
+
* (`Math.min(38, 間隔 * 0.7)`)、 名前と補足で 2 行を使うと 3 行目は入らない。 高さを広げると
|
|
413
|
+
* 段どうしが近づいて箱が重なる。 受け取って描かない欄は、 書き手から効いていないことが
|
|
414
|
+
* 読めないため置かない。
|
|
415
|
+
*/
|
|
385
416
|
export type TreeNodePayload = {
|
|
386
417
|
id: string;
|
|
387
418
|
title: string;
|
|
388
419
|
parent?: string;
|
|
420
|
+
/** 箱の 2 行目に出す補足 */
|
|
389
421
|
subtitle?: string;
|
|
390
|
-
eyebrow?: string;
|
|
391
422
|
};
|
|
392
423
|
|
|
393
424
|
/** journey-map の気持ち 5 段階 */
|
|
@@ -2093,6 +2124,20 @@ export type CdlPhase = {
|
|
|
2093
2124
|
title: string;
|
|
2094
2125
|
body: string;
|
|
2095
2126
|
activate: string[];
|
|
2127
|
+
/**
|
|
2128
|
+
* この phase で「左の起点から描き始める」 node の id (cdl#512)。
|
|
2129
|
+
*
|
|
2130
|
+
* `activate` と同じ形で node id を並べる。 対象の node は phase の進み (0→1) に合わせて
|
|
2131
|
+
* 描かれ、 進みが 1 に達した時点で全体が出る。 現在の対象は `chart-line` だけで、
|
|
2132
|
+
* 折れ線が左端から右へ伸び、 点と値の数字は線の先が届いた時に現れる。
|
|
2133
|
+
*
|
|
2134
|
+
* **`activate` を兼ねさせない**。 1 箱で図全体を描く種別 (`chart-*` 等) では焦点が
|
|
2135
|
+
* その箱に解決されるため、 連続する phase で光り続ける図が実在する。 「光っていたら
|
|
2136
|
+
* 描く」 にすると、 値が動くだけの phase でも線を引き直すことになる。
|
|
2137
|
+
*
|
|
2138
|
+
* 書かない phase では従来どおり全長で描く (DOM に dash 属性も付かない)。
|
|
2139
|
+
*/
|
|
2140
|
+
draw?: string[];
|
|
2096
2141
|
/** 数値 state の線形補間 (write phase で 100→90 等) */
|
|
2097
2142
|
tweens: Array<{ stateId: string; from: number; to: number }>;
|
|
2098
2143
|
/** 任意型 state の即時切替 (この phase に到達したら値を上書き、 lerp なし) */
|
package/src/validate.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CdlDiagram } from "./types";
|
|
2
2
|
import { TONES as TONE_NAMES, NODE_KINDS } from "./types";
|
|
3
3
|
import { templateRefIds } from "./render/utils";
|
|
4
|
+
import { nodeTemplateTexts } from "./render/template-fields";
|
|
4
5
|
import { derivedRefIds } from "./render/derived-values";
|
|
5
6
|
|
|
6
7
|
const TONES = new Set<string>(TONE_NAMES);
|
|
@@ -88,10 +89,26 @@ export function validate(diag: CdlDiagram): void {
|
|
|
88
89
|
// 2 つを 1 つに畳んではいけない。 畳むと `tween` の宛先に derived を書けてしまい、
|
|
89
90
|
// 「式で決まる値を段が動かす」 という成立しない指定が素通りする (derived は毎 frame
|
|
90
91
|
// 計算し直されるので、 段が入れた値は次の frame で消える)。
|
|
91
|
-
|
|
92
|
+
//
|
|
93
|
+
// **描画側で名前になる 3 経路も足す** (#455)。 入力欄 (`inputs`) / 入力欄の式 (`formulas`) /
|
|
94
|
+
// 巻き取りの進み (`scrollTriggers`) は、 いずれも id をそのまま名前にした signal を作り
|
|
95
|
+
// (`createInputSignals` / `createFormulaComputeds` / `createScrollProgressSignals`)、
|
|
96
|
+
// `useInteractiveIntegration` がその値を `stateOverrides` に入れ、 `render.tsx` が描画用の
|
|
97
|
+
// 値に混ぜる。 足さないと、 実際に読める名前が「未定義」 と報告される (実測 = 図録の
|
|
98
|
+
// `interactive.cdl.ts:eventVariety` で `{lastEvent}` と `{received}` が偽陽性になった)。
|
|
99
|
+
//
|
|
100
|
+
// `readouts` は既にある値を見せる側、 `eventBindings` は受け手の結線で、 どちらも新しい
|
|
101
|
+
// 名前を作らない。
|
|
102
|
+
const readableIds = new Set([
|
|
103
|
+
...stateIds,
|
|
104
|
+
...(diag.derived ?? []).map((d) => d.id),
|
|
105
|
+
...(diag.inputs ?? []).map((i) => i.id),
|
|
106
|
+
...(diag.formulas ?? []).map((f) => f.id),
|
|
107
|
+
...(diag.scrollTriggers ?? []).map((t) => t.id),
|
|
108
|
+
]);
|
|
92
109
|
|
|
93
110
|
// 「もしかして」 サジェスト (typo 検出): edit distance 1 で最も近い候補 1 件
|
|
94
|
-
const suggest = (target: string, candidates: Set<string
|
|
111
|
+
const suggest = (target: string, candidates: Set<string>, candidateKind: "lane" | "node" = "lane"): string => {
|
|
95
112
|
const list = [...candidates];
|
|
96
113
|
if (list.length === 0) return "";
|
|
97
114
|
let best = "";
|
|
@@ -103,7 +120,7 @@ export function validate(diag: CdlDiagram): void {
|
|
|
103
120
|
best = c;
|
|
104
121
|
}
|
|
105
122
|
}
|
|
106
|
-
return bestD <= 2 ? ` (もしかして "${best}"?)` : ` (定義済
|
|
123
|
+
return bestD <= 2 ? ` (もしかして "${best}"?)` : ` (定義済 ${candidateKind}: ${list.slice(0, 5).join(", ")}${list.length > 5 ? "..." : ""})`;
|
|
107
124
|
};
|
|
108
125
|
for (const n of diag.nodes) {
|
|
109
126
|
if (!laneIds.has(n.lane)) {
|
|
@@ -115,12 +132,12 @@ export function validate(diag: CdlDiagram): void {
|
|
|
115
132
|
for (const e of diag.edges) {
|
|
116
133
|
if (!nodeIds.has(e.from)) {
|
|
117
134
|
errors.push(
|
|
118
|
-
`[cdl] unknown-ref: edge "${e.id}" の from "${e.from}" が node に存在しません${suggest(e.from, nodeIds)}\n → 修正例: .node("${e.from}", { lane: "...", stack: 0, kind: "actor", title: "..." }) を edge 宣言前に追加`,
|
|
135
|
+
`[cdl] unknown-ref: edge "${e.id}" の from "${e.from}" が node に存在しません${suggest(e.from, nodeIds, "node")}\n → 修正例: .node("${e.from}", { lane: "...", stack: 0, kind: "actor", title: "..." }) を edge 宣言前に追加`,
|
|
119
136
|
);
|
|
120
137
|
}
|
|
121
138
|
if (!nodeIds.has(e.to)) {
|
|
122
139
|
errors.push(
|
|
123
|
-
`[cdl] unknown-ref: edge "${e.id}" の to "${e.to}" が node に存在しません${suggest(e.to, nodeIds)}\n → 修正例: .node("${e.to}", { lane: "...", stack: 0, kind: "...", title: "..." }) を edge 宣言前に追加`,
|
|
140
|
+
`[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 宣言前に追加`,
|
|
124
141
|
);
|
|
125
142
|
}
|
|
126
143
|
if (e.from === e.to) {
|
|
@@ -139,6 +156,20 @@ export function validate(diag: CdlDiagram): void {
|
|
|
139
156
|
errors.push(`[cdl] unknown-ref: phase "${p.id}" の activate "${id}" は node でも edge でもない (typo の可能性)`);
|
|
140
157
|
}
|
|
141
158
|
}
|
|
159
|
+
// 描き始める対象は node に限る (cdl#512)。 edge は `activate` された phase で自分から
|
|
160
|
+
// 伸びるため (`render/edges.tsx` の `SolidEdgePath`)、 `draw` で指す相手にならない
|
|
161
|
+
for (const id of p.draw ?? []) {
|
|
162
|
+
if (!nodeIds.has(id)) {
|
|
163
|
+
errors.push(`[cdl] unknown-ref: phase "${p.id}" の draw "${id}" は node ではない (typo の可能性)${suggest(id, nodeIds, "node")}`);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
// 描き方を持たない種別を指しても図は出るが、 書いた指定は何もしない。
|
|
167
|
+
// 黙って捨てると「書いたのに伸びない」 が手掛かりなしで起きる
|
|
168
|
+
const kind = diag.nodes.find((n) => n.id === id)?.kind;
|
|
169
|
+
if (kind !== "chart-line") {
|
|
170
|
+
warnings.push(`phase "${p.id}" の draw "${id}" は kind "${kind}" で、 左から描く動きを持たない (対象は chart-line)`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
142
173
|
for (const t of p.tweens) {
|
|
143
174
|
if (!stateIds.has(t.stateId)) {
|
|
144
175
|
errors.push(`[cdl] unknown-ref: phase "${p.id}" tween 対象 state "${t.stateId}" 未定義`);
|
|
@@ -158,13 +189,13 @@ export function validate(diag: CdlDiagram): void {
|
|
|
158
189
|
}
|
|
159
190
|
|
|
160
191
|
// ───────────────────────────────────────────────────────────
|
|
161
|
-
// 6. state 参照 (
|
|
192
|
+
// 6. state 参照 ({名前} を読む欄) → 未定義は warning
|
|
162
193
|
// ───────────────────────────────────────────────────────────
|
|
194
|
+
// 見る欄は `nodeTemplateTexts` が SSOT (#455)。 以前は `value` と `rows` の 2 つだけを見て
|
|
195
|
+
// いたが、 描画側は `title` / `visibleIf` / `wBind` 等でも `{名前}` を読むため、 書いた名前が
|
|
196
|
+
// 未定義でも黙って通っていた
|
|
163
197
|
for (const n of diag.nodes) {
|
|
164
|
-
const
|
|
165
|
-
if (n.value) checks.push(n.value);
|
|
166
|
-
if (n.rows) checks.push(...n.rows);
|
|
167
|
-
for (const text of checks) {
|
|
198
|
+
for (const text of nodeTemplateTexts(n)) {
|
|
168
199
|
// 名前の切り出しは描画側と同じ式を使う (`templateRefIds`)。 別に書くと字種がずれ、
|
|
169
200
|
// 置換されうる名前が検証を素通りする
|
|
170
201
|
for (const id of templateRefIds(text)) {
|
|
@@ -193,15 +224,16 @@ export function validate(diag: CdlDiagram): void {
|
|
|
193
224
|
warnings.push(`edge "${e.id}" がどの phase でも activate されていない (unused、 typo の可能性)`);
|
|
194
225
|
}
|
|
195
226
|
}
|
|
196
|
-
// 使われない state も warn (tween / set / node
|
|
227
|
+
// 使われない state も warn (tween / set / node の `{名前}` を読む欄 のいずれにも参照されない)
|
|
197
228
|
const stateUsed = new Set<string>();
|
|
198
229
|
for (const p of diag.phases) {
|
|
199
230
|
for (const t of p.tweens) stateUsed.add(t.stateId);
|
|
200
231
|
for (const s of p.sets) stateUsed.add(s.stateId);
|
|
201
232
|
}
|
|
202
233
|
for (const n of diag.nodes) {
|
|
203
|
-
|
|
204
|
-
|
|
234
|
+
// 読む欄は上の 6 と同じ SSOT から取る。 別々に列挙すると、 片方だけに欄が足されたときに
|
|
235
|
+
// 「未定義として warn しつつ unused としても warn する」 という食い違いが出る (#455)
|
|
236
|
+
for (const text of nodeTemplateTexts(n)) {
|
|
205
237
|
// 名前の切り出しは描画側と同じ式を使う。 別に書くと字種がずれ、 実際は読まれている
|
|
206
238
|
// 状態が「どこでも参照されていない」 と報告される
|
|
207
239
|
for (const id of templateRefIds(text)) stateUsed.add(id);
|
package/src/visual-validate.ts
CHANGED
|
@@ -87,6 +87,7 @@ import {
|
|
|
87
87
|
segmentsCrossRect,
|
|
88
88
|
} from "./layout/collisions";
|
|
89
89
|
import { measureTextWidth } from "./layout/text-width";
|
|
90
|
+
import { droppedLineIsHidden, labelPlan } from "./kinds/shape-region";
|
|
90
91
|
import { templateRefIds } from "./render/utils";
|
|
91
92
|
import {
|
|
92
93
|
bboxOverlaps,
|
|
@@ -129,6 +130,7 @@ import {
|
|
|
129
130
|
isRenderedNode,
|
|
130
131
|
rendersRows,
|
|
131
132
|
rendersTitleText,
|
|
133
|
+
fitsOwnTitleWidth,
|
|
132
134
|
rowBaselineY,
|
|
133
135
|
rowGlyphDepth,
|
|
134
136
|
LANE_BORDER_CLEARANCE_TOL,
|
|
@@ -327,6 +329,8 @@ export type VisualAxis =
|
|
|
327
329
|
// Axis 53 (2026-07-03 追加) = node が diagram viewBox 内に収まっているか。
|
|
328
330
|
// 既存 Axis 14 (label-inside-viewbox) の node 版、 clip 検知の直接手段。
|
|
329
331
|
| "node-inside-viewbox"
|
|
332
|
+
// Axis 67 (`#492` 追加) = 箱が狭くて書いた補いの行が描かれない node を書き手に伝える。
|
|
333
|
+
| "shape-label-dropped"
|
|
330
334
|
// Axis 54 (2026-07-03 追加) = node bbox が所属 lane 内に収まっているか。
|
|
331
335
|
// Axis 15 (lane-cx-consistency) は中心 x のみ、 本 axis は node 4 辺と lane 4 辺の包含関係を検知。
|
|
332
336
|
| "node-inside-lane"
|
|
@@ -471,6 +475,7 @@ function emptyCounts(): Record<VisualAxis, number> {
|
|
|
471
475
|
"validate-performance-budget": 0,
|
|
472
476
|
"node-inside-viewbox": 0,
|
|
473
477
|
"node-inside-lane": 0,
|
|
478
|
+
"shape-label-dropped": 0,
|
|
474
479
|
"edge-inside-viewbox": 0,
|
|
475
480
|
"lane-label-inside-viewbox": 0,
|
|
476
481
|
"lane-label-overlap": 0,
|
|
@@ -1833,6 +1838,9 @@ function runAxes(
|
|
|
1833
1838
|
if (n.id.endsWith("-spacer")) continue;
|
|
1834
1839
|
// 文字を描かない種別は title が幅を超えて切れることが起きない。
|
|
1835
1840
|
if (!rendersTitleText(n.kind)) continue;
|
|
1841
|
+
// 自分で幅に合わせて切る種別も同じ。 字数から幅を出す下の近似は字の大きさ 22 を前提に
|
|
1842
|
+
// しており、 それより小さい字で描く種別に当てると偽陽性になる (#469)
|
|
1843
|
+
if (fitsOwnTitleWidth(n.kind)) continue;
|
|
1836
1844
|
// 常に非表示の node の文字は画面に出ないので、 切れようがない。
|
|
1837
1845
|
if (!isRenderedNode(n)) continue;
|
|
1838
1846
|
const titleTrimmed = n.title?.trim() ?? "";
|
|
@@ -2231,6 +2239,29 @@ function runAxes(
|
|
|
2231
2239
|
}
|
|
2232
2240
|
}
|
|
2233
2241
|
|
|
2242
|
+
// ───────────────────────────────────────────────────────────
|
|
2243
|
+
// Axis 67: shape-label-dropped (箱が狭くて書いた補いの行が描かれない、 #492)
|
|
2244
|
+
//
|
|
2245
|
+
// `shape-` の箱は絵と文字を同じ箱に収める。 箱が狭いと絵に帯を譲るため補いの行 (肩書 →
|
|
2246
|
+
// 説明) を描かない (`#459`)。 **書いたのに図に出ない** ので、 書き手に伝える経路が要る。
|
|
2247
|
+
//
|
|
2248
|
+
// 共有 plan と固有 layout で描く行が種別ごとに違う。 さらに肩書だけを描く種別と説明だけを
|
|
2249
|
+
// 描く種別があるため、 落とした行を 1 行ずつ実際の描画規則と照合する。
|
|
2250
|
+
//
|
|
2251
|
+
// ## 覆えていない範囲
|
|
2252
|
+
//
|
|
2253
|
+
// 共有 plan が行を落とさない高さでは、 固有 layout がその行を描かない種別でも報告されない。
|
|
2254
|
+
// 本軸は箱が狭くて落ちた行だけを扱い、 種別が元から対応しない欄は別の検査範囲とする。
|
|
2255
|
+
for (const n of laid.nodes) {
|
|
2256
|
+
const 落とした = labelPlan(n).落とした.filter((line) => droppedLineIsHidden(n.kind, line));
|
|
2257
|
+
if (落とした.length === 0) continue;
|
|
2258
|
+
push(
|
|
2259
|
+
"shape-label-dropped",
|
|
2260
|
+
`node "${n.id}" (${n.kind}, ${n.w.toFixed(0)}x${n.h.toFixed(0)}) は書いた ${落とした.join(" / ")} が描かれない。 箱を高くするか、 その行を消す`,
|
|
2261
|
+
"warn",
|
|
2262
|
+
);
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2234
2265
|
// ───────────────────────────────────────────────────────────
|
|
2235
2266
|
// Axis 55: edge-inside-viewbox (edge path 全 segment 端点が viewBox 内に収まる)
|
|
2236
2267
|
//
|
|
@@ -4264,6 +4295,7 @@ function validateMetaAxes(
|
|
|
4264
4295
|
"validate-performance-budget",
|
|
4265
4296
|
"node-inside-viewbox",
|
|
4266
4297
|
"node-inside-lane",
|
|
4298
|
+
"shape-label-dropped",
|
|
4267
4299
|
"edge-inside-viewbox",
|
|
4268
4300
|
"lane-label-inside-viewbox",
|
|
4269
4301
|
"lane-label-overlap",
|
|
@@ -4332,6 +4364,7 @@ function validateMetaAxes(
|
|
|
4332
4364
|
"validate-performance-budget",
|
|
4333
4365
|
"node-inside-viewbox",
|
|
4334
4366
|
"node-inside-lane",
|
|
4367
|
+
"shape-label-dropped",
|
|
4335
4368
|
"edge-inside-viewbox",
|
|
4336
4369
|
"lane-label-inside-viewbox",
|
|
4337
4370
|
"lane-label-overlap",
|
|
@@ -4367,7 +4400,7 @@ function validateMetaAxes(
|
|
|
4367
4400
|
//
|
|
4368
4401
|
// 削除済 = engine-meta 51 (mermaid-parity、 #366) / 軸 29 (svg-filter-integrity) /
|
|
4369
4402
|
// 軸 30 (neumorphism-shadow-budget、 どちらも #426 で主題の廃止に伴い削除)。
|
|
4370
|
-
const EXPECTED_AXIS_COUNT =
|
|
4403
|
+
const EXPECTED_AXIS_COUNT = 67;
|
|
4371
4404
|
if (allAxes.length !== EXPECTED_AXIS_COUNT) {
|
|
4372
4405
|
out.push({
|
|
4373
4406
|
axis: "axis-documentation-completeness",
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type { LaidEdge, LaidLane, LaidNode } from "../types";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Edge label の post-pass shift 探索は CAR-430 で削除済 (no-op passthrough)。
|
|
5
|
-
*
|
|
6
|
-
* 旧実装 (v10 系) は 16 方向 × 26 段階の全探索型 shift + Tier 0-3 fallback +
|
|
7
|
-
* PROXIMITY_HARD_CAP + computeFanTShift 等の場当たり logic で label 位置を再計算していた。
|
|
8
|
-
* CAR-426 chain 伝搬 shift の label 層拡張 (CAR-426-A / CAR-428) 完了で
|
|
9
|
-
* `resolveEdgeLabelOverlapsWithChain` (`layout/collisions.ts`) が label overlap 解消の
|
|
10
|
-
* 単一 SSOT になったため、 本 file の shift 探索は 二重権限 として削除された。
|
|
11
|
-
*
|
|
12
|
-
* label 配置の権限フロー (CAR-430 SSOT)。
|
|
13
|
-
* 1. `edges.ts routePath()` = 初期 label 位置 (path 中点 ± 30 world 等) を SSOT で決定
|
|
14
|
-
* 2. `resolveEdgeLabelOverlapsWithChain` (post-pass 2) = overlap 検出時のみ chain 伝搬 shift
|
|
15
|
-
* 3. 本 function = 通過 no-op (旧 API caller の後方互換のため signature 保持)
|
|
16
|
-
*
|
|
17
|
-
* 後方互換のため export は残置、 内部で input を そのまま返す。
|
|
18
|
-
* caller (`layout.ts` post-pass 1) は CAR-430 同 PR で削除済で、 本 function は
|
|
19
|
-
* pipeline 経由では呼ばれない。 外部 caller (dragon 等の direct import) がある可能性を
|
|
20
|
-
* 考慮して signature のみ維持する。
|
|
21
|
-
*/
|
|
22
|
-
export function shiftLabelsAwayFromNodes(
|
|
23
|
-
edges: LaidEdge[],
|
|
24
|
-
_nodes: LaidNode[],
|
|
25
|
-
_lanes: LaidLane[],
|
|
26
|
-
): LaidEdge[] {
|
|
27
|
-
return edges;
|
|
28
|
-
}
|