@cardenelabs/cdl 0.21.0 → 0.21.2

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.21.0",
3
+ "version": "0.21.2",
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,8 @@ import {
7
7
  genericTextShift,
8
8
  genericTitleBaselineY,
9
9
  rowBaselineY,
10
+ CLOUD_ART_CENTER_RISE,
11
+ CLOUD_CIRCLES,
10
12
  目印なしの繰り上げ,
11
13
  } from "../layout/spec";
12
14
  import {
@@ -222,19 +224,15 @@ export function GenericNode({
222
224
  // 各 circle の中心 y は box 中央線上 (もしくは中央より少し上)、 半径は box 高さの 0.35 ~ 0.45。
223
225
  const cx = w / 2;
224
226
  const cy = h / 2;
225
- // 雲全体の中心 y box の中央 (cy) より少し上に
226
- const cloudCenterY = cy - h * 0.05;
227
- // 5 つの円の半径と中心 x オフセット (中央が大きく、 端が小さい)
228
- // 円が w 方向 80% を占めるよう配置
229
- const usableW = w * 0.85;
230
- const halfW = usableW / 2;
231
- const circles = [
232
- { cx: cx - halfW * 0.8, cy: cloudCenterY + h * 0.08, r: h * 0.32 }, // 左端
233
- { cx: cx - halfW * 0.4, cy: cloudCenterY - h * 0.05, r: h * 0.4 }, // 左
234
- { cx: cx, cy: cloudCenterY - h * 0.1, r: h * 0.46 }, // 中央 (最大)
235
- { cx: cx + halfW * 0.4, cy: cloudCenterY - h * 0.05, r: h * 0.4 }, // 右
236
- { cx: cx + halfW * 0.8, cy: cloudCenterY + h * 0.08, r: h * 0.32 }, // 右端
237
- ];
227
+ // 5 つの円の中心と半径は `layout/spec.ts` `CLOUD_CIRCLES` が持つ (#588)
228
+ // 字を置く側が同じ表から絵の芯を出すので、円を動かせば字も追随する。
229
+ // 円が w 方向 85% を占めるよう配置
230
+ const halfW = (w * 0.85) / 2;
231
+ const circles = CLOUD_CIRCLES.map((c) => ({
232
+ cx: cx + halfW * c.cx,
233
+ cy: cy + h * c.cy,
234
+ r: h * c.r,
235
+ }));
238
236
  // 各 circle を fill=雲色、 stroke=outline 色で順に描画。
239
237
  // 全部同じ fill / stroke で描けば視覚的に union された 1 つの雲に見える。
240
238
  // ただし stroke が内側 (重なり部分) でも描画されるとセル割線が見える → 重ね順を工夫。
@@ -304,12 +302,21 @@ export function GenericNode({
304
302
  // 小見出しを書かない時は、そのために空けた帯のぶん字を上へ繰り上げる (#589)。
305
303
  // 中央へ寄せる形と行を持つ形では繰り上げない (前者は塊の置き方が違い、後者は 1 行目と重なる)
306
304
  const 繰り上げ = node.eyebrow || centered ? 0 : 目印なしの繰り上げ(node.kind, hasRows);
307
- const eyebrowY = centered ? h / 2 - 26 : 32 + cylinderShift;
308
- const titleY = genericTitleBaselineY(node.kind, h, hasRows) - 繰り上げ;
309
- const subtitleY = centered ? h / 2 + 28 : 96 + cylinderShift - 繰り上げ;
305
+ /*
306
+ * 中央へ寄せる形では、字を **絵の芯** に合わせる (#588)
307
+ *
308
+ * 雲は箱の上へはみ出し下は手前で終わるので、絵の芯が箱の芯より上にある。 箱の芯に
309
+ * 合わせると絵に対して下へ寄って見えた (実測で上 71.5 / 下 27.9)。
310
+ * ひし形 (`decision`) は箱と絵の芯が同じなので 0 になる。
311
+ */
312
+ const 絵の芯 = centered && STYLES[node.kind]?.shape === "cloud" ? h * CLOUD_ART_CENTER_RISE : 0;
313
+ const eyebrowY = centered ? h / 2 - 26 - 絵の芯 : 32 + cylinderShift;
314
+ const titleY = genericTitleBaselineY(node.kind, h, hasRows) - 繰り上げ - 絵の芯;
315
+ const subtitleY = centered ? h / 2 + 28 - 絵の芯 : 96 + cylinderShift - 繰り上げ;
310
316
  // icon 位置 (diamond / cloud は title の真右の少し上、 中心配置に合わせる)
311
317
  const iconX = centered ? w / 2 + 40 : w - 48;
312
- const iconY = centered ? h / 2 - 44 : 18 + cylinderShift;
318
+ // 目印も字と同じ芯に合わせる (#588)。 字だけ動かすと目印との間隔が変わる
319
+ const iconY = centered ? h / 2 - 44 - 絵の芯 : 18 + cylinderShift;
313
320
  const dominantBaseline = centered ? ("middle" as const) : undefined;
314
321
 
315
322
  return (
@@ -1869,8 +1869,22 @@ export function resolveEdgeLabelOverlapsWithChainAndPropagate(
1869
1869
  for (const { p } of ordered) {
1870
1870
  const axis = p.axis as "x" | "y";
1871
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
+ );
1872
1886
  out.push({ target: { kind: "edge-path", id: e.id }, axis, sign: p.sign, amount });
1873
- 線を動かせた = true;
1887
+ 線を動かせた = 動かした後 > gap;
1874
1888
  break;
1875
1889
  }
1876
1890
  /*
@@ -1903,16 +1917,64 @@ export function resolveEdgeLabelOverlapsWithChainAndPropagate(
1903
1917
  if (近い点) {
1904
1918
  const dx = 芯.x - 近い点.x;
1905
1919
  const dy = 芯.y - 近い点.y;
1906
- const axis: "x" | "y" = Math.abs(dx) >= Math.abs(dy) ? "x" : "y";
1907
- const 向き = axis === "x" ? dx : dy;
1908
- const sign: 1 | -1 = 向き >= 0 ? 1 : -1;
1909
- out.push({
1910
- target: { kind: "label", id: label.id },
1911
- axis,
1912
- sign,
1913
- amount: pathLabelClearance - gap + NEAR_MISS_MARGIN,
1914
- chain: false,
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),
1915
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
+ }
1916
1978
  }
1917
1979
  }
1918
1980
  }
@@ -1124,3 +1124,34 @@ export function 中身から出す高さ(
1124
1124
  * 実測の見込みは 54 なので、下限が効くのは 3 段の高さを著者が縮めた形に限る。
1125
1125
  */
1126
1126
  export const MIN_BOX_H = 44;
1127
+
1128
+ /**
1129
+ * 雲の形を作る 5 つの円 (#588)。 中心と半径を **箱の高さに対する割合** で持つ。
1130
+ *
1131
+ * 描き手 (`kinds/generic.tsx`) がこの表から円を描き、字を置く側が同じ表から
1132
+ * 「絵の芯」 を出す。 別々に持つと、円を動かした時に字だけ取り残される。
1133
+ *
1134
+ * `cx` は箱の中央からの左右のずれ (`w * 0.85 / 2` を 1 とした割合)、
1135
+ * `cy` は箱の中央からの上下のずれ (箱の高さに対する割合)、`r` は半径 (同じく高さに対する割合)。
1136
+ */
1137
+ export const CLOUD_CIRCLES = [
1138
+ { cx: -0.8, cy: -0.05 + 0.08, r: 0.32 },
1139
+ { cx: -0.4, cy: -0.05 - 0.05, r: 0.4 },
1140
+ { cx: 0, cy: -0.05 - 0.1, r: 0.46 },
1141
+ { cx: 0.4, cy: -0.05 - 0.05, r: 0.4 },
1142
+ { cx: 0.8, cy: -0.05 + 0.08, r: 0.32 },
1143
+ ] as const;
1144
+
1145
+ /**
1146
+ * 雲の絵の芯が、箱の芯からどれだけ上にあるか (箱の高さに対する割合、#588)。
1147
+ *
1148
+ * 雲は箱の上へ 0.11 はみ出し、下は 0.15 手前で終わる = 絵の芯は箱の芯より上にある。
1149
+ * 字を箱の芯に合わせると、絵に対して下へ寄って見える (実測で上 71.5 / 下 27.9)。
1150
+ *
1151
+ * 円の表から出すので、円を動かせばこの値も動く。
1152
+ */
1153
+ export const CLOUD_ART_CENTER_RISE = (() => {
1154
+ const 上 = Math.min(...CLOUD_CIRCLES.map((c) => c.cy - c.r));
1155
+ const 下 = Math.max(...CLOUD_CIRCLES.map((c) => c.cy + c.r));
1156
+ return -(上 + 下) / 2;
1157
+ })();