@cardenelabs/cdl 0.9.0 → 0.11.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,27 @@ 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 に達した時点で全体が出る。 起点は種別ごとに違う。
2055
+ *
2056
+ * | kind | 起点 | 動き | 添える値 |
2057
+ * |---|---|---|---|
2058
+ * | `chart-line` | 左端 | 線が右へ伸びる | 点と数字は線の先が届いた時 |
2059
+ * | `chart-bar` | 横軸 | 棒が上へ伸びる | 数字は棒の頭に付いて動く |
2060
+ * | `chart-pie` | 12 時 | 扇が時計回りに開く | 凡例はその扇が開き始めた時 |
2061
+ *
2062
+ * 対象種別の一覧は `validate.ts` の `DRAW_KINDS` が持つ。
2063
+ *
2064
+ * **`activate` を兼ねさせない**。 1 箱で図全体を描く種別 (`chart-*` 等) では焦点が
2065
+ * その箱に解決されるため、 連続する phase で光り続ける図が実在する。 「光っていたら
2066
+ * 描く」 にすると、 値が動くだけの phase でも線を引き直すことになる。
2067
+ *
2068
+ * 書かない phase では従来どおり全長で描く (DOM に dash 属性も付かない)。
2069
+ */
2070
+ draw?: string[];
2050
2071
  /** 数値 state の線形補間 (write phase で 100→90 等) */
