@cardenelabs/cdl 0.121.0 → 0.122.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 +36 -1
- package/README.md +73 -0
- package/SPEC.md +1 -1
- package/dist/index.cjs +464 -66
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -10
- package/dist/index.d.ts +16 -10
- package/dist/index.js +464 -66
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +453 -57
- 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 +453 -57
- package/dist/react.js.map +1 -1
- package/dist/{render-A5AKjNuy.d.cts → render-U_pC-Ksh.d.cts} +21 -6
- package/dist/{render-A5AKjNuy.d.ts → render-U_pC-Ksh.d.ts} +21 -6
- package/package.json +1 -1
- package/src/builder.ts +3 -2
- package/src/index.ts +1 -0
- package/src/kinds/dyn-shape.tsx +22 -9
- package/src/kinds/station.tsx +29 -0
- package/src/layout/collisions.ts +29 -20
- package/src/layout/curve-path.ts +125 -0
- package/src/layout/edges.ts +207 -39
- package/src/layout/head-label.ts +8 -4
- package/src/layout/metro-path.ts +184 -0
- package/src/layout/spec.ts +16 -1
- package/src/layout/tokens.ts +2 -0
- package/src/layout-with-validation.ts +10 -5
- package/src/layout.ts +22 -12
- package/src/render/edges.tsx +1 -0
- package/src/render/nodes.tsx +3 -0
- package/src/types.ts +22 -4
- package/src/visual-validate.ts +24 -4
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
* 持つ edge の labelOffsetX/Y を 0 にリセット (作者の手動 offset で bbox 範囲外に押し出された
|
|
15
15
|
* label を default 位置に戻す)
|
|
16
16
|
* - h2 node-shift-down ... node-overlap error node pair の下 node の stack を +1
|
|
17
|
-
* - h3 edge-back-detour ... edge-node-cross error edge の routing を
|
|
17
|
+
* - h3 edge-back-detour ... edge-node-cross error のうち、自動経路の edge の routing を
|
|
18
|
+
* "back-detour" に切替。 書き手が選んだ曲線と路線図の線は途中の箱を避けない約束なので変えない
|
|
18
19
|
*/
|
|
19
20
|
import type { CdlDiagram, CdlEdge, LaidDiagram } from "./types";
|
|
20
21
|
import { layout } from "./layout";
|
|
@@ -152,11 +153,15 @@ function applyHeuristics(
|
|
|
152
153
|
.filter((id): id is string => id !== null),
|
|
153
154
|
);
|
|
154
155
|
if (h3Edges.size > 0) {
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
156
|
+
const eligibleEdges = new Set(
|
|
157
|
+
edges
|
|
158
|
+
.filter((e) => h3Edges.has(e.id) && (e.routing === undefined || e.routing === "default"))
|
|
159
|
+
.map((e) => e.id),
|
|
158
160
|
);
|
|
159
|
-
if (
|
|
161
|
+
if (eligibleEdges.size > 0) {
|
|
162
|
+
edges = edges.map((e): CdlEdge =>
|
|
163
|
+
eligibleEdges.has(e.id) ? { ...e, routing: "back-detour" } : e,
|
|
164
|
+
);
|
|
160
165
|
applied.push("h3-edge-back-detour");
|
|
161
166
|
mutated = true;
|
|
162
167
|
}
|
package/src/layout.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import {
|
|
16
16
|
bowSharedStraightPaths,
|
|
17
17
|
fanIncomingEndpoints,
|
|
18
|
+
followLabelsToFannedTails,
|
|
18
19
|
hopEdgeCrossings,
|
|
19
20
|
layoutEdges,
|
|
20
21
|
pullOutgoingFirstCorner,
|
|
@@ -212,7 +213,8 @@ export function applyRowLevelNodeShifts<
|
|
|
212
213
|
* - node は lane 内 stack 順で縦並び、 各 stack 間 gap 80px、 上端 80px
|
|
213
214
|
* - lane label は lane 上部 30px 高
|
|
214
215
|
* - contain: true の lane は内部 node 全部を 24px padding で囲む boundary
|
|
215
|
-
* - edge routing は orthogonal L 字 (起点 side → 中継 → 終点 side)
|
|
216
|
+
* - edge routing は orthogonal L 字 (起点 side → 中継 → 終点 side)。`curve` は 3 次曲線、
|
|
217
|
+
* `metro` は横線と傾き ±1 の斜線を円弧で結ぶ
|
|
216
218
|
* - edge label は path 中点から path 進行方向の垂直方向に 50-80px、 node bbox を避ける
|
|
217
219
|
* - viewBox は cdl が auto 計算 (右端 +80 / 下端 +80)
|
|
218
220
|
*/
|
|
@@ -339,10 +341,15 @@ export function layout(diag: CdlDiagram): LaidDiagram {
|
|
|
339
341
|
|
|
340
342
|
// 同じ 2 点を結ぶ複数の線を弓なりに分ける (dragon #941)。 label と path が確定した後に動かす =
|
|
341
343
|
// 確定前に判定すると、 後から動く label の位置を見て分離を諦める。 座標を明示した edge
|
|
342
|
-
// (canvas pivot)
|
|
344
|
+
// (canvas pivot)、`curve`、`metro` は「指定した形が最終 path」 が公開契約なので対象から外す。
|
|
343
345
|
const positionedEdgeIds = new Set(
|
|
344
346
|
diag.edges
|
|
345
|
-
.filter(
|
|
347
|
+
.filter(
|
|
348
|
+
(e) =>
|
|
349
|
+
e.routing === "curve" ||
|
|
350
|
+
e.routing === "metro" ||
|
|
351
|
+
(e.posX1 !== undefined && e.posY1 !== undefined && e.posX2 !== undefined && e.posY2 !== undefined),
|
|
352
|
+
)
|
|
346
353
|
.map((e) => e.id),
|
|
347
354
|
);
|
|
348
355
|
bowSharedStraightPaths(edges, nodesFinal, positionedEdgeIds);
|
|
@@ -354,7 +361,7 @@ export function layout(diag: CdlDiagram): LaidDiagram {
|
|
|
354
361
|
pullOutgoingFirstCorner(edges, nodesFinal);
|
|
355
362
|
// 同じ辺に入る線の終点を辺に沿って離す (#611)。 経路を引き終えてから当てる = 終点を先に
|
|
356
363
|
// 動かすと引き方の分岐が変わり、 迂回していた線が箱を貫通する (実測で 0 件が 7 件になった)。
|
|
357
|
-
fanIncomingEndpoints(edges, nodesFinal);
|
|
364
|
+
const fannedTailShifts = fanIncomingEndpoints(edges, nodesFinal);
|
|
358
365
|
// 同じ通り道を並んで走る区間をずらす (#610)。 弓の後に当てる = 弓で分かれた線を数え直さない。
|
|
359
366
|
// 弧より先に当てる = ずらした後の形で交点を数えないと、 消えた交点に弧が残る。
|
|
360
367
|
//
|
|
@@ -364,6 +371,14 @@ export function layout(diag: CdlDiagram): LaidDiagram {
|
|
|
364
371
|
// 交わる所で上を通る側に弧を差し込む (#608)。 経路が確定してから当てる = 弓や迂回で
|
|
365
372
|
// 形が変わった後の交点を見る。 札を置く前に当てるので、 札は弧を含んだ経路の上に載る。
|
|
366
373
|
hopEdgeCrossings(edges);
|
|
374
|
+
// 終点を離す / 共有区間を分ける / 交差へ弧を入れる pass が全て終わった座標で札を見る (#1000)。
|
|
375
|
+
// 先に見ると、 その後に経路が動いて札が線を覆う形が戻る。
|
|
376
|
+
// 追従と最後の逃がし pass は、 `collectBBoxes` が含める下端の名札の箱上へのはみ出しと
|
|
377
|
+
// 縦列の題を同じ矩形で測る。 追従だけ箱本体で測ると、 置いた札を最後の pass が押し直す (#1002)。
|
|
378
|
+
const labelClearanceObstacles = collectBBoxes(containedLanesFinal, nodesFinal, []).filter(
|
|
379
|
+
(b) => b.kind === "node" || b.kind === "lane-label",
|
|
380
|
+
);
|
|
381
|
+
followLabelsToFannedTails(edges, labelClearanceObstacles, fannedTailShifts);
|
|
367
382
|
repositionParallelLabels(edges, nodesFinal);
|
|
368
383
|
// 縦にずらすだけでは逃げ場が無い札を、 自分の線に沿って動かす (#601)。 ここまでの
|
|
369
384
|
// 置き直しは全て縦方向で、 線に沿って動かす経路が無かった。 いま被っている札だけを
|
|
@@ -371,16 +386,11 @@ export function layout(diag: CdlDiagram): LaidDiagram {
|
|
|
371
386
|
slideLabelsAlongOwnPath(edges, nodesFinal);
|
|
372
387
|
// 経路を動かす pass が全て終わってから、 札を箱と札から離し直す (#659)。
|
|
373
388
|
//
|
|
374
|
-
// 上の chain 伝搬はここより前に走るため、
|
|
375
|
-
//
|
|
389
|
+
// 上の chain 伝搬はここより前に走るため、 その後の経路 pass が動かした最終経路との隙間を
|
|
390
|
+
// ここで見直す。
|
|
376
391
|
// ここで動かすのは札だけで、 箱も経路も触らない = 触ると直前の pass が確定させた形を崩す。
|
|
377
392
|
if (!isChainEdgeLabelDisabled()) {
|
|
378
|
-
edges = resolveLabelClearanceAfterPathPasses(
|
|
379
|
-
edges,
|
|
380
|
-
collectBBoxes(containedLanesFinal, nodesFinal, []).filter(
|
|
381
|
-
(b) => b.kind === "node" || b.kind === "lane-label",
|
|
382
|
-
),
|
|
383
|
-
);
|
|
393
|
+
edges = resolveLabelClearanceAfterPathPasses(edges, labelClearanceObstacles);
|
|
384
394
|
}
|
|
385
395
|
|
|
386
396
|
const bboxes = collectBBoxes(containedLanesFinal, nodesFinal, edges);
|
package/src/render/edges.tsx
CHANGED
package/src/render/nodes.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import { FunctionNode } from "../kinds/function";
|
|
|
7
7
|
import { StorageNode } from "../kinds/storage";
|
|
8
8
|
import { GenericNode } from "../kinds/generic";
|
|
9
9
|
import { MarkStartNode, MarkEndNode } from "../kinds/terminal-mark";
|
|
10
|
+
import { StationNode } from "../kinds/station";
|
|
10
11
|
import { DynShapeNode } from "../kinds/dyn-shape";
|
|
11
12
|
import { ChartLineNode, type ChartLineScale } from "../kinds/chart-line";
|
|
12
13
|
import { ChartPieNode } from "../kinds/chart-pie";
|
|
@@ -449,6 +450,8 @@ export function CdlNodeView({
|
|
|
449
450
|
return <MarkStartNode node={resolvedNode} active={active} />;
|
|
450
451
|
case "mark-end":
|
|
451
452
|
return <MarkEndNode node={resolvedNode} active={active} />;
|
|
453
|
+
case "station":
|
|
454
|
+
return <StationNode node={resolvedNode} active={active} />;
|
|
452
455
|
default:
|
|
453
456
|
// 新 NodeKind 25 種は GenericNode で汎用描画 (kind 別 icon / color / shape)
|
|
454
457
|
return <GenericNode node={resolvedNode} active={active} value={node.value} stateValues={stateValues} 値の名前={値の名前} />;
|
package/src/types.ts
CHANGED
|
@@ -186,6 +186,8 @@ export const NODE_KINDS = [
|
|
|
186
186
|
// 状態遷移図が使う。 印そのものが意味を持つので文字は描かない
|
|
187
187
|
"mark-start",
|
|
188
188
|
"mark-end",
|
|
189
|
+
// 路線図の駅 (#996) ... 線路上の点を小さな丸で示し、名前と説明は丸の外に置く
|
|
190
|
+
"station",
|
|
189
191
|
// 時系列のやり取り (#578) ... 1 つの箱が図を丸ごと描く。 縦が時間で、参加者は箱を持たない
|
|
190
192
|
"sequence-board",
|
|
191
193
|
] as const;
|
|
@@ -207,6 +209,18 @@ export type Side = "top" | "right" | "bottom" | "left";
|
|
|
207
209
|
*/
|
|
208
210
|
export type EdgeStyle = "solid" | "dotted-flow" | "dashed";
|
|
209
211
|
|
|
212
|
+
/**
|
|
213
|
+
* 線の道筋。
|
|
214
|
+
*
|
|
215
|
+
* - `"default"` ... 自動。 箱を避け、角を丸めた直角の線を引く。 書かない時も同じ
|
|
216
|
+
* - `"back-detour"` ... 箱の上を回って前の箱へ戻る
|
|
217
|
+
* - `"curve"` ... 始点から水平に出て終点へ水平に入る 3 次曲線。 途中の箱は避けず、
|
|
218
|
+
* 2 点が同じ高さなら直線になる。 終点の矢印は水平を向く
|
|
219
|
+
* - `"metro"` ... 横線と傾き ±1 の斜線だけで結び、角を半径 r の円弧で丸める。
|
|
220
|
+
* 途中の箱は避けず、2 点が同じ高さなら横 1 本になる
|
|
221
|
+
*/
|
|
222
|
+
export type EdgeRouting = "default" | "back-detour" | "curve" | "metro";
|
|
223
|
+
|
|
210
224
|
/** 辺の役目 (#618)。 `main` = 図の主となる道 */
|
|
211
225
|
export type EdgeRole = "main";
|
|
212
226
|
|
|
@@ -506,8 +520,8 @@ export type CdlNode = {
|
|
|
506
520
|
* 変わるのは kind の既定色を使って描かれる部分で、 枠線 / eyebrow の文字色 / icon /
|
|
507
521
|
* 強調表示中の行の値。 本文の文字色と背景は変わらない。
|
|
508
522
|
*
|
|
509
|
-
* 効くのは `actor` / `function` / `storage` / `event` / `card
|
|
510
|
-
*
|
|
523
|
+
* 効くのは `actor` / `function` / `storage` / `event` / `card`、`person` 以降の汎用 kind、
|
|
524
|
+
* 流れの印、路線図の駅。 それ以外は自前の配色を持つため指定しても変わらず、検査も通す。
|
|
511
525
|
* 一覧は `test/node-styling.test.tsx` が実装から計算した集合と突き合わせている。
|
|
512
526
|
*
|
|
513
527
|
* 既定色と同じ名前を明示指定しても色が変わる kind がある (既定色の hex が `render/tone.ts`
|
|
@@ -867,8 +881,12 @@ export type CdlEdge = {
|
|
|
867
881
|
labelOffsetX?: number;
|
|
868
882
|
/** label の y 方向 fine offset (default 0、 + で下、 - で上) */
|
|
869
883
|
labelOffsetY?: number;
|
|
870
|
-
/**
|
|
871
|
-
|
|
884
|
+
/**
|
|
885
|
+
* 線の道筋。 `default` は箱を避ける自動経路、 `back-detour` は箱の上を回る戻り線、
|
|
886
|
+
* `curve` は途中の箱を避けず、両端で水平になる 3 次曲線。 `metro` は途中の箱を避けず、
|
|
887
|
+
* 横線と傾き ±1 の斜線を半径 r の円弧で結ぶ。2 点が同じ高さなら横 1 本。書かなければ `default`。
|
|
888
|
+
*/
|
|
889
|
+
routing?: EdgeRouting;
|
|
872
890
|
/**
|
|
873
891
|
* CAR-549 SSOT ... label を edge path 上に「被せる」 か「離す」 かの制御 flag。
|
|
874
892
|
* default (false or undefined) = LABEL_INIT_CLEARANCE (34 world、 CAR-630 で 26 → 34 復帰) 離して配置。
|
package/src/visual-validate.ts
CHANGED
|
@@ -143,6 +143,7 @@ import {
|
|
|
143
143
|
TITLE_ROW_MIN_GAP,
|
|
144
144
|
rendersTitleText,
|
|
145
145
|
fitsOwnTitleWidth,
|
|
146
|
+
skipsNodeTitleWidthCheck,
|
|
146
147
|
drawsTitleOutsideBox,
|
|
147
148
|
titleScalesToFit,
|
|
148
149
|
MIN_TITLE_FONT_SIZE,
|
|
@@ -1980,6 +1981,8 @@ function runAxes(
|
|
|
1980
1981
|
const LIFELINE_W_THRESHOLD = 8;
|
|
1981
1982
|
for (const n of laid.nodes) {
|
|
1982
1983
|
if (n.w < LIFELINE_W_THRESHOLD) continue;
|
|
1984
|
+
// 駅は文字を収める箱ではなく線路上の印なので、箱向けの最小寸法を要求しない。
|
|
1985
|
+
if (n.kind === "station") continue;
|
|
1983
1986
|
if (n.w < MIN_NODE_W || n.h < MIN_NODE_H) {
|
|
1984
1987
|
push(
|
|
1985
1988
|
"node-visibility",
|
|
@@ -2016,6 +2019,8 @@ function runAxes(
|
|
|
2016
2019
|
// 自分で幅に合わせて切る種別も同じ。 字数から幅を出す下の近似は字の大きさ 22 を前提に
|
|
2017
2020
|
// しており、 それより小さい字で描く種別に当てると偽陽性になる (#469)
|
|
2018
2021
|
if (fitsOwnTitleWidth(n.kind)) continue;
|
|
2022
|
+
// 箱の外に出す駅名は小さな丸の幅で切らないため、箱幅を上限にする前提が成り立たない。
|
|
2023
|
+
if (skipsNodeTitleWidthCheck(n.kind)) continue;
|
|
2019
2024
|
// 常に非表示の node の文字は画面に出ないので、 切れようがない。
|
|
2020
2025
|
if (!isRenderedNode(n)) continue;
|
|
2021
2026
|
const titleTrimmed = n.title?.trim() ?? "";
|
|
@@ -3111,6 +3116,13 @@ function runAxes(
|
|
|
3111
3116
|
for (let i = 0; i + 1 < sorted.length; i++) {
|
|
3112
3117
|
const upper = sorted[i]!;
|
|
3113
3118
|
const lower = sorted[i + 1]!;
|
|
3119
|
+
// 横の範囲が重ならない箱は同じ lane に横並びで置かれた組なので、縦の隙間を測らない。
|
|
3120
|
+
if (
|
|
3121
|
+
upper.cx + upper.w / 2 <= lower.cx - lower.w / 2 ||
|
|
3122
|
+
lower.cx + lower.w / 2 <= upper.cx - upper.w / 2
|
|
3123
|
+
) {
|
|
3124
|
+
continue;
|
|
3125
|
+
}
|
|
3114
3126
|
const upperBottom = upper.cy + upper.h / 2;
|
|
3115
3127
|
const lowerTop = lower.cy - lower.h / 2;
|
|
3116
3128
|
const gap = lowerTop - upperBottom;
|
|
@@ -4214,17 +4226,25 @@ function runAxes(
|
|
|
4214
4226
|
y1: number;
|
|
4215
4227
|
x2: number;
|
|
4216
4228
|
y2: number;
|
|
4217
|
-
cmd: "L" | "Q" | "C";
|
|
4229
|
+
cmd: "L" | "Q" | "C" | "A";
|
|
4218
4230
|
ctrl?: ReadonlyArray<{ x: number; y: number }>;
|
|
4219
4231
|
}>,
|
|
4220
4232
|
peakY: number,
|
|
4221
4233
|
): Array<{ minX: number; maxX: number }> => {
|
|
4222
4234
|
const out: Array<{ minX: number; maxX: number }> = [];
|
|
4235
|
+
let 円弧の始点: { x: number; y: number } | null = null;
|
|
4223
4236
|
for (const s of segs) {
|
|
4224
|
-
if (
|
|
4225
|
-
|
|
4237
|
+
if (s.cmd === "A") {
|
|
4238
|
+
// 飛び越しの弧は同じ高さの通り道の上の小さな山で、山の前後は 1 本の通り道になる。
|
|
4239
|
+
円弧の始点 ??= { x: s.x1, y: s.y1 };
|
|
4240
|
+
continue;
|
|
4241
|
+
}
|
|
4242
|
+
const 始点 = 円弧の始点 ?? { x: s.x1, y: s.y1 };
|
|
4243
|
+
円弧の始点 = null;
|
|
4244
|
+
if (Math.abs(始点.y - s.y2) > 1) continue;
|
|
4245
|
+
if (Math.abs(始点.y - peakY) > 1) continue;
|
|
4226
4246
|
if (s.cmd !== "L" && !(s.ctrl ?? []).every((c) => Math.abs(c.y - peakY) <= 1)) continue;
|
|
4227
|
-
out.push({ minX: Math.min(
|
|
4247
|
+
out.push({ minX: Math.min(始点.x, s.x2), maxX: Math.max(始点.x, s.x2) });
|
|
4228
4248
|
}
|
|
4229
4249
|
return out;
|
|
4230
4250
|
};
|