@cardenelabs/cdl 0.47.0 → 0.48.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.47.0",
3
+ "version": "0.48.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",
@@ -7,6 +7,7 @@ import {
7
7
  import {
8
8
  extractPathSegments,
9
9
  flattenPathSegments,
10
+ measurePathLabelGapSegs,
10
11
  segmentsCrossRect,
11
12
  segmentsExcludingOwnPath,
12
13
  } from "./collisions";
@@ -2530,37 +2531,108 @@ function placeAvoidingSiblingPaths(args: {
2530
2531
 
2531
2532
  // 動かさない置き方も候補に入れる (跨りが無いならこれが最善)。
2532
2533
  consider([...wants]);
2534
+
2535
+ /**
2536
+ * 塊を「近すぎる線」 の向こうへ押し出すのに要る量 (#805)。 足りていれば 0。
2537
+ *
2538
+ * 兄弟の弧の帯 (`bands`) を越えるだけでは、 **帯の外を通る別の線** が残る。
2539
+ * 実測 = 4 本の弧の下を回る線が、 帯を越えた直後に置いた札の上端を 10 かすめた
2540
+ * (要求は `CLEARANCE_PATH_LABEL` = 14)。 貫いてはいないので `consider` の跨り判定では
2541
+ * 捨てられず、 その後の逃がし pass も列の上下が隣の札で塞がれていて動かせない。
2542
+ *
2543
+ * 押す量は探さずに測る = 邪魔をしている線を札の横幅で切った Y 範囲を取り、 その縁から
2544
+ * 要求ぶん離れる位置までの差を返す。 塊は間隔を保ったまま動かしたいので、 塊の中で
2545
+ * 最も要る量に合わせる。
2546
+ */
2547
+ const 押す量 = (idx: number[], side: "above" | "below", pos: number[]): number => {
2548
+ let 要る = 0;
2549
+ for (const i of idx) {
2550
+ const box = boxes[i]!;
2551
+ const rect = { x: box.x, y: pos[i]! - box.h / 2, w: box.w, h: box.h };
2552
+ for (const probe of probeSegs[i]!) {
2553
+ if (measurePathLabelGapSegs(probe.segs, rect) >= CLEARANCE_PATH_LABEL) continue;
2554
+ let lo = Infinity;
2555
+ let hi = -Infinity;
2556
+ for (const seg of probe.segs) {
2557
+ const c = clipY(seg, box.x, box.x + box.w);
2558
+ if (!c) continue;
2559
+ lo = Math.min(lo, c.lo);
2560
+ hi = Math.max(hi, c.hi);
2561
+ }
2562
+ // 札の横幅の下に 1 本も入らない線は、 押しても隙間が変わらない。
2563
+ if (lo > hi) continue;
2564
+ const 行き先 =
2565
+ side === "above"
2566
+ ? lo - box.h / 2 - CLEARANCE_PATH_LABEL
2567
+ : hi + box.h / 2 + CLEARANCE_PATH_LABEL;
2568
+ 要る = Math.max(要る, side === "above" ? pos[i]! - 行き先 : 行き先 - pos[i]!);
2569
+ }
2570
+ }
2571
+ return Math.max(0, 要る);
2572
+ };
2573
+
2533
2574
  if (bands.some((b) => b !== null)) {
2534
2575
  for (let split = 0; split <= order.length; split++) {
2535
2576
  const above = order.slice(0, split);
2536
2577
  const below = order.slice(split);
2537
- const out = new Array<number>(members.length);
2538
- for (const [idx, side] of [
2539
- [above, "above"],
2540
- [below, "below"],
2541
- ] as const) {
2542
- if (idx.length === 0) continue;
2543
- const ys = spread(idx);
2544
- // 帯に入らない位置まで塊ごと動かす。 必要な移動量が最大の label に合わせる。
2545
- let shift = 0;
2546
- for (let k = 0; k < idx.length; k++) {
2547
- const band = bands[idx[k]!];
2548
- if (!band) continue;
2549
- shift =
2550
- side === "above" ? Math.min(shift, band.lo - ys[k]!) : Math.max(shift, band.hi - ys[k]!);
2551
- }
2552
- for (let k = 0; k < idx.length; k++) out[idx[k]!] = ys[k]! + shift;
2553
- }
2554
- // 上側の末尾と下側の先頭が足りなければ、 下側を塊ごとさらに下げる。 帯を挟んでいても
2555
- // pill どうしの間隔は要る (実測 = 帯を挟んで 100 しか空かず、 要求の 104 に届かない図)。
2556
- if (above.length > 0 && below.length > 0) {
2578
+ /** 上側の末尾と下側の先頭が足りなければ、 下側を塊ごとさらに下げる。 */
2579
+ const 間を空ける = (out: number[]): void => {
2580
+ // 帯を挟んでいても pill どうしの間隔は要る
2581
+ // (実測 = 帯を挟んで 100 しか空かず、 要求の 104 に届かない図)。
2582
+ if (above.length === 0 || below.length === 0) return;
2557
2583
  const lastIdx = above[above.length - 1]!;
2558
2584
  const firstIdx = below[0]!;
2559
2585
  const need = (sizes[lastIdx]! + sizes[firstIdx]!) / 2 + CLEARANCE_LABEL_LABEL;
2560
2586
  const deficit = need - (out[firstIdx]! - out[lastIdx]!);
2561
- if (deficit > 0) for (const i of below) out[i]! += deficit;
2562
- }
2563
- consider(out);
2587
+ if (deficit > 0) for (const i of below) out[i] = out[i]! + deficit;
2588
+ };
2589
+ /** 兄弟の弧の帯を越える所まで塊ごと動かした置き方。 */
2590
+ const 帯を越える = (): number[] => {
2591
+ const out = new Array<number>(members.length);
2592
+ for (const [idx, side] of [
2593
+ [above, "above"],
2594
+ [below, "below"],
2595
+ ] as const) {
2596
+ if (idx.length === 0) continue;
2597
+ const ys = spread(idx);
2598
+ // 帯に入らない位置まで塊ごと動かす。 必要な移動量が最大の label に合わせる。
2599
+ let shift = 0;
2600
+ for (let k = 0; k < idx.length; k++) {
2601
+ const band = bands[idx[k]!];
2602
+ if (!band) continue;
2603
+ shift =
2604
+ side === "above" ? Math.min(shift, band.lo - ys[k]!) : Math.max(shift, band.hi - ys[k]!);
2605
+ }
2606
+ for (let k = 0; k < idx.length; k++) out[idx[k]!] = ys[k]! + shift;
2607
+ }
2608
+ 間を空ける(out);
2609
+ return out;
2610
+ };
2611
+ /**
2612
+ * 帯を越えた置き方を、 さらに「近すぎる線」 の向こうへ押し出した置き方。
2613
+ *
2614
+ * 押した先に別の線が居ることがあるので繰り返す。 **越えられない形では候補にしない**
2615
+ * (`null` を返す) = 線が札の上下を挟む図では、 押しても隙間が足りないまま位置だけが動く。
2616
+ * 跨り判定はその置き方を捨てられないので、 候補にする前にここで落とす。
2617
+ *
2618
+ * 押しは上側を上へ、 下側を下へしか動かさない = 上下の間隔は広がる一方なので、
2619
+ * ここで `間を空ける` を呼び直しても 1 度も効かない (変異試験で確認した)。
2620
+ */
2621
+ const 押して越える = (): number[] | null => {
2622
+ const out = 帯を越える();
2623
+ for (let round = 0; round < 4; round += 1) {
2624
+ const a = 押す量(above, "above", out);
2625
+ const b = 押す量(below, "below", out);
2626
+ if (a === 0 && b === 0) return out;
2627
+ for (const i of above) out[i] = out[i]! - a;
2628
+ for (const i of below) out[i] = out[i]! + b;
2629
+ }
2630
+ return 押す量(above, "above", out) === 0 && 押す量(below, "below", out) === 0 ? out : null;
2631
+ };
2632
+ // 押す前と押した後の両方を候補にする = 押すと悪くなる図では押さない置き方が選ばれる。
2633
+ consider(帯を越える());
2634
+ const 押した = 押して越える();
2635
+ if (押した) consider(押した);
2564
2636
  }
2565
2637
  }
2566
2638
  // 間隔だけを満たす置き方も候補に入れる (帯が無い / 帯を避ける置き方が全て捨てられた時)。