@cardenelabs/cdl 0.66.0 → 0.68.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cardenelabs/cdl",
3
- "version": "0.66.0",
3
+ "version": "0.68.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",
@@ -2,7 +2,7 @@ import type { JSX } from "react";
2
2
  import type { LaidNode, Tone } from "../types";
3
3
  import { TONE } from "../render/tone";
4
4
  import { resolveGanttData } from "../render/payload-binding";
5
- import { 進みを畳む } from "./draw-progress";
5
+ import { 進みを畳む, 段の伸び } from "./draw-progress";
6
6
 
7
7
  /**
8
8
  * gantt-timeline kind ... 1 node に task 配列 (ganttData) を保持し、 帯状 timeline を SVG 描画する。
@@ -29,9 +29,9 @@ export function GanttNode({
29
29
  const y0 = node.cy - node.h / 2;
30
30
  const tasks = resolveGanttData(node.ganttData ?? [], stateValues);
31
31
 
32
- const PAD_TOP = 32;
32
+ const PAD_TOP = 46;
33
33
  const PAD_RIGHT = 48;
34
- const PAD_BOTTOM = 44;
34
+ const PAD_BOTTOM = 26;
35
35
  const PAD_LEFT = 156;
36
36
  const canvasW = node.w - PAD_LEFT - PAD_RIGHT;
37
37
  const canvasH = node.h - PAD_TOP - PAD_BOTTOM;
@@ -39,6 +39,8 @@ export function GanttNode({
39
39
  const rowGap = 20;
40
40
  const rowCount = tasks.length || 1;
41
41
  const rowH = Math.max(28, (canvasH - rowGap * (rowCount + 1)) / rowCount);
42
+ /** 帯そのものの高さ。 行いっぱいに塗ると塊に見えるので細くして行の中で中央に置く */
43
+ const 帯の高さ = Math.min(rowH, 30);
42
44
 
43
45
  // 位置を先に整える (#468 Round 5)。 端も始まりも状態から来るため、 そのまま使うと
44
46
  // 図が壊れる形が 3 つある (いずれも実測)。
