@cardenelabs/cdl 0.9.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.
@@ -2047,6 +2047,20 @@ type CdlPhase = {
2047
2047
  title: string;
2048
2048
  body: string;
2049
2049
  activate: string[];
2050
+ /**
2051
+ * この phase で「左の起点から描き始める」 node の id (cdl#512)。
2052
+ *
2053
+ * `activate` と同じ形で node id を並べる。 対象の node は phase の進み (0→1) に合わせて
2054
+ * 描かれ、 進みが 1 に達した時点で全体が出る。 現在の対象は `chart-line` だけで、
2055
+ * 折れ線が左端から右へ伸び、 点と値の数字は線の先が届いた時に現れる。
2056
+ *
2057
+ * **`activate` を兼ねさせない**。 1 箱で図全体を描く種別 (`chart-*` 等) では焦点が
2058
+ * その箱に解決されるため、 連続する phase で光り続ける図が実在する。 「光っていたら
2059
+ * 描く」 にすると、 値が動くだけの phase でも線を引き直すことになる。
2060
+ *
2061
+ * 書かない phase では従来どおり全長で描く (DOM に dash 属性も付かない)。
2062
+ */
2063
+ draw?: string[];
2050
2064
  /** 数値 state の線形補間 (write phase で 100→90 等) */
2051
2065
  tweens: Array<{
2052
2066
  stateId: string;
@@ -2047,6 +2047,20 @@ type CdlPhase = {
2047
2047
  title: string;
2048
2048
  body: string;
2049
2049
  activate: string[];
2050
+ /**
2051
+ * この phase で「左の起点から描き始める」 node の id (cdl#512)。
2052
+ *
2053
+ * `activate` と同じ形で node id を並べる。 対象の node は phase の進み (0→1) に合わせて
2054
+ * 描かれ、 進みが 1 に達した時点で全体が出る。 現在の対象は `chart-line` だけで、
2055
+ * 折れ線が左端から右へ伸び、 点と値の数字は線の先が届いた時に現れる。
2056
+ *
2057
+ * **`activate` を兼ねさせない**。 1 箱で図全体を描く種別 (`chart-*` 等) では焦点が
2058
+ * その箱に解決されるため、 連続する phase で光り続ける図が実在する。 「光っていたら
2059
+ * 描く」 にすると、 値が動くだけの phase でも線を引き直すことになる。
2060
+ *
2061
+ * 書かない phase では従来どおり全長で描く (DOM に dash 属性も付かない)。
2062
+ */
2063
+ draw?: string[];
2050
2064
  /** 数値 state の線形補間 (write phase で 100→90 等) */
2051
2065
  tweens: Array<{
2052
2066
  stateId: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cardenelabs/cdl",
3
- "version": "0.9.0",
3
+ "version": "0.10.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
@@ -1355,6 +1355,11 @@ export type InputChain = {
1355
1355
 
1356
1356
  export type PhaseBuilder = {
1357
1357
  activate: (...ids: string[]) => PhaseBuilder;
1358
+ /**
1359
+ * この phase で左の起点から描き始める node を指定する (cdl#512)。
1360
+ * 対象は phase の進み (0→1) に合わせて描かれる。 現在の対象種別は `chart-line`。
1361
+ */
1362
+ draw: (...ids: string[]) => PhaseBuilder;
1358
1363
  /** 数値 state の線形補間 (write phase で 100→90 等) */
1359
1364
  tween: (stateId: string, from: number, to: number) => PhaseBuilder;
1360
1365
  /** 任意型 state を即時切替 (この phase 到達で value 上書き、 lerp なし) */
@@ -1364,6 +1369,7 @@ export type PhaseBuilder = {
1364
1369
 
1365
1370
  type PhaseAcc = {
1366
1371
  activate: string[];
1372
+ draw: string[];
1367
1373
  tweens: Array<{ stateId: string; from: number; to: number }>;
1368
1374
  sets: Array<{ stateId: string; value: string | number }>;
1369
1375
  badge?: string;
@@ -1375,6 +1381,10 @@ function makePhaseBuilder(acc: PhaseAcc): PhaseBuilder {
1375
1381
  acc.activate.push(...ids);
1376
1382
  return api;
1377
1383
  },
1384
+ draw(...ids) {
1385
+ acc.draw.push(...ids);
1386
+ return api;
1387
+ },
1378
1388
  tween(stateId, from, to) {
1379
1389
  acc.tweens.push({ stateId, from, to });
1380
1390
  return api;
@@ -3690,7 +3700,7 @@ export function diagram(
3690
3700
  },
3691
3701
  },
3692
3702
  phase(phaseId, opts, build) {
3693
- const acc: PhaseAcc = { activate: [], tweens: [], sets: [] };
3703
+ const acc: PhaseAcc = { activate: [], draw: [], tweens: [], sets: [] };
3694
3704
  build(makePhaseBuilder(acc));
3695
3705
  phases.push({
3696
3706
  id: phaseId,
@@ -3698,6 +3708,8 @@ export function diagram(
3698
3708
  title: opts.title,
3699
3709
  body: opts.body,
3700
3710
  activate: acc.activate,
3711
+ // 空なら欄ごと置かない。 置くと `draw` を使わない図の JSON の形が変わる
3712
+ ...(acc.draw.length > 0 ? { draw: acc.draw } : {}),
3701
3713
  tweens: acc.tweens,
3702
3714
  sets: acc.sets,
3703
3715
  badge: acc.badge,
@@ -7,15 +7,29 @@ import { resolveChartData } from "../render/payload-binding";
7
7
  * layout は node bbox (node.w × node.h) 内側に padding (top/right/bottom/left) を確保し、
8
8
  * その内側 rect を chart canvas とみなす。 縦軸 = value min-max、 横軸 = datum index。
9
9
  * 各 value は数でも `{signal}` でもよく、 後者は状態が動くたび折れ線が追随する。
10
+ *
11
+ * phase の `draw` にこの node の id が載っている間は、 折れ線が左の起点から伸びる (cdl#512)。
12
+ * 伸ばし方は dash (`pathLength` / `strokeDasharray` / `strokeDashoffset`) で、 `points` は
13
+ * 進みに関わらず全点を持つ。 点と値の数字は線の先が自分の位置に届いた時に現れ、
14
+ * 軸 / 格子 / 横軸の名前は最初から出る。
10
15
  */
11
16
  export function ChartLineNode({
12
17
  node,
13
18
  active,
14
19
  stateValues = {},
20
+ drawing = false,
21
+ progress = 1,
15
22
  }: {
16
23
  node: LaidNode;
17
24
  active: boolean;
18
25
  stateValues?: Record<string, string>;
26
+ /**
27
+ * この phase の `draw` に載っているか (cdl#512)。 true の間だけ折れ線が左の起点から
28
+ * 伸び、 点と値の数字は線の先が自分の位置に届いた時に現れる。
29
+ */
30
+ drawing?: boolean;
31
+ /** phase の進み (0→1)。 `drawing` が true の時だけ読む */
32
+ progress?: number;
19
33
  }): JSX.Element {
20
34
  const x0 = node.cx - node.w / 2;
21
35
  const y0 = node.cy - node.h / 2;
@@ -45,6 +59,44 @@ export function ChartLineNode({
45
59
 
46
60
  const points = data.map((d, idx) => `${xAt(idx)},${yAt(d.value)}`).join(" ");
47
61
 
62
+ // dash は polyline の実際の経路長で進むため、 点の表示判定も同じ経路長に揃える。
63
+ // 点番号の比率では、 急傾斜と平坦な区間が混ざる線で先端と点がずれる。
64
+ const 点までの長さ: number[] = [];
65
+ let 線の全長 = 0;
66
+ let 前の点: { x: number; y: number } | undefined;
67
+ for (const [idx, datum] of data.entries()) {
68
+ const 今の点 = { x: xAt(idx), y: yAt(datum.value) };
69
+ if (前の点) {
70
+ 線の全長 += Math.hypot(今の点.x - 前の点.x, 今の点.y - 前の点.y);
71
+ }
72
+ 点までの長さ.push(線の全長);
73
+ 前の点 = 今の点;
74
+ }
75
+
76
+ /**
77
+ * 線がどこまで伸びたか (0..1、 cdl#512)。 描く指定が無ければ常に 1 = 全長。
78
+ *
79
+ * 進みは呼出側から来る値なので、 範囲の外 (負 / 1 超 / NaN) が入りうる。 そのまま
80
+ * `strokeDashoffset` に渡すと線が消えたり逆向きに縮んだりするため、 ここで畳む。
81
+ */
82
+ const 伸び = drawing ? Math.min(1, Math.max(0, Number.isFinite(progress) ? progress : 1)) : 1;
83
+
84
+ /**
85
+ * その点まで線の先が届いたか。
86
+ *
87
+ * 点の位置は polyline の累積経路長で判定する。 **左端は進み 0 でも出す** = そこが
88
+ * 線の起点で、 起点が無いと線がどこから伸びているのか読めない。
89
+ *
90
+ * 点が 1 つの図では線を引かない (下の `data.length > 1`) ため、 割り算の分母が 0 に
91
+ * なる形には入らないが、 呼出の順に依らず成り立つよう先に返す。
92
+ */
93
+ const 線の先が届いた = (idx: number): boolean => {
94
+ if (!drawing) return true;
95
+ if (data.length <= 1) return true;
96
+ if (線の全長 === 0) return true;
97
+ return 伸び >= (点までの長さ[idx] ?? 0) / 線の全長;
98
+ };
99
+
48
100
  const gridCount = 4;
49
101
  const gridTicks = Array.from({ length: gridCount + 1 }, (_, i) => {
50
102
  const v = vMin + (vRange * i) / gridCount;
@@ -114,6 +166,16 @@ export function ChartLineNode({
114
166
  strokeWidth={2.5}
115
167
  strokeLinejoin="round"
116
168
  strokeLinecap="round"
169
+ // 伸ばすのは dash で行い、 **`points` は触らない** (cdl#512)。 点を削る形にすると
170
+ // 利用側が数えている点の数が進みの途中で変わる (dragon の `kind-geometry-check`)。
171
+ //
172
+ // `pathLength={1}` を置くのは、 実長を測るには DOM の ref が要り、 静的な書き出しでは
173
+ // 測れないため。 長さを 1 に正規化すれば進みをそのまま offset に使える。
174
+ //
175
+ // 描く指定が無い図には属性を 1 つも足さない = 既存の図の DOM を変えない
176
+ {...(drawing
177
+ ? { pathLength: 1, strokeDasharray: 1, strokeDashoffset: 1 - 伸び }
178
+ : {})}
117
179
  />
118
180
  )}
119
181
  {data.map((d, idx) => {
@@ -129,33 +191,40 @@ export function ChartLineNode({
129
191
  (prev === undefined || d.value <= prev) && (next === undefined || d.value <= next);
130
192
  const labelAbove = !isValley;
131
193
  const labelY = labelAbove ? cy - 12 : cy + 18;
194
+ // 点と値の数字は線の先に合わせて現れる。 **横軸の名前は最初から出す** =
195
+ // 名前は図の枠 (軸 / 格子) の側で、 これが順に増えると軸そのものが伸びて見える
196
+ const 出す = 線の先が届いた(idx);
132
197
  return (
133
198
  <g key={`pt-${idx}`} data-cdl-unresolved={d.unresolved ? "true" : undefined}>
134
- <circle
135
- cx={cx}
136
- cy={cy}
137
- r={5}
138
- fill="var(--cdl-node-fill, #ffffff)"
139
- stroke="var(--cdl-tone-accent, #2d6a8f)"
140
- strokeWidth={2.5}
141
- />
142
- <circle
143
- cx={cx}
144
- cy={cy}
145
- r={2.5}
146
- fill="var(--cdl-tone-accent, #2d6a8f)"
147
- />
148
- <text
149
- data-cdl-role="chart-line-value"
150
- x={cx}
151
- y={labelY}
152
- fontSize={11}
153
- fontWeight={600}
154
- textAnchor="middle"
155
- fill="var(--cdl-text, #1a1f2a)"
156
- >
157
- {d.value.toLocaleString()}
158
- </text>
199
+ {出す && (
200
+ <>
201
+ <circle
202
+ cx={cx}
203
+ cy={cy}
204
+ r={5}
205
+ fill="var(--cdl-node-fill, #ffffff)"
206
+ stroke="var(--cdl-tone-accent, #2d6a8f)"
207
+ strokeWidth={2.5}
208
+ />
209
+ <circle
210
+ cx={cx}
211
+ cy={cy}
212
+ r={2.5}
213
+ fill="var(--cdl-tone-accent, #2d6a8f)"
214
+ />
215
+ <text
216
+ data-cdl-role="chart-line-value"
217
+ x={cx}
218
+ y={labelY}
219
+ fontSize={11}
220
+ fontWeight={600}
221
+ textAnchor="middle"
222
+ fill="var(--cdl-text, #1a1f2a)"
223
+ >
224
+ {d.value.toLocaleString()}
225
+ </text>
226
+ </>
227
+ )}
159
228
  <text
160
229
  x={cx}
161
230
  y={PAD_TOP + canvasH + 20}
@@ -84,12 +84,18 @@ import { hasUnresolvedRef, interpolate, resolveNumericAttr } from "./utils";
84
84
  export function CdlNodeView({
85
85
  node,
86
86
  active,
87
+ drawing = false,
87
88
  progress,
88
89
  stateValues,
89
90
  currentPhaseId,
90
91
  }: {
91
92
  node: LaidNode;
92
93
  active: boolean;
94
+ /**
95
+ * この phase の `draw` に載っているか (cdl#512)。 true の間だけ、 対象の種別が
96
+ * phase の進み (`progress`) に合わせて左の起点から描かれる。
97
+ */
98
+ drawing?: boolean;
93
99
  progress: number;
94
100
  stateValues: Record<string, string>;
95
101
  currentPhaseId: string | undefined;
@@ -154,7 +160,15 @@ export function CdlNodeView({
154
160
  // 数と語を取る 7 種に対し、mind-map / tree-hierarchy は **名前だけ** を取る (#467)。
155
161
  // 親子の繋がりと、描かれない欄 (補足) は解決しない。
156
162
  case "chart-line":
157
- return <ChartLineNode node={resolvedNode} active={active} stateValues={stateValues} />;
163
+ return (
164
+ <ChartLineNode
165
+ node={resolvedNode}
166
+ active={active}
167
+ stateValues={stateValues}
168
+ drawing={drawing}
169
+ progress={progress}
170
+ />
171
+ );
158
172
  case "chart-pie":
159
173
  return <ChartPieNode node={resolvedNode} active={active} stateValues={stateValues} />;
160
174
  case "chart-bar":
@@ -25,6 +25,9 @@ export function CdlStage({
25
25
  svgRef?: React.RefObject<SVGSVGElement | null>;
26
26
  }): JSX.Element {
27
27
  const activeSet = new Set(currentPhase?.activate ?? []);
28
+ // この phase で左の起点から描き始める node (cdl#512)。 `activate` とは別集合で持つ =
29
+ // 焦点が当たり続ける図で毎 phase 引き直すことになるため、 光っているかを引き金にしない
30
+ const drawSet = new Set(currentPhase?.draw ?? []);
28
31
  // 縦線の終点は全 lane で 1 つに揃える (等長保証)。 値の決め方と、 1 lane の異常が図全体から
29
32
  // 縦線を消していた形の対処は `uniformLifelineEndY` の説明。
30
33
  //
@@ -208,6 +211,7 @@ export function CdlStage({
208
211
  <CdlNodeView
209
212
  node={node}
210
213
  active={activeSet.has(node.id)}
214
+ drawing={drawSet.has(node.id)}
211
215
  progress={progress}
212
216
  stateValues={stateValues}
213
217
  currentPhaseId={currentPhase?.id}
package/src/types.ts CHANGED
@@ -2124,6 +2124,20 @@ export type CdlPhase = {
2124
2124
  title: string;
2125
2125
  body: string;
2126
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[];
2127
2141
  /** 数値 state の線形補間 (write phase で 100→90 等) */
2128
2142
  tweens: Array<{ stateId: string; from: number; to: number }>;
2129
2143
  /** 任意型 state の即時切替 (この phase に到達したら値を上書き、 lerp なし) */
package/src/validate.ts CHANGED
@@ -108,7 +108,7 @@ export function validate(diag: CdlDiagram): void {
108
108
  ]);
109
109
 
110
110
  // 「もしかして」 サジェスト (typo 検出): edit distance 1 で最も近い候補 1 件
111
- const suggest = (target: string, candidates: Set<string>): string => {
111
+ const suggest = (target: string, candidates: Set<string>, candidateKind: "lane" | "node" = "lane"): string => {
112
112
  const list = [...candidates];
113
113
  if (list.length === 0) return "";
114
114
  let best = "";
@@ -120,7 +120,7 @@ export function validate(diag: CdlDiagram): void {
120
120
  best = c;
121
121
  }
122
122
  }
123
- return bestD <= 2 ? ` (もしかして "${best}"?)` : ` (定義済 lane: ${list.slice(0, 5).join(", ")}${list.length > 5 ? "..." : ""})`;
123
+ return bestD <= 2 ? ` (もしかして "${best}"?)` : ` (定義済 ${candidateKind}: ${list.slice(0, 5).join(", ")}${list.length > 5 ? "..." : ""})`;
124
124
  };
125
125
  for (const n of diag.nodes) {
126
126
  if (!laneIds.has(n.lane)) {
@@ -132,12 +132,12 @@ export function validate(diag: CdlDiagram): void {
132
132
  for (const e of diag.edges) {
133
133
  if (!nodeIds.has(e.from)) {
134
134
  errors.push(
135
- `[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 宣言前に追加`,
136
136
  );
137
137
  }
138
138
  if (!nodeIds.has(e.to)) {
139
139
  errors.push(
140
- `[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 宣言前に追加`,
141
141
  );
142
142
  }
143
143
  if (e.from === e.to) {
@@ -156,6 +156,20 @@ export function validate(diag: CdlDiagram): void {
156
156
  errors.push(`[cdl] unknown-ref: phase "${p.id}" の activate "${id}" は node でも edge でもない (typo の可能性)`);
157
157
  }
158
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
+ }
159
173
  for (const t of p.tweens) {
160
174
  if (!stateIds.has(t.stateId)) {
161
175
  errors.push(`[cdl] unknown-ref: phase "${p.id}" tween 対象 state "${t.stateId}" 未定義`);