@cardenelabs/cdl 0.13.1 → 0.15.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 +250 -1
- package/README.md +20 -1
- package/dist/index.cjs +2912 -1723
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -6
- package/dist/index.d.ts +25 -6
- package/dist/index.js +2911 -1724
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +1395 -230
- 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 +1395 -230
- package/dist/react.js.map +1 -1
- package/dist/{render-B5JHcUrb.d.cts → render-BS1E6Mlo.d.cts} +55 -2
- package/dist/{render-B5JHcUrb.d.ts → render-BS1E6Mlo.d.ts} +55 -2
- package/package.json +1 -1
- package/src/builder.ts +24 -0
- package/src/index.ts +2 -1
- package/src/kinds/chart-gauge.tsx +241 -0
- package/src/kinds/chart-pie.tsx +214 -53
- package/src/kinds/chart-radial.tsx +210 -0
- package/src/kinds/chart-stacked-bar.tsx +272 -0
- package/src/kinds/chart-stat.tsx +231 -0
- package/src/kinds/chart-waffle.tsx +197 -0
- package/src/kinds/draw-ratio.ts +44 -0
- package/src/kinds/dyn-shape.tsx +3 -6
- package/src/kinds/generic.tsx +18 -6
- package/src/kinds/mind-map.tsx +52 -22
- package/src/kinds/terminal-mark.tsx +96 -0
- package/src/kinds/tree.tsx +51 -21
- package/src/layout/edges.ts +30 -1
- package/src/layout/self-loop.ts +217 -0
- package/src/layout/spec.ts +81 -4
- package/src/layout/tokens.ts +15 -0
- package/src/presets.ts +33 -3
- package/src/render/edge-head.ts +45 -0
- package/src/render/edges.tsx +4 -2
- package/src/render/interactive-panel.tsx +2 -2
- package/src/render/nodes.tsx +25 -0
- package/src/render/payload-binding.ts +14 -3
- package/src/render/stage.tsx +41 -18
- package/src/render/tone.ts +52 -0
- package/src/types.ts +66 -2
- package/src/validate.ts +13 -5
- package/src/visual-validate.ts +137 -4
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 描く速さを phase の長さと別に決める (cdl#570)。
|
|
3
|
+
*
|
|
4
|
+
* `CdlPhase.drawRatio` は「`draw` の対象を描き終えるまでに phase の何割を使うか」 を表す。
|
|
5
|
+
* ここでは進みの引き伸ばしだけを持ち、 誰に掛けるかは `render/stage.tsx` が決める。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** 割合を書かない phase の既定。 従来どおり phase の終わりに描き終わる */
|
|
9
|
+
export const DRAW_RATIO_DEFAULT = 1;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 書かれた割合を、 使える値に直す。
|
|
13
|
+
*
|
|
14
|
+
* 範囲外 (0 以下 / 1 超 / NaN / Infinity / 未指定) は既定の 1 に落とす。 描く動きは
|
|
15
|
+
* 見え方であって図の意味ではないので、 書き間違いで図が出なくなる方が損が大きい。
|
|
16
|
+
* 書き間違いそのものは `validate` が warning で知らせる。
|
|
17
|
+
*
|
|
18
|
+
* **0 を通すとゼロ除算になる**。 `Number.isFinite` は 0 を通すため、 下限の判定を
|
|
19
|
+
* 別に置く必要がある。
|
|
20
|
+
*/
|
|
21
|
+
export function 描く割合を正す(ratio: number | undefined): number {
|
|
22
|
+
if (ratio === undefined) return DRAW_RATIO_DEFAULT;
|
|
23
|
+
if (!Number.isFinite(ratio)) return DRAW_RATIO_DEFAULT;
|
|
24
|
+
if (ratio <= 0 || ratio > 1) return DRAW_RATIO_DEFAULT;
|
|
25
|
+
return ratio;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* phase の進みを、 描く対象から見た進みに直す。
|
|
30
|
+
*
|
|
31
|
+
* 割合が 1 なら元の値をそのまま返す = 割合を書かない図では計算が入らない。
|
|
32
|
+
*
|
|
33
|
+
* 引き伸ばした結果は 1 で頭打ちにする。 描き終わってから phase が終わるまでの間、
|
|
34
|
+
* 対象は描き終わった姿のまま留まる。
|
|
35
|
+
*
|
|
36
|
+
* 進みが数でない場合は元の値をそのまま返す。 ここで 1 に丸めると、 進みの異常を
|
|
37
|
+
* 受け取る側 (`進みを畳む`) が持つ落とし方と二重になり、 どちらが効いたか読めなくなる。
|
|
38
|
+
*/
|
|
39
|
+
export function 描く進みに直す(progress: number, ratio: number | undefined): number {
|
|
40
|
+
const 割合 = 描く割合を正す(ratio);
|
|
41
|
+
if (割合 === DRAW_RATIO_DEFAULT) return progress;
|
|
42
|
+
if (!Number.isFinite(progress)) return progress;
|
|
43
|
+
return Math.min(1, progress / 割合);
|
|
44
|
+
}
|
package/src/kinds/dyn-shape.tsx
CHANGED
|
@@ -64,13 +64,13 @@ export function DynShapeNode({
|
|
|
64
64
|
case "rect":
|
|
65
65
|
return renderRect(shape, x, y, w, h, stateValues, fillColor, strokeColor, activeOpacity, node);
|
|
66
66
|
case "circle":
|
|
67
|
-
return renderCircle(shape, cx, cy, w, h, stateValues, fillColor, strokeColor, activeOpacity
|
|
67
|
+
return renderCircle(shape, cx, cy, w, h, stateValues, fillColor, strokeColor, activeOpacity);
|
|
68
68
|
case "arc":
|
|
69
|
-
return renderArc(shape, cx, cy, w, h, stateValues, fillColor, strokeColor, activeOpacity
|
|
69
|
+
return renderArc(shape, cx, cy, w, h, stateValues, fillColor, strokeColor, activeOpacity);
|
|
70
70
|
case "wave":
|
|
71
71
|
return <WaveShape shape={shape} x={x} y={y} w={w} h={h} stateValues={stateValues} fillColor={fillColor} strokeColor={strokeColor} opacity={activeOpacity} node={node} />;
|
|
72
72
|
case "polygon":
|
|
73
|
-
return renderPolygon(shape, cx, cy, stateValues, fillColor, strokeColor, activeOpacity
|
|
73
|
+
return renderPolygon(shape, cx, cy, stateValues, fillColor, strokeColor, activeOpacity);
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
|
|
@@ -162,7 +162,6 @@ function renderCircle(
|
|
|
162
162
|
fillColor: string,
|
|
163
163
|
strokeColor: string,
|
|
164
164
|
opacity: number,
|
|
165
|
-
node: LaidNode,
|
|
166
165
|
): JSX.Element {
|
|
167
166
|
const maxR = Math.min(w, h) / 2;
|
|
168
167
|
const r = shape.radius !== undefined ? resolveNumericAttr(shape.radius, stateValues, maxR) : maxR;
|
|
@@ -196,7 +195,6 @@ function renderArc(
|
|
|
196
195
|
fillColor: string,
|
|
197
196
|
strokeColor: string,
|
|
198
197
|
opacity: number,
|
|
199
|
-
node: LaidNode,
|
|
200
198
|
): JSX.Element {
|
|
201
199
|
const maxR = Math.min(w, h) / 2;
|
|
202
200
|
const outerR = shape.outerRadius ?? maxR;
|
|
@@ -325,7 +323,6 @@ function renderPolygon(
|
|
|
325
323
|
fillColor: string,
|
|
326
324
|
strokeColor: string,
|
|
327
325
|
opacity: number,
|
|
328
|
-
node: LaidNode,
|
|
329
326
|
): JSX.Element {
|
|
330
327
|
const sides = Math.min(12, Math.max(3, shape.sides));
|
|
331
328
|
const r = shape.radius !== undefined ? resolveNumericAttr(shape.radius, stateValues, 40) : 40;
|
package/src/kinds/generic.tsx
CHANGED
|
@@ -2,7 +2,12 @@ import type { JSX } from "react";
|
|
|
2
2
|
import type { LaidNode, NodeKind } from "../types";
|
|
3
3
|
import { toneColor } from "./node-tone";
|
|
4
4
|
import { interpolate } from "../render/utils";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
genericTextCentered,
|
|
7
|
+
genericTextShift,
|
|
8
|
+
genericTitleBaselineY,
|
|
9
|
+
rowBaselineY,
|
|
10
|
+
} from "../layout/spec";
|
|
6
11
|
import {
|
|
7
12
|
GENERIC_ROW_LEFT_SIZE_PX,
|
|
8
13
|
rowColumnWidth,
|
|
@@ -281,15 +286,22 @@ export function GenericNode({
|
|
|
281
286
|
// 共通経路は解決できない参照を `{名前}` のまま残すので、 読み手が気付ける。
|
|
282
287
|
const resolvedValue = value !== undefined ? interpolate(value, stateValues) : undefined;
|
|
283
288
|
|
|
284
|
-
|
|
285
|
-
|
|
289
|
+
/*
|
|
290
|
+
* shape 別の text 配置 (diamond / cloud は中心揃え、 rect 等は左上揃え)。
|
|
291
|
+
*
|
|
292
|
+
* **行がある時は中心へ寄せない** (#558)。 題を箱の中心に置くと、上端から数える行と必ず
|
|
293
|
+
* 重なる = 中心に題を置いたまま行を並べる場所が無い。 判定は `spec.ts` が持ち、
|
|
294
|
+
* 検査軸 (`title-row-overlap`) も同じ関数を引く。
|
|
295
|
+
*/
|
|
296
|
+
const hasRows = (node.rows?.length ?? 0) > 0;
|
|
297
|
+
const centered = genericTextCentered(node.kind, hasRows);
|
|
286
298
|
const textAnchor = centered ? ("middle" as const) : ("start" as const);
|
|
287
299
|
const textX = centered ? w / 2 : 20;
|
|
288
|
-
// cylinder は上 ellipse (ry=14) の下に text
|
|
289
|
-
const cylinderShift =
|
|
300
|
+
// cylinder は上 ellipse (ry=14) の下に text を置く。 ずらし量は `spec.ts` が持つ
|
|
301
|
+
const cylinderShift = genericTextShift(node.kind);
|
|
290
302
|
// 中心揃え時は h/2 を baseline 中心 (dominantBaseline=middle で計算するため text y を h/2 ベースに統一)
|
|
291
303
|
const eyebrowY = centered ? h / 2 - 26 : 32 + cylinderShift;
|
|
292
|
-
const titleY =
|
|
304
|
+
const titleY = genericTitleBaselineY(node.kind, h, hasRows);
|
|
293
305
|
const subtitleY = centered ? h / 2 + 28 : 96 + cylinderShift;
|
|
294
306
|
// icon 位置 (diamond / cloud は title の真右の少し上、 中心配置に合わせる)
|
|
295
307
|
const iconX = centered ? w / 2 + 40 : w - 48;
|
package/src/kinds/mind-map.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { JSX } from "react";
|
|
2
|
-
import { 使える幅, 幅に収める,
|
|
2
|
+
import { 使える幅, 幅に収める, 箱に積む, type 積む行 } from "./box-lines";
|
|
3
3
|
import type { LaidNode, Tone, MindBranchNode, MindBranchPayload } from "../types";
|
|
4
4
|
import { TONE } from "../render/tone";
|
|
5
5
|
import { resolveMindData } from "../render/payload-binding";
|
|
@@ -28,6 +28,49 @@ export const MIND_BOX_GAP = 24;
|
|
|
28
28
|
* 深い階層は parent 方向へ扇状展開。 depth 別 tone を rotate。
|
|
29
29
|
* curved edge = bezier で親子を接続、 node は rounded rect で読みやすさ優先。
|
|
30
30
|
*/
|
|
31
|
+
/**
|
|
32
|
+
* 箱の内側に取る余白。 図の外枠と放射の描画領域の間。
|
|
33
|
+
*
|
|
34
|
+
* **module に置く** (#538)。 描く側の中に閉じていた間、 検査側は同じ値を書き写すしかなく、
|
|
35
|
+
* 片方を変えた時にもう片方が古いまま残る。
|
|
36
|
+
*/
|
|
37
|
+
const MIND_PAD = 40;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 放射の中の箱と枝を並べる (#538)。
|
|
41
|
+
*
|
|
42
|
+
* 描く側 (`MindMapNode`) と検査側 (`visual-validate.ts` の `box-line-dropped`) の**唯一の入口**。
|
|
43
|
+
* 名前を解決できない (`mindData` を持たない) 節では `undefined` を返す = 描く側はそこで
|
|
44
|
+
* 空の `<g>` を返しており、 検査側も見る箱を持たない。
|
|
45
|
+
*/
|
|
46
|
+
export function 放射の中身(
|
|
47
|
+
node: LaidNode,
|
|
48
|
+
stateValues: Record<string, string> = {},
|
|
49
|
+
): { nodes: LayoutNode[]; edges: LayoutEdge[] } | undefined {
|
|
50
|
+
// 名前は状態を読める (#467)。 中心と枝の両方を解決層に通す
|
|
51
|
+
const mindData = node.mindData ? resolveMindData(node.mindData, stateValues) : undefined;
|
|
52
|
+
if (!mindData) return undefined;
|
|
53
|
+
return computeMindMapLayout(
|
|
54
|
+
mindData,
|
|
55
|
+
node.w / 2,
|
|
56
|
+
node.h / 2,
|
|
57
|
+
node.w - MIND_PAD * 2,
|
|
58
|
+
node.h - MIND_PAD * 2,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 放射の 1 箱に積む行 (名前 + 補足)。
|
|
64
|
+
*
|
|
65
|
+
* 字の大きさをここに集める (#538)。 理由は `木の行` と同じ。
|
|
66
|
+
*/
|
|
67
|
+
export function 放射の行(n: LayoutNode): 積む行[] {
|
|
68
|
+
return [
|
|
69
|
+
{ 役割: "title", text: n.title, fontSize: n.isRoot ? 15 : 13 },
|
|
70
|
+
{ 役割: "subtitle", text: n.subtitle ?? "", fontSize: 9 },
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
|
|
31
74
|
export function MindMapNode({
|
|
32
75
|
node,
|
|
33
76
|
active,
|
|
@@ -45,17 +88,8 @@ export function MindMapNode({
|
|
|
45
88
|
}): JSX.Element {
|
|
46
89
|
const x0 = node.cx - node.w / 2;
|
|
47
90
|
const y0 = node.cy - node.h / 2;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (!mindData) return <g transform={`translate(${x0} ${y0})`} />;
|
|
51
|
-
|
|
52
|
-
const PAD = 40;
|
|
53
|
-
const canvasCx = node.w / 2;
|
|
54
|
-
const canvasCy = node.h / 2;
|
|
55
|
-
const canvasW = node.w - PAD * 2;
|
|
56
|
-
const canvasH = node.h - PAD * 2;
|
|
57
|
-
|
|
58
|
-
const layout = computeMindMapLayout(mindData, canvasCx, canvasCy, canvasW, canvasH);
|
|
91
|
+
const layout = 放射の中身(node, stateValues);
|
|
92
|
+
if (!layout) return <g transform={`translate(${x0} ${y0})`} />;
|
|
59
93
|
|
|
60
94
|
/**
|
|
61
95
|
* 枝がどこまで伸びたか (0..1、 cdl#520)。 描く指定が無ければ常に 1 = 全部出ている。
|
|
@@ -105,20 +139,16 @@ export function MindMapNode({
|
|
|
105
139
|
// 枝が届いてから箱を出す。 届く前に出すと、繋がっていない箱が宙に浮く
|
|
106
140
|
if (!箱が出る(n.depth)) return null;
|
|
107
141
|
// 補足は箱の中の 2 行目に出す (#471)。 箱の高さは変えないため、 入らない時は落ちる
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
{ 役割: "title" as const, text: n.title, fontSize: n.isRoot ? 15 : 13 },
|
|
111
|
-
{ 役割: "subtitle" as const, text: n.subtitle ?? "", fontSize: 9 },
|
|
112
|
-
],
|
|
113
|
-
n.w,
|
|
114
|
-
n.h,
|
|
115
|
-
);
|
|
142
|
+
const 材料 = 放射の行(n);
|
|
143
|
+
const 行 = 箱に積む(材料, n.w, n.h);
|
|
116
144
|
// 判定は行数でなく「補足を渡したか」 で行う (理由は tree.tsx と同じ、 #471 Round 1)
|
|
117
145
|
const 補足あり = (n.subtitle ?? "").trim() !== "";
|
|
118
|
-
// 補足ありでは `箱に積む` が既に同じ処理を済ませているため、1
|
|
146
|
+
// 補足ありでは `箱に積む` が既に同じ処理を済ませているため、1 行の経路だけ計算する。
|
|
147
|
+
// 名前の大きさは `放射の行` から取る (#538、 理由は tree.tsx と同じ)
|
|
148
|
+
const 名前 = 材料[0]!;
|
|
119
149
|
const 収めた名前 = 補足あり
|
|
120
150
|
? undefined
|
|
121
|
-
: 幅に収める(
|
|
151
|
+
: 幅に収める(名前.text, 名前.fontSize, 使える幅(n.w));
|
|
122
152
|
const 文字の色 = n.isRoot
|
|
123
153
|
? "var(--cdl-text-on-tone, #ffffff)"
|
|
124
154
|
: "var(--cdl-chip-text, #1a1f2a)";
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { JSX } from "react";
|
|
2
|
+
import type { LaidNode } from "../types";
|
|
3
|
+
import { nodeAccent } from "./node-tone";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 流れの **始まりと終わりの印** (#560)。
|
|
7
|
+
*
|
|
8
|
+
* 状態遷移図は始まりを塗った丸、終わりを輪で囲んだ丸で示す。 それまでこれに当たる種別が無く、
|
|
9
|
+
* 箱の小見出しに「初期」「最終」 と書いて代えていた = 印であるべきものが文字になっていた。
|
|
10
|
+
*
|
|
11
|
+
* **文字は描かない**。 印そのものが意味を持つので、丸の中に名前を入れても読めない。
|
|
12
|
+
* `rendersTitleText` はこの 2 種別を「描かない」 側に置いており、全種別を描いて突き合わせる
|
|
13
|
+
* 検査 (`test/node-title-render.test.tsx`) が実際の描画と一致していることを見る。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** 終わりの印の、内側の丸の直径を外側の輪の何割にするか */
|
|
17
|
+
export const MARK_INNER_RATIO = 0.56;
|
|
18
|
+
|
|
19
|
+
/** 終わりの印の輪の太さ。 光っている時は太くする (他の種別の枠線と同じ考え方) */
|
|
20
|
+
const RING_WIDTH_ACTIVE = 6;
|
|
21
|
+
const RING_WIDTH_IDLE = 4;
|
|
22
|
+
|
|
23
|
+
/** 印の既定の色。 `tone` を書けばそちらが勝つ */
|
|
24
|
+
const MARK_DEFAULT = "var(--cdl-tone-accent, #2d6a8f)";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 印の半径。 **箱の短い方の辺いっぱい** に描く。
|
|
28
|
+
*
|
|
29
|
+
* 線は箱の縁で止まるので、印を箱より小さく描くと矢の先と印の間が空いて線が浮く
|
|
30
|
+
* (半分の大きさにすると 24 world 空いた)。 縁まで描けば接する。
|
|
31
|
+
*
|
|
32
|
+
* 短い方から取るのは、段の高さが全ての段で揃えられるため。 同じ段に背の高い箱があると
|
|
33
|
+
* 印の箱も背が高くなり、長い方に合わせると印だけ縦長の楕円になる。 その時は横幅のまま
|
|
34
|
+
* 上下の中央に置く = 上下には隙間が残るが、丸のままで読める側を採る。
|
|
35
|
+
*/
|
|
36
|
+
function markRadius(node: LaidNode): number {
|
|
37
|
+
return Math.min(node.w, node.h) / 2;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 流れの始まり = 塗った丸。
|
|
42
|
+
*
|
|
43
|
+
* **光っても形を変えない**。 他の種別は光った時に枠線を太くするが、塗った丸には太らせる枠が
|
|
44
|
+
* 無い。 枠を足すと線の中心が半径に乗るぶん丸が箱の外へ出て、線との接点がずれる (実測で
|
|
45
|
+
* 太さ 4 の枠を足すと外径が 96 から 100 になった)。 色は `tone` で変えられる。
|
|
46
|
+
*/
|
|
47
|
+
export function MarkStartNode({ node }: { node: LaidNode; active: boolean }): JSX.Element {
|
|
48
|
+
const 色 = nodeAccent(node, MARK_DEFAULT);
|
|
49
|
+
return (
|
|
50
|
+
<g transform={`translate(${node.cx - node.w / 2} ${node.cy - node.h / 2})`}>
|
|
51
|
+
<circle
|
|
52
|
+
data-cdl-role="node-body"
|
|
53
|
+
data-cdl-mark="start"
|
|
54
|
+
cx={node.w / 2}
|
|
55
|
+
cy={node.h / 2}
|
|
56
|
+
r={markRadius(node)}
|
|
57
|
+
fill={色}
|
|
58
|
+
/>
|
|
59
|
+
</g>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 流れの終わり = 輪で囲んだ丸。
|
|
65
|
+
*
|
|
66
|
+
* 輪と中身は同じ色で描く。 別の色にすると 2 つの意味を持つ印に見えるが、表しているのは
|
|
67
|
+
* 「ここで終わる」 の 1 つだけ。
|
|
68
|
+
*/
|
|
69
|
+
export function MarkEndNode({ node, active }: { node: LaidNode; active: boolean }): JSX.Element {
|
|
70
|
+
const 色 = nodeAccent(node, MARK_DEFAULT);
|
|
71
|
+
const r = markRadius(node);
|
|
72
|
+
const 輪の太さ = active ? RING_WIDTH_ACTIVE : RING_WIDTH_IDLE;
|
|
73
|
+
return (
|
|
74
|
+
<g transform={`translate(${node.cx - node.w / 2} ${node.cy - node.h / 2})`}>
|
|
75
|
+
<circle
|
|
76
|
+
data-cdl-role="node-body"
|
|
77
|
+
data-cdl-mark="end"
|
|
78
|
+
cx={node.w / 2}
|
|
79
|
+
cy={node.h / 2}
|
|
80
|
+
// 輪は線の中心を半径に置くので、外側へ太さの半分だけ出る。 印が箱の短い方の辺に
|
|
81
|
+
// 収まるよう、その分だけ縮める
|
|
82
|
+
r={Math.max(0, r - 輪の太さ / 2)}
|
|
83
|
+
fill="var(--cdl-node-fill, #ffffff)"
|
|
84
|
+
stroke={色}
|
|
85
|
+
strokeWidth={輪の太さ}
|
|
86
|
+
/>
|
|
87
|
+
<circle
|
|
88
|
+
data-cdl-role="node-inner"
|
|
89
|
+
cx={node.w / 2}
|
|
90
|
+
cy={node.h / 2}
|
|
91
|
+
r={Math.max(0, r * MARK_INNER_RATIO)}
|
|
92
|
+
fill={色}
|
|
93
|
+
/>
|
|
94
|
+
</g>
|
|
95
|
+
);
|
|
96
|
+
}
|
package/src/kinds/tree.tsx
CHANGED
|
@@ -2,7 +2,7 @@ import type { JSX } from "react";
|
|
|
2
2
|
import type { LaidNode, TreeNodePayload } from "../types";
|
|
3
3
|
import { TONE } from "../render/tone";
|
|
4
4
|
import { resolveTreeData } from "../render/payload-binding";
|
|
5
|
-
import { 使える幅, 幅に収める, 箱に積む, type 積んだ行 } from "../kinds/box-lines";
|
|
5
|
+
import { 使える幅, 幅に収める, 箱に積む, type 積む行, type 積んだ行 } from "../kinds/box-lines";
|
|
6
6
|
import { 進みを畳む, 伸ばすdash, 段の伸び } from "./draw-progress";
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -50,6 +50,49 @@ function 箱の文字({
|
|
|
50
50
|
));
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* 箱の内側に取る余白。 図の外枠と木の描画領域の間。
|
|
55
|
+
*
|
|
56
|
+
* **module に置く** (#538)。 描く側の中に閉じていた間、 検査側は同じ値を書き写すしかなく、
|
|
57
|
+
* 片方を変えた時にもう片方が古いまま残る。
|
|
58
|
+
*/
|
|
59
|
+
const PAD_TOP = 40;
|
|
60
|
+
const PAD_RIGHT = 24;
|
|
61
|
+
const PAD_BOTTOM = 40;
|
|
62
|
+
const PAD_LEFT = 24;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 木の中の箱と枝を並べる (#538)。
|
|
66
|
+
*
|
|
67
|
+
* 描く側 (`TreeNode`) と検査側 (`visual-validate.ts` の `box-line-dropped`) の**唯一の入口**。
|
|
68
|
+
* 余白と canvas の出し方を 2 箇所に書くと、 余白を変えた時に検査だけが古い箱を見る。
|
|
69
|
+
*/
|
|
70
|
+
export function 木の中身(
|
|
71
|
+
node: LaidNode,
|
|
72
|
+
stateValues: Record<string, string> = {},
|
|
73
|
+
): { nodes: LayoutNode[]; edges: LayoutEdge[] } {
|
|
74
|
+
// 名前は状態を読める (#467)。 解決できない名前は書いたまま残して印を付ける
|
|
75
|
+
const treeNodes = resolveTreeData(node.treeData ?? [], stateValues);
|
|
76
|
+
return computeTreeLayout(
|
|
77
|
+
treeNodes,
|
|
78
|
+
node.w - PAD_LEFT - PAD_RIGHT,
|
|
79
|
+
node.h - PAD_TOP - PAD_BOTTOM,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 木の 1 箱に積む行 (名前 + 補足)。
|
|
85
|
+
*
|
|
86
|
+
* 字の大きさをここに集める (#538)。 描く側に直書きすると、 検査側が同じ数を書き写すことになり、
|
|
87
|
+
* 大きさを変えた時に「入るか」 の判定だけが古い値で動く。
|
|
88
|
+
*/
|
|
89
|
+
export function 木の行(n: LayoutNode): 積む行[] {
|
|
90
|
+
return [
|
|
91
|
+
{ 役割: "title", text: n.title, fontSize: n.depth === 0 ? 13 : 12 },
|
|
92
|
+
{ 役割: "subtitle", text: n.subtitle ?? "", fontSize: 9 },
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
|
|
53
96
|
export function TreeNode({
|
|
54
97
|
node,
|
|
55
98
|
active,
|
|
@@ -67,18 +110,9 @@ export function TreeNode({
|
|
|
67
110
|
}): JSX.Element {
|
|
68
111
|
const x0 = node.cx - node.w / 2;
|
|
69
112
|
const y0 = node.cy - node.h / 2;
|
|
70
|
-
// 名前は状態を読める (#467)。 解決できない名前は書いたまま残して印を付ける
|
|
71
|
-
const treeNodes = resolveTreeData(node.treeData ?? [], stateValues);
|
|
72
113
|
const gradientId = `tree-root-grad-${node.id}`;
|
|
73
114
|
|
|
74
|
-
const
|
|
75
|
-
const PAD_RIGHT = 24;
|
|
76
|
-
const PAD_BOTTOM = 40;
|
|
77
|
-
const PAD_LEFT = 24;
|
|
78
|
-
const canvasW = node.w - PAD_LEFT - PAD_RIGHT;
|
|
79
|
-
const canvasH = node.h - PAD_TOP - PAD_BOTTOM;
|
|
80
|
-
|
|
81
|
-
const layout = computeTreeLayout(treeNodes, canvasW, canvasH);
|
|
115
|
+
const layout = 木の中身(node, stateValues);
|
|
82
116
|
|
|
83
117
|
/**
|
|
84
118
|
* 枝がどこまで伸びたか (0..1、 cdl#520)。 描く指定が無ければ常に 1 = 全部出ている。
|
|
@@ -153,22 +187,18 @@ export function TreeNode({
|
|
|
153
187
|
const accent = depthColors[Math.min(n.depth, depthColors.length - 1)] ?? depthColors[0]!;
|
|
154
188
|
const isRoot = n.depth === 0;
|
|
155
189
|
// 補足は箱の中の 2 行目に出す (#471)。 箱の高さは変えないため、 入らない時は落ちる
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
{ 役割: "title" as const, text: n.title, fontSize: isRoot ? 13 : 12 },
|
|
159
|
-
{ 役割: "subtitle" as const, text: n.subtitle ?? "", fontSize: 9 },
|
|
160
|
-
],
|
|
161
|
-
n.w,
|
|
162
|
-
n.h,
|
|
163
|
-
);
|
|
190
|
+
const 材料 = 木の行(n);
|
|
191
|
+
const 行 = 箱に積む(材料, n.w, n.h);
|
|
164
192
|
// **判定は行数でなく「補足を渡したか」 で行う**。 行数で見ると、 箱が低くて補足が落ちた時に
|
|
165
193
|
// 従来の 1 行の経路へ戻り、 幅で切っていない名前が箱から横にはみ出す (#471 Round 1)
|
|
166
194
|
const 補足あり = (n.subtitle ?? "").trim() !== "";
|
|
167
195
|
// 補足ありでは `箱に積む` が既に同じ処理を済ませているため、1 行の経路だけ計算する。
|
|
168
|
-
// 収める手段は縮めるのが先で、切るのは下限まで縮めても入らない時だけ (#534)
|
|
196
|
+
// 収める手段は縮めるのが先で、切るのは下限まで縮めても入らない時だけ (#534)。
|
|
197
|
+
// 名前の大きさは `木の行` から取る = ここに書き写すと 2 経路で別の大きさになる (#538)
|
|
198
|
+
const 名前 = 材料[0]!;
|
|
169
199
|
const 収めた名前 = 補足あり
|
|
170
200
|
? undefined
|
|
171
|
-
: 幅に収める(
|
|
201
|
+
: 幅に収める(名前.text, 名前.fontSize, 使える幅(n.w));
|
|
172
202
|
return (
|
|
173
203
|
<g
|
|
174
204
|
key={`n-${n.id}`}
|
package/src/layout/edges.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
PARALLEL_EDGE_BOW,
|
|
24
24
|
SYMMETRIC_PAIR_SHIFT,
|
|
25
25
|
} from "./spec";
|
|
26
|
+
import { isSelfLoop, layoutSelfLoops } from "./self-loop";
|
|
26
27
|
import { DEFAULT_LABEL_MARGIN } from "./tokens";
|
|
27
28
|
|
|
28
29
|
/** 縦 detour が obstacle の外側を通る時の余白 (world unit)。 */
|
|
@@ -162,6 +163,10 @@ export function layoutEdges(diag: CdlDiagram, nodes: LaidNode[], lanes: LaidLane
|
|
|
162
163
|
// 全 4 field 未指定なら従来通り orthogonal L 字 routing で保証。
|
|
163
164
|
const positionedEdges: LaidEdge[] = [];
|
|
164
165
|
const autoEdges: typeof diag.edges = [];
|
|
166
|
+
// 自分から自分へ戻る辺は、両端が同じ節なので 2 節の位置関係から向きを決める routing が
|
|
167
|
+
// 使えない (どちら側へ出すかを節の外から決められない)。 専用の経路にまとめて渡す (#560)。
|
|
168
|
+
// 座標を固定した辺 (`posX1` 等) は書いた人が形を決めているので、そちらを先に取る
|
|
169
|
+
const selfLoopEdges: typeof diag.edges = [];
|
|
165
170
|
for (const e of diag.edges) {
|
|
166
171
|
if (e.posX1 !== undefined && e.posY1 !== undefined && e.posX2 !== undefined && e.posY2 !== undefined) {
|
|
167
172
|
const from = nmap.get(e.from);
|
|
@@ -180,11 +185,12 @@ export function layoutEdges(diag: CdlDiagram, nodes: LaidNode[], lanes: LaidLane
|
|
|
180
185
|
labelY: midY + (e.labelOffsetY ?? 0),
|
|
181
186
|
labelAnchor: "middle",
|
|
182
187
|
});
|
|
188
|
+
} else if (isSelfLoop(e)) {
|
|
189
|
+
selfLoopEdges.push(e);
|
|
183
190
|
} else {
|
|
184
191
|
autoEdges.push(e);
|
|
185
192
|
}
|
|
186
193
|
}
|
|
187
|
-
|
|
188
194
|
const prepared = autoEdges.map((e) => {
|
|
189
195
|
const from = nmap.get(e.from);
|
|
190
196
|
const to = nmap.get(e.to);
|
|
@@ -207,6 +213,28 @@ export function layoutEdges(diag: CdlDiagram, nodes: LaidNode[], lanes: LaidLane
|
|
|
207
213
|
distributeBySides(list);
|
|
208
214
|
}
|
|
209
215
|
|
|
216
|
+
// 輪はここで経路にする (#560)。 どちら側へ出すかは「他の線が使っていない辺」 で決めるので、
|
|
217
|
+
// 側が確定した後でないと選べない (`distributeBySides` が上で側を書き換える)。
|
|
218
|
+
//
|
|
219
|
+
// 起点側と終点側の両方を数える = 入ってくる線も辺を 1 つ塞ぐ。 `back-detour` は routing が
|
|
220
|
+
// 側を上へ強制するので、その後の値で数える (`routePath` に渡すのと同じ値)。
|
|
221
|
+
const occupiedBySide = new Map<string, Set<Side>>();
|
|
222
|
+
const 使う = (nodeId: string, side: Side) => {
|
|
223
|
+
const s = occupiedBySide.get(nodeId) ?? new Set<Side>();
|
|
224
|
+
s.add(side);
|
|
225
|
+
occupiedBySide.set(nodeId, s);
|
|
226
|
+
};
|
|
227
|
+
for (const p of prepared) {
|
|
228
|
+
const backDetour = p.e.routing === "back-detour";
|
|
229
|
+
使う(p.e.from, backDetour ? "top" : p.fromSide);
|
|
230
|
+
使う(p.e.to, backDetour ? "top" : p.toSide);
|
|
231
|
+
}
|
|
232
|
+
for (const le of positionedEdges) {
|
|
233
|
+
使う(le.from, le.fromSide);
|
|
234
|
+
使う(le.to, le.toSide);
|
|
235
|
+
}
|
|
236
|
+
const loopEdges = layoutSelfLoops(selfLoopEdges, nmap, occupiedBySide);
|
|
237
|
+
|
|
210
238
|
// CAR-429 (CAR-426-B) ... 全 edge の起点 / 終点 / L 字 corner は node 辺 Y 中央固定 (offset=0 SSOT)。
|
|
211
239
|
// 旧 fan-in / fan-out / parallel の sidepoint 分散 (±60/±120) は削除済で、
|
|
212
240
|
// path routing (d 属性) は offset=0 固定で描画される (視覚的に起点認識明確化)。
|
|
@@ -1090,6 +1118,7 @@ export function layoutEdges(diag: CdlDiagram, nodes: LaidNode[], lanes: LaidLane
|
|
|
1090
1118
|
// 元 diag.edges の順序を保持するため id 順で lookup、 positioned は auto より先に列挙。
|
|
1091
1119
|
const laidById = new Map<string, LaidEdge>();
|
|
1092
1120
|
for (const le of autoResults) laidById.set(le.id, le);
|
|
1121
|
+
for (const le of loopEdges) laidById.set(le.id, le);
|
|
1093
1122
|
for (const le of positionedEdges) laidById.set(le.id, le);
|
|
1094
1123
|
return diag.edges.map((e) => laidById.get(e.id)!).filter((e) => e !== undefined);
|
|
1095
1124
|
}
|