@@ -107,6 +109,8 @@ export function GanttNode({
107
109
 
108
110
  const xAt = (idx: number) => PAD_LEFT + idx * cellW;
109
111
  const yAt = (row: number) => PAD_TOP + rowGap + row * (rowH + rowGap);
112
+ /** 帯の上端。 行の真ん中に置く */
113
+ const 帯の上 = (row: number): number => yAt(row) + (rowH - 帯の高さ) / 2;
110
114
 
111
115
  // 目盛りの名前は task が持つ (開始と終了の両方)。 どちらも指していない位置は
112
116
  // **空にする** = index の数字をそのまま出すと、 月や四半期の並びに数字が 1 つ混ざって
@@ -181,6 +185,21 @@ export function GanttNode({
181
185
  */
182
186
  const 伸び = 進みを畳む(drawing, progress);
183
187
 
188
+ /**
189
+ * その行の帯がどこまで伸びたか (0..1、 cdl#873)。
190
+ *
191
+ * **上の行から順に 1 本ずつ伸ばす**。 全部を同じ倍率で伸ばすと、「前の工程が終わってから
192
+ * 次が始まる」 を示す図なのに 4 本が横並びで一斉に伸びる。 依存の向きは上から下なので、
193
+ * 行番号がそのまま順序になる。
194
+ *
195
+ * 区切り方は発想の図と木の図が使う `段の伸び` と同じ。 進みを行数で等分し、その区間に
196
+ * 入ってから出るまでを 0→1 で返す。 **行が増えるほど 1 本あたりは速くなる** = 図全体の
197
+ * 時間は段の `duration` が決めるので、本数で割る以外に配りようがない。
198
+ *
199
+ * 描く指定が無い時は `伸び` が 1 なので、どの行も 1 を返す (今までと同じ)。
200
+ */
201
+ const 行の伸び = (row: number): number => 段の伸び(伸び, row + 1, rowCount);
202
+
184
203
  const taskById = new Map(整えた.map((t) => [t.id, t]));
185
204
  const rowOf = new Map(整えた.map((t, i) => [t.id, i]));
186
205
 
@@ -198,6 +217,41 @@ export function GanttNode({
198
217
  strokeWidth={active ? 3 : 1.25}
199
218
  />
200
219
 
220
+ {/* 行の地。 1 行おきに薄く敷いて、名前と帯を横に繋いで読めるようにする */}
221
+ {整えた.map((t, row) =>
222
+ row % 2 === 0 ? null : (
223
+ <rect
224
+ key={`band-${t.id}`}
225
+ data-cdl-role="gantt-row"
226
+ x={PAD_LEFT}
227
+ y={yAt(row) - rowGap / 2}
228
+ width={canvasW}
229
+ height={rowH + rowGap}
230
+ fill="var(--cdl-surface-sunken, #f4f2ed)"
231
+ opacity={0.55}
232
+ />
233
+ ),
234
+ )}
235
+
236
+ {/* 期間の見出しの下の罫 */}
237
+ <line
238
+ x1={PAD_LEFT}
239
+ y1={PAD_TOP}
240
+ x2={PAD_LEFT + canvasW}
241
+ y2={PAD_TOP}
242
+ stroke="var(--cdl-divider, #d4d4d8)"
243
+ strokeWidth={1}
244
+ />
245
+ {/* 名前の列と図の境。 名前がどの行のものかを目で追える */}
246
+ <line
247
+ x1={PAD_LEFT}
248
+ y1={PAD_TOP}
249
+ x2={PAD_LEFT}
250
+ y2={PAD_TOP + canvasH}
251
+ stroke="var(--cdl-divider, #d4d4d8)"
252
+ strokeWidth={1}
253
+ />
254
+
201
255
  {gridLabels.map(({ 位置: i, label }) => (
202
256
  <g key={`grid-${i}`}>
203
257
  <line
@@ -207,15 +261,14 @@ export function GanttNode({
207
261
  x2={xAt(i)}
208
262
  y2={PAD_TOP + canvasH}
209
263
  stroke="var(--cdl-divider, #d4d4d8)"
210
- strokeWidth={0.75}
211
- strokeDasharray="3 3"
212
- opacity={0.5}
264
+ strokeWidth={1}
265
+ opacity={0.45}
213
266
  />
214
267
  {label === "" ? null : (
215
268
  <text
216
269
  data-cdl-role="gantt-tick"
217
270
  x={xAt(i) + cellW / 2}
218
- y={PAD_TOP + canvasH + 18}
271
+ y={PAD_TOP - 16}
219
272
  fontSize={11}
220
273
  textAnchor="middle"
221
274
  fill="var(--cdl-text-dim, #5a6270)"
@@ -232,14 +285,14 @@ export function GanttNode({
232
285
  x2={xAt(maxIdx)}
233
286
  y2={PAD_TOP + canvasH}
234
287
  stroke="var(--cdl-divider, #d4d4d8)"
235
- strokeWidth={0.75}
236
- opacity={0.5}
288
+ strokeWidth={1}
289
+ opacity={0.45}
237
290
  />
238
291
 
239
292
  {整えた.map((t, row) => (
240
293
  <text
241
294
  key={`label-${t.id}`}
242
- x={PAD_LEFT - 12}
295
+ x={PAD_LEFT - 16}
243
296
  y={yAt(row) + rowH / 2 + 4}
244
297
  fontSize={12}
245
298
  textAnchor="end"
@@ -252,35 +305,51 @@ export function GanttNode({
252
305
 
253
306
  {整えた.map((t, row) => {
254
307
  const bx = 帯の左(t.startIdx);
255
- const by = yAt(row);
308
+ const by = 帯の上(row);
256
309
  // 0 で切るのは二重。 余白が目盛りの幅の半分で頭打ちのため、 引いた結果は必ず
257
310
  // 目盛りの幅の半分以上になる = **この分岐に届く入力を今は作れない** (変異試験で確認)。
258
311
  // 余白の決め方を変えた時に負の幅を出さないために残す
259
312
  const bw = Math.max(0, 帯の右(t.endIdx) - bx);
313
+ const この行 = 行の伸び(row);
314
+ /*
315
+ * 伸びは **幅そのもの** を変える (cdl#875)。
316
+ *
317
+ * 横倍率 (`scale(N 1)`) で伸ばすと丸い両端まで一緒に潰れ、伸びている間だけ
318
+ * 帯が角の詰まった四角に見えていた (実測 = 進み 0.31 で丸みの横幅が 15 から
319
+ * 4.65)。 幅を変えれば丸みは円のまま残り、短い帯は縦長の楕円へ素直に縮む。
320
+ *
321
+ * 倍率を帯の四角だけに掛ける形 (cdl#873) は、担当者の字が一緒に潰れるのを
322
+ * 止めるためだった。 幅で伸ばすなら字はそもそも影響を受けないので、包みごと
323
+ * 外す。
324
+ */
325
+ const 今の幅 = drawing ? bw * この行 : bw;
260
326
  return (
261
- <g
262
- key={`bar-${t.id}`}
263
- {...(drawing
264
- ? { transform: `translate(${bx} 0) scale(${伸び} 1) translate(${-bx} 0)` }
265
- : {})}
266
- >
327
+ <g key={`bar-${t.id}`}>
267
328
  <rect
268
329
  data-cdl-role="gantt-bar"
269
330
  data-cdl-unresolved={t.unresolved ? "true" : undefined}
270
331
  data-cdl-clipped={"clipped" in t && t.clipped ? "true" : undefined}
271
332
  x={bx}
272
333
  y={by}
273
- width={bw}
274
- height={rowH}
275
- rx={4}
334
+ width={今の幅}
335
+ height={帯の高さ}
336
+ rx={帯の高さ / 2}
337
+ ry={帯の高さ / 2}
276
338
  fill={toneColor(t.tone)}
277
339
  fillOpacity={0.9}
278
340
  />
279
- {t.owner && rowH >= 22 && (
341
+ {/*
342
+ 担当者の字は帯の真ん中に置き、**その帯が伸び切ってから出す**。
343
+ 薄く出しながら伸ばす案は採らない = 中央は伸び切った後の位置なので、
344
+ 伸びている間は帯の無い所に字が浮く。
345
+ */}
346
+ {t.owner && 帯の高さ >= 22 && この行 >= 1 && (
280
347
  <text
281
- x={bx + 8}
282
- y={by + rowH / 2 + 4}
348
+ data-cdl-role="gantt-owner"
349
+ x={bx + bw / 2}
350
+ y={by + 帯の高さ / 2 + 4}
283
351
  fontSize={11}
352
+ textAnchor="middle"
284
353
  fill="var(--cdl-node-fill, #ffffff)"
285
354
  fontWeight={600}
286
355
  >
@@ -298,41 +367,57 @@ export function GanttNode({
298
367
  const parentRow = rowOf.get(parent.id)!;
299
368
  const childRow = rowOf.get(t.id)!;
300
369
  const parentBarRight = 帯の右(parent.endIdx);
301
- const parentMidY = yAt(parentRow) + rowH / 2;
302
370
  const childBarLeft = 帯の左(t.startIdx);
303
- const childMidY = yAt(childRow) + rowH / 2;
304
- const arrowHeadSize = 8;
305
- // arrow head 三角形の頂点 (= tipX + arrowHeadSize) が bar 左辺の 10px 手前に来る
306
- // ように配置、 「食い込み」 に見える視覚エラー完全排除
307
- const arrowHeadTipGap = 10;
308
- const tipX = childBarLeft - arrowHeadTipGap - arrowHeadSize;
309
- const tipY = childMidY;
310
- const startX = parentBarRight + 4;
311
- // 親 bar の右端と 矢印の手前の間に縦線を通す余裕があるかで経路を分ける。
312
- //
313
- // 旧実装は `elbowX = Math.max(startX + 6, tipX - 6)` で、 子 bar が親の直後に始まると
314
- // `elbowX > tipX` になり、 縦線が矢印より右に引かれてから左へ戻っていた。
315
- const elbowGap = 6;
316
- const enterX = tipX - elbowGap;
317
- const hasRoom = enterX - startX >= elbowGap;
318
- const path = hasRoom
319
- // 余裕がある ... 右へ → 下へ → 矢印へ
320
- ? `M ${startX} ${parentMidY} L ${enterX} ${parentMidY} L ${enterX} ${tipY} L ${tipX} ${tipY}`
321
- // 余裕がない ... 行間を経由して回り込む (右へ → 行間へ → 左へ → 下へ → 矢印へ)
322
- : `M ${startX} ${parentMidY} L ${startX + elbowGap} ${parentMidY}` +
323
- ` L ${startX + elbowGap} ${(parentMidY + tipY) / 2}` +
324
- ` L ${enterX} ${(parentMidY + tipY) / 2} L ${enterX} ${tipY} L ${tipX} ${tipY}`;
325
- const arrowHeadPath = `M ${tipX + arrowHeadSize} ${tipY} L ${tipX} ${tipY - arrowHeadSize / 2} L ${tipX} ${tipY + arrowHeadSize / 2} Z`;
371
+ /*
372
+ * 矢じりは **幅を高さより広く** 取る (cdl#875)。 帯が高さ 30 の丸帯なので、
373
+ * 細く尖った矢じりだと線だけが浮いて見える。 平たい三角にすると帯と同じ重さで
374
+ * 収まり、向きも読める。
375
+ */
376
+ const 矢じりの幅 = 11;
377
+ const 矢じりの高さ = 7;
378
+ /*
379
+ * 依存の線は **前の帯の下端から出て、次の帯の上端へ入る** 曲線 1 本 (cdl#875)。
380
+ *
381
+ * 横から入れようとすると、前の工程が終わってすぐ次が始まる形で隙間が 6 しかなく、
382
+ * 矢じりと曲がりに要る 18 が入らない。 折れ線だった頃はそこで右へ出てから左へ
383
+ * 戻る Z 字を描き、矢じりは帯に届かず宙に浮いていた。
384
+ *
385
+ * 上下に繋ぐと、行と行の間が必ず `rowGap` ぶん空いているので、どんな並びでも
386
+ * 同じ形で引ける。 出る所も入る所も帯の縁なので、線が帯から生えて帯に刺さる。
387
+ */
388
+ const 丸み = 帯の高さ / 2;
389
+ const 親の左 = 帯の左(parent.startIdx);
390
+ const 子の右 = 帯の右(t.endIdx);
391
+ /*
392
+ * 出入りの点は丸みの真ん中に取る (丸みの上から出すと縁を滑って見える)。
393
+ * ただし **目盛りが細かい図では帯が丸み 2 つぶんより狭くなる**。 その時は
394
+ * 丸みぶん寄せると帯の外へ出てしまうので、寄せ幅を帯の半分で頭打ちにする
395
+ * = 狭い帯では帯の真ん中から出る (位置 85 の実測 = 幅 3 の帯で 172.5 と
396
+ * 右端 160.5 がずれ、線が帯の外から生えていた)。
397
+ */
398
+ const 寄せ = (左: number, 右: number): number => Math.min(丸み, (右 - 左) / 2);
399
+ const 出発X = parentBarRight - 寄せ(親の左, parentBarRight);
400
+ const 出発Y = 帯の上(parentRow) + 帯の高さ;
401
+ const 着地X = childBarLeft + 寄せ(childBarLeft, 子の右);
402
+ const 着地Y = 帯の上(childRow);
403
+ const 縦の終わりY = 着地Y - 矢じりの高さ;
404
+ // 手の長さは行と行の間の高さで決める。 超えると帯を通り越してから戻る
405
+ const 手 = Math.max(6, (縦の終わりY - 出発Y) * 0.55);
406
+ const path =
407
+ `M ${出発X} ${出発Y}` +
408
+ ` C ${出発X} ${出発Y + 手}, ${着地X} ${縦の終わりY - 手}, ${着地X} ${縦の終わりY}`;
409
+ const arrowHeadPath =
410
+ `M ${着地X} ${着地Y}` +
411
+ ` L ${着地X - 矢じりの幅 / 2} ${縦の終わりY}` +
412
+ ` L ${着地X + 矢じりの幅 / 2} ${縦の終わりY} Z`;
326
413
  return (
327
- // 矢印は両端の帯が出てから描く。 帯が無い場所へ矢印だけが伸びると、
328
- // 指す先の無い線になる (cdl#521)
329
- (drawing && 伸び < 1) ? null : (
414
+ (drawing && !(行の伸び(parentRow) >= 1 && 行の伸び(childRow) >= 1)) ? null : (
330
415
  <g key={`dep-${parent.id}-${t.id}`} data-cdl-role="gantt-arrow">
331
416
  <path
332
417
  d={path}
333
418
  fill="none"
334
419
  stroke="var(--cdl-text-mute, #8a8678)"
335
- strokeWidth={1.75}
420
+ strokeWidth={2.4}
336
421
  strokeLinejoin="round"
337
422
  strokeLinecap="round"
338
423
  />