@cardenelabs/cdl 0.122.0 → 0.123.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 +16 -1
- package/README.md +64 -0
- package/dist/index.cjs +1065 -617
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +38 -26
- package/dist/index.d.ts +38 -26
- package/dist/index.js +1065 -618
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +834 -403
- 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 +834 -403
- package/dist/react.js.map +1 -1
- package/dist/{render-U_pC-Ksh.d.cts → render-CVczwfRb.d.cts} +20 -2
- package/dist/{render-U_pC-Ksh.d.ts → render-CVczwfRb.d.ts} +20 -2
- package/package.json +9 -12
- package/src/builder.ts +5 -0
- package/src/index.ts +2 -0
- package/src/kinds/box-lines.ts +17 -4
- package/src/kinds/chart-bar.tsx +4 -0
- package/src/kinds/chart-gauge.tsx +20 -9
- package/src/kinds/chart-pie.tsx +67 -17
- package/src/kinds/chart-radial.tsx +7 -2
- package/src/kinds/chart-slope.tsx +26 -7
- package/src/kinds/chart-stacked-bar.tsx +8 -4
- package/src/kinds/chart-stat.tsx +19 -4
- package/src/kinds/chart-waffle.tsx +7 -2
- package/src/kinds/funnel.tsx +9 -2
- package/src/kinds/generic.tsx +5 -2
- package/src/kinds/mind-map.tsx +26 -10
- package/src/kinds/quadrant.tsx +14 -4
- package/src/kinds/row-align.ts +19 -7
- package/src/kinds/sequence-board-metrics.ts +13 -3
- package/src/kinds/sequence-board.tsx +4 -2
- package/src/kinds/shape-basement.tsx +57 -32
- package/src/kinds/shape-blockchain.tsx +83 -34
- package/src/kinds/shape-commerce.tsx +65 -24
- package/src/kinds/shape-finance.tsx +90 -26
- package/src/kinds/shape-hardware.tsx +48 -17
- package/src/kinds/shape-region.tsx +16 -6
- package/src/kinds/shape-software.tsx +30 -8
- package/src/kinds/text-width.ts +17 -3
- package/src/kinds/tree.tsx +25 -7
- package/src/layout/collisions.ts +21 -8
- package/src/layout/curve-path.ts +3 -2
- package/src/layout/edges.ts +70 -25
- package/src/layout/head-label.ts +3 -1
- package/src/layout/lanes.ts +3 -2
- package/src/layout/metro-path.ts +3 -2
- package/src/layout/nodes.ts +14 -6
- package/src/layout/predict-bbox.ts +6 -3
- package/src/layout/self-loop.ts +10 -4
- package/src/layout/spec.ts +20 -6
- package/src/layout/text-width.ts +25 -8
- package/src/layout.ts +39 -15
- package/src/presets.ts +69 -43
- package/src/render/edges.tsx +18 -3
- package/src/render/interactive-panel.tsx +34 -7
- package/src/render/nodes.tsx +97 -48
- package/src/render/stage.tsx +3 -0
- package/src/text-metrics.ts +9 -0
- package/src/types.ts +19 -0
- package/src/visual-validate.ts +67 -17
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { JSX } from "react";
|
|
2
|
-
import type { LaidNode } from "../types";
|
|
2
|
+
import type { CdlTextMetrics, LaidNode } from "../types";
|
|
3
3
|
import {
|
|
4
4
|
ASCENT,
|
|
5
5
|
DESCENT,
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
type ShapeProps = {
|
|
44
44
|
node: LaidNode;
|
|
45
45
|
active: boolean;
|
|
46
|
+
textMetrics?: CdlTextMetrics;
|
|
46
47
|
};
|
|
47
48
|
|
|
48
49
|
// basement は shape 全体が body なので、 純白ではなく accent 青系薄 tint (#dde8f0) で
|
|
@@ -81,34 +82,44 @@ function labelBlock(
|
|
|
81
82
|
pos: { x: number; y: number },
|
|
82
83
|
size: number,
|
|
83
84
|
band: LabelBand,
|
|
85
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
84
86
|
): { k: number; x: number; baseline: number; 上端: number; 下端: number } {
|
|
85
87
|
const 上 = node.eyebrow ? size + EYEBROW_GAP + EYEBROW_SIZE * ASCENT : size * ASCENT;
|
|
86
88
|
const 下 = node.subtitle ? size + SUB_GAP + SUB_SIZE * DESCENT : size * DESCENT;
|
|
87
89
|
// 3 行のうち一番広い行が横の制約になる
|
|
88
90
|
const 幅 = Math.max(
|
|
89
|
-
textWidth(node.title ?? "", size),
|
|
90
|
-
node.eyebrow ? textWidth(node.eyebrow, EYEBROW_SIZE, EYEBROW_TRACKING) : 0,
|
|
91
|
-
node.subtitle ? textWidth(node.subtitle, SUB_SIZE) : 0,
|
|
91
|
+
textWidth(node.title ?? "", size, 0, textMetrics),
|
|
92
|
+
node.eyebrow ? textWidth(node.eyebrow, EYEBROW_SIZE, EYEBROW_TRACKING, textMetrics) : 0,
|
|
93
|
+
node.subtitle ? textWidth(node.subtitle, SUB_SIZE, 0, textMetrics) : 0,
|
|
92
94
|
);
|
|
93
95
|
// 横で縮める倍率を読める下限で止める (#953)。 切って収まらない幅では下限を当てない。
|
|
94
96
|
//
|
|
95
97
|
// **題が一番広い行のときだけ当てる** (#957)。 塊の倍率は一番広い行で決まるので、
|
|
96
98
|
// 補いの行が幅を決めている箱で下限を当てると、題を 0.6px 上げるために補いの行から
|
|
97
99
|
// 字を落とすことになる = 払うのと得るのが別の行になる
|
|
98
|
-
const 題幅 = textWidth(node.title ?? "", size);
|
|
100
|
+
const 題幅 = textWidth(node.title ?? "", size, 0, textMetrics);
|
|
101
|
+
const 測る = (t: string, s: number, ls: number): number => textWidth(t, s, ls, textMetrics);
|
|
99
102
|
const 下限 =
|
|
100
|
-
題幅 >= 幅 ? 切って収まる下限(node.title ?? "", size, 0, band.w,
|
|
103
|
+
題幅 >= 幅 ? 切って収まる下限(node.title ?? "", size, 0, band.w, 測る) : 0;
|
|
101
104
|
const { k, x, y: baseline } = fitLabel(band, { 上, 下, 幅 }, pos, 下限);
|
|
102
105
|
return { k, x, baseline, 上端: baseline - 上 * k, 下端: baseline + 下 * k };
|
|
103
106
|
}
|
|
104
107
|
|
|
105
|
-
function commonLabel(
|
|
106
|
-
|
|
108
|
+
function commonLabel(
|
|
109
|
+
node: LaidNode,
|
|
110
|
+
pos: { x: number; y: number },
|
|
111
|
+
size: number,
|
|
112
|
+
band: LabelBand,
|
|
113
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
114
|
+
) {
|
|
115
|
+
const 塊 = labelBlock(node, pos, size, band, textMetrics);
|
|
107
116
|
const k = 塊.k;
|
|
108
117
|
const x = 塊.x;
|
|
109
118
|
// 下限で止めた行は帯に入らないので、入るところまで切って `…` を付ける (#953)
|
|
110
119
|
const 切る = (t: string, 大きさ: number, 字間 = 0) =>
|
|
111
|
-
帯で切る(t, 大きさ * k, band.w, (u, f) =>
|
|
120
|
+
帯で切る(t, 大きさ * k, band.w, (u, f) =>
|
|
121
|
+
textWidth(u, f, 字間 * k, textMetrics),
|
|
122
|
+
);
|
|
112
123
|
return (
|
|
113
124
|
<>
|
|
114
125
|
{node.eyebrow && (
|
|
@@ -171,7 +182,7 @@ const RULE_PAD_RATIO = 0.14;
|
|
|
171
182
|
const RULE_SHRINK_RATIO = 0.055;
|
|
172
183
|
|
|
173
184
|
/** shape-file = 縦長 A4 型 file (right-top folded corner、 body に fold 内側 corner 統合) + drop shadow + text lines */
|
|
174
|
-
export function ShapeFileNode({ node, active }: ShapeProps): JSX.Element {
|
|
185
|
+
export function ShapeFileNode({ node, active, textMetrics = "proportional" }: ShapeProps): JSX.Element {
|
|
175
186
|
const 帯 = bodyBand(node, { 影ry: SHADOW_RY });
|
|
176
187
|
const 紙h = 帯.h;
|
|
177
188
|
const 紙w = Math.min(帯.w, 紙h * 0.72);
|
|
@@ -197,7 +208,7 @@ export function ShapeFileNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
197
208
|
const 名前帯 = { x, y, w: 紙w, h: 紙h };
|
|
198
209
|
const 名前位置 = { x: 帯.cx, y: node.cy + node.h * 0.12 };
|
|
199
210
|
// 名前の塊を先に測る。 3 本目が紙頭から固定 90 の位置に出ていたので、 塊に食い込む線は引かない
|
|
200
|
-
const 名前 = labelBlock(node, 名前位置, 16,
|
|
211
|
+
const 名前 = labelBlock(node, 名前位置, 16, 名前帯, textMetrics);
|
|
201
212
|
const 間隔 = Math.min(RULE_GAP_MAX, 紙h * RULE_GAP_RATIO);
|
|
202
213
|
const 線余白 = Math.min(RULE_PAD_MAX, 紙w * RULE_PAD_RATIO);
|
|
203
214
|
const textLines = [0, 1, 2]
|
|
@@ -231,7 +242,7 @@ export function ShapeFileNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
231
242
|
);
|
|
232
243
|
})}
|
|
233
244
|
{/* label = 中央 (text lines の下、 title 大、 subtitle 小、 A4 下半分) */}
|
|
234
|
-
{commonLabel(node, 名前位置, 16,
|
|
245
|
+
{commonLabel(node, 名前位置, 16, 名前帯, textMetrics)}
|
|
235
246
|
</g>
|
|
236
247
|
);
|
|
237
248
|
}
|
|
@@ -241,7 +252,7 @@ const TAB_H_MAX = 22;
|
|
|
241
252
|
const TAB_H_RATIO = 0.3;
|
|
242
253
|
|
|
243
254
|
/** shape-folder = 左上 tab を body 左端に完全張り付け (tab 左 0、 tab 右 35%、 slope 42%) + drop shadow */
|
|
244
|
-
export function ShapeFolderNode({ node, active }: ShapeProps): JSX.Element {
|
|
255
|
+
export function ShapeFolderNode({ node, active, textMetrics = "proportional" }: ShapeProps): JSX.Element {
|
|
245
256
|
const 帯 = bodyBand(node, { 影ry: SHADOW_RY });
|
|
246
257
|
const x = 帯.x;
|
|
247
258
|
const y = 帯.y;
|
|
@@ -266,7 +277,7 @@ export function ShapeFolderNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
266
277
|
const 境界y = y + tabH + Math.min(8, (帯.h - tabH) * 0.2);
|
|
267
278
|
const 端 = Math.min(6, 帯.w * 0.1);
|
|
268
279
|
const 名前位置 = { x: 帯.cx, y: node.cy + 18 };
|
|
269
|
-
const 名前 = labelBlock(node, 名前位置, 16,
|
|
280
|
+
const 名前 = labelBlock(node, 名前位置, 16, 帯, textMetrics);
|
|
270
281
|
return (
|
|
271
282
|
<g>
|
|
272
283
|
{dropShadow(帯.cx, 帯.底, 帯.w * 0.44)}
|
|
@@ -284,7 +295,7 @@ export function ShapeFolderNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
284
295
|
opacity={0.35}
|
|
285
296
|
/>
|
|
286
297
|
)}
|
|
287
|
-
{commonLabel(node, 名前位置, 16,
|
|
298
|
+
{commonLabel(node, 名前位置, 16, 帯, textMetrics)}
|
|
288
299
|
</g>
|
|
289
300
|
);
|
|
290
301
|
}
|
|
@@ -297,7 +308,7 @@ const CLOUD_VB_H = 512;
|
|
|
297
308
|
const CLOUD_W_RATIO = 0.9;
|
|
298
309
|
|
|
299
310
|
/** shape-cloud = Font Awesome cloud icon path を scale + translate + drop shadow */
|
|
300
|
-
export function ShapeCloudNode({ node, active }: ShapeProps): JSX.Element {
|
|
311
|
+
export function ShapeCloudNode({ node, active, textMetrics = "proportional" }: ShapeProps): JSX.Element {
|
|
301
312
|
const 帯 = bodyBand(node, { 影ry: SHADOW_RY });
|
|
302
313
|
const frame = commonFrame(active);
|
|
303
314
|
// Font Awesome "cloud" icon の path (viewBox 640 x 512) を帯に scale + center。
|
|
@@ -328,7 +339,7 @@ export function ShapeCloudNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
328
339
|
/>
|
|
329
340
|
</g>
|
|
330
341
|
{/* label = cloud 内側の中央 */}
|
|
331
|
-
{commonLabel(node, { x: 帯.cx, y: node.cy + node.h * 0.05 }, 16,
|
|
342
|
+
{commonLabel(node, { x: 帯.cx, y: node.cy + node.h * 0.05 }, 16, 帯, textMetrics)}
|
|
332
343
|
</g>
|
|
333
344
|
);
|
|
334
345
|
}
|
|
@@ -341,7 +352,7 @@ const CYLINDER_RY_RATIO = 0.1;
|
|
|
341
352
|
const CYLINDER_DISKS = [0.3, 0.55, 0.8];
|
|
342
353
|
|
|
343
354
|
/** shape-cylinder = 3D 円柱 (上面 ellipse + 側面 + 底面 arc 下半分のみ) + drop shadow */
|
|
344
|
-
export function ShapeCylinderNode({ node, active }: ShapeProps): JSX.Element {
|
|
355
|
+
export function ShapeCylinderNode({ node, active, textMetrics = "proportional" }: ShapeProps): JSX.Element {
|
|
345
356
|
const 帯 = bodyBand(node, { 影ry: CYLINDER_SHADOW_RY });
|
|
346
357
|
const x = 帯.x;
|
|
347
358
|
const y = 帯.y;
|
|
@@ -361,7 +372,7 @@ export function ShapeCylinderNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
361
372
|
const 蓋底 = y + ry * 2;
|
|
362
373
|
const 名前位置 = { x: 帯.cx, y: node.cy + 6 };
|
|
363
374
|
const 名前帯 = { x: 帯.x, y: 蓋底, w: 帯.w, h: Math.max(0, 帯.底 - 蓋底) };
|
|
364
|
-
const 名前 = labelBlock(node, 名前位置, 15,
|
|
375
|
+
const 名前 = labelBlock(node, 名前位置, 15, 名前帯, textMetrics);
|
|
365
376
|
return (
|
|
366
377
|
<g>
|
|
367
378
|
{dropShadow(帯.cx, 帯.底, rx * 0.85, CYLINDER_SHADOW_RY)}
|
|
@@ -386,7 +397,7 @@ export function ShapeCylinderNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
386
397
|
opacity={0.3}
|
|
387
398
|
/>
|
|
388
399
|
))}
|
|
389
|
-
{commonLabel(node, 名前位置, 15,
|
|
400
|
+
{commonLabel(node, 名前位置, 15, 名前帯, textMetrics)}
|
|
390
401
|
</g>
|
|
391
402
|
);
|
|
392
403
|
}
|
|
@@ -395,7 +406,7 @@ export function ShapeCylinderNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
395
406
|
const HEX_DOT_R = 2.5;
|
|
396
407
|
|
|
397
408
|
/** shape-hexagon = 六角形 (component / service 表現、 accent 頂点) + drop shadow */
|
|
398
|
-
export function ShapeHexagonNode({ node, active }: ShapeProps): JSX.Element {
|
|
409
|
+
export function ShapeHexagonNode({ node, active, textMetrics = "proportional" }: ShapeProps): JSX.Element {
|
|
399
410
|
// 点は多角形の外へ出るので、 枠より点の方が広ければ点に合わせて帯を縮める
|
|
400
411
|
const 帯 = bodyBand(node, { 影ry: SHADOW_RY, 外: Math.max(HEX_DOT_R, FRAME_HALF) });
|
|
401
412
|
const cx = 帯.cx;
|
|
@@ -426,13 +437,14 @@ export function ShapeHexagonNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
426
437
|
{ x: cx, y: node.cy + 6 },
|
|
427
438
|
16,
|
|
428
439
|
{ x: 帯.x + cut, y: 帯.y, w: Math.max(0, 帯.w - cut * 2), h: 帯.h },
|
|
440
|
+
textMetrics,
|
|
429
441
|
)}
|
|
430
442
|
</g>
|
|
431
443
|
);
|
|
432
444
|
}
|
|
433
445
|
|
|
434
446
|
/** shape-diamond = ひし形 (decision / 判定) + drop shadow + inner border */
|
|
435
|
-
export function ShapeDiamondNode({ node, active }: ShapeProps): JSX.Element {
|
|
447
|
+
export function ShapeDiamondNode({ node, active, textMetrics = "proportional" }: ShapeProps): JSX.Element {
|
|
436
448
|
const 帯 = bodyBand(node, { 影ry: SHADOW_RY });
|
|
437
449
|
const cx = 帯.cx;
|
|
438
450
|
const cy = 帯.cy;
|
|
@@ -475,6 +487,7 @@ export function ShapeDiamondNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
475
487
|
w: (帯.w * innerScale) / 2,
|
|
476
488
|
h: (帯.h * innerScale) / 2,
|
|
477
489
|
},
|
|
490
|
+
textMetrics,
|
|
478
491
|
)}
|
|
479
492
|
</g>
|
|
480
493
|
);
|
|
@@ -485,7 +498,7 @@ const STACK_OFFSET_MAX = 12;
|
|
|
485
498
|
const STACK_OFFSET_RATIO = 0.15;
|
|
486
499
|
|
|
487
500
|
/** shape-stack = 重ね rect 3 枚 (layer / history) + drop shadow */
|
|
488
|
-
export function ShapeStackNode({ node, active }: ShapeProps): JSX.Element {
|
|
501
|
+
export function ShapeStackNode({ node, active, textMetrics = "proportional" }: ShapeProps): JSX.Element {
|
|
489
502
|
const 帯 = bodyBand(node, { 影ry: SHADOW_RY });
|
|
490
503
|
const x = 帯.x;
|
|
491
504
|
const y = 帯.y;
|
|
@@ -539,6 +552,7 @@ export function ShapeStackNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
539
552
|
{ x: 手前cx, y: node.cy + ずれ + 6 },
|
|
540
553
|
15,
|
|
541
554
|
{ x, y: y + ずれ * 2, w: cardW, h: cardH },
|
|
555
|
+
textMetrics,
|
|
542
556
|
)}
|
|
543
557
|
</g>
|
|
544
558
|
);
|
|
@@ -564,21 +578,32 @@ const PERSON_SHADOW_RX = 55;
|
|
|
564
578
|
* 動かさず **字の大きさだけ横に入る倍率まで下げる** = 各行の張り出しは縮むだけなので、
|
|
565
579
|
* `labelPlan` が保証した縦の収まりは崩れない。
|
|
566
580
|
*/
|
|
567
|
-
function planLabel(
|
|
581
|
+
function planLabel(
|
|
582
|
+
node: LaidNode,
|
|
583
|
+
cx: number,
|
|
584
|
+
幅上限: number,
|
|
585
|
+
plan: ReturnType<typeof labelPlan>,
|
|
586
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
587
|
+
) {
|
|
568
588
|
const 幅 = Math.max(
|
|
569
|
-
textWidth(node.title ?? "", plan.名前.size),
|
|
570
|
-
node.eyebrow && plan.肩書
|
|
571
|
-
|
|
589
|
+
textWidth(node.title ?? "", plan.名前.size, 0, textMetrics),
|
|
590
|
+
node.eyebrow && plan.肩書
|
|
591
|
+
? textWidth(node.eyebrow, plan.肩書.size, EYEBROW_TRACKING, textMetrics)
|
|
592
|
+
: 0,
|
|
593
|
+
node.subtitle && plan.説明 ? textWidth(node.subtitle, plan.説明.size, 0, textMetrics) : 0,
|
|
572
594
|
);
|
|
573
595
|
// 縮めは読める下限で止める (#953)。 下限が無いと長い名前が点にしか見えない大きさになる。
|
|
574
596
|
// 題が一番広い行のときだけ当てる (#957、 上の `labelBlock` と同じ理由)
|
|
575
|
-
const 題幅 = textWidth(node.title ?? "", plan.名前.size);
|
|
597
|
+
const 題幅 = textWidth(node.title ?? "", plan.名前.size, 0, textMetrics);
|
|
598
|
+
const 測る = (t: string, s: number, ls: number): number => textWidth(t, s, ls, textMetrics);
|
|
576
599
|
const 下限 =
|
|
577
|
-
題幅 >= 幅 ? 切って収まる下限(node.title ?? "", plan.名前.size, 0, 幅上限,
|
|
600
|
+
題幅 >= 幅 ? 切って収まる下限(node.title ?? "", plan.名前.size, 0, 幅上限, 測る) : 0;
|
|
578
601
|
const k = 幅 > 0 && 幅上限 > 0 ? Math.min(1, Math.max(幅上限 / 幅, 下限)) : 1;
|
|
579
602
|
// 下限で止めた行は箱に入らないので、入るところまで切って `…` を付ける (#953)
|
|
580
603
|
const 切る = (t: string, 大きさ: number, 字間 = 0) =>
|
|
581
|
-
帯で切る(t, 大きさ * k, 幅上限, (u, f) =>
|
|
604
|
+
帯で切る(t, 大きさ * k, 幅上限, (u, f) =>
|
|
605
|
+
textWidth(u, f, 字間 * k, textMetrics),
|
|
606
|
+
);
|
|
582
607
|
return (
|
|
583
608
|
<>
|
|
584
609
|
{node.eyebrow && plan.肩書 && (
|
|
@@ -628,7 +653,7 @@ function planLabel(node: LaidNode, cx: number, 幅上限: number, plan: ReturnTy
|
|
|
628
653
|
* `shapeRegion` で取り、 入らなければ `fitArt` が比を保ったまま縮める。 名前は箱の下端から
|
|
629
654
|
* 積み上げる (`labelPlan`) = 固定位置に置くと箱の下 176.2 まで伸びていた。
|
|
630
655
|
*/
|
|
631
|
-
export function ShapePersonNode({ node, active }: ShapeProps): JSX.Element {
|
|
656
|
+
export function ShapePersonNode({ node, active, textMetrics = "proportional" }: ShapeProps): JSX.Element {
|
|
632
657
|
const r = shapeRegion(node);
|
|
633
658
|
const cx = r.cx;
|
|
634
659
|
const strokeColor = active ? "var(--cdl-tone-accent, #2d6a8f)" : "var(--cdl-divider, #d4d4d8)";
|
|
@@ -698,7 +723,7 @@ export function ShapePersonNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
698
723
|
/>
|
|
699
724
|
</g>,
|
|
700
725
|
)}
|
|
701
|
-
{planLabel(node, r.cx, Math.max(0, node.w), r.label)}
|
|
726
|
+
{planLabel(node, r.cx, Math.max(0, node.w), r.label, textMetrics)}
|
|
702
727
|
</g>
|
|
703
728
|
);
|
|
704
729
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { JSX } from "react";
|
|
2
|
-
import type { LaidNode } from "../types";
|
|
2
|
+
import type { CdlTextMetrics, LaidNode } from "../types";
|
|
3
3
|
import {
|
|
4
4
|
fitArt,
|
|
5
5
|
labelPlan,
|
|
@@ -27,6 +27,7 @@ import { 字引きを作る, type CdlLang, type CdlTextKey } from "../lang";
|
|
|
27
27
|
type ShapeProps = {
|
|
28
28
|
node: LaidNode;
|
|
29
29
|
active: boolean;
|
|
30
|
+
textMetrics?: CdlTextMetrics;
|
|
30
31
|
/** 描画側の固定の字の言語 (#847)。 渡さない呼び方は英語で描く */
|
|
31
32
|
lang?: CdlLang;
|
|
32
33
|
};
|
|
@@ -136,7 +137,12 @@ function blockchainNodeExtent(strokeWidth: number): ArtExtent {
|
|
|
136
137
|
/**
|
|
137
138
|
* 名前の塊を描く。 位置と大きさは `labelPlan` が決める (箱の下端から積み上げる、 #433)。
|
|
138
139
|
*/
|
|
139
|
-
function bottomLabel(
|
|
140
|
+
function bottomLabel(
|
|
141
|
+
node: LaidNode,
|
|
142
|
+
cx: number,
|
|
143
|
+
plan: ReturnType<typeof labelPlan>,
|
|
144
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
145
|
+
) {
|
|
140
146
|
// 縮めは読める下限で止める (#953)。 止めた行は箱に入らないので、入るところまで切る。
|
|
141
147
|
// 下限を当てるのは題の行だけ (#957) = 補いの行に当てると、収まっている行を大きくしてから
|
|
142
148
|
// 切ることになる
|
|
@@ -144,7 +150,8 @@ function bottomLabel(node: LaidNode, cx: number, plan: ReturnType<typeof labelPl
|
|
|
144
150
|
// **縮める時と切る時で同じ測り方を使う** (#959)。 絵の中の字は等幅の字送りと字形の
|
|
145
151
|
// 大きい方を採る (`絵の中の字の幅`) ので、`幅の倍率` の `textWidth` で倍率を出すと
|
|
146
152
|
// 「収まる」 と言った行を切る側が「はみ出す」 と判定する (実測 = 幅 140 の箱で 9 種)
|
|
147
|
-
const 測る = (u: string, f: number, ls: number) =>
|
|
153
|
+
const 測る = (u: string, f: number, ls: number) =>
|
|
154
|
+
字の幅(u, f, textMetrics) + ls * [...u].length;
|
|
148
155
|
const 素の幅 = 測る(t, 大きさ, 字間);
|
|
149
156
|
const 収まる = 素の幅 > node.w && node.w > 0 ? node.w / 素の幅 : 1;
|
|
150
157
|
const 下限 = 題か ? 切って収まる下限(t, 大きさ, 字間, node.w, 測る) : 0;
|
|
@@ -205,7 +212,12 @@ function EthereumDiamond({ cx, cy, h, colors }: { cx: number; cy: number; h: num
|
|
|
205
212
|
}
|
|
206
213
|
|
|
207
214
|
/** shape-smart-contract = 契約書 (前バージョン: document + 契約書テキスト + function 表示 + 底に回転 gear) */
|
|
208
|
-
export function ShapeSmartContractNode({
|
|
215
|
+
export function ShapeSmartContractNode({
|
|
216
|
+
node,
|
|
217
|
+
active,
|
|
218
|
+
lang,
|
|
219
|
+
textMetrics = "proportional",
|
|
220
|
+
}: ShapeProps): JSX.Element {
|
|
209
221
|
const 字引 = 字引きを作る(lang);
|
|
210
222
|
const frame = commonFrame(active);
|
|
211
223
|
const r = shapeRegion(node);
|
|
@@ -254,7 +266,7 @@ export function ShapeSmartContractNode({ node, active, lang }: ShapeProps): JSX.
|
|
|
254
266
|
},
|
|
255
267
|
];
|
|
256
268
|
const art = (倍率: number): JSX.Element => {
|
|
257
|
-
const 倍 = 打ち消しの倍率(倍率,
|
|
269
|
+
const 倍 = 打ち消しの倍率(倍率, 打ち消す字ら, textMetrics);
|
|
258
270
|
return (
|
|
259
271
|
<g>
|
|
260
272
|
<path data-cdl-role="node-body" d={path} {...frame} strokeLinejoin="round" />
|
|
@@ -284,7 +296,7 @@ export function ShapeSmartContractNode({ node, active, lang }: ShapeProps): JSX.
|
|
|
284
296
|
return (
|
|
285
297
|
<g>
|
|
286
298
|
{fitArt(r, smartContractExtent(frame.strokeWidth), art)}
|
|
287
|
-
{bottomLabel(node, r.cx, r.label)}
|
|
299
|
+
{bottomLabel(node, r.cx, r.label, textMetrics)}
|
|
288
300
|
</g>
|
|
289
301
|
);
|
|
290
302
|
}
|
|
@@ -300,7 +312,11 @@ export function ShapeSmartContractNode({ node, active, lang }: ShapeProps): JSX.
|
|
|
300
312
|
* どちらの書体で描かれても実物以上に出る側へ倒す = 下回ると「収まっていない物を収まっていると
|
|
301
313
|
* 判定」 する向きに外れる (`row-align.ts` の `rowColumnWidth` と同じ考え方)。
|
|
302
314
|
*/
|
|
303
|
-
const 字の幅 = (
|
|
315
|
+
const 字の幅 = (
|
|
316
|
+
text: string,
|
|
317
|
+
size: number,
|
|
318
|
+
textMetrics: CdlTextMetrics = "proportional",
|
|
319
|
+
): number => 絵の中の字の幅(text, size, 0, textMetrics);
|
|
304
320
|
|
|
305
321
|
/**
|
|
306
322
|
* 絵の中の 5 行の既定値。 書き手が渡さなかった欄はこれで描く (#469)。
|
|
@@ -353,10 +369,16 @@ const BLOCK_DEFAULT_FIELDS: readonly {
|
|
|
353
369
|
* 位置が node の大きさに依存する。 `rowBaselineY(kind, index)` は node の大きさを受け取らない
|
|
354
370
|
* ので、 この種別の位置を表せない。 開くなら述語の形から変える話になる (#469 の対象外 scope)。
|
|
355
371
|
*/
|
|
356
|
-
export function ShapeBlockchainBlockNode({
|
|
372
|
+
export function ShapeBlockchainBlockNode({
|
|
373
|
+
node,
|
|
374
|
+
active,
|
|
375
|
+
lang,
|
|
376
|
+
textMetrics = "proportional",
|
|
377
|
+
}: ShapeProps): JSX.Element {
|
|
357
378
|
const 字引 = 字引きを作る(lang);
|
|
358
379
|
const frame = commonFrame(active);
|
|
359
380
|
const r = shapeRegion(node);
|
|
381
|
+
const 測る字 = (text: string, size: number): number => 字の幅(text, size, textMetrics);
|
|
360
382
|
const bw = BLOCK_W;
|
|
361
383
|
const bh = BLOCK_H;
|
|
362
384
|
const cx = r.cx;
|
|
@@ -392,9 +414,10 @@ export function ShapeBlockchainBlockNode({ node, active, lang }: ShapeProps): JS
|
|
|
392
414
|
// 上限は帯の残りの半分。 長い肩書は **切らずに字を縮める** = 分類を表す語なので、
|
|
393
415
|
// 末尾を落とすと何の分類か読めなくなる (`台帳…`)。 縮めた後の幅は上限ちょうどになる
|
|
394
416
|
const 肩書の上限 = (帯の残り - 肩書の隙間) / 2;
|
|
395
|
-
const 肩書の倍率 =
|
|
417
|
+
const 肩書の倍率 =
|
|
418
|
+
肩書 === "" ? 1 : 幅の倍率(肩書, 肩書の字, 0, 肩書の上限, textMetrics);
|
|
396
419
|
const 肩書の指定 = 肩書の字 * 肩書の倍率;
|
|
397
|
-
const 肩書の幅 = 肩書 === "" ? 0 :
|
|
420
|
+
const 肩書の幅 = 肩書 === "" ? 0 : 測る字(肩書, 肩書の指定);
|
|
398
421
|
const 見出しの幅 = 帯の残り - (肩書 === "" ? 0 : 肩書の幅 + 肩書の隙間);
|
|
399
422
|
/*
|
|
400
423
|
* 絵を **倍率を受け取ってから** 組む (#871)。 縮んだ分を字の指定で打ち消す。
|
|
@@ -422,12 +445,12 @@ export function ShapeBlockchainBlockNode({ node, active, lang }: ShapeProps): JS
|
|
|
422
445
|
{
|
|
423
446
|
text: f.value,
|
|
424
447
|
size: 値の字,
|
|
425
|
-
帯: { 幅: bw - 28 -
|
|
448
|
+
帯: { 幅: bw - 28 - 測る字(f.label, 名の字) - 8, 高さ: 行の間隔 },
|
|
426
449
|
},
|
|
427
450
|
]),
|
|
428
451
|
];
|
|
429
452
|
const art = (倍率: number): JSX.Element => {
|
|
430
|
-
const 倍 = 打ち消しの倍率(倍率,
|
|
453
|
+
const 倍 = 打ち消しの倍率(倍率, 打ち消す字ら, textMetrics);
|
|
431
454
|
const 題の大きさ = 見出しの字 * 倍;
|
|
432
455
|
const 肩書の大きさ = 肩書の指定 * 倍;
|
|
433
456
|
const 名の大きさ = 名の字 * 倍;
|
|
@@ -440,7 +463,7 @@ export function ShapeBlockchainBlockNode({ node, active, lang }: ShapeProps): JS
|
|
|
440
463
|
{/* header bar */}
|
|
441
464
|
<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" />
|
|
442
465
|
<text data-cdl-role="blockchain-block-title" data-cdl-counter-scaled="1" x={x + 14} y={y + 22} fontSize={題の大きさ} fontWeight={700} fontFamily="ui-monospace, SFMono-Regular, Menlo, monospace" fill="#ffffff">
|
|
443
|
-
{幅で切る(見出し, 題の大きさ, 見出しの幅,
|
|
466
|
+
{幅で切る(見出し, 題の大きさ, 見出しの幅, 測る字)}
|
|
444
467
|
</text>
|
|
445
468
|
{肩書 !== "" && (
|
|
446
469
|
<text
|
|
@@ -466,7 +489,12 @@ export function ShapeBlockchainBlockNode({ node, active, lang }: ShapeProps): JS
|
|
|
466
489
|
{f.label}
|
|
467
490
|
</text>
|
|
468
491
|
<text data-cdl-role={`blockchain-block-${f.key}`} data-cdl-counter-scaled="1" x={x + bw - 14} y={y + 54 + i * 行の間隔} fontSize={値の大きさ} fontFamily="ui-monospace, SFMono-Regular, Menlo, monospace" fontWeight={600} textAnchor="end" fill="var(--cdl-text, #1a1f2a)">
|
|
469
|
-
{幅で切る(
|
|
492
|
+
{幅で切る(
|
|
493
|
+
f.value,
|
|
494
|
+
値の大きさ,
|
|
495
|
+
bw - 28 - 測る字(f.label, 名の大きさ) - 8,
|
|
496
|
+
測る字,
|
|
497
|
+
)}
|
|
470
498
|
</text>
|
|
471
499
|
<line x1={x + 14} y1={y + 60 + i * 行の間隔} x2={x + bw - 14} y2={y + 60 + i * 行の間隔} stroke="var(--cdl-divider, #d4d4d8)" strokeWidth={0.5} opacity={0.5} />
|
|
472
500
|
</g>
|
|
@@ -484,7 +512,11 @@ export function ShapeBlockchainBlockNode({ node, active, lang }: ShapeProps): JS
|
|
|
484
512
|
}
|
|
485
513
|
|
|
486
514
|
/** shape-blockchain = 汎用 5 block chain (シンプル hash pointer) */
|
|
487
|
-
export function ShapeBlockchainNode({
|
|
515
|
+
export function ShapeBlockchainNode({
|
|
516
|
+
node,
|
|
517
|
+
active,
|
|
518
|
+
textMetrics = "proportional",
|
|
519
|
+
}: ShapeProps): JSX.Element {
|
|
488
520
|
const frame = commonFrame(active);
|
|
489
521
|
const r = shapeRegion(node);
|
|
490
522
|
const cx = r.cx;
|
|
@@ -506,7 +538,7 @@ export function ShapeBlockchainNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
506
538
|
{ text: "0x…", size: CHAIN_HASH_SIZE, 帯: { 幅: CHAIN_TEXT_W, 高さ: CHAIN_ROW_GAP } },
|
|
507
539
|
];
|
|
508
540
|
const art = (倍率: number): JSX.Element => {
|
|
509
|
-
const 倍 = 打ち消しの倍率(倍率,
|
|
541
|
+
const 倍 = 打ち消しの倍率(倍率, 打ち消す字ら, textMetrics);
|
|
510
542
|
return (
|
|
511
543
|
<g>
|
|
512
544
|
{Array.from({ length: blockCount }, (_, i) => i).map((i) => {
|
|
@@ -559,13 +591,17 @@ export function ShapeBlockchainNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
559
591
|
return (
|
|
560
592
|
<g>
|
|
561
593
|
{fitArt(r, chainExtent(), art)}
|
|
562
|
-
{bottomLabel(node, r.cx, r.label)}
|
|
594
|
+
{bottomLabel(node, r.cx, r.label, textMetrics)}
|
|
563
595
|
</g>
|
|
564
596
|
);
|
|
565
597
|
}
|
|
566
598
|
|
|
567
599
|
/** shape-bitcoin-chain = Bitcoin (橙 accent + ₿ symbol) */
|
|
568
|
-
export function ShapeBitcoinChainNode({
|
|
600
|
+
export function ShapeBitcoinChainNode({
|
|
601
|
+
node,
|
|
602
|
+
active,
|
|
603
|
+
textMetrics = "proportional",
|
|
604
|
+
}: ShapeProps): JSX.Element {
|
|
569
605
|
const frame = commonFrame(active);
|
|
570
606
|
const r = shapeRegion(node);
|
|
571
607
|
const cx = r.cx;
|
|
@@ -586,7 +622,7 @@ export function ShapeBitcoinChainNode({ node, active }: ShapeProps): JSX.Element
|
|
|
586
622
|
{ text: "#820", size: BTC_NO_SIZE, 帯: { 幅: CHAIN_TEXT_W, 高さ: 20 } },
|
|
587
623
|
];
|
|
588
624
|
const art = (倍率: number): JSX.Element => {
|
|
589
|
-
const 倍 = 打ち消しの倍率(倍率,
|
|
625
|
+
const 倍 = 打ち消しの倍率(倍率, 打ち消す字ら, textMetrics);
|
|
590
626
|
return (
|
|
591
627
|
<g>
|
|
592
628
|
{Array.from({ length: blockCount }, (_, i) => i).map((i) => {
|
|
@@ -622,13 +658,17 @@ export function ShapeBitcoinChainNode({ node, active }: ShapeProps): JSX.Element
|
|
|
622
658
|
return (
|
|
623
659
|
<g>
|
|
624
660
|
{fitArt(r, chainExtent(), art)}
|
|
625
|
-
{bottomLabel(node, r.cx, r.label)}
|
|
661
|
+
{bottomLabel(node, r.cx, r.label, textMetrics)}
|
|
626
662
|
</g>
|
|
627
663
|
);
|
|
628
664
|
}
|
|
629
665
|
|
|
630
666
|
/** shape-ethereum-chain = Ethereum (紫 accent + 本物 Ethereum Diamond ロゴ) */
|
|
631
|
-
export function ShapeEthereumChainNode({
|
|
667
|
+
export function ShapeEthereumChainNode({
|
|
668
|
+
node,
|
|
669
|
+
active,
|
|
670
|
+
textMetrics = "proportional",
|
|
671
|
+
}: ShapeProps): JSX.Element {
|
|
632
672
|
const frame = commonFrame(active);
|
|
633
673
|
const r = shapeRegion(node);
|
|
634
674
|
const cx = r.cx;
|
|
@@ -653,7 +693,7 @@ export function ShapeEthereumChainNode({ node, active }: ShapeProps): JSX.Elemen
|
|
|
653
693
|
{ text: "#214", size: ETH_NO_SIZE, 帯: { 幅: CHAIN_TEXT_W, 高さ: 20 } },
|
|
654
694
|
];
|
|
655
695
|
const art = (倍率: number): JSX.Element => {
|
|
656
|
-
const 倍 = 打ち消しの倍率(倍率,
|
|
696
|
+
const 倍 = 打ち消しの倍率(倍率, 打ち消す字ら, textMetrics);
|
|
657
697
|
return (
|
|
658
698
|
<g>
|
|
659
699
|
{Array.from({ length: blockCount }, (_, i) => i).map((i) => {
|
|
@@ -691,13 +731,17 @@ export function ShapeEthereumChainNode({ node, active }: ShapeProps): JSX.Elemen
|
|
|
691
731
|
return (
|
|
692
732
|
<g>
|
|
693
733
|
{fitArt(r, chainExtent(), art)}
|
|
694
|
-
{bottomLabel(node, r.cx, r.label)}
|
|
734
|
+
{bottomLabel(node, r.cx, r.label, textMetrics)}
|
|
695
735
|
</g>
|
|
696
736
|
);
|
|
697
737
|
}
|
|
698
738
|
|
|
699
739
|
/** shape-blockchain-node = P2P full node (中央 hex + 6 peer 外側配置) */
|
|
700
|
-
export function ShapeBlockchainNodeNode({
|
|
740
|
+
export function ShapeBlockchainNodeNode({
|
|
741
|
+
node,
|
|
742
|
+
active,
|
|
743
|
+
textMetrics = "proportional",
|
|
744
|
+
}: ShapeProps): JSX.Element {
|
|
701
745
|
const frame = commonFrame(active);
|
|
702
746
|
const r = shapeRegion(node);
|
|
703
747
|
const cx = r.cx;
|
|
@@ -752,13 +796,18 @@ export function ShapeBlockchainNodeNode({ node, active }: ShapeProps): JSX.Eleme
|
|
|
752
796
|
return (
|
|
753
797
|
<g>
|
|
754
798
|
{fitArt(r, blockchainNodeExtent(frame.strokeWidth), art)}
|
|
755
|
-
{bottomLabel(node, r.cx, r.label)}
|
|
799
|
+
{bottomLabel(node, r.cx, r.label, textMetrics)}
|
|
756
800
|
</g>
|
|
757
801
|
);
|
|
758
802
|
}
|
|
759
803
|
|
|
760
804
|
/** shape-rpc-node = JSON-RPC provider */
|
|
761
|
-
export function ShapeRpcNodeNode({
|
|
805
|
+
export function ShapeRpcNodeNode({
|
|
806
|
+
node,
|
|
807
|
+
active,
|
|
808
|
+
lang,
|
|
809
|
+
textMetrics = "proportional",
|
|
810
|
+
}: ShapeProps): JSX.Element {
|
|
762
811
|
const 字引 = 字引きを作る(lang);
|
|
763
812
|
const frame = commonFrame(active);
|
|
764
813
|
const r = shapeRegion(node);
|
|
@@ -788,13 +837,13 @@ export function ShapeRpcNodeNode({ node, active, lang }: ShapeProps): JSX.Elemen
|
|
|
788
837
|
<rect x={x + 16} y={y + bh - 24} width={(bw - 32) * 0.92} height={8} rx={4} fill="#27c93f" stroke="none">
|
|
789
838
|
<animate attributeName="width" values={`${(bw - 32) * 0.86};${(bw - 32) * 0.96};${(bw - 32) * 0.86}`} dur="3s" repeatCount="indefinite" />
|
|
790
839
|
</rect>
|
|
791
|
-
{bottomLabel(node, r.cx, r.label)}
|
|
840
|
+
{bottomLabel(node, r.cx, r.label, textMetrics)}
|
|
792
841
|
</g>
|
|
793
842
|
);
|
|
794
843
|
}
|
|
795
844
|
|
|
796
845
|
/** shape-wallet = FC-S5 忠実移植 (二つ折り札入れ横長 + fold line 中央 + bar clasp + engraved Ξ text + コイン peek 3) */
|
|
797
|
-
export function ShapeWalletNode({ node, active }: ShapeProps): JSX.Element {
|
|
846
|
+
export function ShapeWalletNode({ node, active, textMetrics = "proportional" }: ShapeProps): JSX.Element {
|
|
798
847
|
const frame = commonFrame(active);
|
|
799
848
|
const r = shapeRegion(node);
|
|
800
849
|
const cx = r.cx;
|
|
@@ -885,13 +934,13 @@ export function ShapeWalletNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
885
934
|
{/* body 上端 highlight (FC-S5 元 = M 42 86 L 258 86) */}
|
|
886
935
|
<path d={`M ${x + 2} ${y + 6} L ${x + w - 2} ${y + 6}`} fill="none" stroke="#ffffff" strokeWidth={1} opacity={0.4} />
|
|
887
936
|
</g>
|
|
888
|
-
{bottomLabel(node, r.cx, r.label)}
|
|
937
|
+
{bottomLabel(node, r.cx, r.label, textMetrics)}
|
|
889
938
|
</g>
|
|
890
939
|
);
|
|
891
940
|
}
|
|
892
941
|
|
|
893
942
|
/** shape-nft = NFT card (白 body + accent art frame + 巨大 NFT 文字、 統一デザイン) */
|
|
894
|
-
export function ShapeNftNode({ node, active }: ShapeProps): JSX.Element {
|
|
943
|
+
export function ShapeNftNode({ node, active, textMetrics = "proportional" }: ShapeProps): JSX.Element {
|
|
895
944
|
const frame = commonFrame(active);
|
|
896
945
|
const r = shapeRegion(node);
|
|
897
946
|
const cx = r.cx;
|
|
@@ -914,7 +963,7 @@ export function ShapeNftNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
914
963
|
{ text: "#1024", size: NFT_ID_SIZE, 帯: { 幅: cardW - 16 - 34, 高さ: cardH - artH } },
|
|
915
964
|
];
|
|
916
965
|
const art = (倍率: number): JSX.Element => {
|
|
917
|
-
const 倍 = 打ち消しの倍率(倍率,
|
|
966
|
+
const 倍 = 打ち消しの倍率(倍率, 打ち消す字ら, textMetrics);
|
|
918
967
|
return (
|
|
919
968
|
<g>
|
|
920
969
|
{/* shadow */}
|
|
@@ -959,13 +1008,13 @@ export function ShapeNftNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
959
1008
|
return (
|
|
960
1009
|
<g>
|
|
961
1010
|
{fitArt(r, nftExtent(frame.strokeWidth), art)}
|
|
962
|
-
{bottomLabel(node, r.cx, r.label)}
|
|
1011
|
+
{bottomLabel(node, r.cx, r.label, textMetrics)}
|
|
963
1012
|
</g>
|
|
964
1013
|
);
|
|
965
1014
|
}
|
|
966
1015
|
|
|
967
1016
|
/** shape-token = coin (白 body + accent 色 rim + accent 色 Diamond ロゴ、 統一デザイン) */
|
|
968
|
-
export function ShapeTokenNode({ node, active }: ShapeProps): JSX.Element {
|
|
1017
|
+
export function ShapeTokenNode({ node, active, textMetrics = "proportional" }: ShapeProps): JSX.Element {
|
|
969
1018
|
const frame = commonFrame(active);
|
|
970
1019
|
const r = shapeRegion(node);
|
|
971
1020
|
const cx = r.cx;
|
|
@@ -1002,7 +1051,7 @@ export function ShapeTokenNode({ node, active }: ShapeProps): JSX.Element {
|
|
|
1002
1051
|
stroke: "#1a4a68",
|
|
1003
1052
|
}}
|
|
1004
1053
|
/>
|
|
1005
|
-
{bottomLabel(node, r.cx, r.label)}
|
|
1054
|
+
{bottomLabel(node, r.cx, r.label, textMetrics)}
|
|
1006
1055
|
</g>
|
|
1007
1056
|
);
|
|
1008
1057
|
}
|