@cardenelabs/cdl 0.64.1 → 0.66.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.64.1",
3
+ "version": "0.66.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",
@@ -57,6 +57,16 @@
57
57
  "node": ">=18"
58
58
  },
59
59
  "sideEffects": false,
60
+ "scripts": {
61
+ "build": "tsup",
62
+ "dev": "tsup --watch",
63
+ "typecheck": "tsc --noEmit",
64
+ "test": "cd ../.. && vitest run packages/cdl",
65
+ "lint": "tsc --noEmit",
66
+ "prepublishOnly": "pnpm run typecheck && pnpm run build",
67
+ "prepack": "node scripts/prepack-check.mjs",
68
+ "postpack": "node scripts/prepack-check.mjs"
69
+ },
60
70
  "peerDependencies": {
61
71
  "react": "^19.0.0",
62
72
  "react-dom": "^19.0.0"
@@ -68,12 +78,5 @@
68
78
  "typedoc": "^0.28.19",
69
79
  "typedoc-plugin-markdown": "^4.12.0",
70
80
  "typescript": "^5.8.0"
71
- },
72
- "scripts": {
73
- "build": "tsup",
74
- "dev": "tsup --watch",
75
- "typecheck": "tsc --noEmit",
76
- "test": "cd ../.. && vitest run packages/cdl",
77
- "lint": "tsc --noEmit"
78
81
  }
79
- }
82
+ }
@@ -48,6 +48,44 @@ const VERTICAL_DETOUR_CORNER = 14;
48
48
  export function verticalCornerDir(from: number, to: number): 1 | -1 {
49
49
  return to < from ? -1 : 1;
50
50
  }
51
+
52
+ /**
53
+ * 角の丸みを、その角が使える長さで頭打ちにする (`#865`)。
54
+ *
55
+ * 丸めた角は「手前で半径だけ直線を止め、弧で半径だけ先へ抜ける」 形で作る。 走りがその分を
56
+ * 持っていないと、止めた点が走りの向こう側へ回り込み、直線が逆走して角の外に突起が出る。
57
+ *
58
+ * `走り` には **その走りを 1 つの角が使える長さ** を渡す。 両端の 2 角で使う走り (L 字の縦)
59
+ * は半分を渡し、片端の 1 角しか使わない走り (L 字の横) はそのまま渡す。
60
+ *
61
+ * 0 を返す時は丸める余地が無い。 呼び出し側は弧の切り口が角そのものに重なった形を出すので、
62
+ * 見た目は折れ線のまま繋がる。 `verticesToPath` が同じ規則を別の経路で持っている。
63
+ *
64
+ * 走りの向きは見ない = 経路は右にも左にも上にも下にも走るため、呼び出し側は符号付きの差を
65
+ * そのまま渡せる。 返り値が負にならないのは、半径を負で呼ばない限り長さが下限になるため。
66
+ */
67
+ export function cornerRadiusFor(半径: number, ...走り: readonly number[]): number {
68
+ return Math.min(半径, ...走り.map((v) => Math.abs(v)));
69
+ }
70
+
71
+ /**
72
+ * 横に走る線が箱の中を通るか (`#867`)。
73
+ *
74
+ * 判定は箱そのもので行い、余白を足さない = 絵の検査 (`edge-node-cross`) が箱の矩形で
75
+ * 貫通を数えるため、余白を足すと「検査は通るのに経路だけ動く」 図が出る。
76
+ *
77
+ * 端がちょうど箱の縁に載る形は通さない (不等号は全て開いた側)。 箱の縁から出る線は必ず
78
+ * 縁に載るので、閉じた側にすると自分の出入口を障害物と読む。
79
+ */
80
+ export function runCrossesBox(
81
+ 走り: { readonly y: number; readonly x1: number; readonly x2: number },
82
+ 箱: { readonly cx: number; readonly cy: number; readonly w: number; readonly h: number },
83
+ ): boolean {
84
+ if (走り.y <= 箱.cy - 箱.h / 2 || 走り.y >= 箱.cy + 箱.h / 2) return false;
85
+ return (
86
+ 箱.cx + 箱.w / 2 > Math.min(走り.x1, 走り.x2) && 箱.cx - 箱.w / 2 < Math.max(走り.x1, 走り.x2)
87
+ );
88
+ }
51
89
  /** 縦 detour が source から曲がるまで直進する距離 (world unit)。 EDGE_STUB_OUT を上回る。 */
