@cardenelabs/cdl 0.20.1 → 0.21.1

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.20.1",
3
+ "version": "0.21.1",
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",
@@ -9,6 +9,15 @@ import { measureTextWidth } from "./text-width";
9
9
  */
10
10
  const LANE_LABEL_CLEARANCE_MARGIN = 2;
11
11
 
12
+ /**
13
+ * 近接を解消する時に、要求ちょうどではなくこの分だけ余分に押し出す (world unit)。
14
+ *
15
+ * 判定は `gap < required` の strict 比較だが、要求ちょうどに置くと px へ投影する丸めで
16
+ * 割れる (実測 = 押し出した先が `gap=32.0px (need 32px)` で error)。
17
+ * `LANE_LABEL_CLEARANCE_MARGIN` と同じ理由で、同じ幅を取る。
18
+ */
19
+ const NEAR_MISS_MARGIN = LANE_LABEL_CLEARANCE_MARGIN;
20
+
12
21
  /**
13
22
  * 全要素の AABB を集約。 engine が「キャンバスに何があるか」 を全部把握する SSOT。
14
23
  */
@@ -1570,8 +1579,8 @@ export function resolveEdgeLabelOverlapsWithChainAndPropagate(
1570
1579
  const shift = computeOverlapShift(label, inflated);
1571
1580
  if (!shift) continue;
1572
1581
  // computeOverlapShift は minClearance を上乗せするが、 膨張分で確保済なので差し引く。
1573
- const amount = shift.amount - minClearance;
1574
- if (amount <= 0) continue;
1582
+ const amount = shift.amount - minClearance + NEAR_MISS_MARGIN;
1583
+ if (amount <= NEAR_MISS_MARGIN) continue;
1575
1584
  out.push({
1576
1585
  target: { kind: "label", id: label.id },
1577
1586
  axis: shift.axis,
@@ -1634,6 +1643,51 @@ export function resolveEdgeLabelOverlapsWithChainAndPropagate(
1634
1643
  }
1635
1644
  }
1636
1645
 
1646
+ /*
1647
+ * (2b) label ↔ label の **近接** (重なってはいないが要求 clearance 未満)。
1648
+ *
1649
+ * (2) は重なった時しか動かさない (`computeOverlapShift` が重なり量 0 で null を返す)。
1650
+ * 一方 `clearance` 軸は 36 を要求するので、33 しか空いていない組が残っていた
1651
+ * (実測で見本 7 図が同じ形)。 node と lane-label には (1a) / (1b) で近接の面倒を見る
1652
+ * rule があり、label どうしだけが抜けていた。
1653
+ *
1654
+ * 静止側を要求ぶん膨らませて (1a) と同じ形で押し出す。 押す側は (2) と同じ順序規則
1655
+ * (後に declare した方) にする = 同じ組で (2) と (2b) が逆向きに押し合うのを避ける。
1656
+ *
1657
+ * chain: false = 微調整なので node / path の連鎖は起こさない。
1658
+ */
1659
+ for (let i = 0; i < labels.length; i++) {
1660
+ for (let j = i + 1; j < labels.length; j++) {
1661
+ const a = labels[i]!;
1662
+ const b = labels[j]!;
1663
+ if (computeOverlapShift(a, b)) continue;
1664
+ const orderA = edgeOrder.get(a.id)!;
1665
+ const orderB = edgeOrder.get(b.id)!;
1666
+ const moveBox = orderA > orderB ? a : b;
1667
+ const staticBox = orderA > orderB ? b : a;
1668
+ const required = requiredNearClearance(moveBox.kind, staticBox.kind);
1669
+ const inflated: BBox = {
1670
+ ...staticBox,
1671
+ x: staticBox.x - required,
1672
+ y: staticBox.y - required,
1673
+ w: staticBox.w + required * 2,
1674
+ h: staticBox.h + required * 2,
1675
+ };
1676
+ const shift = computeOverlapShift(moveBox, inflated);
1677
+ if (!shift) continue;
1678
+ // 膨張分で確保済なので、`computeOverlapShift` が上乗せした分を差し引く
1679
+ const amount = shift.amount - minClearance + NEAR_MISS_MARGIN;
1680
+ if (amount <= NEAR_MISS_MARGIN) continue;
1681
+ out.push({
1682
+ target: { kind: "label", id: moveBox.id },
1683
+ axis: shift.axis,
1684
+ sign: shift.sign,
1685
+ amount,
1686
+ chain: false,
1687
+ });
1688
+ }
1689
+ }
1690
+
1637
1691
  // (3) CAR-482 chain 伝搬 = label が shift 方向にある obstacle と衝突するなら、
1638
1692
  // obstacle も同方向同量 shift 連鎖。 label ↔ obstacle が既に (1) で処理されているため、
1639
1693
  // ここでは「shift 済 label の shift 累積方向にある obstacle」 を検出、 obstacle 側 shift 発火。
@@ -1811,12 +1865,118 @@ export function resolveEdgeLabelOverlapsWithChainAndPropagate(
1811
1865
  const ordered = edgeProbes
1812
1866
  .map((p, idx) => ({ p, idx }))
1813
1867
  .sort((a, b) => probeDist[a.idx]! - probeDist[b.idx]!);
1868
+ let 線を動かせた = false;
1814
1869
  for (const { p } of ordered) {
1815
1870
  const axis = p.axis as "x" | "y";
1816
1871
  if (!pathShiftIsSafe(e.id, axis, p.sign, amount)) continue;
1872
+ /*
1873
+ * **動かした先で離れるかを見る**。 「壊さない」 だけを見て動かすと、離れない向きへ
1874
+ * 動かして終わることがある (実測 = 説明が別の線の縦区間の真下に居る形で、繰り返しを
1875
+ * 12 回まで回しても隙間は 0 のまま)。 その時 `線を動かせた` が立つので、下の逃がしにも
1876
+ * 回らない。
1877
+ *
1878
+ * **押し出しは今までどおり出す**。 出さない形にすると、離れないなりに効いていた
1879
+ * 押し出しが消えて別の図が壊れる (実測で 3 図が `overlap=-1`)。 離れなかった時に
1880
+ * 「線で逃がせた」 と数えないことだけを変える = 下の逃がしにも回るようになる。
1881
+ */
1882
+ const 動かした後 = measurePathLabelGapSegs(
1883
+ simulateShiftedSegments(e, axis === "x" ? p.sign * amount : 0, axis === "y" ? p.sign * amount : 0),
1884
+ label,
1885
+ );
1817
1886
  out.push({ target: { kind: "edge-path", id: e.id }, axis, sign: p.sign, amount });
1887
+ 線を動かせた = 動かした後 > gap;
1818
1888
  break;
1819
1889
  }
1890
+ /*
1891
+ * 線をどの向きにも動かせない時は **説明の側を離す**。
1892
+ *
1893
+ * 線を動かさないのは「説明の近接より経路の破損の方が重い」 ためだが、動かせないまま
1894
+ * 何もしないと近接がそのまま残る (実測で `gap=9.0px (need 14px)`)。 説明は動かしても
1895
+ * 経路を壊さないので、こちらを逃がす。
1896
+ *
1897
+ * 向きは、線から見て説明が居る側 (最も近い segment の中点から説明の中心へ向かう向き) の
1898
+ * 縦横どちらか、離れている方の軸を選ぶ。
1899
+ */
1900
+ if (!線を動かせた) {
1901
+ const 芯 = { x: label.x + label.w / 2, y: label.y + label.h / 2 };
1902
+ let 近い点: { x: number; y: number } | null = null;
1903
+ let 最短 = Infinity;
1904
+ for (const seg of segs) {
1905
+ for (const 点 of [
1906
+ { x: seg.x1, y: seg.y1 },
1907
+ { x: seg.x2, y: seg.y2 },
1908
+ { x: (seg.x1 + seg.x2) / 2, y: (seg.y1 + seg.y2) / 2 },
1909
+ ]) {
1910
+ const d = Math.hypot(点.x - 芯.x, 点.y - 芯.y);
1911
+ if (d < 最短) {
1912
+ 最短 = d;
1913
+ 近い点 = 点;
1914
+ }
1915
+ }
1916
+ }
1917
+ if (近い点) {
1918
+ const dx = 芯.x - 近い点.x;
1919
+ const dy = 芯.y - 近い点.y;
1920
+ const 量 = pathLabelClearance - gap + NEAR_MISS_MARGIN;
1921
+ /*
1922
+ * **行き先を確かめてから動かす**。 近い点から離れる向きへ出すだけだと、その先に
1923
+ * 別の線が居ることがある (実測で 3 図が `overlap=-1` になった)。
1924
+ *
1925
+ * 縦横それぞれの向きを、離れている方から順に試す。 動かした先で **どの線とも
1926
+ * いまより近づかない** 向きだけを採る。
1927
+ */
1928
+ const 候補: Array<{ axis: "x" | "y"; sign: 1 | -1 }> =
1929
+ Math.abs(dx) >= Math.abs(dy)
1930
+ ? [
1931
+ { axis: "x", sign: dx >= 0 ? 1 : -1 },
1932
+ { axis: "y", sign: dy >= 0 ? 1 : -1 },
1933
+ ]
1934
+ : [
1935
+ { axis: "y", sign: dy >= 0 ? 1 : -1 },
1936
+ { axis: "x", sign: dx >= 0 ? 1 : -1 },
1937
+ ];
1938
+ const 動かした箱 = (axis: "x" | "y", sign: 1 | -1): BBox => ({
1939
+ ...label,
1940
+ x: label.x + (axis === "x" ? sign * 量 : 0),
1941
+ y: label.y + (axis === "y" ? sign * 量 : 0),
1942
+ });
1943
+ /*
1944
+ * **線ごとに見る**。 全体の最小距離だけを比べると、いま 0 の線が離れるぶん、
1945
+ * 別の線が 0 になっても「悪くなっていない」 と読める (実測でその形になった)。
1946
+ */
1947
+ const 線ごとの距離 = (箱: BBox): Map<string, number> => {
1948
+ const 出 = new Map<string, number>();
1949
+ for (const 他 of edges) {
1950
+ if (他.id === label.id) continue;
1951
+ const segs2 = segmentsExcludingOwnPath(shiftedSegments(他.id, 他.d), ownSegsOf(label.id));
1952
+ if (segs2.length === 0) continue;
1953
+ 出.set(他.id, measurePathLabelGapSegs(segs2, 箱));
1954
+ }
1955
+ return 出;
1956
+ };
1957
+ const いまの距離 = 線ごとの距離(label);
1958
+ const 悪くする = (箱: BBox): boolean => {
1959
+ const 後 = 線ごとの距離(箱);
1960
+ for (const [id, 前の値] of いまの距離) {
1961
+ const 後の値 = 後.get(id) ?? Infinity;
1962
+ // 前より縮めない。 前が要求を満たしていたなら、その要求も割らない
1963
+ if (後の値 < Math.min(前の値, pathLabelClearance)) return true;
1964
+ }
1965
+ return false;
1966
+ };
1967
+ for (const 候 of 候補) {
1968
+ if (悪くする(動かした箱(候.axis, 候.sign))) continue;
1969
+ out.push({
1970
+ target: { kind: "label", id: label.id },
1971
+ axis: 候.axis,
1972
+ sign: 候.sign,
1973
+ amount: 量,
1974
+ chain: false,
1975
+ });
1976
+ break;
1977
+ }
1978
+ }
1979
+ }
1820
1980
  }
1821
1981
  }
1822
1982