@cardenelabs/cdl 0.14.0 → 0.16.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 +249 -1
- package/README.md +20 -1
- package/dist/index.cjs +1243 -92
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -5
- package/dist/index.d.ts +27 -5
- package/dist/index.js +1242 -93
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +1208 -88
- 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 +1208 -88
- 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/terminal-mark.tsx +96 -0
- 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 +68 -4
- 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 +48 -1
|
@@ -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;
|
|
@@ -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/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
|
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import type { CdlEdge, LaidEdge, LaidNode, Side } from "../types";
|
|
2
|
+
import { CLEARANCE_LABEL_LABEL } from "./clearance-constants";
|
|
3
|
+
import { computeLabelBoxW, LABEL_TO_PATH_CLEARANCE, labelPillH } from "./spec";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 自分から自分へ戻る関係の経路 (#560)。
|
|
7
|
+
*
|
|
8
|
+
* 状態遷移図の自己遷移 (催促する)、シーケンス図の自分宛て (控えを書く)、ER の自己関係
|
|
9
|
+
* (REPORTS TO) の 3 図が使う。 それまで `validate` が弾いていた。
|
|
10
|
+
*
|
|
11
|
+
* 形は **箱の 1 辺から出て、外へ膨らんで、同じ辺へ戻る** 三次曲線 1 本。 折れ線ではなく
|
|
12
|
+
* 曲線にしたのは、線の傾きを見る検査 (軸 13) が曲線を対象外にしているのと同じ理由で、
|
|
13
|
+
* 輪の途中の傾きに意味が無いため。 折れ線で描くと「L 字が破綻している」 と読まれる。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** 輪が辺から外へ届く距離 (world)。 制御点ではなく、曲線が実際に到達する距離 */
|
|
17
|
+
export const SELF_LOOP_OUT = 72;
|
|
18
|
+
|
|
19
|
+
/** 出口と入口の間隔 (world)。 辺のどれだけを使うか */
|
|
20
|
+
export const SELF_LOOP_SPAN = 64;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 同じ節に輪が 2 本以上ある時、 2 本目以降を外へずらす幅 (world)。
|
|
24
|
+
*
|
|
25
|
+
* `SELF_LOOP_OUT` と揃えて **3 の倍数** にしてある。 制御点の位置は 3 分の 4 倍で求めるので、
|
|
26
|
+
* 3 の倍数でないと座標が割り切れず、小数点の混じった座標を検査 (軸 33) が拾う。
|
|
27
|
+
*/
|
|
28
|
+
export const SELF_LOOP_STAGGER = 45;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 書かなかった時にどちら側へ出すかを探す順。
|
|
32
|
+
*
|
|
33
|
+
* 節は縦に積むので、上下は前後の節との線が先に使う。 右を最初に見る。
|
|
34
|
+
*
|
|
35
|
+
* **他の線が使っている辺は避ける**。 同じ辺に出すと 2 つ壊れる = 横へ抜ける線が輪の内側を
|
|
36
|
+
* 通って重なり、その線の起点と輪の起点が別の高さになって「1 つの節から出ていない」 ように
|
|
37
|
+
* 見える (検査の軸 2 と軸 61 が両方鳴る、実測した)。 輪は終点を辺の中央付近に置く必要が
|
|
38
|
+
* あるので (軸 63)、同じ辺の中で高さをずらして避けることはできない。
|
|
39
|
+
*/
|
|
40
|
+
export const SELF_LOOP_SIDE_ORDER: readonly Side[] = ["right", "top", "left", "bottom"];
|
|
41
|
+
|
|
42
|
+
/** どの辺も塞がっている時に使う辺。 避けきれないので、少なくとも同じ場所に落とす */
|
|
43
|
+
export const SELF_LOOP_SIDE_FALLBACK: Side = "right";
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 三次曲線の制御点を辺からどれだけ離すか、を決める比。
|
|
47
|
+
*
|
|
48
|
+
* 両端が辺の上にあり、制御点を 2 つとも同じだけ外へ置いた三次曲線は、真ん中 (t=0.5) で
|
|
49
|
+
* 制御点の **4 分の 3** までしか届かない。 届かせたい距離から逆算する。
|
|
50
|
+
*
|
|
51
|
+
* 係数を式で持つのは、`SELF_LOOP_OUT` を変えた時に輪の実寸と語の置き場所が一緒に動く
|
|
52
|
+
* 必要があるため。 書き写すと片方だけ動いて語が輪の中に入る。
|
|
53
|
+
*/
|
|
54
|
+
export const CUBIC_APEX_RATIO = 0.75;
|
|
55
|
+
|
|
56
|
+
/** 終点が辺の中央から離れてよい上限を決める余白。 軸 63 の判定と同じ値 */
|
|
57
|
+
const ENDPOINT_CENTER_MARGIN = 8;
|
|
58
|
+
|
|
59
|
+
/** 出口と入口が重なって輪が線 1 本に潰れない最小の間隔 */
|
|
60
|
+
const SPAN_MIN = 8;
|
|
61
|
+
|
|
62
|
+
function controlOffset(out: number): number {
|
|
63
|
+
return out / CUBIC_APEX_RATIO;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 辺の中心と、その辺の外向き */
|
|
67
|
+
function sideGeometry(n: LaidNode, side: Side): { x: number; y: number; ux: number; uy: number } {
|
|
68
|
+
switch (side) {
|
|
69
|
+
case "right":
|
|
70
|
+
return { x: n.cx + n.w / 2, y: n.cy, ux: 1, uy: 0 };
|
|
71
|
+
case "left":
|
|
72
|
+
return { x: n.cx - n.w / 2, y: n.cy, ux: -1, uy: 0 };
|
|
73
|
+
case "top":
|
|
74
|
+
return { x: n.cx, y: n.cy - n.h / 2, ux: 0, uy: -1 };
|
|
75
|
+
case "bottom":
|
|
76
|
+
return { x: n.cx, y: n.cy + n.h / 2, ux: 0, uy: 1 };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 出口と入口の間隔を、その辺に収まる範囲へ丸める。
|
|
82
|
+
*
|
|
83
|
+
* 終点が辺の中央から離れすぎると軸 63 (`arrow-endpoint-center`) が「角に刺さっている」 と
|
|
84
|
+
* 判定する。 判定の境目は辺の半分から 8 を引いた値なので、そこへ収まるまで縮める。
|
|
85
|
+
* 縮めても最小値は残す = 0 にすると出口と入口が重なって輪が潰れる。
|
|
86
|
+
*/
|
|
87
|
+
export function fitSpan(sideLength: number, wanted: number): number {
|
|
88
|
+
const 限度 = sideLength - 2 * ENDPOINT_CENTER_MARGIN;
|
|
89
|
+
return Math.max(SPAN_MIN, Math.min(wanted, 限度));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 1 つの節にある輪 `count` 本に、他の線が使っていない辺を **1 本ずつ配る**。
|
|
94
|
+
*
|
|
95
|
+
* 「使っている」 は `occupied` が持つ = 呼出側が、輪でない辺の起点側と終点側の両方を入れる。
|
|
96
|
+
* 入れ忘れると輪がその線の上に重なるので、呼出側は 2 経路 (自動で引く線と、座標を固定した線)
|
|
97
|
+
* のどちらも入れる。
|
|
98
|
+
*
|
|
99
|
+
* **1 辺に 2 本まとめない**。 同じ辺に重ねると輪が入れ子になり、内側の輪の語を外側の輪の
|
|
100
|
+
* 外へ出さざるをえない = 語が自分の線から遠のく (検査の軸 7 が「どの線の語か読めない」 と
|
|
101
|
+
* 判定する。 実測で 3 本を入れ子にすると一番内側が 160 離れて error になった)。
|
|
102
|
+
*
|
|
103
|
+
* 空いている辺より輪が多い時だけ、余った分を先頭の辺へ戻して重ねる。 そこは重なる形しか
|
|
104
|
+
* 残っていないので、少なくとも同じ場所に集める。
|
|
105
|
+
*/
|
|
106
|
+
export function pickFreeSides(occupied: ReadonlySet<Side> | undefined, count: number): Side[] {
|
|
107
|
+
const 空き = SELF_LOOP_SIDE_ORDER.filter((s) => !occupied?.has(s));
|
|
108
|
+
const 使う = 空き.length > 0 ? 空き : [SELF_LOOP_SIDE_FALLBACK];
|
|
109
|
+
return Array.from({ length: count }, (_, i) => 使う[i % 使う.length]!);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 同じ節・同じ辺の輪をまとめて経路にする。
|
|
114
|
+
*
|
|
115
|
+
* **語は輪の外 (膨らんだ先) に置く**。 輪の上に載せると曲線が語で隠れ、どちら回りかが読めない。
|
|
116
|
+
*
|
|
117
|
+
* 2 本以上ある時、輪は入れ子になる = 内側の輪の外は、外側の輪の **内側** にある。
|
|
118
|
+
* 輪ごとに「自分の外」 へ語を置くと、内側の語が外側の輪の上に載る。 語は全部
|
|
119
|
+
* **一番外の輪のさらに外** に揃えて置き、辺に沿って積んで語どうしの重なりを外す。
|
|
120
|
+
*
|
|
121
|
+
* 1 本だけの時は積む必要が無いので、辺の中央の真横に 1 つ置くだけになる。
|
|
122
|
+
*/
|
|
123
|
+
export function routeSelfLoopGroup(edges: CdlEdge[], node: LaidNode, side: Side): LaidEdge[] {
|
|
124
|
+
const horizontal = side === "left" || side === "right";
|
|
125
|
+
const g = sideGeometry(node, side);
|
|
126
|
+
const span = fitSpan(horizontal ? node.h : node.w, SELF_LOOP_SPAN);
|
|
127
|
+
const half = span / 2;
|
|
128
|
+
// 辺に沿った向き (出口 → 入口)。 横向きの辺なら上から下、縦向きの辺なら左から右
|
|
129
|
+
const 沿 = horizontal ? { x: 0, y: 1 } : { x: 1, y: 0 };
|
|
130
|
+
|
|
131
|
+
// 制御点は曲線より外にあるので、path から囲みを起こす検査は輪を実寸より大きく見る。
|
|
132
|
+
// 語はその外側に置く = 大きめに見た囲みとも重ならない
|
|
133
|
+
const 最外 = controlOffset(SELF_LOOP_OUT + (edges.length - 1) * SELF_LOOP_STAGGER);
|
|
134
|
+
const 語まで = 最外 + LABEL_TO_PATH_CLEARANCE;
|
|
135
|
+
|
|
136
|
+
// 積む向きの語の大きさ。 横の辺なら縦に積むので高さ、縦の辺なら横に積むので幅
|
|
137
|
+
const 語幅 = edges.map((e) =>
|
|
138
|
+
horizontal ? labelPillH(e.sub) : computeLabelBoxW(e.label, e.sub),
|
|
139
|
+
);
|
|
140
|
+
const 位置: number[] = [];
|
|
141
|
+
let 積 = 0;
|
|
142
|
+
for (const w of 語幅) {
|
|
143
|
+
位置.push(積 + w / 2);
|
|
144
|
+
積 += w + CLEARANCE_LABEL_LABEL;
|
|
145
|
+
}
|
|
146
|
+
const 全長 = Math.max(0, 積 - CLEARANCE_LABEL_LABEL);
|
|
147
|
+
|
|
148
|
+
return edges.map((edge, index) => {
|
|
149
|
+
const c = controlOffset(SELF_LOOP_OUT + index * SELF_LOOP_STAGGER);
|
|
150
|
+
const x1 = g.x - 沿.x * half;
|
|
151
|
+
const y1 = g.y - 沿.y * half;
|
|
152
|
+
const x2 = g.x + 沿.x * half;
|
|
153
|
+
const y2 = g.y + 沿.y * half;
|
|
154
|
+
const ずれ = 位置[index]! - 全長 / 2;
|
|
155
|
+
return {
|
|
156
|
+
...edge,
|
|
157
|
+
d: `M ${x1} ${y1} C ${x1 + g.ux * c} ${y1 + g.uy * c} ${x2 + g.ux * c} ${y2 + g.uy * c} ${x2} ${y2}`,
|
|
158
|
+
fromSide: side,
|
|
159
|
+
toSide: side,
|
|
160
|
+
labelX: g.x + g.ux * 語まで + 沿.x * ずれ + (edge.labelOffsetX ?? 0),
|
|
161
|
+
labelY: g.y + g.uy * 語まで + 沿.y * ずれ + (edge.labelOffsetY ?? 0),
|
|
162
|
+
labelAnchor: side === "left" ? "end" : side === "right" ? "start" : "middle",
|
|
163
|
+
} satisfies LaidEdge;
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** その辺が自分から自分へ戻るか */
|
|
168
|
+
export function isSelfLoop(e: { from: string; to: string }): boolean {
|
|
169
|
+
return e.from === e.to;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 図の中の輪をまとめて経路にする。
|
|
174
|
+
*
|
|
175
|
+
* 同じ節・同じ辺に 2 本以上あると重なるので、その並びの中の順番を渡して外へずらす。
|
|
176
|
+
* 節が見つからない辺は **落とさずに投げる** = 黙って消すと、書いたのに描かれない線になる。
|
|
177
|
+
*
|
|
178
|
+
* @param occupiedBySide 節ごとに、輪でない線が使っている辺。 これを渡さないと輪が既存の線に重なる
|
|
179
|
+
*/
|
|
180
|
+
export function layoutSelfLoops(
|
|
181
|
+
edges: CdlEdge[],
|
|
182
|
+
nodes: Map<string, LaidNode>,
|
|
183
|
+
occupiedBySide: ReadonlyMap<string, ReadonlySet<Side>>,
|
|
184
|
+
): LaidEdge[] {
|
|
185
|
+
// 辺の割り当ては節ごとに 1 度で決める。 輪を 1 本ずつ処理して選び直すと、2 本目が
|
|
186
|
+
// 1 本目の辺を「空いている」 と見て同じ場所に重なる
|
|
187
|
+
const 割当 = new Map<string, Side[]>();
|
|
188
|
+
for (const edge of edges) {
|
|
189
|
+
if (edge.side) continue;
|
|
190
|
+
const list = 割当.get(edge.from) ?? [];
|
|
191
|
+
list.push("right");
|
|
192
|
+
割当.set(edge.from, list);
|
|
193
|
+
}
|
|
194
|
+
for (const [nodeId, list] of 割当) {
|
|
195
|
+
割当.set(nodeId, pickFreeSides(occupiedBySide.get(nodeId), list.length));
|
|
196
|
+
}
|
|
197
|
+
const 消費 = new Map<string, number>();
|
|
198
|
+
|
|
199
|
+
const 束 = new Map<string, { node: LaidNode; side: Side; edges: CdlEdge[] }>();
|
|
200
|
+
for (const edge of edges) {
|
|
201
|
+
const node = nodes.get(edge.from);
|
|
202
|
+
if (!node) throw new Error(`cdl layout: edge "${edge.id}" の from "${edge.from}" が node に無い`);
|
|
203
|
+
let side = edge.side;
|
|
204
|
+
if (!side) {
|
|
205
|
+
const i = 消費.get(edge.from) ?? 0;
|
|
206
|
+
消費.set(edge.from, i + 1);
|
|
207
|
+
side = 割当.get(edge.from)![i]!;
|
|
208
|
+
}
|
|
209
|
+
const key = `${edge.from}::${side}`;
|
|
210
|
+
const 既 = 束.get(key);
|
|
211
|
+
if (既) 既.edges.push(edge);
|
|
212
|
+
else 束.set(key, { node, side, edges: [edge] });
|
|
213
|
+
}
|
|
214
|
+
// 語の置き場所は束の中の並びで決まるので、束ごとにまとめて経路にする。
|
|
215
|
+
// 返す並びは呼出側が id で引き直すため、ここでは束の順で構わない
|
|
216
|
+
return [...束.values()].flatMap((b) => routeSelfLoopGroup(b.edges, b.node, b.side));
|
|
217
|
+
}
|
package/src/layout/spec.ts
CHANGED
|
@@ -62,9 +62,10 @@ export function hasRenderedLabel(e: { label?: string; sub?: string }): boolean {
|
|
|
62
62
|
/**
|
|
63
63
|
* `title` を文字として描かない node の種別。
|
|
64
64
|
*
|
|
65
|
-
*
|
|
65
|
+
* 3 群ある。 形だけを描く `dyn-*` 5 種は `DynShapeNode` に回り、 `node.title` を一度も参照しない
|
|
66
66
|
* (描くのは形と数値の小さな label だけ)。 payload を自前 layout する chart / timeline / analytic
|
|
67
|
-
* 10 種は、 描くのが payload 側の label で `title` は使わない。
|
|
67
|
+
* 10 種は、 描くのが payload 側の label で `title` は使わない。 流れの始まりと終わりの印 2 種は
|
|
68
|
+
* 印そのものが意味を持ち、 丸の中に名前を入れても読めない (#560)。
|
|
68
69
|
*
|
|
69
70
|
* 一覧は手で持つと描画側とずれるため、 `test/node-title-render.test.tsx` が `NODE_KINDS` を
|
|
70
71
|
* **全件 render** して「title の文字が出るか」 と本述語の返り値の一致を検査する。 新しい種別を
|
|
@@ -81,12 +82,20 @@ const KINDS_WITHOUT_TITLE_TEXT: ReadonlySet<string> = new Set([
|
|
|
81
82
|
"chart-pie",
|
|
82
83
|
"chart-line",
|
|
83
84
|
"chart-bar",
|
|
85
|
+
"chart-gauge",
|
|
86
|
+
"chart-radial",
|
|
87
|
+
"chart-stat",
|
|
88
|
+
"chart-waffle",
|
|
89
|
+
"chart-stacked-bar",
|
|
84
90
|
"gantt-timeline",
|
|
85
91
|
"mind-map",
|
|
86
92
|
"funnel-stages",
|
|
87
93
|
"quadrant-matrix",
|
|
88
94
|
"tree-hierarchy",
|
|
89
95
|
"journey-map",
|
|
96
|
+
// 印そのものが意味を持つ (#560)
|
|
97
|
+
"mark-start",
|
|
98
|
+
"mark-end",
|
|
90
99
|
]);
|
|
91
100
|
|
|
92
101
|
/**
|
|
@@ -189,7 +198,45 @@ export function rowBaselineY(kind: string | undefined, index: number): number |
|
|
|
189
198
|
if (!rendersRows(kind)) return null;
|
|
190
199
|
return kind === "storage"
|
|
191
200
|
? ROW_TOP_STORAGE + index * ROW_PITCH_STORAGE
|
|
192
|
-
: ROW_TOP_GENERIC + index * ROW_PITCH_GENERIC;
|
|
201
|
+
: ROW_TOP_GENERIC + genericTextShift(kind) + index * ROW_PITCH_GENERIC;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* `kinds/generic.tsx` が字の塊を下へずらす量 (SSOT)。
|
|
206
|
+
*
|
|
207
|
+
* 円柱は上の楕円 (ry=14) の内側に字を置けないため、題も行もこの分だけ下がる。
|
|
208
|
+
* **題と行が同じ値を引く** = 別々に持つと片方だけ動いて重なる (#558 で実際に重なっていた)。
|
|
209
|
+
*/
|
|
210
|
+
export function genericTextShift(kind: string | undefined): number {
|
|
211
|
+
return KINDS_SHAPE_CYLINDER.has(kind ?? "") ? GENERIC_TEXT_SHIFT_CYLINDER : 0;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** 円柱で描く種別。 `kinds/generic.tsx` の `STYLES` が持つ形と 1 対 1 で対応する */
|
|
215
|
+
const KINDS_SHAPE_CYLINDER: ReadonlySet<string> = new Set(["database"]);
|
|
216
|
+
|
|
217
|
+
/** 中央に字を寄せる種別 (`cloud` / `decision`)。 同じく `STYLES` の形と対応する */
|
|
218
|
+
const KINDS_TEXT_CENTERED: ReadonlySet<string> = new Set(["cloud", "decision"]);
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* その node が字を中央へ寄せるか (SSOT)。
|
|
222
|
+
*
|
|
223
|
+
* **行を持つ時は寄せない**。 題を箱の中心に置くと、上端から数える行 (`ROW_TOP_GENERIC`) と
|
|
224
|
+
* 必ず重なる = 中心に題を置いたまま行を並べる場所が無い。 行がある時だけ上揃えに落とす。
|
|
225
|
+
*/
|
|
226
|
+
export function genericTextCentered(kind: string | undefined, hasRows: boolean): boolean {
|
|
227
|
+
return !hasRows && KINDS_TEXT_CENTERED.has(kind ?? "");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* `kinds/generic.tsx` の題の baseline (SSOT)。
|
|
232
|
+
*
|
|
233
|
+
* 中央寄せの時だけ箱の高さに依るので `h` を受ける。 検査軸 (`title-row-overlap`) が
|
|
234
|
+
* 行との間隔を測るために同じ関数を引く。
|
|
235
|
+
*/
|
|
236
|
+
export function genericTitleBaselineY(kind: string | undefined, h: number, hasRows: boolean): number {
|
|
237
|
+
return genericTextCentered(kind, hasRows)
|
|
238
|
+
? h / 2 + GENERIC_TITLE_CENTER_OFFSET
|
|
239
|
+
: GENERIC_TITLE_TOP + genericTextShift(kind);
|
|
193
240
|
}
|
|
194
241
|
|
|
195
242
|
/** `kinds/storage.tsx` の 1 行目 baseline と行送り。 */
|
|
@@ -200,6 +247,25 @@ export const ROW_PITCH_STORAGE = 56;
|
|
|
200
247
|
export const ROW_TOP_GENERIC = 100;
|
|
201
248
|
export const ROW_PITCH_GENERIC = 28;
|
|
202
249
|
|
|
250
|
+
/** 円柱で字の塊を下へずらす量。 上の楕円 (ry=14) の内側を避ける */
|
|
251
|
+
export const GENERIC_TEXT_SHIFT_CYLINDER = 28;
|
|
252
|
+
|
|
253
|
+
/** `kinds/generic.tsx` の上揃え時の題の baseline と、中央寄せ時の中心からの差 */
|
|
254
|
+
export const GENERIC_TITLE_TOP = 68;
|
|
255
|
+
export const GENERIC_TITLE_CENTER_OFFSET = 4;
|
|
256
|
+
|
|
257
|
+
/** `kinds/generic.tsx` の題の字の大きさ。 題の字形がどこまで下へ出るかの見積もりに使う */
|
|
258
|
+
export const GENERIC_TITLE_FONT = 20;
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* 字形の上端を baseline から何割上と **見込むか** (cap 高さの目安)。
|
|
262
|
+
*
|
|
263
|
+
* `STORAGE_ROW_DIVIDER_Y` が区切り線の位置を出すのに使っていた 0.72 を、
|
|
264
|
+
* 題と行の間隔を見る軸 (`title-row-overlap`) と共有する。 別々に持つと片方だけ動く。
|
|
265
|
+
*/
|
|
266
|
+
export const GLYPH_CAP_RATIO = 0.72;
|
|
267
|
+
|
|
268
|
+
|
|
203
269
|
/**
|
|
204
270
|
* 行の文字寸法。 `kinds/storage.tsx` は 26px、 `kinds/generic.tsx` は左 18px / 右 21px。
|
|
205
271
|
* descender の見積もりには大きい方を使う。
|
|
@@ -220,7 +286,7 @@ export const STORAGE_TITLE_BASELINE = 78;
|
|
|
220
286
|
* 1 行目の字形の上端は baseline から font size の 0.72 倍だけ上と見込む (cap 高さの目安)。
|
|
221
287
|
*/
|
|
222
288
|
export const STORAGE_ROW_DIVIDER_Y = Math.round(
|
|
223
|
-
(STORAGE_TITLE_BASELINE + (ROW_TOP_STORAGE - ROW_FONT_STORAGE *
|
|
289
|
+
(STORAGE_TITLE_BASELINE + (ROW_TOP_STORAGE - ROW_FONT_STORAGE * GLYPH_CAP_RATIO)) / 2,
|
|
224
290
|
);
|
|
225
291
|
|
|
226
292
|
/**
|
|
@@ -242,6 +308,17 @@ export const STORAGE_ROW_DIVIDER_Y = Math.round(
|
|
|
242
308
|
*/
|
|
243
309
|
export const ROW_GLYPH_DEPTH_RATIO = 0.4;
|
|
244
310
|
|
|
311
|
+
/**
|
|
312
|
+
* 題の字形の下端と 1 行目の字形の上端が触れないために要ると **見込む** 間隔。
|
|
313
|
+
*
|
|
314
|
+
* 題は下へ `GENERIC_TITLE_FONT * ROW_GLYPH_DEPTH_RATIO`、行は上へ
|
|
315
|
+
* `ROW_FONT_GENERIC * GLYPH_CAP_RATIO` 伸びると見込む。 その和を下限にする。
|
|
316
|
+
*
|
|
317
|
+
* 仮定の範囲は `ROW_GLYPH_DEPTH_RATIO` の説明と同じ = preset が出す文字の範囲を押さえる。
|
|
318
|
+
*/
|
|
319
|
+
export const TITLE_ROW_MIN_GAP =
|
|
320
|
+
GENERIC_TITLE_FONT * ROW_GLYPH_DEPTH_RATIO + ROW_FONT_GENERIC * GLYPH_CAP_RATIO;
|
|
321
|
+
|
|
245
322
|
/**
|
|
246
323
|
* 最終行の baseline から下に、 字形が枠に触れないために要ると **見込む** 量。
|
|
247
324
|
*
|
package/src/layout/tokens.ts
CHANGED
|
@@ -52,6 +52,16 @@ export const TOKENS = {
|
|
|
52
52
|
"chart-line": { w: 640, h: 360 },
|
|
53
53
|
"chart-pie": { w: 640, h: 320 },
|
|
54
54
|
"chart-bar": { w: 640, h: 360 },
|
|
55
|
+
// 上半分の弧 + 下段の内訳。 弧が浅いぶん pie より低くて済む (cdl#549)
|
|
56
|
+
"chart-gauge": { w: 640, h: 320 },
|
|
57
|
+
// 左に一覧 + 右に同心の弧。 件数ぶん内側へ重なるので pie と同じ高さを取る (cdl#550)
|
|
58
|
+
"chart-radial": { w: 640, h: 320 },
|
|
59
|
+
// 件ごとに区画を割り、名前 / 数値 / 弧 / 割合 を縦に積む。 数値を大きく組むので縦が要る (cdl#553)
|
|
60
|
+
"chart-stat": { w: 640, h: 320 },
|
|
61
|
+
// 左に一覧 + 右に 10x10 の格子。 格子が正方形なので pie と同じ高さで足りる (cdl#552)
|
|
62
|
+
"chart-waffle": { w: 640, h: 320 },
|
|
63
|
+
// 帯を最大 2 本 + 下に一覧。 縦は帯 2 本ぶんで足りる (cdl#551)
|
|
64
|
+
"chart-stacked-bar": { w: 640, h: 320 },
|
|
55
65
|
// Timeline / Mind 系 (CAR-994 Phase B)
|
|
56
66
|
"gantt-timeline": { w: 720, h: 360 },
|
|
57
67
|
"mind-map": { w: 720, h: 480 },
|
|
@@ -123,6 +133,11 @@ export const TOKENS = {
|
|
|
123
133
|
"shape-lawyer": { w: 280, h: 300 },
|
|
124
134
|
"shape-trader": { w: 300, h: 300 },
|
|
125
135
|
"shape-customer-service": { w: 320, h: 300 },
|
|
136
|
+
// 流れの始まりと終わりの印 2 種 (#560) ... 正方形にして丸が縦横どちらにも歪まないようにする。
|
|
137
|
+
// 印は箱いっぱいに描く (線が箱の縁で止まるため) ので、この値が丸の直径になる。
|
|
138
|
+
// 箱としての最小 (80x40) より大きく、名前を持つ箱 (card 320x150) よりは明らかに小さい
|
|
139
|
+
"mark-start": { w: 96, h: 96 },
|
|
140
|
+
"mark-end": { w: 96, h: 96 },
|
|
126
141
|
} as Record<NodeKind, { w: number; h: number }>,
|
|
127
142
|
|
|
128
143
|
// row gap (stack 間の vertical gap、 lane 内 / 跨ぎ共通)
|