52
90
  const VERTICAL_DETOUR_EXIT = 45;
53
91
 
@@ -1912,11 +1950,35 @@ function routePath(
1912
1950
  const labelIntersect = oTop < labelBottom && oBottom > labelTop;
1913
1951
  return segIntersect || labelIntersect;
1914
1952
  };
1953
+ // 折れる位置を決めると、横の走りは A.x → midX と midX → B.x の 2 本になる。 この 2 本が
1954
+ // 箱の中を通るかを見る (`#867`)。
1955
+ //
1956
+ // **退いた側で横の走りが伸びる**。 縦の走りだけを見て箱の向こう側へ退くと、避けたはずの
1957
+ // 同じ箱を横の走りが横切る = 実測で「右へ 228 退く」 と「左へ 228 退く」 が並び、同点を
1958
+ // 右に倒す規則が塞がる側を選んでいた (dragon#2011)。
1959
+ const 横の走りが塞がる = (mx: number): boolean =>
1960
+ labelObstacles.some((o) => {
1961
+ if (o.id === from.id || o.id === to.id) return false;
1962
+ return (
1963
+ runCrossesBox({ y: A.y, x1: A.x, x2: mx }, o) ||
1964
+ runCrossesBox({ y: B.y, x1: mx, x2: B.x }, o)
1965
+ );
1966
+ });
1915
1967
  // xDir 方向に obstacle を避けて midX を retreat する。
1916
1968
  let iter = 0;
1917
1969
  while (iter < 12) {
1918
1970
  const band = labelBand(midX);
1919
- const hit = labelObstacles.find((o) => intersects(o, band));
1971
+ // 縦の走りと札の帯に当たる箱を先に探し、当たらなければ横の走りが通る箱を探す。
1972
+ // 後者を見ないと、初期の midX で既に横の走りが箱を横切っている形は 1 度も退かない。
1973
+ const hit =
1974
+ labelObstacles.find((o) => intersects(o, band)) ??
1975
+ labelObstacles.find(
1976
+ (o) =>
1977
+ o.id !== from.id &&
1978
+ o.id !== to.id &&
1979
+ (runCrossesBox({ y: A.y, x1: A.x, x2: midX }, o) ||
1980
+ runCrossesBox({ y: B.y, x1: midX, x2: B.x }, o)),
1981
+ );
1920
1982
  if (!hit) break;
1921
1983
  // hit を xDir 方向に回避、 obstacle right/left から labelHalfW + clearance を確保
1922
1984
  // xDir=1 (右向き) なら hit.right + labelHalfW + clearance より右へ、
@@ -1926,13 +1988,34 @@ function routePath(
1926
1988
  const hitRight = hit.cx + hit.w / 2;
1927
1989
  const retreatRight = hitRight + labelHalfW + CLEARANCE_NODE_LABEL + 4;
1928
1990
  const retreatLeft = hitLeft - labelHalfW - CLEARANCE_NODE_LABEL - 4;
1929
- // 現 midX と近い方の retreat 側を選ぶ。 midX を大きく動かさない方向優先。
1991
+ // 退く向きは「横の走りが箱を横切らない側」 を先に見る (`#867`)。 片方だけが塞がるなら
1992
+ // 塞がらない側へ退く。 どちらも同じ (両方塞がる / 両方通る) 時だけ、今までどおり
1993
+ // midX を大きく動かさない側を選ぶ。
1994
+ const 右が塞がる = 横の走りが塞がる(retreatRight);
1995
+ const 左が塞がる = 横の走りが塞がる(retreatLeft);
1930
1996
  const dRight = Math.abs(retreatRight - midX);
1931
1997
  const dLeft = Math.abs(retreatLeft - midX);
1932
- midX = dRight <= dLeft ? retreatRight : retreatLeft;
1998
+ const 次 =
1999
+ 右が塞がる === 左が塞がる
2000
+ ? dRight <= dLeft
2001
+ ? retreatRight
2002
+ : retreatLeft
2003
+ : 右が塞がる
2004
+ ? retreatLeft
2005
+ : retreatRight;
2006
+ // 同じ位置へ退こうとしたら止める。 進まない繰り返しは上限まで回すだけで形が変わらない。
2007
+ if (次 === midX) break;
2008
+ midX = 次;
1933
2009
  iter++;
1934
2010
  }
1935
2011
  }
