@cardenelabs/cdl 0.113.0 → 0.114.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.113.0",
3
+ "version": "0.114.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",
@@ -1,10 +1,17 @@
1
1
  import type { JSX } from "react";
2
2
  import { 使える幅, 幅に収める, 箱に積む, type 積む行 } from "./box-lines";
3
+ /*
4
+ 名前を描く幅の見積り。 **切る側と同じ物差しを使う** (#966)。
5
+
6
+ `箱に積む` と `幅に収める` はこの `textWidth` で切るかどうかを決める。 置き場所を別の
7
+ 見積り (`layout/text-width` の `measureTextWidth`) で決めていたため、ちょうど収まる位置に
8
+ 置いたつもりの名前が描く側で切られた (実測 = `Features` が `Featur…` になった)。
9
+ */
10
+ import { textWidth as 名前の幅 } from "./text-width";
3
11
  import type { LaidNode, Tone, MindBranchNode, MindBranchPayload } from "../types";
4
12
  import { TONE } from "../render/tone";
5
13
  import { resolveMindData } from "../render/payload-binding";
6
14
  import { 進みを畳む, 伸ばすdash, 段の伸び } from "./draw-progress";
7
- import { measureTextWidth } from "../layout/text-width";
8
15
 
9
16
  const MIND_TONES: Tone[] = ["accent", "teal", "success", "warning", "info", "error"];
10
17
 
