@cardenelabs/cdl 0.64.0 → 0.64.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/CHANGELOG.md +30 -0
- package/dist/index.cjs +27 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +27 -16
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +27 -16
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +27 -16
- package/dist/react.js.map +1 -1
- package/package.json +9 -12
- package/src/layout/edges.ts +8 -15
- package/src/layout/spec.ts +59 -1
- package/src/visual-validate.ts +12 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cardenelabs/cdl",
|
|
3
|
-
"version": "0.64.
|
|
3
|
+
"version": "0.64.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",
|
|
@@ -57,16 +57,6 @@
|
|
|
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
|
-
},
|
|
70
60
|
"peerDependencies": {
|
|
71
61
|
"react": "^19.0.0",
|
|
72
62
|
"react-dom": "^19.0.0"
|
|
@@ -78,5 +68,12 @@
|
|
|
78
68
|
"typedoc": "^0.28.19",
|
|
79
69
|
"typedoc-plugin-markdown": "^4.12.0",
|
|
80
70
|
"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"
|
|
81
78
|
}
|
|
82
|
-
}
|
|
79
|
+
}
|
package/src/layout/edges.ts
CHANGED
|
@@ -18,9 +18,10 @@ import {
|
|
|
18
18
|
EDGE_STUB_OUT,
|
|
19
19
|
hasRenderedLabel,
|
|
20
20
|
labelInitClearance,
|
|
21
|
-
|
|
21
|
+
laneVerticalRange,
|
|
22
22
|
labelPathClearanceHorizontal,
|
|
23
23
|
labelPillH,
|
|
24
|
+
rectCrossesLaneBorder,
|
|
24
25
|
spreadWithMinimalShift,
|
|
25
26
|
PARALLEL_EDGE_BOW,
|
|
26
27
|
SYMMETRIC_PAIR_SHIFT,
|
|
@@ -172,6 +173,8 @@ export function layoutEdges(diag: CdlDiagram, nodes: LaidNode[], lanes: LaidLane
|
|
|
172
173
|
(diag.viewport?.labelMargin ?? diag.viewport?.gap ?? DEFAULT_LABEL_MARGIN) - DEFAULT_LABEL_MARGIN;
|
|
173
174
|
const nmap = new Map(nodes.map((n) => [n.id, n]));
|
|
174
175
|
const lmap = new Map(lanes.map((l) => [l.id, l]));
|
|
176
|
+
// label が lane の枠線を跨ぐかを見る 2 か所 (左への迂回と label の反転) が使う縦の範囲 (#863)。
|
|
177
|
+
const laneRangeById = new Map(lanes.map((l) => [l.id, laneVerticalRange(l, nodes)]));
|
|
175
178
|
|
|
176
179
|
// canvas pivot 新 spec (dragon canvas pivot spec §layout-role-conversion)。
|
|
177
180
|
// posX1 / posY1 / posX2 / posY2 全て set 済 edge は「user が固定した絶対 path」 = 直線 path
|
|
@@ -614,14 +617,12 @@ export function layoutEdges(diag: CdlDiagram, nodes: LaidNode[], lanes: LaidLane
|
|
|
614
617
|
const pill = { x: centerX - boxW / 2, y: (exitY + returnY) / 2 - boxH / 2, w: boxW, h: boxH };
|
|
615
618
|
if (others.some((n) => rectsOverlap(pill, nodeRect(n)))) return true;
|
|
616
619
|
|
|
617
|
-
// 発着 lane
|
|
620
|
+
// 発着 lane の枠線は跨いでよい。 跨ぐかは visual-validate の lane-border-clearance と同じ関数で見る。
|
|
618
621
|
const endpointLanes = new Set(
|
|
619
622
|
nodes.filter((n) => n.id === c.from || n.id === c.to).map((n) => n.lane),
|
|
620
623
|
);
|
|
621
624
|
return lanes.some(
|
|
622
|
-
(l) =>
|
|
623
|
-
!endpointLanes.has(l.id) &&
|
|
624
|
-
(crossesVerticalLine(pill, l.x) || crossesVerticalLine(pill, l.x + l.width)),
|
|
625
|
+
(l) => !endpointLanes.has(l.id) && rectCrossesLaneBorder(pill, l, laneRangeById.get(l.id)) !== undefined,
|
|
625
626
|
);
|
|
626
627
|
};
|
|
627
628
|
|
|
@@ -968,13 +969,13 @@ export function layoutEdges(diag: CdlDiagram, nodes: LaidNode[], lanes: LaidLane
|
|
|
968
969
|
nodes.reduce((acc, n) => Math.min(acc, rectGap(box, nodeRect(n))), Infinity);
|
|
969
970
|
if (minNodeGap(alt) < Math.min(CLEARANCE_NODE_LABEL, minNodeGap(cur)) - 1e-6) continue;
|
|
970
971
|
|
|
971
|
-
// 発着 lane
|
|
972
|
+
// 発着 lane の枠線は跨いでよい。 跨ぐかは visual-validate の lane-border-clearance と同じ関数で見る。
|
|
972
973
|
const endpointLanes = new Set(
|
|
973
974
|
[laneOfNode.get(r.e.from), laneOfNode.get(r.e.to)].filter((v): v is string => v !== undefined),
|
|
974
975
|
);
|
|
975
976
|
const crossesLaneBorder = lanes.some((l) => {
|
|
976
977
|
if (endpointLanes.has(l.id)) return false;
|
|
977
|
-
return
|
|
978
|
+
return rectCrossesLaneBorder(alt, l, laneRangeById.get(l.id)) !== undefined;
|
|
978
979
|
});
|
|
979
980
|
if (crossesLaneBorder) continue;
|
|
980
981
|
|
|
@@ -1465,14 +1466,6 @@ function segmentsBBox(segs: Array<{ x1: number; y1: number; x2: number; y2: numb
|
|
|
1465
1466
|
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
1466
1467
|
}
|
|
1467
1468
|
|
|
1468
|
-
/**
|
|
1469
|
-
* rect が縦線 x = lineX を跨ぐか。 `visual-validate` の lane-border-clearance と同一規約で、
|
|
1470
|
-
* 端が線に触れるだけ (許容差 LANE_BORDER_CLEARANCE_TOL 以内) は跨いだ扱いにしない。
|
|
1471
|
-
*/
|
|
1472
|
-
function crossesVerticalLine(rect: Rect, lineX: number): boolean {
|
|
1473
|
-
return rect.x < lineX - LANE_BORDER_CLEARANCE_TOL && rect.x + rect.w > lineX + LANE_BORDER_CLEARANCE_TOL;
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
1469
|
function distributeBySides(
|
|
1477
1470
|
list: Array<{
|
|
1478
1471
|
e: CdlEdge;
|
package/src/layout/spec.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import { CLEARANCE_LABEL_LABEL, CLEARANCE_NODE_LABEL } from "./clearance-constants";
|
|
19
19
|
import { measureTextWidth } from "./text-width";
|
|
20
20
|
import { EDGE_LABEL_TEXT } from "../label-text";
|
|
21
|
-
import type { CdlEdge, LaidEdge } from "../types";
|
|
21
|
+
import type { CdlEdge, LaidEdge, LaidLane, LaidNode } from "../types";
|
|
22
22
|
|
|
23
23
|
// ─── edge label pill bbox SSOT (CAR-431 で 3 定義箇所を集約) ──
|
|
24
24
|
|
|
@@ -951,6 +951,64 @@ export const COLUMN_GAP_VARIANCE_TOL = 40;
|
|
|
951
951
|
*/
|
|
952
952
|
export const LANE_BORDER_CLEARANCE_TOL = 2;
|
|
953
953
|
|
|
954
|
+
/** lane が占める縦の範囲 (world unit)。 `laneVerticalRange` が返す。 */
|
|
955
|
+
export type LaneVerticalRange = { top: number; bottom: number };
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* 縦の範囲で比べてよい lane の上端と下端。 上下いっぱいの列として比べる lane は `undefined` (#863)。
|
|
959
|
+
*
|
|
960
|
+
* 範囲を持つのは 2 種だけ。
|
|
961
|
+
*
|
|
962
|
+
* | lane | 範囲 |
|
|
963
|
+
* |---|---|
|
|
964
|
+
* | `contain` | 中の node を囲む枠 (`recomputeContainedLanes` が決める `y` / `height`)。 描画もこの枠を描く |
|
|
965
|
+
* | `posX` / `posY` / `posH` を揃えた lane | 著者が置いた `posY` から `posH` まで |
|
|
966
|
+
*
|
|
967
|
+
* それ以外の lane の `height` は名札の帯 (`LANE_LABEL_H`) の高さで、 列の縦の広がりではない。
|
|
968
|
+
* 列は中の node が段の位置に並ぶ下端まで続く。 `posH` を持たない固定の lane も、 `posX` と `posY` が
|
|
969
|
+
* 揃わず layout が座標を採らなかった lane も同じ。 lifeline の lane は縦線が図の下まで伸びる。
|
|
970
|
+
* どれも上下いっぱいの列として比べないと、 列の中にある説明文を取りこぼす。
|
|
971
|
+
*
|
|
972
|
+
* 範囲を持つ lane でも、 中の node が範囲の外にあれば、 その node の上下まで広げる。 高さを指定した
|
|
973
|
+
* lane に座標を持たない node を入れると、 node は段の位置に並んで範囲の外に出るため。
|
|
974
|
+
*/
|
|
975
|
+
export function laneVerticalRange(lane: LaidLane, nodes: readonly LaidNode[]): LaneVerticalRange | undefined {
|
|
976
|
+
const pinnedWithHeight = lane.posX !== undefined && lane.posY !== undefined && lane.posH !== undefined;
|
|
977
|
+
if (!lane.contain && !pinnedWithHeight) return undefined;
|
|
978
|
+
let top = lane.y;
|
|
979
|
+
let bottom = lane.y + lane.height;
|
|
980
|
+
for (const n of nodes) {
|
|
981
|
+
if (n.lane !== lane.id) continue;
|
|
982
|
+
top = Math.min(top, n.cy - n.h / 2);
|
|
983
|
+
bottom = Math.max(bottom, n.cy + n.h / 2);
|
|
984
|
+
}
|
|
985
|
+
return { top, bottom };
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* rect が lane の左右の枠線 (`lane.x` / `lane.x + lane.width`) を跨ぐ側。 跨がなければ `undefined` (#863)。
|
|
990
|
+
*
|
|
991
|
+
* 検証器の軸 64 (`lane-border-clearance`) と、 engine が説明文の置き場所を選ぶ 2 か所
|
|
992
|
+
* (`layout/edges.ts` の左への迂回と説明文の反転) がこの関数で判定する。 規則を 1 か所に置き、
|
|
993
|
+
* engine が避ける枠線と検証器が指摘する枠線を食い違わせない。
|
|
994
|
+
*
|
|
995
|
+
* `range` (`laneVerticalRange` の値) があれば、 rect と縦に重なる時だけ比べる。 接するだけ
|
|
996
|
+
* (重なりの高さ 0) は重ならない扱い。 横は枠線から `LANE_BORDER_CLEARANCE_TOL` 以内に端が
|
|
997
|
+
* 触れるだけなら跨がない扱い。 左右の両方を跨ぐ rect は左を返す。
|
|
998
|
+
*/
|
|
999
|
+
export function rectCrossesLaneBorder(
|
|
1000
|
+
rect: { x: number; y: number; w: number; h: number },
|
|
1001
|
+
lane: Pick<LaidLane, "x" | "width">,
|
|
1002
|
+
range: LaneVerticalRange | undefined,
|
|
1003
|
+
): "left" | "right" | undefined {
|
|
1004
|
+
if (range !== undefined && (rect.y + rect.h <= range.top || rect.y >= range.bottom)) return undefined;
|
|
1005
|
+
const crosses = (lineX: number) =>
|
|
1006
|
+
rect.x < lineX - LANE_BORDER_CLEARANCE_TOL && rect.x + rect.w > lineX + LANE_BORDER_CLEARANCE_TOL;
|
|
1007
|
+
if (crosses(lane.x)) return "left";
|
|
1008
|
+
if (crosses(lane.x + lane.width)) return "right";
|
|
1009
|
+
return undefined;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
954
1012
|
// ─── near-collision policy SSOT (CAR-424 で collisions.ts から集約) ──
|
|
955
1013
|
|
|
956
1014
|
/**
|
package/src/visual-validate.ts
CHANGED
|
@@ -131,7 +131,6 @@ import {
|
|
|
131
131
|
DETOUR_SLOT_GAP,
|
|
132
132
|
EDGE_STUB_OUT,
|
|
133
133
|
LABEL_PILL_PAD_X,
|
|
134
|
-
computeLabelBoxW,
|
|
135
134
|
buildDiagramAltText,
|
|
136
135
|
computeLabelBBoxWorld,
|
|
137
136
|
hasRenderedLabel,
|
|
@@ -143,7 +142,8 @@ import {
|
|
|
143
142
|
fitsOwnTitleWidth,
|
|
144
143
|
rowBaselineY,
|
|
145
144
|
rowGlyphDepth,
|
|
146
|
-
|
|
145
|
+
laneVerticalRange,
|
|
146
|
+
rectCrossesLaneBorder,
|
|
147
147
|
MIN_COLUMN_ALIGNMENT_TOLERANCE,
|
|
148
148
|
MIN_ROW_ALIGNMENT_TOLERANCE,
|
|
149
149
|
ROW_GAP_VARIANCE_TOL,
|
|
@@ -4246,8 +4246,14 @@ function runAxes(
|
|
|
4246
4246
|
//
|
|
4247
4247
|
// 例外 = edge 発着 node の lane (from lane / to lane) 内は判定対象外、 その 2 lane 内なら
|
|
4248
4248
|
// label が lane 縁近くにあっても違和感なし。
|
|
4249
|
+
//
|
|
4250
|
+
// 縦の範囲を持つ lane (`contain` の枠と、 `posX` / `posY` / `posH` を揃えた lane) は、 label と
|
|
4251
|
+
// 縦に重なる時だけ比べる (#863)。 別の段に置いた lane の枠線の延長上に label が来ても、 枠線は
|
|
4252
|
+
// そこに描かれていない。 判定の規則は `rectCrossesLaneBorder` (`layout/spec.ts`) が持ち、
|
|
4253
|
+
// engine が label の置き場所を選ぶ時も同じ関数を使う。
|
|
4249
4254
|
// ───────────────────────────────────────────────────────────
|
|
4250
4255
|
{
|
|
4256
|
+
const rangeByLane = new Map(laid.lanes.map((lane) => [lane.id, laneVerticalRange(lane, laid.nodes)]));
|
|
4251
4257
|
for (const e of laid.edges) {
|
|
4252
4258
|
if (!hasRenderedLabel(e)) continue;
|
|
4253
4259
|
const fromNode = nodeById.get(e.from);
|
|
@@ -4258,29 +4264,14 @@ function runAxes(
|
|
|
4258
4264
|
const allowedLanes = new Set<string>();
|
|
4259
4265
|
if (fromNode) allowedLanes.add(fromNode.lane);
|
|
4260
4266
|
if (toNode) allowedLanes.add(toNode.lane);
|
|
4261
|
-
|
|
4262
|
-
const boxW = computeLabelBoxW(e.label, e.sub);
|
|
4263
|
-
const anchor = e.labelAnchor;
|
|
4264
|
-
const boxLeftX = anchor === "start" ? 0 : anchor === "end" ? -boxW : -boxW / 2;
|
|
4265
|
-
const bx = e.labelX + boxLeftX;
|
|
4266
|
-
const bxRight = bx + boxW;
|
|
4267
|
+
const box = computeLabelBBoxWorld(e);
|
|
4267
4268
|
for (const lane of laid.lanes) {
|
|
4268
4269
|
if (allowedLanes.has(lane.id)) continue;
|
|
4269
|
-
const
|
|
4270
|
-
|
|
4271
|
-
// border 貫通 = bbox が border 線と交差 (border 左端 と bbox 左/右端 の関係)
|
|
4272
|
-
// 左 border 貫通 = bx < laneLeft && bxRight > laneLeft + TOL
|
|
4273
|
-
// 右 border 貫通 = bx < laneRight - TOL && bxRight > laneRight
|
|
4274
|
-
const crossesLeft =
|
|
4275
|
-
bx < laneLeft - LANE_BORDER_CLEARANCE_TOL &&
|
|
4276
|
-
bxRight > laneLeft + LANE_BORDER_CLEARANCE_TOL;
|
|
4277
|
-
const crossesRight =
|
|
4278
|
-
bx < laneRight - LANE_BORDER_CLEARANCE_TOL &&
|
|
4279
|
-
bxRight > laneRight + LANE_BORDER_CLEARANCE_TOL;
|
|
4280
|
-
if (crossesLeft || crossesRight) {
|
|
4270
|
+
const side = rectCrossesLaneBorder(box, lane, rangeByLane.get(lane.id));
|
|
4271
|
+
if (side !== undefined) {
|
|
4281
4272
|
push(
|
|
4282
4273
|
"lane-border-clearance",
|
|
4283
|
-
`edge "${e.id}" label bbox (${
|
|
4274
|
+
`edge "${e.id}" label bbox (${box.x.toFixed(0)}..${(box.x + box.w).toFixed(0)}) が非発着 lane "${lane.id}" (${lane.x.toFixed(0)}..${(lane.x + lane.width).toFixed(0)}) の ${side === "left" ? "左" : "右"} border を貫通`,
|
|
4284
4275
|
);
|
|
4285
4276
|
break; // 1 edge 1 発火に留める
|
|
4286
4277
|
}
|