@cardenelabs/cdl 0.30.2 → 0.31.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.
@@ -405,6 +405,12 @@ type CdlNode = {
405
405
  * 数値 chart の全 datum を 1 node に格納し、 kinds/chart-*.tsx が自己完結で SVG 描画する。
406
406
  */
407
407
  chartData?: ChartDatumPayload[];
408
+ /** chart-line の線から下を、上端 0.22 から下端 0 へ薄く塗る */
409
+ chartFillUnder?: boolean;
410
+ /** chart-line の値を、点への到着直後だけ下からせり上げる */
411
+ chartValueRise?: boolean;
412
+ /** chart-line を描き切った後、粒で経路をなぞって点を順に出す */
413
+ chartTrace?: boolean;
408
414
  /**
409
415
  * gantt-timeline 専用の task payload。 task 全件を 1 node に格納し、
410
416
  * kinds/gantt.tsx が縦軸 task lane + 横軸日付 rect bar で SVG 描画する。
@@ -405,6 +405,12 @@ type CdlNode = {
405
405
  * 数値 chart の全 datum を 1 node に格納し、 kinds/chart-*.tsx が自己完結で SVG 描画する。
406
406
  */
407
407
  chartData?: ChartDatumPayload[];
408
+ /** chart-line の線から下を、上端 0.22 から下端 0 へ薄く塗る */
409
+ chartFillUnder?: boolean;
410
+ /** chart-line の値を、点への到着直後だけ下からせり上げる */
411
+ chartValueRise?: boolean;
412
+ /** chart-line を描き切った後、粒で経路をなぞって点を順に出す */
413
+ chartTrace?: boolean;
408
414
  /**
409
415
  * gantt-timeline 専用の task payload。 task 全件を 1 node に格納し、
410
416
  * kinds/gantt.tsx が縦軸 task lane + 横軸日付 rect bar で SVG 描画する。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cardenelabs/cdl",
3
- "version": "0.30.2",
3
+ "version": "0.31.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",
@@ -3,6 +3,36 @@ import type { LaidNode } from "../types";
3
3
  import { resolveChartData } from "../render/payload-binding";
4
4
  import { 進みを畳む, 伸ばすdash } from "./draw-progress";
5
5
 
6
+ export type ChartLineScale = { min: number; max: number; ticks: number[] };
7
+
8
+ const 数を書く = (value: number): string =>
9
+ value.toLocaleString("en-US", { maximumFractionDigits: 12 });
10
+
11
+ export function 折れ線の目盛(values: readonly number[]): ChartLineScale {
12
+ let rawMin = values.length > 0 ? Math.min(...values) : 0;
13
+ let rawMax = values.length > 0 ? Math.max(...values) : 1;
14
+ if (rawMin === rawMax) {
15
+ const room = 10 ** Math.floor(Math.log10(Math.abs(rawMin) || 1) - 1);
16
+ rawMin -= room;
17
+ rawMax += room;
18
+ }
19
+
20
+ const roughStep = (rawMax - rawMin) / 4;
21
+ const power = 10 ** Math.floor(Math.log10(roughStep));
22
+ const fraction = roughStep / power;
23
+ const niceFraction =
24
+ fraction <= 1 ? 1 : fraction <= 2 ? 2 : fraction <= 2.5 ? 2.5 : fraction <= 5 ? 5 : 10;
25
+ const step = niceFraction * power;
26
+ const min = Math.floor(rawMin / step) * step;
27
+ const max = Math.ceil(rawMax / step) * step;
28
+ const tickCount = Math.round((max - min) / step);
29
+ const ticks = Array.from({ length: tickCount + 1 }, (_, i) => {
30
+ const value = min + step * i;
31
+ return Math.abs(value) < step * 1e-12 ? 0 : value;
32
+ });
33
+ return { min, max, ticks };
34
+ }
35
+
6
36
  /**
7
37
  * chart-line kind ... 1 node に datum 配列 (chartData) を保持し、 SVG polyline で折れ線描画する。
8
38
  * layout は node bbox (node.w × node.h) 内側に padding (top/right/bottom/left) を確保し、
@@ -18,12 +48,15 @@ export function ChartLineNode({
18
48
  node,
19
49
  active,
20
50
  stateValues = {},
51
+ scale: fixedScale,
21
52
  drawing = false,
22
53
  progress = 1,
23
54
  }: {
24
55
  node: LaidNode;
25
56
  active: boolean;
26
57
  stateValues?: Record<string, string>;
58
+ /** 全 phase を通して固定する目盛。 単独描画では現在の系列から決める */
59
+ scale?: ChartLineScale;
27
60
  /**
28
61
  * この phase の `draw` に載っているか (cdl#512)。 true の間だけ折れ線が左の起点から
29
62
  * 伸び、 点と値の数字は線の先が自分の位置に届いた時に現れる。
@@ -43,10 +76,8 @@ export function ChartLineNode({
43
76
  const canvasW = node.w - PAD_LEFT - PAD_RIGHT;
44
77
  const canvasH = node.h - PAD_TOP - PAD_BOTTOM;
45
78
 
46
- const values = data.map((d) => d.value);
47
- const vMin = values.length > 0 ? Math.min(...values) : 0;
48
- const vMax = values.length > 0 ? Math.max(...values) : 1;
49
- const vRange = vMax - vMin || 1;
79
+ const scale = fixedScale ?? 折れ線の目盛(data.map((d) => d.value));
80
+ const vRange = scale.max - scale.min;
50
81
 
51
82
  // 最大値 / 最小値の点が描画領域の上下端に張り付くと、 値 label が線に重なる。
52
83
  // 上下に label 1 行ぶんの余白を確保して、 その内側に折れ線を描く。
@@ -54,64 +85,62 @@ export function ChartLineNode({
54
85
  const plotTop = PAD_TOP + LABEL_ROOM;
55
86
  const plotH = Math.max(canvasH - LABEL_ROOM * 2, 1);
56
87
 
88
+ const POINT_EDGE_ROOM = 24;
89
+ const plotW = Math.max(canvasW - POINT_EDGE_ROOM * 2, 1);
57
90
  const xAt = (idx: number) =>
58
- PAD_LEFT + (data.length <= 1 ? canvasW / 2 : (canvasW * idx) / (data.length - 1));
59
- const yAt = (v: number) => plotTop + plotH - ((v - vMin) / vRange) * plotH;
91
+ PAD_LEFT + POINT_EDGE_ROOM + (data.length <= 1 ? plotW / 2 : (plotW * idx) / (data.length - 1));
92
+ const yAt = (v: number) => plotTop + plotH - ((v - scale.min) / vRange) * plotH;
60
93
 
61
- const points = data.map((d, idx) => `${xAt(idx)},${yAt(d.value)}`).join(" ");
94
+ const plotBottom = PAD_TOP + canvasH;
62
95
 
63
96
  // dash は polyline の実際の経路長で進むため、 点の表示判定も同じ経路長に揃える。
64
97
  // 点番号の比率では、 急傾斜と平坦な区間が混ざる線で先端と点がずれる。
65
- const 点までの長さ: number[] = [];
66
98
  let 線の全長 = 0;
67
99
  let 前の点: { x: number; y: number } | undefined;
68
- for (const [idx, datum] of data.entries()) {
69
- const 今の点 = { x: xAt(idx), y: yAt(datum.value) };
100
+ const 描画点 = data.map((datum, idx) => {
101
+ const 今の点 = { datum, x: xAt(idx), y: yAt(datum.value), ここまでの長さ: 0 };
70
102
  if (前の点) {
71
103
  線の全長 += Math.hypot(今の点.x - 前の点.x, 今の点.y - 前の点.y);
72
104
  }
73
- 点までの長さ.push(線の全長);
105
+ 今の点.ここまでの長さ = 線の全長;
74
106
  前の点 = 今の点;
75
- }
107
+ return 今の点;
108
+ });
109
+ const points = 描画点.map((点) => `${点.x},${点.y}`).join(" ");
76
110
 
77
- /** 線がどこまで伸びたか (0..1、 cdl#512)。 畳み方は `draw-progress.ts` が持つ */
78
- const 伸び = 進みを畳む(drawing, progress);
111
+ /** phase がどこまで進んだか (0..1、 cdl#512)。 畳み方は `draw-progress.ts` が持つ */
112
+ const 段の進み = 進みを畳む(drawing, progress);
113
+ const 粒を走らせる = drawing && node.chartTrace === true;
114
+ // 0.5 で線を完成させてから 0.55 まで間を置くため、線と粒の順が混ざらない。
115
+ const 伸び = 粒を走らせる ? Math.min(1, 段の進み / 0.5) : 段の進み;
116
+ const 粒の進み = 粒を走らせる ? Math.min(1, Math.max(0, (段の進み - 0.55) / 0.45)) : 伸び;
117
+ const 粒が出る = 粒を走らせる && 段の進み >= 0.55 && 段の進み < 1;
118
+ const 到着窓 = 0.12;
79
119
 
80
- /**
81
- * その点まで線の先が届いたか。
82
- *
83
- * 点の位置は polyline の累積経路長で判定する。 **左端は進み 0 でも出す** = そこが
84
- * 線の起点で、 起点が無いと線がどこから伸びているのか読めない。
85
- *
86
- * 点が 1 つの図では線を引かない (下の `data.length > 1`) ため、 割り算の分母が 0 に
87
- * なる形には入らないが、 呼出の順に依らず成り立つよう先に返す。
88
- */
89
- const 線の先が届いた = (idx: number): boolean => {
90
- if (!drawing) return true;
91
- if (data.length <= 1) return true;
92
- if (線の全長 === 0) return true;
93
- return 伸び >= (点までの長さ[idx] ?? 0) / 線の全長;
94
- };
95
-
96
- const gridCount = 4;
97
- const gridTicks = Array.from({ length: gridCount + 1 }, (_, i) => {
98
- const v = vMin + (vRange * i) / gridCount;
99
- return { y: yAt(v), value: v };
100
- });
120
+ let 粒の位置: { x: number; y: number } | undefined;
121
+ const 最初の点 = 描画点[0];
122
+ if (粒が出る && 最初の点) {
123
+ const 粒までの長さ = 粒の進み * 線の全長;
124
+ let 区間の前 = 最初の点;
125
+ let 区間の後 = 最初の点;
126
+ for (const of 描画点.slice(1)) {
127
+ 区間の後 = 点;
128
+ if (点.ここまでの長さ >= 粒までの長さ) break;
129
+ 区間の前 = 点;
130
+ }
131
+ const 区間の始め = 区間の前.ここまでの長さ;
132
+ const 区間の長さ = 区間の後.ここまでの長さ - 区間の始め;
133
+ const 区間の進み = 区間の長さ === 0 ? 0 : (粒までの長さ - 区間の始め) / 区間の長さ;
134
+ 粒の位置 = {
135
+ x: 区間の前.x + (区間の後.x - 区間の前.x) * 区間の進み,
136
+ y: 区間の前.y + (区間の後.y - 区間の前.y) * 区間の進み,
137
+ };
138
+ }
139
+
140
+ const gridTicks = scale.ticks.map((value) => ({ y: yAt(value), value }));
101
141
 
102
142
  return (
103
143
  <g transform={`translate(${x0} ${y0})`}>
104
- <rect
105
- data-cdl-role="node-body"
106
- x={0}
107
- y={0}
108
- width={node.w}
109
- height={node.h}
110
- rx={16}
111
- fill="var(--cdl-node-fill, #ffffff)"
112
- stroke={active ? "var(--cdl-tone-accent, #2d6a8f)" : "var(--cdl-divider, #d4d4d8)"}
113
- strokeWidth={active ? 3 : 1.25}
114
- />
115
144
  {gridTicks.map((t, i) => (
116
145
  <g key={`grid-${i}`}>
117
146
  <line
@@ -131,35 +160,41 @@ export function ChartLineNode({
131
160
  textAnchor="end"
132
161
  fill="var(--cdl-text-dim, #5a6270)"
133
162
  >
134
- {Math.round(t.value)}
163
+ {数を書く(t.value)}
135
164
  </text>
136
165
  </g>
137
166
  ))}
138
167
 
139
- <line
140
- x1={PAD_LEFT}
141
- y1={plotTop + plotH}
142
- x2={PAD_LEFT + canvasW}
143
- y2={plotTop + plotH}
144
- stroke="var(--cdl-text-mute, #8a8678)"
145
- strokeWidth={1.25}
146
- />
147
- <line
148
- x1={PAD_LEFT}
149
- y1={plotTop}
150
- x2={PAD_LEFT}
151
- y2={plotTop + plotH}
152
- stroke="var(--cdl-text-mute, #8a8678)"
153
- strokeWidth={1.25}
154
- />
155
-
168
+ {node.chartFillUnder && data.length > 1 && (
169
+ <>
170
+ <defs>
171
+ <linearGradient
172
+ id={`chart-line-fill-${node.id}`}
173
+ gradientUnits="userSpaceOnUse"
174
+ x1={0}
175
+ y1={plotTop}
176
+ x2={0}
177
+ y2={plotBottom}
178
+ >
179
+ <stop offset="0%" stopColor="var(--cdl-tone-accent, #2d6a8f)" stopOpacity={0.22} />
180
+ <stop offset="100%" stopColor="var(--cdl-tone-accent, #2d6a8f)" stopOpacity={0} />
181
+ </linearGradient>
182
+ </defs>
183
+ <path
184
+ data-cdl-role="chart-line-fill-under"
185
+ d={`M ${xAt(0)} ${plotBottom} L ${points.replaceAll(",", " ")} L ${xAt(data.length - 1)} ${plotBottom} Z`}
186
+ fill={`url(#chart-line-fill-${node.id})`}
187
+ stroke="none"
188
+ />
189
+ </>
190
+ )}
156
191
  {data.length > 1 && (
157
192
  <polyline
158
193
  data-cdl-role="chart-line"
159
194
  points={points}
160
195
  fill="none"
161
196
  stroke="var(--cdl-tone-accent, #2d6a8f)"
162
- strokeWidth={2.5}
197
+ strokeWidth={active ? 3 : 2}
163
198
  strokeLinejoin="round"
164
199
  strokeLinecap="round"
165
200
  // 伸ばすのは dash で行い、 **`points` は触らない** (cdl#512)。 点を削る形にすると
@@ -172,53 +207,96 @@ export function ChartLineNode({
172
207
  {...伸ばすdash(drawing, 伸び)}
173
208
  />
174
209
  )}
175
- {data.map((d, idx) => {
176
- const cx = xAt(idx);
177
- const cy = yAt(d.value);
210
+ {粒の位置 && (
211
+ <circle
212
+ data-cdl-role="chart-line-trace"
213
+ cx={粒の位置.x}
214
+ cy={粒の位置.y}
215
+ r={4.5}
216
+ fill="var(--cdl-now)"
217
+ />
218
+ )}
219
+ {描画点.map(({ datum: d, x: cx, y: cy, ここまでの長さ }, idx) => {
178
220
  // value label は marker の上に置くのが基本、 canvas 上端 6px より内側になる場合は
179
221
  // 下に反転して他要素と重ならないようにする
180
222
  // 折れ線に重ならない側へ置く。 山 (両隣より高い) なら上、 谷なら下。
181
223
  // 単調な区間では上に置く。 上下端には LABEL_ROOM を確保してあるので反転は不要。
182
- const prev = idx > 0 ? data[idx - 1]!.value : undefined;
183
- const next = idx < data.length - 1 ? data[idx + 1]!.value : undefined;
224
+ const prev = data[idx - 1]?.value;
225
+ const next = data[idx + 1]?.value;
184
226
  const isValley =
185
227
  (prev === undefined || d.value <= prev) && (next === undefined || d.value <= next);
186
228
  const labelAbove = !isValley;
187
229
  const labelY = labelAbove ? cy - 12 : cy + 18;
188
230
  // 点と値の数字は線の先に合わせて現れる。 **横軸の名前は最初から出す** =
189
231
  // 名前は図の枠 (軸 / 格子) の側で、 これが順に増えると軸そのものが伸びて見える
190
- const 出す = 線の先が届いた(idx);
191
- return (
192
- <g key={`pt-${idx}`} data-cdl-unresolved={d.unresolved ? "true" : undefined}>
193
- {出す && (
194
- <>
195
- <circle
196
- cx={cx}
197
- cy={cy}
198
- r={5}
199
- fill="var(--cdl-node-fill, #ffffff)"
200
- stroke="var(--cdl-tone-accent, #2d6a8f)"
201
- strokeWidth={2.5}
202
- />
203
- <circle
232
+ const 到着後 = 線の全長 === 0 ? 粒の進み : 粒の進み - ここまでの長さ / 線の全長;
233
+ // 粒を使う図は線を描き切ってから点を出す。 左端は粒の開始と同時に出す。
234
+ const 出す = !drawing || ((!粒を走らせる || 段の進み >= 0.55) && 到着後 >= 0);
235
+ // ちょうど 0.12 は窓の外に含め、輪と到着色を残さない。
236
+ const 光る = drawing && 出す && 到着後 < 到着窓;
237
+ const 光の進み = 光る ? 到着後 / 到着窓 : 1;
238
+ let 点と値: JSX.Element | undefined;
239
+ if (出す) {
240
+ const 光の半径 = 6 + 16 * 光の進み;
241
+ const 値 = (
242
+ <text
243
+ data-cdl-role="chart-line-value"
244
+ x={cx}
245
+ y={labelY}
246
+ fontSize={11}
247
+ fontWeight={600}
248
+ textAnchor="middle"
249
+ fill="var(--cdl-text, #1a1f2a)"
250
+ >
251
+ {数を書く(d.value)}
252
+ </text>
253
+ );
254
+ 点と値 = (
255
+ <>
256
+ {光る && (
257
+ // marker の `circle` を数える利用側へ混ぜず、専用 role の等径 ellipse として輪を出す。
258
+ <ellipse
259
+ data-cdl-role="chart-line-arrival-ring"
204
260
  cx={cx}
205
261
  cy={cy}
206
- r={2.5}
207
- fill="var(--cdl-tone-accent, #2d6a8f)"
262
+ rx={光の半径}
263
+ ry={光の半径}
264
+ fill="none"
265
+ stroke="var(--cdl-now)"
266
+ strokeWidth={2}
267
+ opacity={0.85 * (1 - 光の進み)}
208
268
  />
209
- <text
210
- data-cdl-role="chart-line-value"
211
- x={cx}
212
- y={labelY}
213
- fontSize={11}
214
- fontWeight={600}
215
- textAnchor="middle"
216
- fill="var(--cdl-text, #1a1f2a)"
269
+ )}
270
+ <circle
271
+ cx={cx}
272
+ cy={cy}
273
+ r={5}
274
+ fill="var(--cdl-node-fill, #ffffff)"
275
+ stroke={光る ? "var(--cdl-now)" : "var(--cdl-tone-accent, #2d6a8f)"}
276
+ strokeWidth={2.5}
277
+ />
278
+ <circle
279
+ cx={cx}
280
+ cy={cy}
281
+ r={2.5}
282
+ fill={光る ? "var(--cdl-now)" : "var(--cdl-tone-accent, #2d6a8f)"}
283
+ />
284
+ {node.chartValueRise && 光る ? (
285
+ <g
286
+ data-cdl-role="chart-line-value-rise"
287
+ transform={`translate(0 ${8 * (1 - 光の進み)})`}
217
288
  >
218
- {d.value.toLocaleString()}
219
- </text>
220
- </>
221
- )}
289
+ {}
290
+ </g>
291
+ ) : (
292
+
293
+ )}
294
+ </>
295
+ );
296
+ }
297
+ return (
298
+ <g key={`pt-${idx}`} data-cdl-unresolved={d.unresolved ? "true" : undefined}>
299
+ {点と値}
222
300
  <text
223
301
  x={cx}
224
302
  y={PAD_TOP + canvasH + 20}
@@ -3328,8 +3328,116 @@ function 末尾を辺に沿ってずらす(
3328
3328
  }
3329
3329
  // 助走の折れは丸めない。 短い 2 折れなので、 丸めると切り口が区間からはみ出す。
3330
3330
  const 折れ1は直前の折れ = Math.abs(折れ1.x - last.x1) <= 0.5 && Math.abs(折れ1.y - last.y1) <= 0.5;
3331
+ /*
3332
+ * 助走が最後の区間いっぱいに達すると、 1 つ目の折れが直前の折れと重なる (#663)。
3333
+ * その形で 2 つ目の折れを足すと、 直前の折れからずらし量ぶん (実測 28) の直線ができ、
3334
+ * 前後を角丸に挟まれて潰れた角に見える。
3335
+ *
3336
+ * 折れを足す代わりに **直前の折れごと** ずらすと、 手前の区間が伸び縮みするだけで
3337
+ * 短い直線ができない。 手前を辿ってずらす向きと平行な区間を探し、 そこで吸収する。
3338
+ */
3339
+ if (折れ1は直前の折れ) {
3340
+ const 折り返さない = 直前の折れごとずらす(segs, 横, d, nodes, e);
3341
+ if (折り返さない) return 折り返さない;
3342
+ }
3331
3343
  if (!折れ1は直前の折れ) parts.push(`L ${round1(折れ1.x)} ${round1(折れ1.y)}`);
3332
3344
  parts.push(`L ${round1(折れ2.x)} ${round1(折れ2.y)}`);
3333
3345
  parts.push(`L ${round1(終点.x)} ${round1(終点.y)}`);
3334
3346
  return parts.join(" ");
3335
3347
  }
3348
+
3349
+ /**
3350
+ * 折れを足さずに、 最後の区間の手前を丸ごとずらす (#663)。
3351
+ *
3352
+ * ずらす向きと **平行な** 区間まで手前へ辿り、 その区間の終わりだけを動かす。
3353
+ * 平行な区間は伸び縮みするだけなので折れが増えない。 その先 (最後の区間まで) は
3354
+ * 形を保ったまま平行移動する。
3355
+ *
3356
+ * 見つからない / 吸収する区間が短くなりすぎる / 箱に当たる 場合は `null` を返し、
3357
+ * 呼出側の従来の形 (折れを 2 つ足す) に任せる。
3358
+ */
3359
+ function 直前の折れごとずらす(
3360
+ segs: ReturnType<typeof extractPathSegments>,
3361
+ 横: boolean,
3362
+ d: number,
3363
+ nodes: readonly LaidNode[],
3364
+ e: LaidEdge,
3365
+ ): string | null {
3366
+ // 吸収する区間は、 ずらす向きと平行 = 横にずらすなら縦の区間。
3367
+ const 平行か = (s: (typeof segs)[number]): boolean =>
3368
+ 横 ? Math.abs(s.x2 - s.x1) < 0.5 && Math.abs(s.y2 - s.y1) > 0.5
3369
+ : Math.abs(s.y2 - s.y1) < 0.5 && Math.abs(s.x2 - s.x1) > 0.5;
3370
+
3371
+ let 吸収 = -1;
3372
+ for (let i = segs.length - 2; i >= 0; i -= 1) {
3373
+ if (平行か(segs[i]!)) {
3374
+ 吸収 = i;
3375
+ break;
3376
+ }
3377
+ }
3378
+ /*
3379
+ * **平行な区間が 1 つも無い形は検査で作れなかった**。 呼出側は「最後の区間が辺に垂直に
3380
+ * 入る」 かつ「助走が最後の区間いっぱいに達する」 形でしか ここへ来ないため、 その手前には
3381
+ * 必ず向きの変わる区間がある。 型の上では起こりうるので、 落ちる代わりに従来の形へ返す。
3382
+ */
3383
+ if (吸収 < 0) return null;
3384
+
3385
+ // 吸収する区間が伸び縮みした後も、 折れとして読める長さを残す。
3386
+ const 元の長さ = 横
3387
+ ? Math.abs(segs[吸収]!.y2 - segs[吸収]!.y1)
3388
+ : Math.abs(segs[吸収]!.x2 - segs[吸収]!.x1);
3389
+ const 向き = 横
3390
+ ? Math.sign(segs[吸収]!.y2 - segs[吸収]!.y1)
3391
+ : Math.sign(segs[吸収]!.x2 - segs[吸収]!.x1);
3392
+ const 後の長さ = 元の長さ + 向き * d;
3393
+ if (後の長さ < ENDPOINT_FAN_RUNUP) return null;
3394
+
3395
+ const ずらす = <T extends { x: number; y: number }>(p: T): T =>
3396
+ (横 ? { ...p, y: p.y + d } : { ...p, x: p.x + d });
3397
+
3398
+ // 吸収する区間の終わりから後ろを全て平行移動する。
3399
+ const 新segs = segs.map((s, i) => {
3400
+ // 吸収する区間は終わりだけを動かす (下で置き直す)。 それより前は触らない。
3401
+ if (i <= 吸収) return s;
3402
+ return {
3403
+ ...s,
3404
+ x1: ずらす({ x: s.x1, y: s.y1 }).x,
3405
+ y1: ずらす({ x: s.x1, y: s.y1 }).y,
3406
+ x2: ずらす({ x: s.x2, y: s.y2 }).x,
3407
+ y2: ずらす({ x: s.x2, y: s.y2 }).y,
3408
+ ...(s.ctrl ? { ctrl: s.ctrl.map((c) => ずらす(c)) } : {}),
3409
+ };
3410
+ });
3411
+ // 吸収する区間の終わりは、 上の map では x2/y2 を個別に扱えないので置き直す。
3412
+ 新segs[吸収] = {
3413
+ ...segs[吸収]!,
3414
+ x2: 横 ? segs[吸収]!.x2 : segs[吸収]!.x2 + d,
3415
+ y2: 横 ? segs[吸収]!.y2 + d : segs[吸収]!.y2,
3416
+ };
3417
+
3418
+ if (
3419
+ nodes.some(
3420
+ (n) =>
3421
+ n.id !== e.from &&
3422
+ n.id !== e.to &&
3423
+ segmentsCrossRect(
3424
+ 新segs.slice(吸収).map((s) => ({ x1: s.x1, y1: s.y1, x2: s.x2, y2: s.y2 })),
3425
+ nodeRect(n),
3426
+ ),
3427
+ )
3428
+ ) {
3429
+ return null;
3430
+ }
3431
+
3432
+ const head = 新segs[0]!;
3433
+ const parts: string[] = [`M ${round1(head.x1)} ${round1(head.y1)}`];
3434
+ for (const s of 新segs) {
3435
+ if (s.cmd === "Q") {
3436
+ const c = s.ctrl?.[0];
3437
+ parts.push(c ? `Q ${round1(c.x)} ${round1(c.y)}, ${round1(s.x2)} ${round1(s.y2)}` : `L ${round1(s.x2)} ${round1(s.y2)}`);
3438
+ continue;
3439
+ }
3440
+ parts.push(`L ${round1(s.x2)} ${round1(s.y2)}`);
3441
+ }
3442
+ return parts.join(" ");
3443
+ }
@@ -1,4 +1,12 @@
1
- import { EDGE_HEAD_DEFAULT, EDGE_HEAD_FILL_DEFAULT, type EdgeHead, type EdgeHeadFill } from "../types";
1
+ import {
2
+ EDGE_HEAD_DEFAULT,
3
+ EDGE_HEAD_FILL_DEFAULT,
4
+ type EdgeHead,
5
+ type EdgeHeadFill,
6
+ } from "../types";
7
+ import { hasTone } from "./tone";
8
+
9
+ export const EDGE_HEAD_MUTED_MARKER_ID = "cdl-arrow-muted";
2
10
 
3
11
  /**
4
12
  * 関係の端の形の **描き方と marker の名前** (SSOT、 #560)。
@@ -88,6 +96,20 @@ export function edgeHeadMarkerId(
88
96
  : `cdl-arrow-${tone}-${head}${塗}${小}`;
89
97
  }
90
98
 
99
+ /** 辺の端が実際に参照する marker 名。定義側と描画側で同じ判定を使う。 */
100
+ export function edgeHeadMarkerName(
101
+ tone: string,
102
+ head: EdgeHead | undefined,
103
+ small: boolean,
104
+ fill: EdgeHeadFill | undefined,
105
+ ): string | undefined {
106
+ const 実際の形 = head ?? EDGE_HEAD_DEFAULT;
107
+ if (実際の形 === "none") return undefined;
108
+ return hasTone(tone)
109
+ ? edgeHeadMarkerId(tone, 実際の形, small, fill ?? EDGE_HEAD_FILL_DEFAULT)
110
+ : EDGE_HEAD_MUTED_MARKER_ID;
111
+ }
112
+
91
113
  /**
92
114
  * marker を 1 つ描くのに要る 4 つ。 定義側 (`render/stage.tsx`) が引く。
93
115
  *
@@ -1,9 +1,8 @@
1
1
  import type { JSX } from "react";
2
- import { EDGE_HEAD_DEFAULT, EDGE_HEAD_FILL_DEFAULT, type LaidEdge } from "../types";
3
- import { edgeHeadMarkerId } from "./edge-head";
4
2
  import { computeLabelBBoxWorld, hasRenderedLabel, LABEL_PILL_PAD_X } from "../layout/spec";
5
- import { hasTone, toneColor } from "./tone";
6
- import type { EdgeRole, Tone } from "../types";
3
+ import type { EdgeRole, LaidEdge, Tone } from "../types";
4
+ import { edgeHeadMarkerName } from "./edge-head";
5
+ import { toneColor } from "./tone";
7
6
 
8
7
  /**
9
8
  * 辺の色 (#618)。 役目を書いた辺だけ「いま」 の色で引く。
@@ -75,18 +74,13 @@ export function CdlEdgeView({
75
74
  // その色なら 0.9 で 4.51-4.92 と 4.5:1 を満たす (0.8 では 3.69-4.04 で届かない)。
76
75
  // 実線と点線で同じ組を使い、 光らせた側が薄くなる逆転を作らない。
77
76
  // 端の形は線の種類と別の軸 (#560)。 書かなければ既定の塗った三角で、名前も従来のまま
78
- const 端の名前 = (h: typeof edge.head, f: typeof edge.headFill): string | undefined => {
79
- const 形 = h ?? EDGE_HEAD_DEFAULT;
80
- // 何も描かない端は marker を持たない (#578)。 参照すると解決できない url が残る
81
- if (形 === "none") return undefined;
82
- return hasTone(edge.tone)
83
- ? edgeHeadMarkerId(edge.tone, 形, !active, f ?? EDGE_HEAD_FILL_DEFAULT)
84
- : "cdl-arrow-muted";
85
- };
86
- const marker = 端の名前(edge.head, edge.headFill);
77
+ const marker = edgeHeadMarkerName(edge.tone, edge.head, !active, edge.headFill);
87
78
  // 出どころ側の印 (#578)。 marker は経路の向きに従って回るので、始点に置いた印は
88
79
  // 自動で外向きになる (`orient="auto-start-reverse"`)。 書かない図では端が無い
89
- const tailMarker = edge.tailHead === undefined ? undefined : 端の名前(edge.tailHead, edge.tailHeadFill);
80
+ const tailMarker =
81
+ edge.tailHead === undefined
82
+ ? undefined
83
+ : edgeHeadMarkerName(edge.tone, edge.tailHead, !active, edge.tailHeadFill);
90
84
 
91
85
  const style = edge.style ?? "solid";
92
86
  const isDotted = style === "dotted-flow";
@@ -8,7 +8,7 @@ import { StorageNode } from "../kinds/storage";
8
8
  import { GenericNode } from "../kinds/generic";
9
9
  import { MarkStartNode, MarkEndNode } from "../kinds/terminal-mark";
10
10
  import { DynShapeNode } from "../kinds/dyn-shape";
11
- import { ChartLineNode } from "../kinds/chart-line";
11
+ import { ChartLineNode, type ChartLineScale } from "../kinds/chart-line";
12
12
  import { ChartPieNode } from "../kinds/chart-pie";
13
13
  import { ChartGaugeNode } from "../kinds/chart-gauge";
14
14
  import { ChartRadialNode } from "../kinds/chart-radial";
@@ -95,6 +95,7 @@ export function CdlNodeView({
95
95
  progress,
96
96
  stateValues,
97
97
  currentPhaseId,
98
+ chartLineScale,
98
99
  }: {
99
100
  node: LaidNode;
100
101
  active: boolean;
@@ -106,6 +107,7 @@ export function CdlNodeView({
106
107
  progress: number;
107
108
  stateValues: Record<string, string>;
108
109
  currentPhaseId: string | undefined;
110
+ chartLineScale?: ChartLineScale;
109
111
  }): JSX.Element {
110
112
  const value = node.value ? interpolate(node.value, stateValues) : undefined;
111
113
  const rows = (node.rows ?? []).map((r) => interpolate(r, stateValues));
@@ -175,6 +177,7 @@ export function CdlNodeView({
175
177
  node={resolvedNode}
176
178
  active={active}
177
179
  stateValues={stateValues}
180
+ scale={chartLineScale}
178
181
  drawing={drawing}
179
182
  progress={progress}
180
183
  />