2012
+ // 角の丸みは `midX` が決まってから頭打ちにする (`#865`)。 上の繰り返しが札を避けて `midX` を
2013
+ // 中央から動かすため、中央のつもりで先に計算すると、動いた後の横の走りを測り損ねる。
2014
+ //
2015
+ // 縦の走りは両端の 2 角で使うので半分を渡し、横の走りは片端の 1 角しか使わないのでそのまま
2016
+ // 渡す。 頭打ちにしないと、中心の高さの差が 28 (丸み 14 の 2 倍) より近い対で縦の走りが
2017
+ // 逆走する = 実測で 506 → 516 の対が `500 520` から `500 502` へ 18 戻る形を出していた。
2018
+ const lCorner = cornerRadiusFor(corner, (B.y - A.y) / 2, midX - A.x, B.x - midX);
1936
2019
  // label 初期位置 = L 字 corner の垂直 segment 中点、 A.y と B.y の中間 高さ (routing v6)。
1937
2020
  // 旧実装は labelX=(A.x+midX)/2, labelY=A.y の水平 segment 上に置いていたが、 label 幅の広い
1938
2021
  // edge (幅 230px 等) では A 側 node bbox に食い込んで overlap 発生 (rollback pattern e3 の
@@ -1990,10 +2073,10 @@ function routePath(
1990
2073
  // enter 水平 (539.11→400) 139 world、 最長 = mid 垂直、 中点 = (553.11, 410)、
1991
2074
  // pill (label 幅 102 × 高さ 36) 中心 = (553.11, 410) で垂直 segment 上 straddle。
1992
2075
  if (edge?.overlay) {
1993
- const exitEndX = midX - xDir * corner;
1994
- const enterStartX = midX + xDir * corner;
1995
- const midStartY = A.y + yDir * corner;
1996
- const midEndY = B.y - yDir * corner;
2076
+ const exitEndX = midX - xDir * lCorner;
2077
+ const enterStartX = midX + xDir * lCorner;
2078
+ const midStartY = A.y + yDir * lCorner;
2079
+ const midEndY = B.y - yDir * lCorner;
1997
2080
  const segments = [
1998
2081
  { len: Math.abs(exitEndX - A.x), midXVal: (A.x + exitEndX) / 2, midYVal: A.y },
1999
2082
  { len: Math.abs(midEndY - midStartY), midXVal: midX, midYVal: (midStartY + midEndY) / 2 },
@@ -2002,10 +2085,10 @@ function routePath(
2002
2085
  const longest = segments.reduce((a, b) => (b.len > a.len ? b : a));
2003
2086
  return {
2004
2087
  d: `M ${A.x} ${A.y}
2005
- L ${midX - xDir * corner} ${A.y}
2006
- Q ${midX} ${A.y}, ${midX} ${A.y + yDir * corner}
2007
- L ${midX} ${B.y - yDir * corner}
2008
- Q ${midX} ${B.y}, ${midX + xDir * corner} ${B.y}
2088
+ L ${midX - xDir * lCorner} ${A.y}
2089
+ Q ${midX} ${A.y}, ${midX} ${A.y + yDir * lCorner}
2090
+ L ${midX} ${B.y - yDir * lCorner}
2091
+ Q ${midX} ${B.y}, ${midX + xDir * lCorner} ${B.y}
2009
2092
  L ${B.x} ${B.y}`,
2010
2093
  labelX: longest.midXVal,
2011
2094
  labelY: longest.midYVal,
@@ -2014,10 +2097,10 @@ function routePath(
2014
2097
  }
2015
2098
  return {
2016
2099
  d: `M ${A.x} ${A.y}
2017
- L ${midX - xDir * corner} ${A.y}
2018
- Q ${midX} ${A.y}, ${midX} ${A.y + yDir * corner}
2019
- L ${midX} ${B.y - yDir * corner}
2020
- Q ${midX} ${B.y}, ${midX + xDir * corner} ${B.y}
2100
+ L ${midX - xDir * lCorner} ${A.y}
2101
+ Q ${midX} ${A.y}, ${midX} ${A.y + yDir * lCorner}
2102
+ L ${midX} ${B.y - yDir * lCorner}
2103
+ Q ${midX} ${B.y}, ${midX + xDir * lCorner} ${B.y}
2021
2104
  L ${B.x} ${B.y}`,
2022
2105
  labelX: midX,
2023
2106
  labelY: verticalMidY,