@cardenelabs/cdl 0.83.0 → 0.102.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 +851 -1
- package/dist/index.cjs +878 -186
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -3
- package/dist/index.d.ts +17 -3
- package/dist/index.js +878 -186
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +869 -178
- 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 +869 -178
- package/dist/react.js.map +1 -1
- package/dist/{render-DfXCs3Eh.d.cts → render-oEmwYN-q.d.cts} +21 -3
- package/dist/{render-DfXCs3Eh.d.ts → render-oEmwYN-q.d.ts} +21 -3
- package/package.json +1 -1
- package/src/kinds/chart-gauge.tsx +20 -3
- package/src/kinds/chart-legend-number.tsx +33 -0
- package/src/kinds/chart-number.ts +33 -0
- package/src/kinds/chart-radial.tsx +24 -7
- package/src/kinds/chart-stat.tsx +35 -5
- package/src/kinds/chart-waffle.tsx +25 -7
- package/src/kinds/dyn-shape.tsx +72 -12
- package/src/kinds/funnel.tsx +50 -2
- package/src/kinds/mind-map.tsx +206 -64
- package/src/kinds/quadrant.tsx +318 -45
- package/src/kinds/shape-blockchain.tsx +50 -18
- package/src/kinds/shape-commerce.tsx +27 -4
- package/src/kinds/shape-region.tsx +42 -1
- package/src/kinds/shape-software.tsx +51 -12
- package/src/kinds/storage.tsx +16 -8
- package/src/kinds/tree.tsx +46 -10
- package/src/lang.ts +5 -5
- package/src/layout/nodes.ts +3 -1
- package/src/layout/spec.ts +154 -25
- package/src/layout/text-width.ts +25 -0
- package/src/layout/viewbox.ts +32 -0
- package/src/presets.ts +21 -7
- package/src/render/nodes.tsx +77 -0
- package/src/render/payload-binding.ts +108 -25
- package/src/render/stage.tsx +24 -2
- package/src/types.ts +117 -38
- package/src/visual-validate.ts +61 -1
package/src/kinds/mind-map.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import type { LaidNode, Tone, MindBranchNode, MindBranchPayload } from "../types
|
|
|
4
4
|
import { TONE } from "../render/tone";
|
|
5
5
|
import { resolveMindData } from "../render/payload-binding";
|
|
6
6
|
import { 進みを畳む, 伸ばすdash, 段の伸び } from "./draw-progress";
|
|
7
|
+
import { measureTextWidth } from "../layout/text-width";
|
|
7
8
|
|
|
8
9
|
const MIND_TONES: Tone[] = ["accent", "teal", "success", "warning", "info", "error"];
|
|
9
10
|
|
|
@@ -21,6 +22,30 @@ const MIND_TONES: Tone[] = ["accent", "teal", "success", "warning", "info", "err
|
|
|
21
22
|
*/
|
|
22
23
|
export const MIND_BOX_GAP = 24;
|
|
23
24
|
|
|
25
|
+
/**
|
|
26
|
+
* 枝の太さ。 中心に近いほど太く、外へ行くほど細い (#907)。
|
|
27
|
+
*
|
|
28
|
+
* 放射の図は「中心から外へ広がる」 ことを描く図なので、太さが一様だと構造が絵に出ない。
|
|
29
|
+
*
|
|
30
|
+
* **下限を置く**。 段数に上限が無いため、引き算だけだと深い図で 0 を割り、線が消える。
|
|
31
|
+
* 消えた線は「繋がっていない」 と読まれるので、細くても残す方を採る。
|
|
32
|
+
*/
|
|
33
|
+
export const 枝の太さ = (段: number): number => Math.max(1.2, 3.2 - 段 * 0.7);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 箱の高さの上限 (cdl#913)。
|
|
37
|
+
*
|
|
38
|
+
* 葉が少ない図では帯の間隔が広く開くので、箱もそのぶん高くする。 上限が無いと、
|
|
39
|
+
* 葉が 1 枚の図で箱だけがカードいっぱいに伸びる。
|
|
40
|
+
*
|
|
41
|
+
* 中心 (52) を超えない範囲で段の差を残す = 中心 > 第 1 階層 > その先 の順に低くする。
|
|
42
|
+
*/
|
|
43
|
+
const BRANCH_BOX_H_MAX = 48;
|
|
44
|
+
const SUB_BOX_H_MAX = 40;
|
|
45
|
+
|
|
46
|
+
/** 隣り合う枝の縦の間隔の上限 (cdl#913)。 置かないと、枝の少ない図で枝が細長く伸びる */
|
|
47
|
+
const ROW_GAP_MAX = 96;
|
|
48
|
+
|
|
24
49
|
/**
|
|
25
50
|
* mind-map kind ... 1 node に mindData (rootId / rootTitle / branches) を保持し、
|
|
26
51
|
* 中心 root + 放射 branch を SVG 描画する。
|
|
@@ -118,7 +143,9 @@ export function MindMapNode({
|
|
|
118
143
|
{layout.edges.map((e, i) => {
|
|
119
144
|
// 循環を閉じる枝は根から外へ向かう枝ではないため、全 node が出てから表示する
|
|
120
145
|
const 枝の伸び = e.deferUntilComplete
|
|
121
|
-
?
|
|
146
|
+
? 伸び >= 1
|
|
147
|
+
? 1
|
|
148
|
+
: 0
|
|
122
149
|
: 段の伸び(伸び, e.depth, 最大深さ);
|
|
123
150
|
return (
|
|
124
151
|
<path
|
|
@@ -127,7 +154,7 @@ export function MindMapNode({
|
|
|
127
154
|
d={e.d}
|
|
128
155
|
fill="none"
|
|
129
156
|
stroke={toneColor(e.tone)}
|
|
130
|
-
strokeWidth={
|
|
157
|
+
strokeWidth={枝の太さ(e.depth)}
|
|
131
158
|
strokeOpacity={0.55}
|
|
132
159
|
strokeLinecap="round"
|
|
133
160
|
{...伸ばすdash(drawing, 枝の伸び)}
|
|
@@ -140,63 +167,79 @@ export function MindMapNode({
|
|
|
140
167
|
if (!箱が出る(n.depth)) return null;
|
|
141
168
|
// 補足は箱の中の 2 行目に出す (#471)。 箱の高さは変えないため、 入らない時は落ちる
|
|
142
169
|
const 材料 = 放射の行(n);
|
|
143
|
-
const 行 = 箱に積む(材料, n.w, n.h);
|
|
144
170
|
// 判定は行数でなく「補足を渡したか」 で行う (理由は tree.tsx と同じ、 #471 Round 1)
|
|
145
171
|
const 補足あり = (n.subtitle ?? "").trim() !== "";
|
|
146
172
|
// 補足ありでは `箱に積む` が既に同じ処理を済ませているため、1 行の経路だけ計算する。
|
|
147
173
|
// 名前の大きさは `放射の行` から取る (#538、 理由は tree.tsx と同じ)
|
|
148
174
|
const 名前 = 材料[0]!;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
175
|
+
/*
|
|
176
|
+
末端は枠を持たない (#907)。 枠があると名前が枠の幅まで縮められるか切られる。
|
|
177
|
+
枠の幅は段の深さで決まるため、深い図では下限まで縮んで名前が読めなくなる。
|
|
178
|
+
|
|
179
|
+
代わりに名前を **中心から外へ** 伸ばす。 起点は枠があった位置の内側の端で、
|
|
180
|
+
使える幅はそこから描画域の端まで。 外向きなら隣の段とぶつからない
|
|
181
|
+
= 隣の段は中心側に在り、外側には描画域の端しか無い。
|
|
182
|
+
*/
|
|
183
|
+
const 枠なし = !n.isRoot && n.isLeaf;
|
|
184
|
+
const 外へ = n.side === "right";
|
|
185
|
+
const 文字のx = 枠なし ? n.x + (外へ ? -n.w / 2 : n.w / 2) : n.x;
|
|
186
|
+
const 文字の寄せ = 枠なし ? (外へ ? "start" : "end") : "middle";
|
|
187
|
+
const 描画域の端 = 外へ ? node.w - MIND_PAD : MIND_PAD;
|
|
188
|
+
const 使える = 枠なし ? Math.abs(描画域の端 - 文字のx) : 使える幅(n.w);
|
|
189
|
+
const 収めた名前 = 補足あり ? undefined : 幅に収める(名前.text, 名前.fontSize, 使える);
|
|
190
|
+
// 積む側は箱の幅から余白を引いて使える幅を出すので、枠が無い時は同じ幅になる
|
|
191
|
+
// 箱の幅を渡す。 助っ人の引数を増やすと木の側にも要らない欄が付く
|
|
192
|
+
const 行 = 箱に積む(材料, 枠なし ? 使える + (n.w - 使える幅(n.w)) : n.w, n.h);
|
|
152
193
|
const 文字の色 = n.isRoot
|
|
153
194
|
? "var(--cdl-text-on-tone, #ffffff)"
|
|
154
195
|
: "var(--cdl-chip-text, #1a1f2a)";
|
|
155
196
|
return (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
197
|
+
<g key={`n-${n.id}`} data-cdl-unresolved={n.unresolved ? "true" : undefined}>
|
|
198
|
+
{枠なし ? null : (
|
|
199
|
+
<rect
|
|
200
|
+
x={n.x - n.w / 2}
|
|
201
|
+
y={n.y - n.h / 2}
|
|
202
|
+
width={n.w}
|
|
203
|
+
height={n.h}
|
|
204
|
+
rx={n.isRoot ? 22 : 10}
|
|
205
|
+
fill={n.isRoot ? toneColor(n.tone ?? "accent") : "var(--cdl-chip-fill, #f4f6fb)"}
|
|
206
|
+
stroke={n.isRoot ? toneColor(n.tone ?? "accent") : toneColor(n.tone)}
|
|
207
|
+
strokeWidth={n.isRoot ? 2 : 1.75}
|
|
208
|
+
/>
|
|
209
|
+
)}
|
|
210
|
+
{収めた名前 === undefined ? (
|
|
211
|
+
行.map((r) => (
|
|
212
|
+
<text
|
|
213
|
+
key={r.役割}
|
|
214
|
+
data-cdl-role={r.役割 === "subtitle" ? "mind-node-subtitle" : undefined}
|
|
215
|
+
x={文字のx}
|
|
216
|
+
y={n.y + r.dy}
|
|
217
|
+
fontSize={r.fontSize}
|
|
218
|
+
fontWeight={r.役割 === "title" ? (n.isRoot ? 700 : 600) : 500}
|
|
219
|
+
textAnchor={文字の寄せ}
|
|
220
|
+
fill={文字の色}
|
|
221
|
+
opacity={r.役割 === "subtitle" ? 0.85 : undefined}
|
|
222
|
+
>
|
|
223
|
+
{r.text}
|
|
224
|
+
</text>
|
|
225
|
+
))
|
|
226
|
+
) : (
|
|
227
|
+
// 補足が無くても **幅に収める** (#526)。 `箱に積む` を通らないこの経路だけが
|
|
228
|
+
// 幅を見ておらず、長い名前がそのまま箱の外へ出て枝の線と重なっていた
|
|
229
|
+
// (実測 = 中心の箱で文字 163px に対し箱 120px)。
|
|
230
|
+
// 収める手段は縮めるのが先で、切るのは下限まで縮めても入らない時だけ (#534)
|
|
169
231
|
<text
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
fontWeight={r.役割 === "title" ? (n.isRoot ? 700 : 600) : 500}
|
|
176
|
-
textAnchor="middle"
|
|
232
|
+
x={文字のx}
|
|
233
|
+
y={n.y + 5}
|
|
234
|
+
fontSize={収めた名前.fontSize}
|
|
235
|
+
fontWeight={n.isRoot ? 700 : 600}
|
|
236
|
+
textAnchor={文字の寄せ}
|
|
177
237
|
fill={文字の色}
|
|
178
|
-
opacity={r.役割 === "subtitle" ? 0.85 : undefined}
|
|
179
238
|
>
|
|
180
|
-
{
|
|
239
|
+
{収めた名前.text}
|
|
181
240
|
</text>
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
// 補足が無くても **幅に収める** (#526)。 `箱に積む` を通らないこの経路だけが
|
|
185
|
-
// 幅を見ておらず、長い名前がそのまま箱の外へ出て枝の線と重なっていた
|
|
186
|
-
// (実測 = 中心の箱で文字 163px に対し箱 120px)。
|
|
187
|
-
// 収める手段は縮めるのが先で、切るのは下限まで縮めても入らない時だけ (#534)
|
|
188
|
-
<text
|
|
189
|
-
x={n.x}
|
|
190
|
-
y={n.y + 5}
|
|
191
|
-
fontSize={収めた名前.fontSize}
|
|
192
|
-
fontWeight={n.isRoot ? 700 : 600}
|
|
193
|
-
textAnchor="middle"
|
|
194
|
-
fill={文字の色}
|
|
195
|
-
>
|
|
196
|
-
{収めた名前.text}
|
|
197
|
-
</text>
|
|
198
|
-
)}
|
|
199
|
-
</g>
|
|
241
|
+
)}
|
|
242
|
+
</g>
|
|
200
243
|
);
|
|
201
244
|
})}
|
|
202
245
|
</g>
|
|
@@ -214,6 +257,10 @@ type LayoutNode = {
|
|
|
214
257
|
h: number;
|
|
215
258
|
tone?: Tone;
|
|
216
259
|
isRoot: boolean;
|
|
260
|
+
/** 子を持たないか。 末端は枠を持たず、名前を外へ伸ばす */
|
|
261
|
+
isLeaf: boolean;
|
|
262
|
+
/** 中心の左右どちら側に置かれたか。 名前を伸ばす向きを決める */
|
|
263
|
+
side: "left" | "right";
|
|
217
264
|
/** 中心からの段数 (中心が 0、 cdl#520)。 段ごとに順に伸ばすのに使う */
|
|
218
265
|
depth: number;
|
|
219
266
|
/** 名前を状態から解決できなかった項目に付く印 (#467) */
|
|
@@ -229,6 +276,25 @@ type LayoutEdge = {
|
|
|
229
276
|
deferUntilComplete: boolean;
|
|
230
277
|
};
|
|
231
278
|
|
|
279
|
+
/**
|
|
280
|
+
* その箱が横にどこまで描かれるか (cdl#913)。
|
|
281
|
+
*
|
|
282
|
+
* **枠なしの末端は箱で測れない**。 末端は枠を持たず (#907)、名前を枠があった位置の
|
|
283
|
+
* 内側の端から **中心の反対側へ** 伸ばす。 箱の幅で測ると、短い名前の末端が実際より
|
|
284
|
+
* 外まで占めているように見え、寄せ幅がそのぶんずれる
|
|
285
|
+
* (実測 = 左の余り 21% に対し右の余り 27%)。
|
|
286
|
+
*
|
|
287
|
+
* 名前が描画域に入らない時、描く側は字を縮めてから切る。 ここで出すのは縮める前の幅 =
|
|
288
|
+
* 実際に描く幅の上限になる。 縮むのは名前が溢れる図だけで、その時も箱の幅で測るより近い。
|
|
289
|
+
*/
|
|
290
|
+
function 描く横の端(n: LayoutNode): [number, number] {
|
|
291
|
+
if (n.isRoot || !n.isLeaf) return [n.x - n.w / 2, n.x + n.w / 2];
|
|
292
|
+
const 起点 = n.x + (n.side === "right" ? -n.w / 2 : n.w / 2);
|
|
293
|
+
const 名前 = 放射の行(n)[0]!;
|
|
294
|
+
const 幅 = measureTextWidth(名前.text, { fontSize: 名前.fontSize, fontWeight: 600 });
|
|
295
|
+
return n.side === "right" ? [起点, 起点 + 幅] : [起点 - 幅, 起点];
|
|
296
|
+
}
|
|
297
|
+
|
|
232
298
|
export function computeMindMapLayout(
|
|
233
299
|
mindData: MindBranchPayload,
|
|
234
300
|
cx: number,
|
|
@@ -251,6 +317,9 @@ export function computeMindMapLayout(
|
|
|
251
317
|
w: rootW,
|
|
252
318
|
h: rootH,
|
|
253
319
|
isRoot: true,
|
|
320
|
+
// 中心は枝を持たなくても枠を描く = 図の起点なので、無いと何から広がるのかが読めない
|
|
321
|
+
isLeaf: false,
|
|
322
|
+
side: "right",
|
|
254
323
|
// 中心は起点なので段数 0 (cdl#520)
|
|
255
324
|
depth: 0,
|
|
256
325
|
...((mindData as { unresolved?: true }).unresolved ? { unresolved: true as const } : {}),
|
|
@@ -293,7 +362,8 @@ export function computeMindMapLayout(
|
|
|
293
362
|
const firstCount = firstLevel.length;
|
|
294
363
|
const leftCount = Math.ceil(firstCount / 2);
|
|
295
364
|
const branchW = 130;
|
|
296
|
-
|
|
365
|
+
// 箱の高さは帯の間隔から出す (cdl#913)。 決め打ちの高さは上限としてだけ残る
|
|
366
|
+
// (`BRANCH_BOX_H_MAX` / `SUB_BOX_H_MAX`)
|
|
297
367
|
const subBranchW = 100;
|
|
298
368
|
const subBranchH = 30;
|
|
299
369
|
|
|
@@ -316,7 +386,10 @@ export function computeMindMapLayout(
|
|
|
316
386
|
return kids.reduce((sum, k) => sum + leafCountOf(k.id, seen), 0);
|
|
317
387
|
};
|
|
318
388
|
|
|
319
|
-
const maxSubDepth =
|
|
389
|
+
const maxSubDepth =
|
|
390
|
+
firstLevel.length > 0
|
|
391
|
+
? Math.max(0, ...firstLevel.map((br) => subDepthOf(br.id, new Set())))
|
|
392
|
+
: 0;
|
|
320
393
|
|
|
321
394
|
// branch を root からどれだけ離すか。 深いネストがあるほど sub 世代のぶん内側に寄せる。
|
|
322
395
|
const minLayer1XOffset = rootW / 2 + branchW / 2 + MIND_BOX_GAP;
|
|
@@ -335,7 +408,8 @@ export function computeMindMapLayout(
|
|
|
335
408
|
//
|
|
336
409
|
// **箱の幅を先に確保し、 余った分を間隔に回す**。 浅い図では望ましい値になり、 深い図では
|
|
337
410
|
// 従来どおりまで詰まる = 箱が潰れる形は増やさない。
|
|
338
|
-
const gapRoom =
|
|
411
|
+
const gapRoom =
|
|
412
|
+
maxSubDepth > 0 ? (room - branchW / 2 - maxSubDepth * MIN_SUB_W) / maxSubDepth : 0;
|
|
339
413
|
const gap = Math.max(8, Math.min(PREFERRED_GAP, gapRoom));
|
|
340
414
|
// 最深 box の外端が canvas 端に収まる effSubW を解く。
|
|
341
415
|
// layer1XOffset + branchW/2 + effSubW*maxSubDepth + maxSubDepth*gap <= canvasW/2
|
|
@@ -348,33 +422,60 @@ export function computeMindMapLayout(
|
|
|
348
422
|
: subBranchW;
|
|
349
423
|
const step1 = (branchW + effSubW) / 2 + gap; // branch → 第 1 sub
|
|
350
424
|
const stepN = effSubW + gap; // sub → sub
|
|
351
|
-
const subOffset = (subDepth: number): number =>
|
|
425
|
+
const subOffset = (subDepth: number): number =>
|
|
426
|
+
subDepth <= 0 ? 0 : step1 + (subDepth - 1) * stepN;
|
|
352
427
|
|
|
353
428
|
// 縦レーン。 左右それぞれの branch 群の葉数合計でレーンを割る。
|
|
354
429
|
const leftBranches = firstLevel.slice(0, leftCount);
|
|
355
430
|
const rightBranches = firstLevel.slice(leftCount);
|
|
356
431
|
const leavesOfSide = (brs: MindBranchNode[]): number =>
|
|
357
|
-
Math.max(
|
|
432
|
+
Math.max(
|
|
433
|
+
1,
|
|
434
|
+
brs.reduce((sum, br) => sum + leafCountOf(br.id, new Set()), 0),
|
|
435
|
+
);
|
|
358
436
|
const sideLaneCount = Math.max(leavesOfSide(leftBranches), leavesOfSide(rightBranches));
|
|
359
|
-
|
|
360
|
-
|
|
437
|
+
// 帯の高さはカードから決める (cdl#913)。 葉の数で頭打ちにすると、葉が 3 枚の図は
|
|
438
|
+
// 3 × 44 = 132 に固定され、カードが 600 あっても中身は真ん中の 132 にしか広がらない。
|
|
439
|
+
// 見本帳の実測で、放射の図は文字がカードの 13% しか覆っていなかった。
|
|
440
|
+
//
|
|
441
|
+
// **一番高い箱のぶんを先に引く**。 引かずにカードの 9 割を帯にすると、端の帯に置いた箱が
|
|
442
|
+
// カードから出て、末尾の一様縮小が効く。 縮小は横にも効くため、枝の長さまで縮む
|
|
443
|
+
// (実測 = 枝の長さが 24 を下回り `render-polish-240` が落ちた)。
|
|
444
|
+
const laneBandH = Math.max(0, canvasH - BRANCH_BOX_H_MAX);
|
|
445
|
+
const rowGap =
|
|
446
|
+
sideLaneCount > 1
|
|
447
|
+
? Math.min(laneBandH / (sideLaneCount - 1), ROW_GAP_MAX)
|
|
448
|
+
: subBranchH + 14;
|
|
361
449
|
// box 高は必ず rowGap の一定割合に収める。 加算的な floor を置くと、 leaf が多く rowGap が
|
|
362
450
|
// 小さいとき floor が rowGap を超えて縦重なりする。 rowGap の 70% にすれば規模によらず gap が残る。
|
|
363
|
-
|
|
364
|
-
|
|
451
|
+
//
|
|
452
|
+
// **上限を決め打ちの高さではなく別に持つ** (cdl#913)。 葉が少ない図では rowGap が広く開くので、
|
|
453
|
+
// 決め打ちを上限にすると箱だけが取り残されて間が空く。 一方で上限を外すと、葉が 1 枚の図で
|
|
454
|
+
// 箱だけが伸びる。 葉が多い図の挙動 (rowGap の 70%) は変えない。
|
|
455
|
+
const effSubH = Math.min(SUB_BOX_H_MAX, rowGap * 0.7);
|
|
456
|
+
const effBranchH = Math.min(BRANCH_BOX_H_MAX, rowGap * 0.7);
|
|
365
457
|
// canvas の上下端 (full-node 座標)。 lane はこの内側に収める。
|
|
366
|
-
|
|
367
|
-
const
|
|
458
|
+
// 引くのは **高いほうの箱** の半分 = 第 1 階層の箱は末端より高く、末端に合わせると端で出る
|
|
459
|
+
const 端の余り = Math.max(effSubH, effBranchH) / 2;
|
|
460
|
+
const laneTop = cy - canvasH / 2 + 端の余り;
|
|
461
|
+
const laneBottom = cy + canvasH / 2 - 端の余り;
|
|
462
|
+
/**
|
|
463
|
+
* 帯の中の縦位置 (cdl#913)。
|
|
464
|
+
*
|
|
465
|
+
* **実際に使う高さで中央に置く**。 帯の上端から置くと、間隔に上限が効いた図で
|
|
466
|
+
* 中身が上に寄る = 帯は広いのに使うのは一部、という形になる。
|
|
467
|
+
*/
|
|
468
|
+
const 使う高さ = (sideLaneCount - 1) * rowGap;
|
|
368
469
|
const laneY = (lane: number): number => {
|
|
369
470
|
if (sideLaneCount <= 1) return cy;
|
|
370
|
-
const y = cy -
|
|
471
|
+
const y = cy - 使う高さ / 2 + lane * rowGap;
|
|
371
472
|
return Math.max(laneTop, Math.min(laneBottom, y));
|
|
372
473
|
};
|
|
373
474
|
|
|
374
475
|
// scale 後の root 半幅を使う (最終スケールで root も縮む)。
|
|
375
|
-
const rootEdgeXFor = (isLeft: boolean): number => {
|
|
476
|
+
const rootEdgeXFor = (isLeft: boolean, 中心のx: number): number => {
|
|
376
477
|
const half = nodeHalfW.get(mindData.rootId) ?? rootW / 2;
|
|
377
|
-
return
|
|
478
|
+
return 中心のx + (isLeft ? -half : half);
|
|
378
479
|
};
|
|
379
480
|
|
|
380
481
|
// subtree を再帰配置する。 leaf は次のレーン、 親は子レーンの中央。 x は深さで決まる。
|
|
@@ -400,7 +501,14 @@ export function computeMindMapLayout(
|
|
|
400
501
|
laneRef.cursor += 1;
|
|
401
502
|
} else {
|
|
402
503
|
const ys = kids.map((k, i) =>
|
|
403
|
-
placeSubtree(
|
|
504
|
+
placeSubtree(
|
|
505
|
+
k,
|
|
506
|
+
subDepth + 1,
|
|
507
|
+
isLeft,
|
|
508
|
+
myTone ?? MIND_TONES[i % MIND_TONES.length]!,
|
|
509
|
+
placed,
|
|
510
|
+
laneRef,
|
|
511
|
+
),
|
|
404
512
|
);
|
|
405
513
|
y = (ys[0]! + ys[ys.length - 1]!) / 2;
|
|
406
514
|
}
|
|
@@ -417,6 +525,8 @@ export function computeMindMapLayout(
|
|
|
417
525
|
depth: subDepth + 1,
|
|
418
526
|
tone: myTone,
|
|
419
527
|
isRoot: false,
|
|
528
|
+
isLeaf: kids.length === 0,
|
|
529
|
+
side: isLeft ? "left" : "right",
|
|
420
530
|
...((node as { unresolved?: true }).unresolved ? { unresolved: true as const } : {}),
|
|
421
531
|
});
|
|
422
532
|
nodePos.set(node.id, { x, y });
|
|
@@ -427,7 +537,10 @@ export function computeMindMapLayout(
|
|
|
427
537
|
};
|
|
428
538
|
|
|
429
539
|
const placeSide = (brs: MindBranchNode[], isLeft: boolean, toneBase: number): void => {
|
|
430
|
-
|
|
540
|
+
// 葉の少ない側を帯の中で中央へ寄せる (cdl#913)。 0 番から置くと、左が 3 枚で右が 1 枚の
|
|
541
|
+
// 図で右の 1 枚が帯の上端に付き、中心の箱と高さが揃わない
|
|
542
|
+
const 葉数 = brs.reduce((sum, br) => sum + leafCountOf(br.id, new Set()), 0);
|
|
543
|
+
const laneRef = { cursor: Math.max(0, (sideLaneCount - 葉数) / 2) };
|
|
431
544
|
const placed = new Set<string>();
|
|
432
545
|
brs.forEach((br, i) => {
|
|
433
546
|
const tone = br.tone ?? MIND_TONES[(toneBase + i) % MIND_TONES.length]!;
|
|
@@ -464,6 +577,36 @@ export function computeMindMapLayout(
|
|
|
464
577
|
}
|
|
465
578
|
}
|
|
466
579
|
|
|
580
|
+
// 全体を横中央へ寄せる (cdl#913)。
|
|
581
|
+
//
|
|
582
|
+
// 中心の箱はカードの真ん中に置いているが、左右で段数が違うと伸びた側に絵の重心が寄る。
|
|
583
|
+
// 左が 2 段で右が 1 段の図では、右にだけ余りが残る。
|
|
584
|
+
//
|
|
585
|
+
// **箱を置き終えた後に、塊ごと動かす**。 置く時に寄せると、寄せ幅が置く順に依存する。
|
|
586
|
+
// 動かすのは横だけ = 縦は帯が既に中央で、動かすと上下の釣り合いが崩れる。
|
|
587
|
+
{
|
|
588
|
+
let 左端 = Number.POSITIVE_INFINITY;
|
|
589
|
+
let 右端 = Number.NEGATIVE_INFINITY;
|
|
590
|
+
for (const n of nodes) {
|
|
591
|
+
const [l, r] = 描く横の端(n);
|
|
592
|
+
左端 = Math.min(左端, l);
|
|
593
|
+
右端 = Math.max(右端, r);
|
|
594
|
+
}
|
|
595
|
+
// 整数に丸める。 端数を足すと、枝の長さが座標の差から出るぶん丸め誤差が乗る
|
|
596
|
+
// (実測 = 24 であるべき枝が 23.999999999999943 になり `render-polish-240` が落ちた)。
|
|
597
|
+
// 寄せは見た目の調整なので、1px 未満の精度は要らない
|
|
598
|
+
const 寄せ = Math.round((cx - 左端 - (右端 - cx)) / 2);
|
|
599
|
+
if (Number.isFinite(寄せ) && 寄せ !== 0) {
|
|
600
|
+
for (const n of nodes) {
|
|
601
|
+
n.x += 寄せ;
|
|
602
|
+
nodePos.set(n.id, { x: n.x, y: n.y });
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/** 中心の箱の横位置。 横中央へ寄せた後の値を使う = 寄せる前だと枝が箱の辺から離れる */
|
|
608
|
+
const rootX = nodePos.get(mindData.rootId)?.x ?? cx;
|
|
609
|
+
|
|
467
610
|
// 枝の段数は「その枝の先 (子)」 の段数にする (cdl#520)。 箱を置き終えた後に引く
|
|
468
611
|
const 段数 = new Map(nodes.map((n) => [n.id, n.depth]));
|
|
469
612
|
|
|
@@ -477,7 +620,7 @@ export function computeMindMapLayout(
|
|
|
477
620
|
const childDepth = 段数.get(br.id) ?? 1;
|
|
478
621
|
const parentDepth = 段数.get(br.parent) ?? 0;
|
|
479
622
|
if (br.parent === mindData.rootId) {
|
|
480
|
-
const startX = rootEdgeXFor(isLeft);
|
|
623
|
+
const startX = rootEdgeXFor(isLeft, rootX);
|
|
481
624
|
const endX = childPos.x + (isLeft ? childHalfW : -childHalfW);
|
|
482
625
|
edges.push({
|
|
483
626
|
d: makeCurve(startX, cy, endX, childPos.y, isLeft),
|
|
@@ -508,7 +651,6 @@ function makeCurve(x1: number, y1: number, x2: number, y2: number, _leftBound: b
|
|
|
508
651
|
return `M ${x1} ${y1} C ${midX} ${y1}, ${midX} ${y2}, ${x2} ${y2}`;
|
|
509
652
|
}
|
|
510
653
|
|
|
511
|
-
|
|
512
654
|
function toneColor(tone: Tone | undefined, alpha = 1): string {
|
|
513
655
|
const hex = tone ? TONE[tone] : TONE.accent;
|
|
514
656
|
if (alpha >= 1) return hex;
|