@cardenelabs/cdl 0.67.0 → 0.69.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.67.0",
3
+ "version": "0.69.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 { 伸ばすdash, 進みを畳む, 段の伸び } 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 つ混ざって
@@ -166,7 +170,7 @@ export function GanttNode({
166
170
  //
167
171
  // 固定 (6) だと、 目盛りが細かい図で帯が消える。 1 目盛りの幅が 6 ちょうどだと幅 0、
168
172
  // それより小さいと負になる (実測 = 位置 1e6 で -5.999、 位置 85 で 0)。
169
- // 目盛りが広い図 (通常の工程表) では 6 のままなので見た目は変わらない。
173
+ // 目盛りが広い図 (通常のガントチャート) では 6 のままなので見た目は変わらない。
170
174
  const 帯の余白 = Math.min(6, cellW / 2);
171
175
  // 帯の左端と右端。 **依存矢印も同じ式を使う** (#468 Round 9)。 別々に持つと、 余白を
172
176
  // 可変にした時に矢印の基準点だけが旧い端に取り残される
@@ -176,7 +180,7 @@ export function GanttNode({
176
180
  /**
177
181
  * 帯がどこまで伸びたか (0..1、 cdl#521)。 描く指定が無ければ常に 1 = 全長。
178
182
  *
179
- * **帯ごとに始端が違う**。 棒グラフは全部が同じ横軸から伸びるが、工程表は工程ごとに
183
+ * **帯ごとに始端が違う**。 棒グラフは全部が同じ横軸から伸びるが、ガントチャートは工程ごとに
180
184
  * 始まる位置が違うため、包みも帯ごとに作って不動点をその帯の左端に置く。
181
185
  */
182
186
  const 伸び = 進みを畳む(drawing, progress);
@@ -199,6 +203,54 @@ export function GanttNode({
199
203
  const taskById = new Map(整えた.map((t) => [t.id, t]));
200
204
  const rowOf = new Map(整えた.map((t, i) => [t.id, i]));
201
205
 
206
+ /**
207
+ * 行の時間のうち、**入ってくる依存の線を引くのに使う割合** (cdl#877)。
208
+ *
209
+ * 残りで帯を伸ばす。 行の持ち時間そのものは変えないので、図全体の長さは変わらない。
210
+ */
211
+ const 線の取り分 = 0.35;
212
+
213
+ const 区切る = (v: number): number => Math.min(1, Math.max(0, v));
214
+
215
+ /** その行に、上の行から入ってくる依存の線があるか */
216
+ const 上から来る線 = (row: number): boolean => {
217
+ const t = 整えた[row];
218
+ if (t === undefined || t.dependsOn === undefined) return false;
219
+ const 親 = taskById.get(t.dependsOn);
220
+ if (親 === undefined) return false;
221
+ const 親の行 = rowOf.get(親.id);
222
+ return 親の行 !== undefined && 親の行 < row;
223
+ };
224
+
225
+ /**
226
+ * その行の帯がどこまで伸びたか (0..1、 cdl#877)。
227
+ *
228
+ * **入ってくる線がある行は、線を引き終えてから伸び始める**。 前の工程が終わり、線が
229
+ * 次の工程へ届き、そこから帯が伸びる = 図が示している順序をそのまま時間に写す。
230
+ * 線の無い行 (最初の工程や、下から上へ戻る依存) は行の時間を丸ごと使う。
231
+ */
232
+ const 帯の伸び = (row: number): number => {
233
+ const r = 行の伸び(row);
234
+ return 上から来る線(row) ? 区切る((r - 線の取り分) / (1 - 線の取り分)) : r;
235
+ };
236
+
237
+ /**
238
+ * その線がどこまで引けたか (0..1、 cdl#877)。
239
+ *
240
+ * 以前は両端の行が伸び切った瞬間に線が丸ごと現れていた。 線が根元から先へ伸びれば
241
+ * 「どちらからどちらへ」 が動きそのもので読める。
242
+ *
243
+ * **親が下にある依存は順に読ませる向きが無い**。 その時だけは両端が伸び切ってから
244
+ * 一度に出す (上から下へ読む図で、下から戻る線を先に引くと順序が逆に見える)。
245
+ */
246
+ const 線の伸び = (親の行: number, 子の行: number): number => {
247
+ if (!drawing) return 1;
248
+ if (親の行 >= 子の行) {
249
+ return 行の伸び(親の行) >= 1 && 行の伸び(子の行) >= 1 ? 1 : 0;
250
+ }
251
+ return 区切る(行の伸び(子の行) / 線の取り分);
252
+ };
253
+
202
254
  return (
203
255
  <g transform={`translate(${x0} ${y0})`}>
204
256
  <rect
@@ -213,6 +265,41 @@ export function GanttNode({
213
265
  strokeWidth={active ? 3 : 1.25}
214
266
  />
215
267
 
268
+ {/* 行の地。 1 行おきに薄く敷いて、名前と帯を横に繋いで読めるようにする */}
269
+ {整えた.map((t, row) =>
270
+ row % 2 === 0 ? null : (
271
+ <rect
272
+ key={`band-${t.id}`}
273
+ data-cdl-role="gantt-row"
274
+ x={PAD_LEFT}
275
+ y={yAt(row) - rowGap / 2}
276
+ width={canvasW}
277
+ height={rowH + rowGap}
278
+ fill="var(--cdl-surface-sunken, #f4f2ed)"
279
+ opacity={0.55}
280
+ />
281
+ ),
282
+ )}
283
+
284
+ {/* 期間の見出しの下の罫 */}
285
+ <line
286
+ x1={PAD_LEFT}
287
+ y1={PAD_TOP}
288
+ x2={PAD_LEFT + canvasW}
289
+ y2={PAD_TOP}
290
+ stroke="var(--cdl-divider, #d4d4d8)"
291
+ strokeWidth={1}
292
+ />
293
+ {/* 名前の列と図の境。 名前がどの行のものかを目で追える */}
294
+ <line
295
+ x1={PAD_LEFT}
296
+ y1={PAD_TOP}
297
+ x2={PAD_LEFT}
298
+ y2={PAD_TOP + canvasH}
299
+ stroke="var(--cdl-divider, #d4d4d8)"
300
+ strokeWidth={1}
301
+ />
302
+
216
303
  {gridLabels.map(({ 位置: i, label }) => (
217
304
  <g key={`grid-${i}`}>
218
305
  <line
@@ -222,15 +309,14 @@ export function GanttNode({
222
309
  x2={xAt(i)}
223
310
  y2={PAD_TOP + canvasH}
224
311
  stroke="var(--cdl-divider, #d4d4d8)"
225
- strokeWidth={0.75}
226
- strokeDasharray="3 3"
227
- opacity={0.5}
312
+ strokeWidth={1}
313
+ opacity={0.45}
228
314
  />
229
315
  {label === "" ? null : (
230
316
  <text
231
317
  data-cdl-role="gantt-tick"
232
318
  x={xAt(i) + cellW / 2}
233
- y={PAD_TOP + canvasH + 18}
319
+ y={PAD_TOP - 16}
234
320
  fontSize={11}
235
321
  textAnchor="middle"
236
322
  fill="var(--cdl-text-dim, #5a6270)"
@@ -247,14 +333,14 @@ export function GanttNode({
247
333
  x2={xAt(maxIdx)}
248
334
  y2={PAD_TOP + canvasH}
249
335
  stroke="var(--cdl-divider, #d4d4d8)"
250
- strokeWidth={0.75}
251
- opacity={0.5}
336
+ strokeWidth={1}
337
+ opacity={0.45}
252
338
  />
253
339
 
254
340
  {整えた.map((t, row) => (
255
341
  <text
256
342
  key={`label-${t.id}`}
257
- x={PAD_LEFT - 12}
343
+ x={PAD_LEFT - 16}
258
344
  y={yAt(row) + rowH / 2 + 4}
259
345
  fontSize={12}
260
346
  textAnchor="end"
@@ -267,47 +353,49 @@ export function GanttNode({
267
353
 
268
354
  {整えた.map((t, row) => {
269
355
  const bx = 帯の左(t.startIdx);
270
- const by = yAt(row);
356
+ const by = 帯の上(row);
271
357
  // 0 で切るのは二重。 余白が目盛りの幅の半分で頭打ちのため、 引いた結果は必ず
272
358
  // 目盛りの幅の半分以上になる = **この分岐に届く入力を今は作れない** (変異試験で確認)。
273
359
  // 余白の決め方を変えた時に負の幅を出さないために残す
274
360
  const bw = Math.max(0, 帯の右(t.endIdx) - bx);
275
- const この行 = 行の伸び(row);
361
+ const この行 = 帯の伸び(row);
362
+ /*
363
+ * 伸びは **幅そのもの** を変える (cdl#875)。
364
+ *
365
+ * 横倍率 (`scale(N 1)`) で伸ばすと丸い両端まで一緒に潰れ、伸びている間だけ
366
+ * 帯が角の詰まった四角に見えていた (実測 = 進み 0.31 で丸みの横幅が 15 から
367
+ * 4.65)。 幅を変えれば丸みは円のまま残り、短い帯は縦長の楕円へ素直に縮む。
368
+ *
369
+ * 倍率を帯の四角だけに掛ける形 (cdl#873) は、担当者の字が一緒に潰れるのを
370
+ * 止めるためだった。 幅で伸ばすなら字はそもそも影響を受けないので、包みごと
371
+ * 外す。
372
+ */
373
+ const 今の幅 = drawing ? bw * この行 : bw;
276
374
  return (
277
375
  <g key={`bar-${t.id}`}>
278
- {/*
279
- 倍率は **帯の四角だけ** に掛ける (cdl#873)。 以前は担当者の字も同じ包みに
280
- 入れていたため、伸びている間ずっと字が横に潰れていた (実測 = 進み 0.6 で
281
- 字の横幅が 6 割)。 字は倍率のかからない外側に置く。
282
- */}
283
- <g
284
- {...(drawing
285
- ? { transform: `translate(${bx} 0) scale(${この行} 1) translate(${-bx} 0)` }
286
- : {})}
287
- >
288
- <rect
289
- data-cdl-role="gantt-bar"
290
- data-cdl-unresolved={t.unresolved ? "true" : undefined}
291
- data-cdl-clipped={"clipped" in t && t.clipped ? "true" : undefined}
292
- x={bx}
293
- y={by}
294
- width={bw}
295
- height={rowH}
296
- rx={4}
297
- fill={toneColor(t.tone)}
298
- fillOpacity={0.9}
299
- />
300
- </g>
376
+ <rect
377
+ data-cdl-role="gantt-bar"
378
+ data-cdl-unresolved={t.unresolved ? "true" : undefined}
379
+ data-cdl-clipped={"clipped" in t && t.clipped ? "true" : undefined}
380
+ x={bx}
381
+ y={by}
382
+ width={今の幅}
383
+ height={帯の高さ}
384
+ rx={帯の高さ / 2}
385
+ ry={帯の高さ / 2}
386
+ fill={toneColor(t.tone)}
387
+ fillOpacity={0.9}
388
+ />
301
389
  {/*
302
390
  担当者の字は帯の真ん中に置き、**その帯が伸び切ってから出す**。
303
391
  薄く出しながら伸ばす案は採らない = 中央は伸び切った後の位置なので、
304
392
  伸びている間は帯の無い所に字が浮く。
305
393
  */}
306
- {t.owner && rowH >= 22 && この行 >= 1 && (
394
+ {t.owner && 帯の高さ >= 22 && この行 >= 1 && (
307
395
  <text
308
396
  data-cdl-role="gantt-owner"
309
397
  x={bx + bw / 2}
310
- y={by + rowH / 2 + 4}
398
+ y={by + 帯の高さ / 2 + 4}
311
399
  fontSize={11}
312
400
  textAnchor="middle"
313
401
  fill="var(--cdl-node-fill, #ffffff)"
@@ -327,57 +415,72 @@ export function GanttNode({
327
415
  const parentRow = rowOf.get(parent.id)!;
328
416
  const childRow = rowOf.get(t.id)!;
329
417
  const parentBarRight = 帯の右(parent.endIdx);
330
- const parentMidY = yAt(parentRow) + rowH / 2;
331
418
  const childBarLeft = 帯の左(t.startIdx);
332
- const childMidY = yAt(childRow) + rowH / 2;
333
- const arrowHeadSize = 8;
334
- // arrow head 三角形の頂点 (= tipX + arrowHeadSize) が bar 左辺の 10px 手前に来る
335
- // ように配置、 「食い込み」 に見える視覚エラー完全排除
336
- const arrowHeadTipGap = 10;
337
- const tipX = childBarLeft - arrowHeadTipGap - arrowHeadSize;
338
- const tipY = childMidY;
339
- const startX = parentBarRight + 4;
340
- // 親 bar の右端と 矢印の手前の間に縦線を通す余裕があるかで経路を分ける。
341
- //
342
- // 旧実装は `elbowX = Math.max(startX + 6, tipX - 6)` で、 子 bar が親の直後に始まると
343
- // `elbowX > tipX` になり、 縦線が矢印より右に引かれてから左へ戻っていた。
344
- const elbowGap = 6;
345
- const enterX = tipX - elbowGap;
346
- const hasRoom = enterX - startX >= elbowGap;
347
- const path = hasRoom
348
- // 余裕がある ... 右へ → 下へ → 矢印へ
349
- ? `M ${startX} ${parentMidY} L ${enterX} ${parentMidY} L ${enterX} ${tipY} L ${tipX} ${tipY}`
350
- // 余裕がない ... 行間を経由して回り込む (右へ → 行間へ → 左へ → 下へ → 矢印へ)
351
- : `M ${startX} ${parentMidY} L ${startX + elbowGap} ${parentMidY}` +
352
- ` L ${startX + elbowGap} ${(parentMidY + tipY) / 2}` +
353
- ` L ${enterX} ${(parentMidY + tipY) / 2} L ${enterX} ${tipY} L ${tipX} ${tipY}`;
354
- const arrowHeadPath = `M ${tipX + arrowHeadSize} ${tipY} L ${tipX} ${tipY - arrowHeadSize / 2} L ${tipX} ${tipY + arrowHeadSize / 2} Z`;
355
- // 矢印は両端の帯が出てから描く。 帯が無い場所へ矢印だけが伸びると、
356
- // 指す先の無い線になる (cdl#521)。
357
- //
358
- // **見るのは両端の行の伸びで、図全体の進みではない** (cdl#873)。 行ごとに順へ
359
- // 変えた後も全体の進みで見ると、帯が 4 本とも並んでから矢印がまとめて出る。
360
- const 両端が出た = 行の伸び(parentRow) >= 1 && 行の伸び(childRow) >= 1;
361
- return (
362
- (drawing && !両端が出た) ? null : (
419
+ /*
420
+ * 矢じりは **幅を高さより広く** 取る (cdl#875)。 帯が高さ 30 の丸帯なので、
421
+ * 細く尖った矢じりだと線だけが浮いて見える。 平たい三角にすると帯と同じ重さで
422
+ * 収まり、向きも読める。
423
+ */
424
+ const 矢じりの幅 = 11;
425
+ const 矢じりの高さ = 7;
426
+ /*
427
+ * 依存の線は **前の帯の下端から出て、次の帯の上端へ入る** 曲線 1 本 (cdl#875)。
428
+ *
429
+ * 横から入れようとすると、前の工程が終わってすぐ次が始まる形で隙間が 6 しかなく、
430
+ * 矢じりと曲がりに要る 18 が入らない。 折れ線だった頃はそこで右へ出てから左へ
431
+ * 戻る Z 字を描き、矢じりは帯に届かず宙に浮いていた。
432
+ *
433
+ * 上下に繋ぐと、行と行の間が必ず `rowGap` ぶん空いているので、どんな並びでも
434
+ * 同じ形で引ける。 出る所も入る所も帯の縁なので、線が帯から生えて帯に刺さる。
435
+ */
436
+ const 丸み = 帯の高さ / 2;
437
+ const 親の左 = 帯の左(parent.startIdx);
438
+ const 子の右 = 帯の右(t.endIdx);
439
+ /*
440
+ * 出入りの点は丸みの真ん中に取る (丸みの上から出すと縁を滑って見える)。
441
+ * ただし **目盛りが細かい図では帯が丸み 2 つぶんより狭くなる**。 その時は
442
+ * 丸みぶん寄せると帯の外へ出てしまうので、寄せ幅を帯の半分で頭打ちにする
443
+ * = 狭い帯では帯の真ん中から出る (位置 85 の実測 = 幅 3 の帯で 172.5 と
444
+ * 右端 160.5 がずれ、線が帯の外から生えていた)。
445
+ */
446
+ const 寄せ = (左: number, 右: number): number => Math.min(丸み, (右 - 左) / 2);
447
+ const 出発X = parentBarRight - 寄せ(親の左, parentBarRight);
448
+ const 出発Y = 帯の上(parentRow) + 帯の高さ;
449
+ const 着地X = childBarLeft + 寄せ(childBarLeft, 子の右);
450
+ const 着地Y = 帯の上(childRow);
451
+ const 縦の終わりY = 着地Y - 矢じりの高さ;
452
+ // 手の長さは行と行の間の高さで決める。 超えると帯を通り越してから戻る
453
+ const 手 = Math.max(6, (縦の終わりY - 出発Y) * 0.55);
454
+ const path =
455
+ `M ${出発X} ${出発Y}` +
456
+ ` C ${出発X} ${出発Y + 手}, ${着地X} ${縦の終わりY - 手}, ${着地X} ${縦の終わりY}`;
457
+ const arrowHeadPath =
458
+ `M ${着地X} ${着地Y}` +
459
+ ` L ${着地X - 矢じりの幅 / 2} ${縦の終わりY}` +
460
+ ` L ${着地X + 矢じりの幅 / 2} ${縦の終わりY} Z`;
461
+ const 引けた = 線の伸び(parentRow, childRow);
462
+ return 引けた <= 0 ? null : (
363
463
  <g key={`dep-${parent.id}-${t.id}`} data-cdl-role="gantt-arrow">
364
464
  <path
365
465
  d={path}
366
466
  fill="none"
367
467
  stroke="var(--cdl-text-mute, #8a8678)"
368
- strokeWidth={1.75}
468
+ strokeWidth={2.4}
369
469
  strokeLinejoin="round"
370
470
  strokeLinecap="round"
471
+ {...伸ばすdash(drawing, 引けた)}
371
472
  />
372
- <path
373
- d={arrowHeadPath}
374
- fill="var(--cdl-text-mute, #8a8678)"
375
- stroke="var(--cdl-text-mute, #8a8678)"
376
- strokeWidth={0.5}
377
- strokeLinejoin="round"
378
- />
473
+ {/* 矢じりは線が届いてから置く。 先に出すと、線の無い所に三角が浮く */}
474
+ {引けた < 1 ? null : (
475
+ <path
476
+ d={arrowHeadPath}
477
+ fill="var(--cdl-text-mute, #8a8678)"
478
+ stroke="var(--cdl-text-mute, #8a8678)"
479
+ strokeWidth={0.5}
480
+ strokeLinejoin="round"
481
+ />
482
+ )}
379
483
  </g>
380
- )
381
484
  );
382
485
  })}
383
486
  </g>
package/src/presets.ts CHANGED
@@ -34,7 +34,7 @@ export type BindingRef = `{${名前の頭}${string}}`;
34
34
  * その文字列が **全体で** 状態を読む書き方かどうか (#468 Round 5)。
35
35
  *
36
36
  * `hasUnresolvedRef` は「参照らしい部分を含むか」 を見るため、 `Phase {A}` のような通常の
37
- * 名前まで参照とみなす。 工程表の入口は名前と参照を同じ欄で受けるので、 部分一致で判定すると
37
+ * 名前まで参照とみなす。 ガントチャートの入口は名前と参照を同じ欄で受けるので、 部分一致で判定すると
38
38
  * **正当な名前が黙って消える** (実測 = `start: "Phase {A}"` が位置 `"Phase {A}"` になり、
39
39
  * 名前の欄が空になった)。
40
40
  *