@@ -302,11 +309,11 @@ type LayoutEdge = {
302
309
  * 名前が描画域に入らない時、描く側は字を縮めてから切る。 ここで出すのは縮める前の幅 =
303
310
  * 実際に描く幅の上限になる。 縮むのは名前が溢れる図だけで、その時も箱の幅で測るより近い。
304
311
  */
305
- function 描く横の端(n: LayoutNode): [number, number] {
312
+ export function 描く横の端(n: LayoutNode): [number, number] {
306
313
  if (n.isRoot || !n.isLeaf) return [n.x - n.w / 2, n.x + n.w / 2];
307
314
  const 起点 = n.x + (n.side === "right" ? -n.w / 2 : n.w / 2);
308
315
  const 名前 = 放射の行(n)[0]!;
309
- const 幅 = measureTextWidth(名前.text, { fontSize: 名前.fontSize, fontWeight: 600 });
316
+ const 幅 = 名前の幅(名前.text, 名前.fontSize);
310
317
  return n.side === "right" ? [起点, 起点 + 幅] : [起点 - 幅, 起点];
311
318
  }
312
319
 
@@ -401,72 +408,108 @@ export function computeMindMapLayout(
401
408
  return kids.reduce((sum, k) => sum + leafCountOf(k.id, seen), 0);
402
409
  };
403
410
 
404
- const maxSubDepth =
405
- firstLevel.length > 0
406
- ? Math.max(0, ...firstLevel.map((br) => subDepthOf(br.id, new Set())))
407
- : 0;
411
+ // 左右の振り分け。 横の置き場所を側ごとに決めるので、割付より先に分ける (#966)
412
+ const leftBranches = firstLevel.slice(0, leftCount);
413
+ const rightBranches = firstLevel.slice(leftCount);
408
414
 
409
415
  // branch を root からどれだけ離すか。 深いネストがあるほど sub 世代のぶん内側に寄せる。
410
416
  const minLayer1XOffset = rootW / 2 + branchW / 2 + MIND_BOX_GAP;
411
417
  const preferredSubDx = subBranchW + 20;
412
- const reserve = maxSubDepth > 0 ? branchW / 2 + maxSubDepth * preferredSubDx : 0;
413
- const maxLayer1XOffset = Math.max(minLayer1XOffset, canvasW / 2 - reserve);
414
- /*
415
- 第 1 階層の葉の名前のうち一番広いもの。 葉は枠を持たず、名前を枠があった位置の内側の端から
416
- 外へ伸ばす (#907)。 つまり **枝をどこまで外に置けるかは名前の幅が決める**。
417
-
418
- 葉が 1 つも無ければ 0 になり、下の上限は描画域の端そのものになる = 名前では決まらない。
419
- */
420
- const 葉の名前の最大幅 = Math.max(
421
- 0,
422
- ...firstLevel
423
- .filter((br) => (childrenOf.get(br.id) ?? []).length === 0)
424
- .map((br) => {
425
- const 名前 = 放射の行({ title: br.title, isRoot: false })[0]!;
426
- return measureTextWidth(名前.text, { fontSize: 名前.fontSize, fontWeight: 600 });
427
- }),
428
- );
429
- /*
430
- 名前の右端が描画域の端に届く位置。 枝は `layer1XOffset` だけ中心から離れ、名前は
431
- そこから `branchW / 2` 内側を起点に外へ伸びるので、右端は
432
- `offset - branchW / 2 + 名前の幅` になる。 これが `canvasW / 2` に一致する offset が上限。
433
-
434
- ここを決め打ちの割合 (以前は `canvasW * 0.32`) にすると、名前が短い図でも枝が中央寄りのまま
435
- 留まり、外側に誰も使わない余白が残る (#963 の実測で横の使用率 66%)。
436
- */
437
- const 名前で決まる上限 = canvasW / 2 + branchW / 2 - 葉の名前の最大幅;
438
- const layer1XOffset = Math.max(minLayer1XOffset, Math.min(名前で決まる上限, maxLayer1XOffset));
439
-
440
- // branch の外側に残る横の余白。 これを深さで割り、 sub box もそこに収まる幅に縮める。
441
- // 単一 chain は同じ縦レーンを共有するので、 横 step が box より広くないと重なる。
442
- const room = canvasW / 2 - layer1XOffset;
443
418
  /** 箱の中の文字が読める最小の幅。 これを割ると文字だけが縮まず箱からはみ出す。 */
444
419
  const MIN_SUB_W = 56;
445
420
  /** 親と子の箱の間隔として望ましい値。 中心 → 第 1 階層と同じ `MIND_BOX_GAP` を使う。 */
446
421
  const PREFERRED_GAP = MIND_BOX_GAP;
447
- //
448
- // **箱の幅を先に確保し、 余った分を間隔に回す**。 浅い図では望ましい値になり、 深い図では
449
- // 従来どおりまで詰まる = 箱が潰れる形は増やさない。
450
- const gapRoom =
451
- maxSubDepth > 0 ? (room - branchW / 2 - maxSubDepth * MIN_SUB_W) / maxSubDepth : 0;
452
- const gap = Math.max(8, Math.min(PREFERRED_GAP, gapRoom));
453
- // 最深 box の外端が canvas 端に収まる effSubW を解く。
454
- // layer1XOffset + branchW/2 + effSubW*maxSubDepth + maxSubDepth*gap <= canvasW/2
455
- // (step1 = (branchW+effSubW)/2 + gap、 stepN = effSubW + gap、 最深 box 半幅 effSubW/2 込み)
456
- // 下限は極小 (1)。 大きな floor は深いネストで subOffset の累積を膨らませる。 位置の
457
- // はみ出しは最終スケール (下部) が捌くので、 ここでは重なり回避に必要な最小だけ残す。
458
- const effSubW =
459
- maxSubDepth > 0
460
- ? Math.max(1, Math.min(subBranchW, (room - branchW / 2 - maxSubDepth * gap) / maxSubDepth))
461
- : subBranchW;
462
- const step1 = (branchW + effSubW) / 2 + gap; // branch → 第 1 sub
463
- const stepN = effSubW + gap; // sub → sub
464
- const subOffset = (subDepth: number): number =>
465
- subDepth <= 0 ? 0 : step1 + (subDepth - 1) * stepN;
422
+ /** 枝の名前を描く幅。 字の級は `放射の行` から取る (#538、 書き写すと 2 経路で別の値になる) */
423
+ const 枝の名前の幅 = (title: string): number => {
424
+ const 名前 = 放射の行({ title, isRoot: false })[0]!;
425
+ return 名前の幅(名前.text, 名前.fontSize);
426
+ };
427
+
428
+ /** 片側の横の割付。 枝の離れと、その側の sub box の幅と、深さごとの追い出し量を持つ */
429
+ type 横の割付 = {
430
+ layer1XOffset: number;
431
+ effSubW: number;
432
+ subOffset: (subDepth: number) => number;
433
+ };
434
+
435
+ /*
436
+ 横の割付を **枝ごとに** 出す (#966)。
437
+
438
+ それまでは図全体で 1 つの値を使っていた。 入れ子を持つ枝は子のぶんを内側に空ける必要が
439
+ あるので、その都合が入れ子を持たない枝にも効く。 左が入れ子で右が葉 1 枚の図では
440
+ 右の枝も中心から 149 に留まり、右には空ける相手が居ないのにその 171 を誰も使わない
441
+ (実測 = 左が 320 届くのに右は 108 で、札の横を 52% しか使わなかった)。
442
+
443
+ 式そのものは #964 と同じで、材料をその枝の subtree だけから取る。
444
+ 枝は自分の中身が許すところまで外へ出る。
445
+ */
446
+ const 枝の横を決める = (br: MindBranchNode): 横の割付 => {
447
+ const maxSubDepth = subDepthOf(br.id, new Set());
448
+ /*
449
+ どこまで外に出られるかは **枝が末端かどうか** で変わる (#966)。
450
+
451
+ 子を持つ枝は箱が描かれるので、子の世代のぶんを内側に空けて箱ごと描画域に収める。
452
+ 末端の枝は枠を持たず (#907)、名前を枠があった位置の内側の端から外へ伸ばすだけなので、
453
+ 収めるのは名前で、箱ではない。 箱まで収めると、名前より 41 広い箱の都合で
454
+ 名前が描画域の内側で終わる (実測 = 名前 24 の図で端まで 41 余った)。
455
+
456
+ 名前の右端は `offset - branchW / 2 + 名前の幅` に来る。 これが `canvasW / 2` に
457
+ 一致する offset が末端の上限になる。 決め打ちの割合 (以前は `canvasW * 0.32`) だと
458
+ 名前の長さと無関係に中央寄りへ留まった (#964 の実測で横の使用率 66%)。
459
+ */
460
+ const 上限 =
461
+ maxSubDepth > 0
462
+ ? canvasW / 2 - (branchW / 2 + maxSubDepth * preferredSubDx)
463
+ : canvasW / 2 + branchW / 2 - 枝の名前の幅(br.title);
464
+ /*
465
+ **整数に落とす** (#966)。 枝の長さは座標の差から出るので、端数を持ったまま置くと
466
+ 丸め誤差が乗る (実測 = 24 であるべき枝が 23.99999999999997 になり `render-polish-240`
467
+ が落ちた)。 下に倒すのは、上限を 1px でも超えると描く側が名前を 1 級縮めるため。
468
+ すぐ下の寄せが整数に丸めているのと同じ理由になる。
469
+ */
470
+ const layer1XOffset = Math.floor(Math.max(minLayer1XOffset, 上限));
471
+
472
+ // branch の外側に残る横の余白。 これを深さで割り、 sub box もそこに収まる幅に縮める。
473
+ // 単一 chain は同じ縦レーンを共有するので、 横 step が box より広くないと重なる。
474
+ const room = canvasW / 2 - layer1XOffset;
475
+ //
476
+ // **箱の幅を先に確保し、 余った分を間隔に回す**。 浅い図では望ましい値になり、 深い図では
477
+ // 従来どおりまで詰まる = 箱が潰れる形は増やさない。
478
+ const gapRoom =
479
+ maxSubDepth > 0 ? (room - branchW / 2 - maxSubDepth * MIN_SUB_W) / maxSubDepth : 0;
480
+ const gap = Math.floor(Math.max(8, Math.min(PREFERRED_GAP, gapRoom)));
481
+ // 最深 box の外端が canvas 端に収まる effSubW を解く。
482
+ // layer1XOffset + branchW/2 + effSubW*maxSubDepth + maxSubDepth*gap <= canvasW/2
483
+ // (step1 = (branchW+effSubW)/2 + gap、 stepN = effSubW + gap、 最深 box 半幅 effSubW/2 込み)
484
+ // 下限は極小 (1)。 大きな floor は深いネストで subOffset の累積を膨らませる。 位置の
485
+ // はみ出しは最終スケール (下部) が捌くので、 ここでは重なり回避に必要な最小だけ残す。
486
+ const effSubW =
487
+ maxSubDepth > 0
488
+ ? Math.floor(
489
+ Math.max(1, Math.min(subBranchW, (room - branchW / 2 - maxSubDepth * gap) / maxSubDepth)),
490
+ )
491
+ : subBranchW;
492
+ const step1 = (branchW + effSubW) / 2 + gap; // branch → 第 1 sub
493
+ const stepN = effSubW + gap; // sub → sub
494
+ return {
495
+ layer1XOffset,
496
+ effSubW,
497
+ subOffset: (subDepth: number): number =>
498
+ subDepth <= 0 ? 0 : step1 + (subDepth - 1) * stepN,
499
+ };
500
+ };
501
+ /** 枝ごとの横の割付。 同じ枝を 2 度計算しない */
502
+ const 横の割付表 = new Map<string, 横の割付>(
503
+ firstLevel.map((br) => [br.id, 枝の横を決める(br)]),
504
+ );
505
+ /** 一番内側に置かれた枝の離れ。 帯の間隔の上限に使う */
506
+ const 一番内側の離れ = Math.min(
507
+ ...(firstLevel.length > 0
508
+ ? firstLevel.map((br) => 横の割付表.get(br.id)!.layer1XOffset)
509
+ : [minLayer1XOffset]),
510
+ );
466
511
 
467
512
  // 縦レーン。 左右それぞれの branch 群の葉数合計でレーンを割る。
468
- const leftBranches = firstLevel.slice(0, leftCount);
469
- const rightBranches = firstLevel.slice(leftCount);
470
513
  const leavesOfSide = (brs: MindBranchNode[]): number =>
471
514
  Math.max(
472
515
  1,
@@ -483,7 +526,12 @@ export function computeMindMapLayout(
483
526
  const laneBandH = Math.max(0, canvasH - BRANCH_BOX_H_MAX);
484
527
  const rowGap =
485
528
  sideLaneCount > 1
486
- ? Math.min(laneBandH / (sideLaneCount - 1), 縦の間隔の上限(layer1XOffset))
529
+ ? Math.min(
530
+ laneBandH / (sideLaneCount - 1),
531
+ // 帯は図で 1 つなので、**一番内側の枝** から上限を取る (#966)。
532
+ // 外へ出た枝に合わせると、内側に残った枝の傾きが 45 度を超える
533
+ 縦の間隔の上限(一番内側の離れ),
534
+ )
487
535
  : subBranchH + 14;
488
536
  // box 高は必ず rowGap の一定割合に収める。 加算的な floor を置くと、 leaf が多く rowGap が
489
537
  // 小さいとき floor が rowGap を超えて縦重なりする。 rowGap の 70% にすれば規模によらず gap が残る。
@@ -525,11 +573,12 @@ export function computeMindMapLayout(
525
573
  tone: Tone | undefined,
526
574
  placed: Set<string>,
527
575
  laneRef: { cursor: number },
576
+ 横: 横の割付,
528
577
  ): number => {
529
578
  placed.add(node.id);
530
579
  const dirX = isLeft ? -1 : 1;
531
- const x = cx + dirX * (layer1XOffset + subOffset(subDepth));
532
- const w = subDepth === 0 ? branchW : effSubW;
580
+ const x = cx + dirX * (横.layer1XOffset + 横.subOffset(subDepth));
581
+ const w = subDepth === 0 ? branchW : 横.effSubW;
533
582
  const h = subDepth === 0 ? effBranchH : effSubH;
534
583
  const myTone = node.tone ?? tone;
535
584
 
@@ -547,6 +596,7 @@ export function computeMindMapLayout(
547
596
  myTone ?? MIND_TONES[i % MIND_TONES.length]!,
548
597
  placed,
549
598
  laneRef,
599
+ 横,
550
600
  ),
551
601
  );
552
602
  y = (ys[0]! + ys[ys.length - 1]!) / 2;
@@ -583,7 +633,7 @@ export function computeMindMapLayout(
583
633
  const placed = new Set<string>();
584
634
  brs.forEach((br, i) => {
585
635
  const tone = br.tone ?? MIND_TONES[(toneBase + i) % MIND_TONES.length]!;
586
- placeSubtree(br, 0, isLeft, tone, placed, laneRef);
636
+ placeSubtree(br, 0, isLeft, tone, placed, laneRef, 横の割付表.get(br.id) ?? 枝の横を決める(br));
587
637
  });
588
638
  };
589
639
  placeSide(leftBranches, true, 0);