@cardenelabs/cdl 0.121.0 → 0.123.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/CHANGELOG.md +51 -1
- package/README.md +137 -0
- package/SPEC.md +1 -1
- package/dist/index.cjs +1513 -667
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +51 -33
- package/dist/index.d.ts +51 -33
- package/dist/index.js +1513 -668
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +1271 -444
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1271 -444
- package/dist/react.js.map +1 -1
- package/dist/{render-A5AKjNuy.d.cts → render-CVczwfRb.d.cts} +40 -7
- package/dist/{render-A5AKjNuy.d.ts → render-CVczwfRb.d.ts} +40 -7
- package/package.json +9 -12
- package/src/builder.ts +8 -2
- package/src/index.ts +3 -0
- package/src/kinds/box-lines.ts +17 -4
- package/src/kinds/chart-bar.tsx +4 -0
- package/src/kinds/chart-gauge.tsx +20 -9
- package/src/kinds/chart-pie.tsx +67 -17
- package/src/kinds/chart-radial.tsx +7 -2
- package/src/kinds/chart-slope.tsx +26 -7
- package/src/kinds/chart-stacked-bar.tsx +8 -4
- package/src/kinds/chart-stat.tsx +19 -4
- package/src/kinds/chart-waffle.tsx +7 -2
- package/src/kinds/dyn-shape.tsx +22 -9
- package/src/kinds/funnel.tsx +9 -2
- package/src/kinds/generic.tsx +5 -2
- package/src/kinds/mind-map.tsx +26 -10
- package/src/kinds/quadrant.tsx +14 -4
- package/src/kinds/row-align.ts +19 -7
- package/src/kinds/sequence-board-metrics.ts +13 -3
- package/src/kinds/sequence-board.tsx +4 -2
- package/src/kinds/shape-basement.tsx +57 -32
- package/src/kinds/shape-blockchain.tsx +83 -34
- package/src/kinds/shape-commerce.tsx +65 -24
- package/src/kinds/shape-finance.tsx +90 -26
- package/src/kinds/shape-hardware.tsx +48 -17
- package/src/kinds/shape-region.tsx +16 -6
- package/src/kinds/shape-software.tsx +30 -8
- package/src/kinds/station.tsx +29 -0
- package/src/kinds/text-width.ts +17 -3
- package/src/kinds/tree.tsx +25 -7
- package/src/layout/collisions.ts +50 -28
- package/src/layout/curve-path.ts +126 -0
- package/src/layout/edges.ts +271 -58
- package/src/layout/head-label.ts +11 -5
- package/src/layout/lanes.ts +3 -2
- package/src/layout/metro-path.ts +185 -0
- package/src/layout/nodes.ts +14 -6
- package/src/layout/predict-bbox.ts +6 -3
- package/src/layout/self-loop.ts +10 -4
- package/src/layout/spec.ts +36 -7
- package/src/layout/text-width.ts +25 -8
- package/src/layout/tokens.ts +2 -0
- package/src/layout-with-validation.ts +10 -5
- package/src/layout.ts +55 -21
- package/src/presets.ts +69 -43
- package/src/render/edges.tsx +19 -3
- package/src/render/interactive-panel.tsx +34 -7
- package/src/render/nodes.tsx +100 -48
- package/src/render/stage.tsx +3 -0
- package/src/text-metrics.ts +9 -0
- package/src/types.ts +41 -4
- package/src/visual-validate.ts +91 -21
package/src/visual-validate.ts
CHANGED
|
@@ -77,7 +77,14 @@
|
|
|
77
77
|
* test が無いと、 次に種別を足した時に同じ状態へ戻る。
|
|
78
78
|
* ───────────────────────────────────────────────────────────
|
|
79
79
|
*/
|
|
80
|
-
import {
|
|
80
|
+
import {
|
|
81
|
+
EDGE_HEADS,
|
|
82
|
+
type CdlDiagram,
|
|
83
|
+
type CdlTextMetrics,
|
|
84
|
+
type LaidDiagram,
|
|
85
|
+
type LaidNode,
|
|
86
|
+
type NodeKind,
|
|
87
|
+
} from "./types";
|
|
81
88
|
import { CLEARANCE_LANE_LABEL, DIST_LABEL_PATH_MAX } from "./layout/clearance-constants";
|
|
82
89
|
import { layout } from "./layout";
|
|
83
90
|
import {
|
|
@@ -92,7 +99,7 @@ import {
|
|
|
92
99
|
} from "./layout/collisions";
|
|
93
100
|
import { measureTextWidth } from "./layout/text-width";
|
|
94
101
|
import { droppedLineIsHidden, labelPlan } from "./kinds/shape-region";
|
|
95
|
-
import { 箱に積む, type 積む行 } from "./kinds/box-lines";
|
|
102
|
+
import { 箱に積む, 箱の字を測る, type 積む行 } from "./kinds/box-lines";
|
|
96
103
|
import { 木の中身, 木の行 } from "./kinds/tree";
|
|
97
104
|
import { 放射の中身, 放射の行 } from "./kinds/mind-map";
|
|
98
105
|
import { templateRefIds } from "./render/utils";
|
|
@@ -143,6 +150,7 @@ import {
|
|
|
143
150
|
TITLE_ROW_MIN_GAP,
|
|
144
151
|
rendersTitleText,
|
|
145
152
|
fitsOwnTitleWidth,
|
|
153
|
+
skipsNodeTitleWidthCheck,
|
|
146
154
|
drawsTitleOutsideBox,
|
|
147
155
|
titleScalesToFit,
|
|
148
156
|
MIN_TITLE_FONT_SIZE,
|
|
@@ -586,7 +594,12 @@ function emptyCounts(): Record<VisualAxis, number> {
|
|
|
586
594
|
}
|
|
587
595
|
|
|
588
596
|
/** 箱に行を積む種別の、 中の 1 箱。 */
|
|
589
|
-
type 積む箱 = {
|
|
597
|
+
type 積む箱 = {
|
|
598
|
+
行: 積む行[];
|
|
599
|
+
w: number;
|
|
600
|
+
h: number;
|
|
601
|
+
測る: (text: string, fontSize: number) => number;
|
|
602
|
+
};
|
|
590
603
|
|
|
591
604
|
/**
|
|
592
605
|
* 木と放射の中の箱を、 **描く時と同じ手順で** 集める (`#538`)。
|
|
@@ -600,10 +613,23 @@ type 積む箱 = { 行: 積む行[]; w: number; h: number };
|
|
|
600
613
|
*
|
|
601
614
|
* 行を積まない種別では空を返す。
|
|
602
615
|
*/
|
|
603
|
-
function 積む箱を集める(
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
616
|
+
function 積む箱を集める(
|
|
617
|
+
n: LaidNode,
|
|
618
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
619
|
+
): 積む箱[] {
|
|
620
|
+
const 箱 = (
|
|
621
|
+
b: { w: number; h: number },
|
|
622
|
+
行: 積む行[],
|
|
623
|
+
測る: 積む箱["測る"],
|
|
624
|
+
): 積む箱 => ({ 行, w: b.w, h: b.h, 測る });
|
|
625
|
+
if (n.kind === "tree-hierarchy") {
|
|
626
|
+
const 測る = 箱の字を測る(textMetrics);
|
|
627
|
+
return 木の中身(n, {}, textMetrics).nodes.map((b) => 箱(b, 木の行(b), 測る));
|
|
628
|
+
}
|
|
629
|
+
if (n.kind === "mind-map") {
|
|
630
|
+
const 測る = 箱の字を測る(textMetrics);
|
|
631
|
+
return (放射の中身(n, {}, textMetrics)?.nodes ?? []).map((b) => 箱(b, 放射の行(b), 測る));
|
|
632
|
+
}
|
|
607
633
|
return [];
|
|
608
634
|
}
|
|
609
635
|
|
|
@@ -1971,6 +1997,7 @@ function runAxes(
|
|
|
1971
1997
|
/** 軸 23 が基準にする器。 入口で確かめ済 (`器を確かめる`)。 高さ `undefined` は上限なし。 */
|
|
1972
1998
|
器: { 幅: number; 高さ: number | undefined },
|
|
1973
1999
|
): VisualValidationReport {
|
|
2000
|
+
const textMetrics = laid.textMetrics ?? "proportional";
|
|
1974
2001
|
// ───────────────────────────────────────────────────────────
|
|
1975
2002
|
// Axis 1: node-visibility (bbox 面積 / 最小寸法)
|
|
1976
2003
|
//
|
|
@@ -1980,6 +2007,8 @@ function runAxes(
|
|
|
1980
2007
|
const LIFELINE_W_THRESHOLD = 8;
|
|
1981
2008
|
for (const n of laid.nodes) {
|
|
1982
2009
|
if (n.w < LIFELINE_W_THRESHOLD) continue;
|
|
2010
|
+
// 駅は文字を収める箱ではなく線路上の印なので、箱向けの最小寸法を要求しない。
|
|
2011
|
+
if (n.kind === "station") continue;
|
|
1983
2012
|
if (n.w < MIN_NODE_W || n.h < MIN_NODE_H) {
|
|
1984
2013
|
push(
|
|
1985
2014
|
"node-visibility",
|
|
@@ -1991,7 +2020,7 @@ function runAxes(
|
|
|
1991
2020
|
// ───────────────────────────────────────────────────────────
|
|
1992
2021
|
// Axis 2: edge-label-overlap (edge label ↔ node の AABB collision)
|
|
1993
2022
|
// ───────────────────────────────────────────────────────────
|
|
1994
|
-
const bboxes = collectBBoxes(laid.lanes, laid.nodes, laid.edges);
|
|
2023
|
+
const bboxes = collectBBoxes(laid.lanes, laid.nodes, laid.edges, textMetrics);
|
|
1995
2024
|
const collisions = detectCollisions(bboxes, laid.edges);
|
|
1996
2025
|
for (const c of collisions) {
|
|
1997
2026
|
const involvesEdgeLabel = c.a.kind === "edge-label" || c.b.kind === "edge-label";
|
|
@@ -2016,6 +2045,8 @@ function runAxes(
|
|
|
2016
2045
|
// 自分で幅に合わせて切る種別も同じ。 字数から幅を出す下の近似は字の大きさ 22 を前提に
|
|
2017
2046
|
// しており、 それより小さい字で描く種別に当てると偽陽性になる (#469)
|
|
2018
2047
|
if (fitsOwnTitleWidth(n.kind)) continue;
|
|
2048
|
+
// 箱の外に出す駅名は小さな丸の幅で切らないため、箱幅を上限にする前提が成り立たない。
|
|
2049
|
+
if (skipsNodeTitleWidthCheck(n.kind)) continue;
|
|
2019
2050
|
// 常に非表示の node の文字は画面に出ないので、 切れようがない。
|
|
2020
2051
|
if (!isRenderedNode(n)) continue;
|
|
2021
2052
|
const titleTrimmed = n.title?.trim() ?? "";
|
|
@@ -2036,7 +2067,11 @@ function runAxes(
|
|
|
2036
2067
|
// 名前と図表の題) は内側の余白に縛られないので、足すと収まっている題まで挙がる。
|
|
2037
2068
|
const 余白 = drawsTitleOutsideBox(n.kind) ? 0 : TITLE_BOX_PADDING;
|
|
2038
2069
|
const 使える幅 = n.w - 余白;
|
|
2039
|
-
const 実幅 = measureTextWidth(titleTrimmed, {
|
|
2070
|
+
const 実幅 = measureTextWidth(titleTrimmed, {
|
|
2071
|
+
fontSize: titleFontSize(n.kind),
|
|
2072
|
+
bound: "drawn",
|
|
2073
|
+
textMetrics,
|
|
2074
|
+
});
|
|
2040
2075
|
if (titleScalesToFit(n.kind, n.h)) {
|
|
2041
2076
|
// 収まるまで縮める種別は、題がはみ出すことが起きない。 代わりに字が読めない大きさまで
|
|
2042
2077
|
// 落ちるので、**読める下限を割るか** を見る (#951)。 下限まで縮めても収まらない時
|
|
@@ -2152,7 +2187,15 @@ function runAxes(
|
|
|
2152
2187
|
: null;
|
|
2153
2188
|
const 相手 = 動かす === n.a ? n.b : n.a;
|
|
2154
2189
|
const reason =
|
|
2155
|
-
動かす !== null
|
|
2190
|
+
動かす !== null
|
|
2191
|
+
? (札が動けない理由(
|
|
2192
|
+
動かす.id,
|
|
2193
|
+
相手,
|
|
2194
|
+
laid.edges,
|
|
2195
|
+
札の障害物,
|
|
2196
|
+
textMetrics,
|
|
2197
|
+
) ?? undefined)
|
|
2198
|
+
: undefined;
|
|
2156
2199
|
push(
|
|
2157
2200
|
"clearance",
|
|
2158
2201
|
`${n.a.kind}:${n.a.id} ↔ ${n.b.kind}:${n.b.id} gap=${n.gap.toFixed(1)}px (need ${n.required}px)`,
|
|
@@ -2189,7 +2232,10 @@ function runAxes(
|
|
|
2189
2232
|
if (!hasRenderedLabel(e)) continue;
|
|
2190
2233
|
const segs = extractPathSegments(e.d);
|
|
2191
2234
|
if (segs.length === 0) continue;
|
|
2192
|
-
const minDist = measurePathLabelGapSegs(
|
|
2235
|
+
const minDist = measurePathLabelGapSegs(
|
|
2236
|
+
flattenPathSegments(segs),
|
|
2237
|
+
computeLabelBBoxWorld(e, textMetrics),
|
|
2238
|
+
);
|
|
2193
2239
|
if (!Number.isFinite(minDist)) continue;
|
|
2194
2240
|
if (minDist > PROXIMITY_ERROR_THRESHOLD) {
|
|
2195
2241
|
push(
|
|
@@ -2259,9 +2305,16 @@ function runAxes(
|
|
|
2259
2305
|
// ───────────────────────────────────────────────────────────
|
|
2260
2306
|
for (const e of laid.edges) {
|
|
2261
2307
|
if (!hasRenderedLabel(e)) continue;
|
|
2262
|
-
const mainW = measureTextWidth(e.label ?? "", {
|
|
2308
|
+
const mainW = measureTextWidth(e.label ?? "", {
|
|
2309
|
+
fontSize: EDGE_LABEL_TEXT.main.fontSize,
|
|
2310
|
+
textMetrics,
|
|
2311
|
+
});
|
|
2263
2312
|
const subW = e.sub
|
|
2264
|
-
? measureTextWidth(e.sub, {
|
|
2313
|
+
? measureTextWidth(e.sub, {
|
|
2314
|
+
fontSize: EDGE_LABEL_TEXT.sub.fontSize,
|
|
2315
|
+
fontFamily: "mono",
|
|
2316
|
+
textMetrics,
|
|
2317
|
+
})
|
|
2265
2318
|
: 0;
|
|
2266
2319
|
const expectedBoxW = Math.max(mainW, subW) + LABEL_PILL_PAD_X * 2;
|
|
2267
2320
|
const bbox = bboxes.find((b) => b.kind === "edge-label" && b.id === e.id);
|
|
@@ -2494,7 +2547,9 @@ function runAxes(
|
|
|
2494
2547
|
// 共有 plan が行を落とさない高さでは、 固有 layout がその行を描かない種別でも報告されない。
|
|
2495
2548
|
// 本軸は箱が狭くて落ちた行だけを扱い、 種別が元から対応しない欄は別の検査範囲とする。
|
|
2496
2549
|
for (const n of laid.nodes) {
|
|
2497
|
-
const 落とした = labelPlan(n).落とした.filter((line) =>
|
|
2550
|
+
const 落とした = labelPlan(n).落とした.filter((line) =>
|
|
2551
|
+
droppedLineIsHidden(n.kind, line),
|
|
2552
|
+
);
|
|
2498
2553
|
if (落とした.length === 0) continue;
|
|
2499
2554
|
push(
|
|
2500
2555
|
"shape-label-dropped",
|
|
@@ -2526,7 +2581,7 @@ function runAxes(
|
|
|
2526
2581
|
// なので、 対象から外すと変えた時に落ちても見えない。
|
|
2527
2582
|
// ───────────────────────────────────────────────────────────
|
|
2528
2583
|
for (const n of laid.nodes) {
|
|
2529
|
-
const 箱 = 積む箱を集める(n);
|
|
2584
|
+
const 箱 = 積む箱を集める(n, textMetrics);
|
|
2530
2585
|
if (箱.length === 0) continue;
|
|
2531
2586
|
/** 補足を書いた箱の数。 書いていない箱は元から描かれないので数えない。 */
|
|
2532
2587
|
let 対象 = 0;
|
|
@@ -2536,7 +2591,7 @@ function runAxes(
|
|
|
2536
2591
|
const 宣言 = b.行.slice(1).filter((r) => r.text.trim() !== "");
|
|
2537
2592
|
if (宣言.length === 0) continue;
|
|
2538
2593
|
対象 += 1;
|
|
2539
|
-
const 残った = new Set(箱に積む(b.行, b.w, b.h).map((r) => r.役割));
|
|
2594
|
+
const 残った = new Set(箱に積む(b.行, b.w, b.h, b.測る).map((r) => r.役割));
|
|
2540
2595
|
if (宣言.every((r) => 残った.has(r.役割))) continue;
|
|
2541
2596
|
落ちた.push({ title: b.行[0]?.text ?? "", h: b.h });
|
|
2542
2597
|
}
|
|
@@ -3111,6 +3166,13 @@ function runAxes(
|
|
|
3111
3166
|
for (let i = 0; i + 1 < sorted.length; i++) {
|
|
3112
3167
|
const upper = sorted[i]!;
|
|
3113
3168
|
const lower = sorted[i + 1]!;
|
|
3169
|
+
// 横の範囲が重ならない箱は同じ lane に横並びで置かれた組なので、縦の隙間を測らない。
|
|
3170
|
+
if (
|
|
3171
|
+
upper.cx + upper.w / 2 <= lower.cx - lower.w / 2 ||
|
|
3172
|
+
lower.cx + lower.w / 2 <= upper.cx - upper.w / 2
|
|
3173
|
+
) {
|
|
3174
|
+
continue;
|
|
3175
|
+
}
|
|
3114
3176
|
const upperBottom = upper.cy + upper.h / 2;
|
|
3115
3177
|
const lowerTop = lower.cy - lower.h / 2;
|
|
3116
3178
|
const gap = lowerTop - upperBottom;
|
|
@@ -4214,17 +4276,25 @@ function runAxes(
|
|
|
4214
4276
|
y1: number;
|
|
4215
4277
|
x2: number;
|
|
4216
4278
|
y2: number;
|
|
4217
|
-
cmd: "L" | "Q" | "C";
|
|
4279
|
+
cmd: "L" | "Q" | "C" | "A";
|
|
4218
4280
|
ctrl?: ReadonlyArray<{ x: number; y: number }>;
|
|
4219
4281
|
}>,
|
|
4220
4282
|
peakY: number,
|
|
4221
4283
|
): Array<{ minX: number; maxX: number }> => {
|
|
4222
4284
|
const out: Array<{ minX: number; maxX: number }> = [];
|
|
4285
|
+
let 円弧の始点: { x: number; y: number } | null = null;
|
|
4223
4286
|
for (const s of segs) {
|
|
4224
|
-
if (
|
|
4225
|
-
|
|
4287
|
+
if (s.cmd === "A") {
|
|
4288
|
+
// 飛び越しの弧は同じ高さの通り道の上の小さな山で、山の前後は 1 本の通り道になる。
|
|
4289
|
+
円弧の始点 ??= { x: s.x1, y: s.y1 };
|
|
4290
|
+
continue;
|
|
4291
|
+
}
|
|
4292
|
+
const 始点 = 円弧の始点 ?? { x: s.x1, y: s.y1 };
|
|
4293
|
+
円弧の始点 = null;
|
|
4294
|
+
if (Math.abs(始点.y - s.y2) > 1) continue;
|
|
4295
|
+
if (Math.abs(始点.y - peakY) > 1) continue;
|
|
4226
4296
|
if (s.cmd !== "L" && !(s.ctrl ?? []).every((c) => Math.abs(c.y - peakY) <= 1)) continue;
|
|
4227
|
-
out.push({ minX: Math.min(
|
|
4297
|
+
out.push({ minX: Math.min(始点.x, s.x2), maxX: Math.max(始点.x, s.x2) });
|
|
4228
4298
|
}
|
|
4229
4299
|
return out;
|
|
4230
4300
|
};
|
|
@@ -4382,7 +4452,7 @@ function runAxes(
|
|
|
4382
4452
|
const allowedLanes = new Set<string>();
|
|
4383
4453
|
if (fromNode) allowedLanes.add(fromNode.lane);
|
|
4384
4454
|
if (toNode) allowedLanes.add(toNode.lane);
|
|
4385
|
-
const box = computeLabelBBoxWorld(e);
|
|
4455
|
+
const box = computeLabelBBoxWorld(e, textMetrics);
|
|
4386
4456
|
for (const lane of laid.lanes) {
|
|
4387
4457
|
if (allowedLanes.has(lane.id)) continue;
|
|
4388
4458
|
const side = rectCrossesLaneBorder(box, lane, rangeByLane.get(lane.id));
|