@cardenelabs/cdl 0.6.1 → 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 +820 -364
- 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 +820 -364
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +692 -254
- 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 +692 -254
- 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 +77 -16
- 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 +121 -24
- 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/nodes.tsx +6 -5
- package/src/render/payload-binding.ts +88 -3
- 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
package/src/kinds/quadrant.tsx
CHANGED
|
@@ -35,6 +35,10 @@ export function QuadrantNode({
|
|
|
35
35
|
// 名前の前後に矢印と間隔 (12) が付くので、 その分も見込む。
|
|
36
36
|
const AXIS_LABEL_FONT = 12;
|
|
37
37
|
const PAD_MIN = 60;
|
|
38
|
+
// 項目の枠。 名前の字幅に左右の余白を足した幅にする (#457)
|
|
39
|
+
const ITEM_FONT = 13;
|
|
40
|
+
const ITEM_PAD_X = 12;
|
|
41
|
+
const ITEM_MIN_W = 48;
|
|
38
42
|
const axisLabelPad = (text: string): number =>
|
|
39
43
|
Math.max(PAD_MIN, measureTextWidth(`← ${text}`, { fontSize: AXIS_LABEL_FONT, fontWeight: 700 }) + 12);
|
|
40
44
|
// 名前が長くても図そのものは残す。 上限を置かないと、 名前が箱の幅を超えた時に
|
|
@@ -145,6 +149,26 @@ export function QuadrantNode({
|
|
|
145
149
|
const qxStart = (isLeft ? PAD_LEFT : cx) + 18;
|
|
146
150
|
const qyStart = (isTop ? PAD_TOP : cy) + 44;
|
|
147
151
|
const qHalfW = halfW - 36;
|
|
152
|
+
// 枠の幅は名前に合わせる。 固定だと短い名前で枠が間延びし、 狭い図では名前が枠から
|
|
153
|
+
// 出る (実測 = 5 文字の「重複を消す」 が 70px で、 枠が狭い図では越えた)。
|
|
154
|
+
//
|
|
155
|
+
// 頭打ちは象限に収まる幅 (qHalfW)。 これを超えると隣の象限へ出るため、 名前が長い時は
|
|
156
|
+
// 枠を伸ばさず名前が枠から出る形にする。 軸の名前と同じ考え方で、 溢れても図は残す。
|
|
157
|
+
//
|
|
158
|
+
// 下限 (ITEM_MIN_W) は **頭打ちより優先しない**。 図が極端に狭いと qHalfW が下限を
|
|
159
|
+
// 下回り、 下限を通すと枠が象限から出る。
|
|
160
|
+
//
|
|
161
|
+
// 余地が無い図では項目を描かない。 qHalfW は負にもなり (実測 = 幅 180 の図で -6)、
|
|
162
|
+
// 幅を 0 に切っても **枠の開始位置 (qxStart) が既に象限の外** にある
|
|
163
|
+
// (実測 = 幅 120 の図で開始 78、 象限の右端 60)。 描く場所が無い時に 0 幅の枠を
|
|
164
|
+
// 象限の外へ置くより、 何も描かない方が図として正しい。
|
|
165
|
+
const 枠の余地 = qHalfW;
|
|
166
|
+
if (枠の余地 <= 0) return null;
|
|
167
|
+
const 枠の幅 = (title: string) =>
|
|
168
|
+
Math.min(
|
|
169
|
+
枠の余地,
|
|
170
|
+
Math.max(ITEM_MIN_W, measureTextWidth(title, { fontSize: ITEM_FONT, fontWeight: 600 }) + ITEM_PAD_X * 2),
|
|
171
|
+
);
|
|
148
172
|
const qHalfH = halfH - 60;
|
|
149
173
|
const gap = 10;
|
|
150
174
|
const rowH = 34;
|
|
@@ -158,14 +182,16 @@ export function QuadrantNode({
|
|
|
158
182
|
data-cdl-unresolved={it.unresolved ? "true" : undefined}
|
|
159
183
|
x={qxStart}
|
|
160
184
|
y={py}
|
|
161
|
-
width={
|
|
185
|
+
width={枠の幅(it.title)}
|
|
162
186
|
height={rowH}
|
|
163
187
|
rx={6}
|
|
164
188
|
fill="var(--cdl-node-fill, #ffffff)"
|
|
165
189
|
stroke={quadrantAccent[q]}
|
|
166
190
|
strokeWidth={1.5}
|
|
167
191
|
/>
|
|
168
|
-
|
|
192
|
+
{/* 字間を 0 に固定する。 枠の幅は字幅の見積り (letterSpacing 0 前提) で決めるため、
|
|
193
|
+
外から字間を継承すると見積りより名前が広がって枠から出る (#457 Round 4) */}
|
|
194
|
+
<text x={qxStart + ITEM_PAD_X} y={py + rowH / 2 + 5} fontSize={ITEM_FONT} fontWeight={600} letterSpacing={0} fill="var(--cdl-text, #1a1f2a)">
|
|
169
195
|
{it.title}
|
|
170
196
|
</text>
|
|
171
197
|
</g>
|
|
@@ -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
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { JSX } from "react";
|
|
2
2
|
import type { LaidNode, TreeNodePayload } from "../types";
|
|
3
3
|
import { TONE } from "../render/tone";
|
|
4
|
+
import { resolveTreeData } from "../render/payload-binding";
|
|
5
|
+
import { 箱に積む, type 積んだ行 } from "../kinds/box-lines";
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* tree-hierarchy kind ... 1 node に tree node 配列 (treeData) を保持し、
|
|
@@ -11,10 +13,55 @@ import { TONE } from "../render/tone";
|
|
|
11
13
|
* - depth 1+ は tone 別 accent の left-border + 白 fill card
|
|
12
14
|
* - parent-child edge は elbow line + 中間 join dot で「分岐点」 を強調
|
|
13
15
|
*/
|
|
14
|
-
|
|
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
|
+
|
|
52
|
+
export function TreeNode({
|
|
53
|
+
node,
|
|
54
|
+
active,
|
|
55
|
+
stateValues = {},
|
|
56
|
+
}: {
|
|
57
|
+
node: LaidNode;
|
|
58
|
+
active: boolean;
|
|
59
|
+
stateValues?: Record<string, string>;
|
|
60
|
+
}): JSX.Element {
|
|
15
61
|
const x0 = node.cx - node.w / 2;
|
|
16
62
|
const y0 = node.cy - node.h / 2;
|
|
17
|
-
|
|
63
|
+
// 名前は状態を読める (#467)。 解決できない名前は書いたまま残して印を付ける
|
|
64
|
+
const treeNodes = resolveTreeData(node.treeData ?? [], stateValues);
|
|
18
65
|
const gradientId = `tree-root-grad-${node.id}`;
|
|
19
66
|
|
|
20
67
|
const PAD_TOP = 40;
|
|
@@ -78,8 +125,24 @@ export function TreeNode({ node, active }: { node: LaidNode; active: boolean }):
|
|
|
78
125
|
{layout.nodes.map((n) => {
|
|
79
126
|
const accent = depthColors[Math.min(n.depth, depthColors.length - 1)] ?? depthColors[0]!;
|
|
80
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() !== "";
|
|
81
140
|
return (
|
|
82
|
-
<g
|
|
141
|
+
<g
|
|
142
|
+
key={`n-${n.id}`}
|
|
143
|
+
data-cdl-unresolved={n.unresolved ? "true" : undefined}
|
|
144
|
+
transform={`translate(${PAD_LEFT + n.x - n.w / 2} ${PAD_TOP + n.y - n.h / 2})`}
|
|
145
|
+
>
|
|
83
146
|
{isRoot ? (
|
|
84
147
|
<>
|
|
85
148
|
<rect
|
|
@@ -91,16 +154,26 @@ export function TreeNode({ node, active }: { node: LaidNode; active: boolean }):
|
|
|
91
154
|
rx={12}
|
|
92
155
|
fill={`url(#${gradientId})`}
|
|
93
156
|
/>
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
+
)}
|
|
104
177
|
</>
|
|
105
178
|
) : (
|
|
106
179
|
<>
|
|
@@ -116,16 +189,26 @@ export function TreeNode({ node, active }: { node: LaidNode; active: boolean }):
|
|
|
116
189
|
strokeWidth={1.5}
|
|
117
190
|
/>
|
|
118
191
|
<rect x={0} y={0} width={4} height={n.h} rx={2} fill={accent} />
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
+
)}
|
|
129
212
|
</>
|
|
130
213
|
)}
|
|
131
214
|
</g>
|
|
@@ -138,11 +221,15 @@ export function TreeNode({ node, active }: { node: LaidNode; active: boolean }):
|
|
|
138
221
|
type LayoutNode = {
|
|
139
222
|
id: string;
|
|
140
223
|
title: string;
|
|
224
|
+
/** 箱の 2 行目に出す補足 (#471) */
|
|
225
|
+
subtitle?: string;
|
|
141
226
|
x: number;
|
|
142
227
|
y: number;
|
|
143
228
|
w: number;
|
|
144
229
|
h: number;
|
|
145
230
|
depth: number;
|
|
231
|
+
/** 名前を状態から解決できなかった項目に付く印 (#467) */
|
|
232
|
+
unresolved?: true;
|
|
146
233
|
};
|
|
147
234
|
|
|
148
235
|
export function computeTreeLayout(
|
|
@@ -231,7 +318,17 @@ export function computeTreeLayout(
|
|
|
231
318
|
}
|
|
232
319
|
const y = depth * depthGap + depthGap / 2;
|
|
233
320
|
posOf.set(n.id, { x, y });
|
|
234
|
-
nodes.push({
|
|
321
|
+
nodes.push({
|
|
322
|
+
id: n.id,
|
|
323
|
+
title: n.title,
|
|
324
|
+
...(n.subtitle !== undefined ? { subtitle: n.subtitle } : {}),
|
|
325
|
+
x,
|
|
326
|
+
y,
|
|
327
|
+
w: nodeW,
|
|
328
|
+
h: nodeH,
|
|
329
|
+
depth,
|
|
330
|
+
...((n as { unresolved?: true }).unresolved ? { unresolved: true as const } : {}),
|
|
331
|
+
});
|
|
235
332
|
return x;
|
|
236
333
|
};
|
|
237
334
|
for (const r of roots) if (!placed.has(r.id)) place(r, 0);
|
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
|
|