@cardenelabs/cdl 0.7.0 → 0.9.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 +401 -0
- package/SPEC.md +32 -17
- package/dist/index.cjs +700 -306
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +68 -14
- package/dist/index.d.ts +68 -14
- package/dist/index.js +700 -306
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +572 -196
- 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 +572 -196
- package/dist/react.js.map +1 -1
- package/dist/{render-DquCvgOB.d.cts → render-hmFRAabt.d.cts} +33 -2
- package/dist/{render-DquCvgOB.d.ts → render-hmFRAabt.d.ts} +33 -2
- package/package.json +6 -3
- package/src/kinds/box-lines.ts +120 -0
- package/src/kinds/gantt.tsx +152 -24
- package/src/kinds/mind-map.tsx +50 -12
- package/src/kinds/quadrant.tsx +28 -2
- package/src/kinds/shape-blockchain.tsx +65 -12
- package/src/kinds/shape-region.tsx +152 -34
- package/src/kinds/tree.tsx +92 -20
- package/src/layout/collisions.ts +10 -2
- package/src/layout/footer-shape.ts +86 -24
- package/src/layout/lifeline.ts +80 -0
- package/src/layout/spec.ts +21 -2
- package/src/layout/text-width.ts +92 -4
- package/src/layout/viewbox.ts +0 -8
- package/src/layout.ts +8 -1
- package/src/presets.ts +120 -22
- package/src/render/header.tsx +3 -1
- package/src/render/payload-binding.ts +31 -8
- package/src/render/stage.tsx +17 -34
- package/src/render/template-fields.ts +212 -0
- package/src/render/utils.ts +32 -2
- package/src/render.tsx +2 -9
- package/src/types.ts +33 -2
- package/src/validate.ts +27 -9
- package/src/visual-validate.ts +34 -1
- package/src/layout/label-shift.ts +0 -28
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { JSX } from "react";
|
|
2
2
|
import type { LaidNode } from "../types";
|
|
3
3
|
import { fitArt, labelPlan, shapeRegion, textWidth, type ArtExtent } from "./shape-region";
|
|
4
|
+
import { 幅で切る } from "./box-lines";
|
|
5
|
+
import { MONO_ADVANCE_RATIO, displayWidth } from "./row-align";
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* Shape-driven blockchain / web3 10 kinds (CAR-1111 Phase 2-D)。
|
|
@@ -235,7 +237,53 @@ export function ShapeSmartContractNode({ node, active }: ShapeProps): JSX.Elemen
|
|
|
235
237
|
);
|
|
236
238
|
}
|
|
237
239
|
|
|
238
|
-
/**
|
|
240
|
+
/**
|
|
241
|
+
* この絵の中の字の塊の横幅 (#469)。
|
|
242
|
+
*
|
|
243
|
+
* **等幅と比例の見積りの大きい方を取る**。 種別は `ui-monospace` を指定するが、 それが実際に
|
|
244
|
+
* 効くかは載せる側の CSS で決まる。 dragon の `cdl-theme.css` は `font-family` を `!important`
|
|
245
|
+
* で当てており、 掛かる先は `node-label` / `edge-label` / `lane-label` の 3 つ (実測) なので
|
|
246
|
+
* 今はこの絵の文字に当たらない。 とはいえ載せる側の都合で変わる値に幅の計算を預けたくない。
|
|
247
|
+
*
|
|
248
|
+
* どちらの書体で描かれても実物以上に出る側へ倒す = 下回ると「収まっていない物を収まっていると
|
|
249
|
+
* 判定」 する向きに外れる (`row-align.ts` の `rowColumnWidth` と同じ考え方)。
|
|
250
|
+
*/
|
|
251
|
+
const 字の幅 = (text: string, size: number): number =>
|
|
252
|
+
Math.max(displayWidth(text) * size * MONO_ADVANCE_RATIO, textWidth(text, size));
|
|
253
|
+
|
|
254
|
+
/** 絵の中の 5 行の既定値。 書き手が渡さなかった欄はこれで描く (#469) */
|
|
255
|
+
const BLOCK_DEFAULT_FIELDS = [
|
|
256
|
+
{ label: "hash", value: "0xaf31c9d2..." },
|
|
257
|
+
{ label: "prev", value: "0x7b3f92c1..." },
|
|
258
|
+
{ label: "merkle", value: "0xc1892f4a..." },
|
|
259
|
+
{ label: "nonce", value: "82,914" },
|
|
260
|
+
{ label: "tx", value: "142 tx" },
|
|
261
|
+
] as const;
|
|
262
|
+
|
|
263
|
+
/** 見出しの既定値 */
|
|
264
|
+
const BLOCK_DEFAULT_TITLE = "Block #421,873";
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* shape-blockchain-block = 単一 block 詳細 (header + 5 fields: hash/prev/merkle/nonce/tx + chain link icon)。
|
|
268
|
+
*
|
|
269
|
+
* **書き手が渡した題と副題を描く** (#469)。 以前は絵の中の文字がすべて種別に直接
|
|
270
|
+
* 書かれていたため、 題を付けても絵に出ず、 副題に `{名前}` を置いても見える文字は 1 つも
|
|
271
|
+
* 変わらなかった (変わるのは `data-cdl-subtitle` の控えだけ)。
|
|
272
|
+
*
|
|
273
|
+
* | 描く場所 | 読む欄 |
|
|
274
|
+
* |---|---|
|
|
275
|
+
* | 見出し | `title` |
|
|
276
|
+
* | hash の値 | `subtitle` |
|
|
277
|
+
*
|
|
278
|
+
* 渡さなかった欄は既定値で描くため、 指定の無い図の見え方は変わらない。 `{名前}` の置換は
|
|
279
|
+
* `render/nodes.tsx` が先に済ませる。
|
|
280
|
+
*
|
|
281
|
+
* **残り 4 行 (prev / merkle / nonce / tx) は `rows` に開けない**。 `rows` を描く種別は
|
|
282
|
+
* `layout/spec.ts` の `rowBaselineY` が「行の baseline が node 上端から何 world 下か」 を
|
|
283
|
+
* 返す契約を負うが、 この絵は `fitArt` が node の大きさに合わせて丸ごと縮尺するため、 行の
|
|
284
|
+
* 位置が node の大きさに依存する。 `rowBaselineY(kind, index)` は node の大きさを受け取らない
|
|
285
|
+
* ので、 この種別の位置を表せない。 開くなら述語の形から変える話になる (#469 の対象外 scope)。
|
|
286
|
+
*/
|
|
239
287
|
export function ShapeBlockchainBlockNode({ node, active }: ShapeProps): JSX.Element {
|
|
240
288
|
const frame = commonFrame(active);
|
|
241
289
|
const r = shapeRegion(node);
|
|
@@ -245,6 +293,17 @@ export function ShapeBlockchainBlockNode({ node, active }: ShapeProps): JSX.Elem
|
|
|
245
293
|
const cy = r.cy;
|
|
246
294
|
const x = cx - bw / 2;
|
|
247
295
|
const y = cy - bh / 2;
|
|
296
|
+
|
|
297
|
+
// 書き手の値で置き換える。 空文字と空白だけの値は「渡していない」 と同じ扱いにする
|
|
298
|
+
const 渡された = (v: string | undefined): string => (v ?? "").trim();
|
|
299
|
+
const 見出し = 渡された(node.title) || BLOCK_DEFAULT_TITLE;
|
|
300
|
+
const fields = BLOCK_DEFAULT_FIELDS.map((f) => ({
|
|
301
|
+
label: f.label,
|
|
302
|
+
value: f.label === "hash" ? 渡された(node.subtitle) || f.value : f.value,
|
|
303
|
+
}));
|
|
304
|
+
|
|
305
|
+
// 見出しは右上の丸 (半径 5.5、 中心が bw - 16) に掛からないところまで
|
|
306
|
+
const 見出しの幅 = bw - 14 - 30;
|
|
248
307
|
const art = (
|
|
249
308
|
<g>
|
|
250
309
|
{/* 3D shadow */}
|
|
@@ -252,24 +311,18 @@ export function ShapeBlockchainBlockNode({ node, active }: ShapeProps): JSX.Elem
|
|
|
252
311
|
<rect data-cdl-role="node-body" x={x} y={y} width={bw} height={bh} rx={8} {...frame} />
|
|
253
312
|
{/* header bar */}
|
|
254
313
|
<path d={`M ${x} ${y + 32} L ${x} ${y + 8} Q ${x} ${y}, ${x + 8} ${y} L ${x + bw - 8} ${y} Q ${x + bw} ${y}, ${x + bw} ${y + 8} L ${x + bw} ${y + 32} Z`} fill="var(--cdl-tone-accent, #2d6a8f)" fillOpacity={0.9} stroke="none" />
|
|
255
|
-
<text x={x + 14} y={y + 22} fontSize={13} fontWeight={700} fontFamily="ui-monospace, SFMono-Regular, Menlo, monospace" fill="#ffffff">
|
|
256
|
-
|
|
314
|
+
<text data-cdl-role="blockchain-block-title" x={x + 14} y={y + 22} fontSize={13} fontWeight={700} fontFamily="ui-monospace, SFMono-Regular, Menlo, monospace" fill="#ffffff">
|
|
315
|
+
{幅で切る(見出し, 13, 見出しの幅, 字の幅)}
|
|
257
316
|
</text>
|
|
258
317
|
<circle cx={x + bw - 16} cy={y + 16} r={5.5} fill="#7cd88f" stroke="#ffffff" strokeWidth={1.5} />
|
|
259
318
|
{/* fields */}
|
|
260
|
-
{
|
|
261
|
-
{ label: "hash", value: "0xaf31c9d2..." },
|
|
262
|
-
{ label: "prev", value: "0x7b3f92c1..." },
|
|
263
|
-
{ label: "merkle", value: "0xc1892f4a..." },
|
|
264
|
-
{ label: "nonce", value: "82,914" },
|
|
265
|
-
{ label: "tx", value: "142 tx" },
|
|
266
|
-
].map((f, i) => (
|
|
319
|
+
{fields.map((f, i) => (
|
|
267
320
|
<g key={i}>
|
|
268
321
|
<text x={x + 14} y={y + 54 + i * 22} fontSize={10.5} fontFamily="ui-monospace, SFMono-Regular, Menlo, monospace" fill="var(--cdl-text-dim, #5a6270)">
|
|
269
322
|
{f.label}
|
|
270
323
|
</text>
|
|
271
|
-
<text x={x + bw - 14} y={y + 54 + i * 22} fontSize={11} fontFamily="ui-monospace, SFMono-Regular, Menlo, monospace" fontWeight={600} textAnchor="end" fill="var(--cdl-text, #1a1f2a)">
|
|
272
|
-
{f.value}
|
|
324
|
+
<text data-cdl-role={`blockchain-block-${f.label}`} x={x + bw - 14} y={y + 54 + i * 22} fontSize={11} fontFamily="ui-monospace, SFMono-Regular, Menlo, monospace" fontWeight={600} textAnchor="end" fill="var(--cdl-text, #1a1f2a)">
|
|
325
|
+
{幅で切る(f.value, 11, bw - 28 - 字の幅(f.label, 10.5) - 8, 字の幅)}
|
|
273
326
|
</text>
|
|
274
327
|
<line x1={x + 14} y1={y + 60 + i * 22} x2={x + bw - 14} y2={y + 60 + i * 22} stroke="var(--cdl-divider, #d4d4d8)" strokeWidth={0.5} opacity={0.5} />
|
|
275
328
|
</g>
|
|
@@ -11,22 +11,16 @@
|
|
|
11
11
|
* | 高さに依存 | 23 | 箱を大きくすると収まる。 絵が固定の寸法で描かれ、 箱の高さを見ていない |
|
|
12
12
|
* | 高さに依らない | 13 | どんな箱でも出る。 幾何そのものの誤り (`#434`) |
|
|
13
13
|
*
|
|
14
|
-
* ##
|
|
14
|
+
* ## 小さい箱の読みやすさ (#459)
|
|
15
15
|
*
|
|
16
16
|
* **収めた分だけ絵は小さくなる**。 高さ 72 の箱に「絵 180px + 名前 + 説明 + 肩書」 を入れると
|
|
17
|
-
*
|
|
17
|
+
* 変更前は絵に残るのが 28px、 倍率が 0.156 になり、 3px の線も 0.47px まで細くなっていた。
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* | 名前だけ | 0.290 | 0.87 |
|
|
22
|
-
* | + 説明 | 0.223 | 0.67 |
|
|
23
|
-
* | + 説明 + 肩書 | 0.156 | 0.47 |
|
|
24
|
-
*
|
|
25
|
-
* 直すには「絵を優先して字を削る」 か「字を優先して絵を描かない」 かの判断が要り、 製品の
|
|
26
|
-
* 方針として決める必要がある。 `#436` で扱う。
|
|
19
|
+
* 絵の帯が 45px を割る時は、 補いの行を肩書 → 説明の順で落とす。 名前は必ず残す。 高さ 72
|
|
20
|
+
* では名前だけの時と同じ倍率 0.308 まで戻り、 3px の線を約 0.92px で描ける。
|
|
27
21
|
*/
|
|
28
22
|
import type { JSX } from "react";
|
|
29
|
-
import type { LaidNode } from "../types";
|
|
23
|
+
import type { LaidNode, NodeKind } from "../types";
|
|
30
24
|
|
|
31
25
|
// 既存の呼出 (検査を含む) がここから取っているため、 そのまま出し直す。
|
|
32
26
|
export { textWidth } from "./text-width";
|
|
@@ -66,6 +60,88 @@ export const DESCENT = 0.25;
|
|
|
66
60
|
/** 絵と名前の塊の間に空ける量。 */
|
|
67
61
|
const ART_GAP = 4;
|
|
68
62
|
|
|
63
|
+
/**
|
|
64
|
+
* 絵に残す帯の下限 (`#459`)。 これを割るなら補いの行 (肩書 → 説明) を描かない。
|
|
65
|
+
*
|
|
66
|
+
* ## 値の出どころ
|
|
67
|
+
*
|
|
68
|
+
* 人型の絵は自然な大きさで縦 180 ある。 `fitArt` は帯に収まるまで一様に縮めるので、 帯が
|
|
69
|
+
* `B` なら倍率は `B / 180` になり、 絵の中の 3 の線は `3B / 180` で描かれる。
|
|
70
|
+
*
|
|
71
|
+
* 倍率 0.25 を下限に採り、 `0.25 × 180 = 45` を帯の下限とした。 この倍率で 3 の線は 0.75、
|
|
72
|
+
* 8 の字は 2 になる。
|
|
73
|
+
*
|
|
74
|
+
* ## なぜ箱の高さの割合にしないか
|
|
75
|
+
*
|
|
76
|
+
* 割合にすると中くらいの箱まで巻き込む。 高さ 106 の箱は 3 行で帯が 39 (割合 0.37) しか無く、
|
|
77
|
+
* 割合 0.65 を下限に置くと **高さ 191 未満の箱が全て** 補いの行を失う (実測)。
|
|
78
|
+
* 絵が読めるかは箱の高さではなく帯の実寸で決まるので、 実寸で判定する。
|
|
79
|
+
*
|
|
80
|
+
* ## 覆えていない範囲
|
|
81
|
+
*
|
|
82
|
+
* 下限は人型 (縦 180) を基準にした 1 つの値で、 絵の大きさが種別ごとに違うことは見ていない。
|
|
83
|
+
* 小さい絵の種別では帯が足りていても行を落とすことがある。 種別ごとの広がりは描く直前
|
|
84
|
+
* (`fitArt` の呼出) にしか分からず、 名前の塊を組む時点では手に入らない。
|
|
85
|
+
*/
|
|
86
|
+
const ART_MIN_BAND = 45;
|
|
87
|
+
|
|
88
|
+
/** 補いの行のうち、 箱が狭くて描けなかったもの。 落とす順は肩書が先。 */
|
|
89
|
+
export type DroppedLabelLine = "肩書" | "説明";
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* `labelPlan` が落としても固有 layout で描く種別 (`#492`)。
|
|
93
|
+
*
|
|
94
|
+
* 肩書と説明を一括で判定してはいけない。 `shape-terminal` は肩書だけを描き、
|
|
95
|
+
* `shape-blockchain-block` は説明だけを描く。 種別単位で除外すると片方を誤報するか見逃す。
|
|
96
|
+
*
|
|
97
|
+
* 描き方は配置結果から分からないため一覧を持つ。 `test/shared-label-plan-kinds.test.tsx` が全
|
|
98
|
+
* `shape-` 種別の両方の行を実際に描き、 一覧との一致を確かめる。
|
|
99
|
+
*/
|
|
100
|
+
const 落としても描く種別: Readonly<Record<DroppedLabelLine, ReadonlySet<NodeKind>>> = {
|
|
101
|
+
肩書: new Set([
|
|
102
|
+
// basement 系 = `commonLabel` が自前の帯に置く
|
|
103
|
+
"shape-file",
|
|
104
|
+
"shape-folder",
|
|
105
|
+
"shape-cloud",
|
|
106
|
+
"shape-cylinder",
|
|
107
|
+
"shape-hexagon",
|
|
108
|
+
"shape-diamond",
|
|
109
|
+
"shape-stack",
|
|
110
|
+
// software 系 = 固有 layout
|
|
111
|
+
"shape-window",
|
|
112
|
+
"shape-terminal",
|
|
113
|
+
"shape-code-block",
|
|
114
|
+
"shape-message-bubble",
|
|
115
|
+
"shape-gear",
|
|
116
|
+
// hardware 系のうち固有 layout を持つもの
|
|
117
|
+
"shape-iot-sensor",
|
|
118
|
+
"shape-robot-arm",
|
|
119
|
+
]),
|
|
120
|
+
説明: new Set([
|
|
121
|
+
// basement 系 = `commonLabel` が自前の帯に置く
|
|
122
|
+
"shape-file",
|
|
123
|
+
"shape-folder",
|
|
124
|
+
"shape-cloud",
|
|
125
|
+
"shape-cylinder",
|
|
126
|
+
"shape-hexagon",
|
|
127
|
+
"shape-diamond",
|
|
128
|
+
"shape-stack",
|
|
129
|
+
// software 系 = 固有 layout
|
|
130
|
+
"shape-window",
|
|
131
|
+
"shape-message-bubble",
|
|
132
|
+
"shape-gear",
|
|
133
|
+
// hardware / blockchain 系のうち固有 layout を持つもの
|
|
134
|
+
"shape-iot-sensor",
|
|
135
|
+
"shape-robot-arm",
|
|
136
|
+
"shape-blockchain-block",
|
|
137
|
+
]),
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/** この種別で、共有 plan が落とした特定の行が実際にも描かれないか。 */
|
|
141
|
+
export function droppedLineIsHidden(kind: NodeKind, line: DroppedLabelLine): boolean {
|
|
142
|
+
return kind.startsWith("shape-") && !落としても描く種別[line].has(kind);
|
|
143
|
+
}
|
|
144
|
+
|
|
69
145
|
/** 名前の塊の 1 行。 */
|
|
70
146
|
export type LabelLine = { y: number; size: number };
|
|
71
147
|
|
|
@@ -91,6 +167,13 @@ export function labelPlan(node: LaidNode): {
|
|
|
91
167
|
* なる (高さ 53 で実測、 review 指摘)。 一番小さい行で判定する。
|
|
92
168
|
*/
|
|
93
169
|
読める: boolean;
|
|
170
|
+
/**
|
|
171
|
+
* 箱が狭くて描かなかった補いの行 (`#459`)。 書いていない行は載らない。
|
|
172
|
+
*
|
|
173
|
+
* 書き手に「書いた文字が出ていない」 を伝えるための出口。 描画側は行が `null` かどうかだけを
|
|
174
|
+
* 見るので、 この欄は伝えるためだけに持つ。
|
|
175
|
+
*/
|
|
176
|
+
落とした: DroppedLabelLine[];
|
|
94
177
|
} {
|
|
95
178
|
const 高さ = Math.max(0, node.h);
|
|
96
179
|
const 底 = node.cy + 高さ / 2;
|
|
@@ -125,33 +208,67 @@ export function labelPlan(node: LaidNode): {
|
|
|
125
208
|
|
|
126
209
|
let 説明あり = Boolean(node.subtitle);
|
|
127
210
|
let 肩書あり = Boolean(node.eyebrow);
|
|
128
|
-
|
|
129
|
-
if (!入る(説明あり, 肩書あり)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const 下の行大 = 説明あり ? 小さい字 : size;
|
|
137
|
-
if (積む(size, 説明あり, 肩書あり, 下の行大 * DESCENT).上端 >= 天) {
|
|
138
|
-
名前大 = size;
|
|
139
|
-
break;
|
|
140
|
-
}
|
|
211
|
+
const 落とした: DroppedLabelLine[] = [];
|
|
212
|
+
if (!入る(説明あり, 肩書あり) && 肩書あり) {
|
|
213
|
+
肩書あり = false;
|
|
214
|
+
落とした.push("肩書");
|
|
215
|
+
}
|
|
216
|
+
if (!入る(説明あり, 肩書あり) && 説明あり) {
|
|
217
|
+
説明あり = false;
|
|
218
|
+
落とした.push("説明");
|
|
141
219
|
}
|
|
142
220
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
221
|
+
/** 描く行を決めた上で、 字の大きさと baseline を決める。 */
|
|
222
|
+
const 組む = (説明あり: boolean, 肩書あり: boolean) => {
|
|
223
|
+
// 箱に入る一番大きい字を探す。 上限は「高さに比例」 と「読める上限」 の小さい方
|
|
224
|
+
const 上限 = Math.min(TITLE_MAX, Math.max(1, Math.round(高さ * TITLE_RATIO)));
|
|
225
|
+
let 名前大 = 0;
|
|
226
|
+
for (let size = 上限; size >= 1; size--) {
|
|
227
|
+
const 小さい字 = Math.max(1, Math.round(size * SUB_RATIO));
|
|
228
|
+
const 下の行大 = 説明あり ? 小さい字 : size;
|
|
229
|
+
if (積む(size, 説明あり, 肩書あり, 下の行大 * DESCENT).上端 >= 天) {
|
|
230
|
+
名前大 = size;
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// **1 でも入らない箱がある**。 名前 1 行でも上下の張り出しで 0.97 要るため、 高さ 0.97 未満の
|
|
236
|
+
// 箱では整数の大きさに解が無い (review 指摘)。 名前は落とせないので、 ちょうど入る大きさまで
|
|
237
|
+
// 下げる。 見えない字にはなるが、 箱の外へ出て隣の領域を汚すよりは良い
|
|
238
|
+
if (名前大 === 0) 名前大 = 高さ / (ASCENT + DESCENT);
|
|
147
239
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
240
|
+
// 下の余白は余った分までしか取れない。 固定の 4 を無条件に取ると塊が上へ出る
|
|
241
|
+
const 小さい字 = Math.max(1, Math.round(名前大 * SUB_RATIO));
|
|
242
|
+
const 張り出し = (説明あり ? 小さい字 : 名前大) * DESCENT;
|
|
243
|
+
const 余り = Math.max(0, 積む(名前大, 説明あり, 肩書あり, 張り出し).上端 - 天);
|
|
244
|
+
const 下余白 = Math.min(Math.max(BOTTOM_PAD, 張り出し), 張り出し + 余り);
|
|
245
|
+
|
|
246
|
+
return 積む(名前大, 説明あり, 肩書あり, 下余白);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
/** この組み方で絵に残る帯。 `shapeRegion` が使う式と同じものをここでも使う。 */
|
|
250
|
+
const 絵の帯 = (組: ReturnType<typeof 組む>): number => Math.max(0, 組.上端 - ART_GAP - 天);
|
|
251
|
+
|
|
252
|
+
// 絵に残る帯が下限を割るなら、 補いの行を落として絵に譲る (#459)。
|
|
253
|
+
//
|
|
254
|
+
// **落とす順は肩書 → 説明で、 名前は落とさない**。 名前が無い箱は何を指しているか分からず、
|
|
255
|
+
// 絵が読めても図の意味が失われる。
|
|
256
|
+
//
|
|
257
|
+
// 絵を描かない案 (`#459` の案 B) は採らない = `shape-` は種別ごとの絵を持つことが存在理由で、
|
|
258
|
+
// 絵を落とすと `card` と区別が付かなくなる。 種別ごとに簡略版の絵を持つ案 (案 C) は 49 種
|
|
259
|
+
// それぞれに絵を足すことになり、 本 Issue の範囲を超える。
|
|
260
|
+
let 組 = 組む(説明あり, 肩書あり);
|
|
261
|
+
if (絵の帯(組) < ART_MIN_BAND && 肩書あり) {
|
|
262
|
+
肩書あり = false;
|
|
263
|
+
落とした.push("肩書");
|
|
264
|
+
組 = 組む(説明あり, 肩書あり);
|
|
265
|
+
}
|
|
266
|
+
if (絵の帯(組) < ART_MIN_BAND && 説明あり) {
|
|
267
|
+
説明あり = false;
|
|
268
|
+
落とした.push("説明");
|
|
269
|
+
組 = 組む(説明あり, 肩書あり);
|
|
270
|
+
}
|
|
153
271
|
|
|
154
|
-
const 組 = 積む(名前大, 説明あり, 肩書あり, 下余白);
|
|
155
272
|
const 一番小さい行 = Math.min(
|
|
156
273
|
...[組.名前, 組.説明, 組.肩書].filter((l) => l !== null).map((l) => l.size),
|
|
157
274
|
);
|
|
@@ -161,6 +278,7 @@ export function labelPlan(node: LaidNode): {
|
|
|
161
278
|
肩書: 組.肩書,
|
|
162
279
|
top: 組.上端,
|
|
163
280
|
読める: 一番小さい行 >= TITLE_MIN,
|
|
281
|
+
落とした,
|
|
164
282
|
};
|
|
165
283
|
}
|
|
166
284
|
|
package/src/kinds/tree.tsx
CHANGED
|
@@ -2,6 +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
6
|
|
|
6
7
|
/**
|
|
7
8
|
* tree-hierarchy kind ... 1 node に tree node 配列 (treeData) を保持し、
|
|
@@ -12,6 +13,42 @@ import { resolveTreeData } from "../render/payload-binding";
|
|
|
12
13
|
* - depth 1+ は tone 別 accent の left-border + 白 fill card
|
|
13
14
|
* - parent-child edge は elbow line + 中間 join dot で「分岐点」 を強調
|
|
14
15
|
*/
|
|
16
|
+
/**
|
|
17
|
+
* 箱の中に名前と補足を積んで描く (#471)。
|
|
18
|
+
*
|
|
19
|
+
* 補足が無い node は呼ばない = 従来の 1 行の `<text>` をそのまま残すため。 積む形に寄せると
|
|
20
|
+
* baseline の出し方が変わり、 補足を渡していない既存の図まで文字の位置が動く。
|
|
21
|
+
*/
|
|
22
|
+
function 箱の文字({
|
|
23
|
+
行,
|
|
24
|
+
x,
|
|
25
|
+
箱の高さ,
|
|
26
|
+
fill,
|
|
27
|
+
名前の太さ,
|
|
28
|
+
}: {
|
|
29
|
+
行: 積んだ行[];
|
|
30
|
+
x: number;
|
|
31
|
+
箱の高さ: number;
|
|
32
|
+
fill: string;
|
|
33
|
+
名前の太さ: number;
|
|
34
|
+
}): JSX.Element[] {
|
|
35
|
+
return 行.map((r) => (
|
|
36
|
+
<text
|
|
37
|
+
key={r.役割}
|
|
38
|
+
data-cdl-role={r.役割 === "subtitle" ? "tree-node-subtitle" : undefined}
|
|
39
|
+
x={x}
|
|
40
|
+
y={箱の高さ / 2 + r.dy}
|
|
41
|
+
fontSize={r.fontSize}
|
|
42
|
+
fontWeight={r.役割 === "title" ? 名前の太さ : 500}
|
|
43
|
+
textAnchor="middle"
|
|
44
|
+
fill={fill}
|
|
45
|
+
opacity={r.役割 === "subtitle" ? 0.85 : undefined}
|
|
46
|
+
>
|
|
47
|
+
{r.text}
|
|
48
|
+
</text>
|
|
49
|
+
));
|
|
50
|
+
}
|
|
51
|
+
|
|
15
52
|
export function TreeNode({
|
|
16
53
|
node,
|
|
17
54
|
active,
|
|
@@ -88,6 +125,18 @@ export function TreeNode({
|
|
|
88
125
|
{layout.nodes.map((n) => {
|
|
89
126
|
const accent = depthColors[Math.min(n.depth, depthColors.length - 1)] ?? depthColors[0]!;
|
|
90
127
|
const isRoot = n.depth === 0;
|
|
128
|
+
// 補足は箱の中の 2 行目に出す (#471)。 箱の高さは変えないため、 入らない時は落ちる
|
|
129
|
+
const 行 = 箱に積む(
|
|
130
|
+
[
|
|
131
|
+
{ 役割: "title" as const, text: n.title, fontSize: isRoot ? 13 : 12 },
|
|
132
|
+
{ 役割: "subtitle" as const, text: n.subtitle ?? "", fontSize: 9 },
|
|
133
|
+
],
|
|
134
|
+
n.w,
|
|
135
|
+
n.h,
|
|
136
|
+
);
|
|
137
|
+
// **判定は行数でなく「補足を渡したか」 で行う**。 行数で見ると、 箱が低くて補足が落ちた時に
|
|
138
|
+
// 従来の 1 行の経路へ戻り、 幅で切っていない名前が箱から横にはみ出す (#471 Round 1)
|
|
139
|
+
const 補足あり = (n.subtitle ?? "").trim() !== "";
|
|
91
140
|
return (
|
|
92
141
|
<g
|
|
93
142
|
key={`n-${n.id}`}
|
|
@@ -105,16 +154,26 @@ export function TreeNode({
|
|
|
105
154
|
rx={12}
|
|
106
155
|
fill={`url(#${gradientId})`}
|
|
107
156
|
/>
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
157
|
+
{補足あり ? (
|
|
158
|
+
箱の文字({
|
|
159
|
+
行,
|
|
160
|
+
x: n.w / 2,
|
|
161
|
+
箱の高さ: n.h,
|
|
162
|
+
fill: "var(--cdl-text-on-tone, #ffffff)",
|
|
163
|
+
名前の太さ: 700,
|
|
164
|
+
})
|
|
165
|
+
) : (
|
|
166
|
+
<text
|
|
167
|
+
x={n.w / 2}
|
|
168
|
+
y={n.h / 2 + 5}
|
|
169
|
+
fontSize={13}
|
|
170
|
+
fontWeight={700}
|
|
171
|
+
textAnchor="middle"
|
|
172
|
+
fill="var(--cdl-text-on-tone, #ffffff)"
|
|
173
|
+
>
|
|
174
|
+
{n.title}
|
|
175
|
+
</text>
|
|
176
|
+
)}
|
|
118
177
|
</>
|
|
119
178
|
) : (
|
|
120
179
|
<>
|
|
@@ -130,16 +189,26 @@ export function TreeNode({
|
|
|
130
189
|
strokeWidth={1.5}
|
|
131
190
|
/>
|
|
132
191
|
<rect x={0} y={0} width={4} height={n.h} rx={2} fill={accent} />
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
192
|
+
{補足あり ? (
|
|
193
|
+
箱の文字({
|
|
194
|
+
行,
|
|
195
|
+
x: n.w / 2 + 2,
|
|
196
|
+
箱の高さ: n.h,
|
|
197
|
+
fill: "var(--cdl-chip-text, #1a1f2a)",
|
|
198
|
+
名前の太さ: 600,
|
|
199
|
+
})
|
|
200
|
+
) : (
|
|
201
|
+
<text
|
|
202
|
+
x={n.w / 2 + 2}
|
|
203
|
+
y={n.h / 2 + 4}
|
|
204
|
+
fontSize={12}
|
|
205
|
+
fontWeight={600}
|
|
206
|
+
textAnchor="middle"
|
|
207
|
+
fill="var(--cdl-chip-text, #1a1f2a)"
|
|
208
|
+
>
|
|
209
|
+
{n.title}
|
|
210
|
+
</text>
|
|
211
|
+
)}
|
|
143
212
|
</>
|
|
144
213
|
)}
|
|
145
214
|
</g>
|
|
@@ -152,6 +221,8 @@ export function TreeNode({
|
|
|
152
221
|
type LayoutNode = {
|
|
153
222
|
id: string;
|
|
154
223
|
title: string;
|
|
224
|
+
/** 箱の 2 行目に出す補足 (#471) */
|
|
225
|
+
subtitle?: string;
|
|
155
226
|
x: number;
|
|
156
227
|
y: number;
|
|
157
228
|
w: number;
|
|
@@ -250,6 +321,7 @@ export function computeTreeLayout(
|
|
|
250
321
|
nodes.push({
|
|
251
322
|
id: n.id,
|
|
252
323
|
title: n.title,
|
|
324
|
+
...(n.subtitle !== undefined ? { subtitle: n.subtitle } : {}),
|
|
253
325
|
x,
|
|
254
326
|
y,
|
|
255
327
|
w: nodeW,
|
package/src/layout/collisions.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { BBox, LaidEdge, LaidLane, LaidNode } from "../types";
|
|
2
2
|
import { CHAIN_SHIFT_MAX_ACCUMULATED, computeLabelBBoxWorld, EDGE_STUB_OUT, hasRenderedLabel, LABEL_TO_PATH_CLEARANCE, requiredNearClearance } from "./spec";
|
|
3
|
+
import { footerShapeDrops } from "./footer-shape";
|
|
3
4
|
import { measureTextWidth } from "./text-width";
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -26,14 +27,21 @@ export function collectBBoxes(lanes: LaidLane[], nodes: LaidNode[], edges: LaidE
|
|
|
26
27
|
});
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
// 下端の名札は絵が箱の上端より上へ出る (`SHAPE_DRAW_UP_EXTENT`)。 出た分を外接矩形に
|
|
31
|
+
// 含めないと、 描かれている範囲と衝突 / 近接の判定材料が食い違う (#478)。
|
|
32
|
+
//
|
|
33
|
+
// 名札を集める条件は `lifelineFooterNodes` が唯一持ち、 出る量は種別と箱の高さだけで
|
|
34
|
+
// 決まるので、 名札を下げた後に呼んでも同じ値になる。
|
|
35
|
+
const footerDraw = footerShapeDrops(lanes, nodes);
|
|
29
36
|
for (const n of nodes) {
|
|
37
|
+
const up = footerDraw.get(n.id) ?? 0;
|
|
30
38
|
out.push({
|
|
31
39
|
kind: "node",
|
|
32
40
|
id: n.id,
|
|
33
41
|
x: n.cx - n.w / 2,
|
|
34
|
-
y: n.cy - n.h / 2,
|
|
42
|
+
y: n.cy - n.h / 2 - up,
|
|
35
43
|
w: n.w,
|
|
36
|
-
h: n.h,
|
|
44
|
+
h: n.h + up,
|
|
37
45
|
});
|
|
38
46
|
}
|
|
39
47
|
|
|
@@ -28,35 +28,45 @@ export const SHAPE_DRAW_UP_EXTENT: Readonly<Record<string, number>> = {
|
|
|
28
28
|
/**
|
|
29
29
|
* 縦線を持つ lane の **下端の名札** を集める。
|
|
30
30
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
31
|
+
* 判定は node の `role` で行う。 順序図を組み立てる側 (`presets.ts` の `build()`) が
|
|
32
|
+
* `role: "lifeline-footer"` を付けるため、 描画側は位置を見る必要が無い。
|
|
33
33
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
34
|
+
* **位置で推測していた形は誤りだった** (`#458`)。 「node が 2 つ以上ある lane の一番下」 と
|
|
35
|
+
* 決めていたため、 手で組んだ図で一番下が名札でない `shape-` の node だと、 その絵が下がった。
|
|
36
|
+
*
|
|
37
|
+
* 目印に加えて 2 つの条件を見る。 どちらも **どれが名札かの判定ではなく、 名札として扱うと
|
|
38
|
+
* 図が壊れる形を除く** ためのもの。
|
|
39
|
+
*
|
|
40
|
+
* | 条件 | 除く理由 |
|
|
41
|
+
* |---|---|
|
|
42
|
+
* | `lane.lifeline` が真 | 縦線が無い lane では絵を下げる理由 (縦線と絵の重なり) が起きない |
|
|
43
|
+
* | その lane に node が 2 つ以上 | 1 つしか無い lane の唯一の node を名札にすると縦線が退化する |
|
|
44
|
+
*
|
|
45
|
+
* 2 つ目は実測に基づく。 縦線の終点は **全 lane で 1 つに揃える** ため、 名札の位置が異常な lane が
|
|
46
|
+
* 1 つあるだけで図の全 lane から縦線が消えていた (1 node の lane を名札にして実測、 通常の lane と
|
|
47
|
+
* 混在させても全て消えた)。 1 node の lane にあるのは上端の名札のはずで、 それを下端として扱う形
|
|
48
|
+
* 自体が誤り。 終点の選び方そのものは `uniformLifelineEndY` (`#479`) が直した。
|
|
49
|
+
*
|
|
50
|
+
* 同じ lane に名札が複数あれば全て返す = 数を絞る根拠が無く、 絞るとどれを残すかで再び位置の
|
|
51
|
+
* 推測に戻る。
|
|
52
|
+
*
|
|
53
|
+
* 縦線の終点 (`uniformLifelineEndY`) と絵を下げる量 (`applyFooterShapeDrops`) と外接矩形
|
|
54
|
+
* (`collectBBoxes`) が同じ名札を指す必要があるため、 「どれが下端の名札か」 の判定をここに
|
|
55
|
+
* 1 つだけ置く。
|
|
36
56
|
*/
|
|
37
57
|
export function lifelineFooterNodes(lanes: LaidLane[], nodes: LaidNode[]): LaidNode[] {
|
|
38
|
-
// lane
|
|
39
|
-
//
|
|
40
|
-
const
|
|
41
|
-
for (const n of nodes)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
else byLane.set(n.lane, [n]);
|
|
45
|
-
}
|
|
46
|
-
const footers: LaidNode[] = [];
|
|
58
|
+
// lane ごとの node 数を 1 度だけ数える。 node ごとに全 node を走査すると node 数の 2 乗に
|
|
59
|
+
// 膨らみ、 `CdlStage` は動きの 1 コマごとに描き直すため毎コマ払う
|
|
60
|
+
const 数 = new Map<string, number>();
|
|
61
|
+
for (const n of nodes) 数.set(n.lane, (数.get(n.lane) ?? 0) + 1);
|
|
62
|
+
|
|
63
|
+
const lifelineLanes = new Set<string>();
|
|
47
64
|
for (const lane of lanes) {
|
|
48
|
-
if (
|
|
49
|
-
const inside = byLane.get(lane.id);
|
|
50
|
-
if (inside === undefined || inside.length < 2) continue;
|
|
51
|
-
// 一番下 = cy が最大。 並べ替えずに 1 回の走査で選ぶ。 同じ cy が複数ある時は
|
|
52
|
-
// **後に現れた方** を採る (安定な並べ替えで末尾を取るのと同じ結果にする)
|
|
53
|
-
let footer = inside[0]!;
|
|
54
|
-
for (const n of inside) {
|
|
55
|
-
if (n.cy >= footer.cy) footer = n;
|
|
56
|
-
}
|
|
57
|
-
footers.push(footer);
|
|
65
|
+
if (lane.lifeline) lifelineLanes.add(lane.id);
|
|
58
66
|
}
|
|
59
|
-
return
|
|
67
|
+
return nodes.filter(
|
|
68
|
+
(n) => n.role === "lifeline-footer" && lifelineLanes.has(n.lane) && (数.get(n.lane) ?? 0) >= 2,
|
|
69
|
+
);
|
|
60
70
|
}
|
|
61
71
|
|
|
62
72
|
/**
|
|
@@ -95,3 +105,55 @@ export function footerShapeDropsOf(footers: LaidNode[]): Map<string, number> {
|
|
|
95
105
|
}
|
|
96
106
|
return drops;
|
|
97
107
|
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 下端の名札を、 下げる量だけ実際に動かす (`#478`)。
|
|
111
|
+
*
|
|
112
|
+
* **`cy` は「箱の中心」 のまま**。 絵は箱の下端を基準に組み立てられるため、 箱を動かせば
|
|
113
|
+
* 絵も同じだけ動く。 絵の上端は `cy + h / 2 - SHAPE_DRAW_UP_EXTENT[kind]` で、 箱と
|
|
114
|
+
* `SHAPE_DRAW_UP_EXTENT` から一意に導ける (`footerShapeDrawTop`)。
|
|
115
|
+
*
|
|
116
|
+
* ## 描画だけを動かす形を採らない理由
|
|
117
|
+
*
|
|
118
|
+
* 元は描画側が名札を包む `<g>` に `translate(0, dy)` を掛けており、 **箱ごと下へ動いて
|
|
119
|
+
* 見えるのに座標は元のまま** だった。 `cy` も `data-cdl-cy` も外接矩形も動かないため、
|
|
120
|
+
* 座標を読む側 (縦線の終点 / 図の下端 / 外接矩形 / 矢印の接続先) が実際の描画と食い違う。
|
|
121
|
+
*
|
|
122
|
+
* ## 「絵」 ではなく「箱」 を `cy` とする理由
|
|
123
|
+
*
|
|
124
|
+
* `cy` / `w` / `h` は全ての種別で **箱** を表し、 矢印の接続先 / 当たり判定 / `data-cdl-cy` /
|
|
125
|
+
* 絵の組み立ての原点がこの意味に依存している。 一部の種別だけ `cy` を絵の中心に変えると、
|
|
126
|
+
* 同じ field が node によって別の意味を持つ。
|
|
127
|
+
*
|
|
128
|
+
* 絵の範囲は箱から導けるが、 逆は導きにくい = 絵を `cy` にすると箱の位置を出すのに同じ
|
|
129
|
+
* `SHAPE_DRAW_UP_EXTENT` が要り、 依存の向きが 1 つ増えるだけで得るものが無い。
|
|
130
|
+
*
|
|
131
|
+
* ## 下げる量は位置に依存しない
|
|
132
|
+
*
|
|
133
|
+
* `footerShapeDropY` が見るのは種別と箱の高さだけなので、 動かした後に呼び直しても同じ値を
|
|
134
|
+
* 返す。 したがって後段が名札をさらに動かしても、 絵の上端は常に
|
|
135
|
+
* `cy + h / 2 - extent` として正しく導ける。
|
|
136
|
+
*/
|
|
137
|
+
export function applyFooterShapeDrops(lanes: LaidLane[], nodes: LaidNode[]): LaidNode[] {
|
|
138
|
+
const drops = footerShapeDrops(lanes, nodes);
|
|
139
|
+
if (drops.size === 0) return nodes;
|
|
140
|
+
return nodes.map((n) => {
|
|
141
|
+
// posX / posY が揃った node は「指定した位置から動かさない」公開契約。 絵が箱の上へ出る
|
|
142
|
+
// 範囲は collectBBoxes / footerShapeDrawTop が扱うため、 絶対配置を崩してまで箱を下げない。
|
|
143
|
+
if (n.posX !== undefined && n.posY !== undefined) return n;
|
|
144
|
+
const dy = drops.get(n.id);
|
|
145
|
+
return dy === undefined ? n : { ...n, cy: n.cy + dy };
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 下げた名札の **絵の上端**。 縦線の終点と外接矩形の上端がこの値を使う。
|
|
151
|
+
*
|
|
152
|
+
* 絵は箱の下端から上へ `SHAPE_DRAW_UP_EXTENT` 伸びるので、 上端は `cy + h / 2 - extent`。
|
|
153
|
+
* 下げた量が `extent - h` である分、 これは `cy - h / 2 - 下げた量` と等しい。
|
|
154
|
+
*
|
|
155
|
+
* 下げていない名札 (箱に収まる種別 / 収まる高さ) では箱の上端をそのまま返す。
|
|
156
|
+
*/
|
|
157
|
+
export function footerShapeDrawTop(node: LaidNode): number {
|
|
158
|
+
return node.cy - node.h / 2 - footerShapeDropY(node.kind, node.h);
|
|
159
|
+
}
|