2051
2072
  tweens: Array<{
2052
2073
  stateId: string;
@@ -2047,6 +2047,27 @@ 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 に達した時点で全体が出る。 起点は種別ごとに違う。
2055
+ *
2056
+ * | kind | 起点 | 動き | 添える値 |
2057
+ * |---|---|---|---|
2058
+ * | `chart-line` | 左端 | 線が右へ伸びる | 点と数字は線の先が届いた時 |
2059
+ * | `chart-bar` | 横軸 | 棒が上へ伸びる | 数字は棒の頭に付いて動く |
2060
+ * | `chart-pie` | 12 時 | 扇が時計回りに開く | 凡例はその扇が開き始めた時 |
2061
+ *
2062
+ * 対象種別の一覧は `validate.ts` の `DRAW_KINDS` が持つ。
2063
+ *
2064
+ * **`activate` を兼ねさせない**。 1 箱で図全体を描く種別 (`chart-*` 等) では焦点が
2065
+ * その箱に解決されるため、 連続する phase で光り続ける図が実在する。 「光っていたら
2066
+ * 描く」 にすると、 値が動くだけの phase でも線を引き直すことになる。
2067
+ *
2068
+ * 書かない phase では従来どおり全長で描く (DOM に dash 属性も付かない)。
2069
+ */
2070
+ draw?: string[];
2050
2071
  /** 数値 state の線形補間 (write phase で 100→90 等) */
2051
2072
  tweens: Array<{
2052
2073
  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.11.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,
@@ -6,15 +6,26 @@ import { resolveChartData } from "../render/payload-binding";
6
6
  * chart-bar kind ... 1 node に datum 配列 (chartData) を保持し、 SVG rect で棒グラフ描画する。
7
7
  * layout は node bbox 内側の canvas に縦棒を等間隔配置、 縦軸 = value 0-max、 横軸 = datum label。
8
8
  * 各 value は数でも `{signal}` でもよく、 後者は状態が動くたび棒の高さが追随する。
9
+ *
10
+ * phase の `draw` にこの node の id が載っている間は、 棒が横軸から上へ伸びる (cdl#516)。
11
+ * 伸ばすのは棒を包む `<g>` の `transform` で、 `rect` の `x` / `y` / `width` / `height` は
12
+ * 進みに関わらず実値のまま。 値の数字は棒の頭に付いて動く。
13
+ * 軸 / 格子 / 横軸の名前は最初から出る。
9
14
  */
10
15
  export function ChartBarNode({
11
16
  node,
12
17
  active,
13
18
  stateValues = {},
19
+ drawing = false,
20
+ progress = 1,
14
21
  }: {
15
22
  node: LaidNode;
16
23
  active: boolean;
17
24
  stateValues?: Record<string, string>;
25
+ /** この phase の `draw` に載っているか (cdl#516) */
26
+ drawing?: boolean;
27
+ /** phase の進み (0→1)。 `drawing` が true の時だけ読む */
28
+ progress?: number;
18
29
  }): JSX.Element {
19
30
  const x0 = node.cx - node.w / 2;
20
31
  const y0 = node.cy - node.h / 2;
@@ -43,6 +54,16 @@ export function ChartBarNode({
43
54
  return { y: yAt(v), value: v };
44
55
  });
45
56
 
57
+ /**
58
+ * 棒がどこまで伸びたか (0..1、 cdl#516)。 描く指定が無ければ常に 1 = 全高。
59
+ *
60
+ * 進みは呼出側から来る値なので範囲の外 (負 / 1 超 / NaN) が入りうる。 そのまま倍率に
61
+ * 渡すと棒が上下反転したり突き抜けたりするため、 ここで畳む。
62
+ */
63
+ const 伸び = drawing ? Math.min(1, Math.max(0, Number.isFinite(progress) ? progress : 1)) : 1;
64
+ /** 棒の足元。 ここを動かさない点として上へ伸ばす */
65
+ const 横軸のy = PAD_TOP + canvasH;
66
+
46
67
  return (
47
68
  <g transform={`translate(${x0} ${y0})`}>
48
69
  <rect
@@ -93,22 +114,38 @@ export function ChartBarNode({
93
114
  const bx = xAt(idx);
94
115
  const by = yAt(d.value);
95
116
  const bh = PAD_TOP + canvasH - by;
117
+ // 見えている棒の頭。 数字はここに付いて動く = 空いた場所に数字だけ浮かない
118
+ const 見えている頭 = 横軸のy - (横軸のy - by) * 伸び;
119
+ /**
120
+ * **`rect` の属性は触らない** (cdl#516)。 高さを直接動かすと、棒の高さで値を測る
121
+ * 利用側の検査が進みの途中で別の値を読む。 足元を動かさない点にした倍率で包む。
122
+ */
123
+ const 棒 = (
124
+ <rect
125
+ data-cdl-role="chart-bar"
126
+ data-cdl-unresolved={d.unresolved ? "true" : undefined}
127
+ x={bx}
128
+ y={by}
129
+ width={barW}
130
+ height={bh}
131
+ rx={2}
132
+ fill="var(--cdl-tone-accent, #2d6a8f)"
133
+ fillOpacity={0.9}
134
+ />
135
+ );
96
136
  return (
97
137
  <g key={`bar-${idx}`}>
98
- <rect
99
- data-cdl-role="chart-bar"
100
- data-cdl-unresolved={d.unresolved ? "true" : undefined}
101
- x={bx}
102
- y={by}
103
- width={barW}
104
- height={bh}
105
- rx={2}
106
- fill="var(--cdl-tone-accent, #2d6a8f)"
107
- fillOpacity={0.9}
108
- />
138
+ {/* 描く指定が無い図には包みを足さない = 既存の図の DOM を 1 要素も変えない */}
139
+ {drawing ? (
140
+ <g transform={`translate(0 ${横軸のy}) scale(1 ${伸び}) translate(0 ${-横軸のy})`}>
141
+ {}
142
+ </g>
143
+ ) : (
144
+
145
+ )}
109
146
  <text
110
147
  x={bx + barW / 2}
111
- y={by - 6}
148
+ y={見えている頭 - 6}
112
149
  fontSize={11}
113
150
  textAnchor="middle"
114
151
  fill="var(--cdl-text, #1a1f2a)"
@@ -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}
@@ -1,4 +1,4 @@
1
- import type { JSX } from "react";
1
+ import { useId, type JSX } from "react";
2
2
  import type { LaidNode, Tone } from "../types";
3
3
  import { TONE } from "../render/tone";
4
4
  import { resolveChartData } from "../render/payload-binding";
@@ -7,16 +7,29 @@ import { resolveChartData } from "../render/payload-binding";
7
7
  * chart-pie kind ... 1 node に datum 配列 (chartData) を保持し、 SVG path arc で円グラフ描画する。
8
8
  * layout は node bbox 内側の左半分に円を配置、 右半分に凡例 (label + %) を stack 表示する。
9
9
  * 各 value は数でも `{signal}` でもよく、 後者は状態が動くたび扇の角度が追随する。
10
+ *
11
+ * phase の `draw` にこの node の id が載っている間は、 円が 12 時から時計回りに開く (cdl#516)。
12
+ * 開き方は扇形の切り抜き (`clipPath`) で、 各扇の `d` は進みに関わらず実値のまま。
13
+ * 凡例はその扇が開き始めた時に出る。
10
14
  */
11
15
  export function ChartPieNode({
12
16
  node,
13
17
  active,
14
18
  stateValues = {},
19
+ drawing = false,
20
+ progress = 1,
15
21
  }: {
16
22
  node: LaidNode;
17
23
  active: boolean;
18
24
  stateValues?: Record<string, string>;
25
+ /** この phase の `draw` に載っているか (cdl#516) */
26
+ drawing?: boolean;
27
+ /** phase の進み (0→1)。 `drawing` が true の時だけ読む */
28
+ progress?: number;
19
29
  }): JSX.Element {
30
+ // node id は diagram 内だけで一意。 同じ page に複数 diagram がある時も SVG の参照先が
31
+ // ぶつからないよう、 React が component instance ごとに割り当てる id を使う。
32
+ const instanceId = useId().replace(/[^A-Za-z0-9_-]/g, "");
20
33
  const x0 = node.cx - node.w / 2;
21
34
  const y0 = node.cy - node.h / 2;
22
35
  const data = resolveChartData(node.chartData ?? [], stateValues);
@@ -31,12 +44,16 @@ export function ChartPieNode({
31
44
  const cy = PAD_TOP + chartAreaH / 2;
32
45
 
33
46
  let cumAngle = -Math.PI / 2;
47
+ /** その扇が始まるまでの取り分の累計。 開き始めたかの判定に使う (cdl#516) */
48
+ let cumFrac = 0;
34
49
  const slices = data.map((d) => {
35
50
  const frac = d.value / total;
36
51
  const angle = frac * Math.PI * 2;
37
52
  const a0 = cumAngle;
38
53
  const a1 = cumAngle + angle;
39
54
  cumAngle = a1;
55
+ const startFrac = cumFrac;
56
+ cumFrac += frac;
40
57
 
41
58
  const x1 = cx + radius * Math.cos(a0);
42
59
  const y1 = cy + radius * Math.sin(a0);
@@ -44,9 +61,21 @@ export function ChartPieNode({
44
61
  const y2 = cy + radius * Math.sin(a1);
45
62
  const large = angle > Math.PI ? 1 : 0;
46
63
  const path = `M ${cx} ${cy} L ${x1} ${y1} A ${radius} ${radius} 0 ${large} 1 ${x2} ${y2} Z`;
47
- return { d, frac, path };
64
+ return { d, frac, path, startFrac };
48
65
  });
49
66
 
67
+ /**
68
+ * 円がどこまで開いたか (0..1、 cdl#516)。 描く指定が無ければ常に 1 = 全周。
69
+ *
70
+ * 進みは呼出側から来る値なので範囲の外 (負 / 1 超 / NaN) が入りうる。 そのまま角度に
71
+ * 掛けると 1 周を超えて切り抜きが壊れるため、 ここで畳む。
72
+ */
73
+ const 開き = drawing ? Math.min(1, Math.max(0, Number.isFinite(progress) ? progress : 1)) : 1;
74
+ // SVG の id は document 全体の名前空間。 node id ではなく instance id で分ける
75
+ const 切り抜きid = `cdl-pie-draw-${instanceId}`;
76
+ /** その扇が開き始めたか。 開き始めた扇の凡例だけを出す */
77
+ const 開き始めた = (startFrac: number): boolean => !drawing || 開き > startFrac;
78
+
50
79
  const legendX = pieAreaW + 8;
51
80
  const legendGap = Math.min(28, chartAreaH / Math.max(data.length, 1));
52
81
  const legendStartY = PAD_TOP + (chartAreaH - legendGap * data.length) / 2 + legendGap / 2;
@@ -64,20 +93,40 @@ export function ChartPieNode({
64
93
  stroke={active ? "var(--cdl-tone-accent, #2d6a8f)" : "var(--cdl-divider, #d4d4d8)"}
65
94
  strokeWidth={active ? 3 : 1.25}
66
95
  />
67
- {slices.map((s, idx) => (
68
- <path
69
- key={`slice-${idx}`}
70
- data-cdl-role="chart-pie-slice"
71
- data-cdl-unresolved={s.d.unresolved ? "true" : undefined}
72
- d={s.path}
73
- fill={sliceColor(s.d.tone, idx)}
74
- fillOpacity={active ? 0.95 : 0.85}
75
- stroke="var(--cdl-node-fill, #ffffff)"
76
- strokeWidth={1.5}
77
- />
78
- ))}
96
+ {drawing && (
97
+ <defs>
98
+ <clipPath id={切り抜きid}>{扇形の切り抜き(cx, cy, radius + 2, 開き)}</clipPath>
99
+ </defs>
100
+ )}
101
+ {/*
102
+ **扇の `d` は触らない** (cdl#516)。 進みで再計算すると、扇の形から取り分を測る
103
+ 利用側が進みの途中で別の値を読む。 見える範囲だけを切り抜きで変える。
104
+
105
+ 描く指定が無い図には包みも `clip-path` も足さない = 既存の図の DOM を変えない
106
+ */}
107
+ <g {...(drawing ? { clipPath: `url(#${切り抜きid})` } : {})}>
108
+ {slices.map((s, idx) => (
109
+ <path
110
+ key={`slice-${idx}`}
111
+ data-cdl-role="chart-pie-slice"
112
+ data-cdl-unresolved={s.d.unresolved ? "true" : undefined}
113
+ d={s.path}
114
+ fill={sliceColor(s.d.tone, idx)}
115
+ fillOpacity={active ? 0.95 : 0.85}
116
+ stroke="var(--cdl-node-fill, #ffffff)"
117
+ strokeWidth={1.5}
118
+ />
119
+ ))}
120
+ </g>
79
121
 
80
- {slices.map((s, idx) => (
122
+ {/*
123
+ 行の位置は `idx` から決まる。 いまの開き方は必ず先頭からの連続なので、開いた扇だけを
124
+ `filter` で抜いても位置は同じになる (変異試験で実測 = 差が出ない)。
125
+
126
+ それでも `idx` を保つ形で書くのは、位置の計算が抜き方に依存しないため。 開く順が
127
+ 先頭からでなくなった時に、この場所を直し忘れても位置がずれない
128
+ */}
129
+ {slices.map((s, idx) => !開き始めた(s.startFrac) ? null : (
81
130
  <g key={`legend-${idx}`} transform={`translate(${legendX} ${legendStartY + idx * legendGap})`}>
82
131
  <rect
83
132
  x={0}
@@ -110,6 +159,22 @@ export function ChartPieNode({
110
159
  }
111
160
 
112
161
 
162
+ /**
163
+ * 12 時から時計回りに開く扇形 (cdl#516)。 円を切り抜く形に使う。
164
+ *
165
+ * 1 周ちょうどは扇形で表せない (始点と終点が同じ点になり、 弧の向きが決まらない)。
166
+ * その時だけ円をそのまま返す = 進み 1 で図が消える形を避ける。
167
+ */
168
+ function 扇形の切り抜き(cx: number, cy: number, r: number, 開き: number): JSX.Element | null {
169
+ if (開き <= 0) return null;
170
+ if (開き >= 1) return <circle cx={cx} cy={cy} r={r} />;
171
+ const a0 = -Math.PI / 2;
172
+ const a1 = a0 + 開き * Math.PI * 2;
173
+ const large = 開き > 0.5 ? 1 : 0;
174
+ const d = `M ${cx} ${cy} L ${cx + r * Math.cos(a0)} ${cy + r * Math.sin(a0)} A ${r} ${r} 0 ${large} 1 ${cx + r * Math.cos(a1)} ${cy + r * Math.sin(a1)} Z`;
175
+ return <path d={d} />;
176
+ }
177
+
113
178
  const FALLBACK_ORDER: Tone[] = ["accent", "teal", "warning", "success", "info", "error"];
114
179
 
115
180
  function sliceColor(tone: Tone | undefined, idx: number): string {
@@ -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,11 +160,35 @@ 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
- return <ChartPieNode node={resolvedNode} active={active} stateValues={stateValues} />;
173
+ return (
174
+ <ChartPieNode
175
+ node={resolvedNode}
176
+ active={active}
177
+ stateValues={stateValues}
178
+ drawing={drawing}
179
+ progress={progress}
180
+ />
181
+ );
160
182
  case "chart-bar":
161
- return <ChartBarNode node={resolvedNode} active={active} stateValues={stateValues} />;
183
+ return (
184
+ <ChartBarNode
185
+ node={resolvedNode}
186
+ active={active}
187
+ stateValues={stateValues}
188
+ drawing={drawing}
189
+ progress={progress}
190
+ />
191
+ );
162
192
  case "gantt-timeline":
163
193
  return <GanttNode node={resolvedNode} active={active} stateValues={stateValues} />;
164
194
  case "mind-map":
@@ -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,27 @@ 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 に達した時点で全体が出る。 起点は種別ごとに違う。
2132
+ *
2133
+ * | kind | 起点 | 動き | 添える値 |
2134
+ * |---|---|---|---|
2135
+ * | `chart-line` | 左端 | 線が右へ伸びる | 点と数字は線の先が届いた時 |
2136
+ * | `chart-bar` | 横軸 | 棒が上へ伸びる | 数字は棒の頭に付いて動く |
2137
+ * | `chart-pie` | 12 時 | 扇が時計回りに開く | 凡例はその扇が開き始めた時 |
2138
+ *
2139
+ * 対象種別の一覧は `validate.ts` の `DRAW_KINDS` が持つ。
2140
+ *
2141
+ * **`activate` を兼ねさせない**。 1 箱で図全体を描く種別 (`chart-*` 等) では焦点が
2142
+ * その箱に解決されるため、 連続する phase で光り続ける図が実在する。 「光っていたら
2143
+ * 描く」 にすると、 値が動くだけの phase でも線を引き直すことになる。
2144
+ *
2145
+ * 書かない phase では従来どおり全長で描く (DOM に dash 属性も付かない)。
2146
+ */
2147
+ draw?: string[];
2127
2148
  /** 数値 state の線形補間 (write phase で 100→90 等) */
2128
2149
  tweens: Array<{ stateId: string; from: number; to: number }>;
2129
2150
  /** 任意型 state の即時切替 (この phase に到達したら値を上書き、 lerp なし) */