@cardenelabs/cdl 0.106.0 → 0.108.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 +77 -1
- package/dist/index.cjs +132 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +132 -39
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +247 -154
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +247 -154
- package/dist/react.js.map +1 -1
- package/package.json +1 -1
- package/src/kinds/shape-basement.tsx +20 -8
- package/src/kinds/shape-blockchain.tsx +17 -10
- package/src/kinds/shape-commerce.tsx +15 -6
- package/src/kinds/shape-finance.tsx +15 -6
- package/src/kinds/shape-hardware.tsx +15 -7
- package/src/kinds/shape-region.tsx +63 -2
- package/src/layout/spec.ts +81 -0
- package/src/layout/text-width.ts +52 -4
- package/src/visual-validate.ts +25 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cardenelabs/cdl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.108.0",
|
|
4
4
|
"description": "CDL (Chainome Diagram Language). Mermaid-like declarative DSL that compiles to animated SVG diagrams. Built for blockchain / Solidity flows, generic enough for sequence / flow / state / ER / topology diagrams.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "cardene777",
|
|
@@ -11,8 +11,10 @@ import {
|
|
|
11
11
|
labelPlan,
|
|
12
12
|
shapeRegion,
|
|
13
13
|
textWidth,
|
|
14
|
+
切って収まる下限,
|
|
14
15
|
type ArtExtent,
|
|
15
16
|
} from "./shape-region";
|
|
17
|
+
import { 幅で切る } from "./box-lines";
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
20
|
* Shape-driven basement 8 kinds (CAR-1099、 CAR-1130 people ベース統一版)。
|
|
@@ -88,7 +90,9 @@ function labelBlock(
|
|
|
88
90
|
node.eyebrow ? textWidth(node.eyebrow, EYEBROW_SIZE, EYEBROW_TRACKING) : 0,
|
|
89
91
|
node.subtitle ? textWidth(node.subtitle, SUB_SIZE) : 0,
|
|
90
92
|
);
|
|
91
|
-
|
|
93
|
+
// 横で縮める倍率を読める下限で止める (#953)。 切って収まらない幅では下限を当てない
|
|
94
|
+
const 下限 = 切って収まる下限(node.title ?? "", size, 0, band.w, textWidth);
|
|
95
|
+
const { k, x, y: baseline } = fitLabel(band, { 上, 下, 幅 }, pos, 下限);
|
|
92
96
|
return { k, x, baseline, 上端: baseline - 上 * k, 下端: baseline + 下 * k };
|
|
93
97
|
}
|
|
94
98
|
|
|
@@ -96,6 +100,9 @@ function commonLabel(node: LaidNode, pos: { x: number; y: number }, size: number
|
|
|
96
100
|
const 塊 = labelBlock(node, pos, size, band);
|
|
97
101
|
const k = 塊.k;
|
|
98
102
|
const x = 塊.x;
|
|
103
|
+
// 下限で止めた行は帯に入らないので、入るところまで切って `…` を付ける (#953)
|
|
104
|
+
const 切る = (t: string, 大きさ: number, 字間 = 0) =>
|
|
105
|
+
幅で切る(t, 大きさ * k, band.w, (u, f) => textWidth(u, f, 字間 * k));
|
|
99
106
|
return (
|
|
100
107
|
<>
|
|
101
108
|
{node.eyebrow && (
|
|
@@ -108,7 +115,7 @@ function commonLabel(node: LaidNode, pos: { x: number; y: number }, size: number
|
|
|
108
115
|
textAnchor="middle"
|
|
109
116
|
fill="var(--cdl-tone-accent, #2d6a8f)"
|
|
110
117
|
>
|
|
111
|
-
{node.eyebrow}
|
|
118
|
+
{切る(node.eyebrow, EYEBROW_SIZE, EYEBROW_TRACKING)}
|
|
112
119
|
</text>
|
|
113
120
|
)}
|
|
114
121
|
<text
|
|
@@ -120,7 +127,7 @@ function commonLabel(node: LaidNode, pos: { x: number; y: number }, size: number
|
|
|
120
127
|
textAnchor="middle"
|
|
121
128
|
fill="var(--cdl-text, #1a1f2a)"
|
|
122
129
|
>
|
|
123
|
-
{node.title}
|
|
130
|
+
{切る(node.title ?? "", size)}
|
|
124
131
|
</text>
|
|
125
132
|
{node.subtitle && (
|
|
126
133
|
<text
|
|
@@ -130,7 +137,7 @@ function commonLabel(node: LaidNode, pos: { x: number; y: number }, size: number
|
|
|
130
137
|
textAnchor="middle"
|
|
131
138
|
fill="var(--cdl-text-dim, #5a6270)"
|
|
132
139
|
>
|
|
133
|
-
{node.subtitle}
|
|
140
|
+
{切る(node.subtitle, SUB_SIZE)}
|
|
134
141
|
</text>
|
|
135
142
|
)}
|
|
136
143
|
</>
|
|
@@ -557,7 +564,12 @@ function planLabel(node: LaidNode, cx: number, 幅上限: number, plan: ReturnTy
|
|
|
557
564
|
node.eyebrow && plan.肩書 ? textWidth(node.eyebrow, plan.肩書.size, EYEBROW_TRACKING) : 0,
|
|
558
565
|
node.subtitle && plan.説明 ? textWidth(node.subtitle, plan.説明.size) : 0,
|
|
559
566
|
);
|
|
560
|
-
|
|
567
|
+
// 縮めは読める下限で止める (#953)。 下限が無いと長い名前が点にしか見えない大きさになる
|
|
568
|
+
const 下限 = 切って収まる下限(node.title ?? "", plan.名前.size, 0, 幅上限, textWidth);
|
|
569
|
+
const k = 幅 > 0 && 幅上限 > 0 ? Math.min(1, Math.max(幅上限 / 幅, 下限)) : 1;
|
|
570
|
+
// 下限で止めた行は箱に入らないので、入るところまで切って `…` を付ける (#953)
|
|
571
|
+
const 切る = (t: string, 大きさ: number, 字間 = 0) =>
|
|
572
|
+
幅で切る(t, 大きさ * k, 幅上限, (u, f) => textWidth(u, f, 字間 * k));
|
|
561
573
|
return (
|
|
562
574
|
<>
|
|
563
575
|
{node.eyebrow && plan.肩書 && (
|
|
@@ -570,7 +582,7 @@ function planLabel(node: LaidNode, cx: number, 幅上限: number, plan: ReturnTy
|
|
|
570
582
|
textAnchor="middle"
|
|
571
583
|
fill="var(--cdl-tone-accent, #2d6a8f)"
|
|
572
584
|
>
|
|
573
|
-
{node.eyebrow}
|
|
585
|
+
{切る(node.eyebrow, plan.肩書.size, EYEBROW_TRACKING)}
|
|
574
586
|
</text>
|
|
575
587
|
)}
|
|
576
588
|
<text
|
|
@@ -582,7 +594,7 @@ function planLabel(node: LaidNode, cx: number, 幅上限: number, plan: ReturnTy
|
|
|
582
594
|
textAnchor="middle"
|
|
583
595
|
fill="var(--cdl-text, #1a1f2a)"
|
|
584
596
|
>
|
|
585
|
-
{node.title}
|
|
597
|
+
{切る(node.title ?? "", plan.名前.size)}
|
|
586
598
|
</text>
|
|
587
599
|
{node.subtitle && plan.説明 && (
|
|
588
600
|
<text
|
|
@@ -592,7 +604,7 @@ function planLabel(node: LaidNode, cx: number, 幅上限: number, plan: ReturnTy
|
|
|
592
604
|
textAnchor="middle"
|
|
593
605
|
fill="var(--cdl-text-dim, #5a6270)"
|
|
594
606
|
>
|
|
595
|
-
{node.subtitle}
|
|
607
|
+
{切る(node.subtitle, plan.説明.size)}
|
|
596
608
|
</text>
|
|
597
609
|
)}
|
|
598
610
|
</>
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
labelPlan,
|
|
6
6
|
shapeRegion,
|
|
7
7
|
幅の倍率,
|
|
8
|
+
切って収まる下限,
|
|
8
9
|
打ち消しの倍率,
|
|
9
10
|
絵の中の字の幅,
|
|
10
11
|
type ArtExtent,
|
|
@@ -135,26 +136,32 @@ function blockchainNodeExtent(strokeWidth: number): ArtExtent {
|
|
|
135
136
|
* 名前の塊を描く。 位置と大きさは `labelPlan` が決める (箱の下端から積み上げる、 #433)。
|
|
136
137
|
*/
|
|
137
138
|
function bottomLabel(node: LaidNode, cx: number, plan: ReturnType<typeof labelPlan>) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
:
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
139
|
+
// 縮めは読める下限で止める (#953)。 止めた行は箱に入らないので、入るところまで切る
|
|
140
|
+
const 収める = (t: string, 大きさ: number, 字間: number) => {
|
|
141
|
+
const 測る = (u: string, f: number, ls: number) => 字の幅(u, f) + ls * [...u].length;
|
|
142
|
+
const 倍率 = Math.max(幅の倍率(t, 大きさ, 字間, node.w), 切って収まる下限(t, 大きさ, 字間, node.w, 測る));
|
|
143
|
+
const size = 大きさ * 倍率;
|
|
144
|
+
return { 倍率, text: 幅で切る(t, size, node.w, (u, f) => 字の幅(u, f) + 字間 * 倍率 * [...u].length) };
|
|
145
|
+
};
|
|
146
|
+
const 肩 = node.eyebrow && plan.肩書 ? 収める(node.eyebrow, plan.肩書.size, 2) : { 倍率: 1, text: node.eyebrow ?? "" };
|
|
147
|
+
const 名 = 収める(node.title ?? "", plan.名前.size, 0);
|
|
148
|
+
const 説 = node.subtitle && plan.説明 ? 収める(node.subtitle, plan.説明.size, 0) : { 倍率: 1, text: node.subtitle ?? "" };
|
|
149
|
+
const 肩書倍率 = 肩.倍率;
|
|
150
|
+
const 名前倍率 = 名.倍率;
|
|
151
|
+
const 説明倍率 = 説.倍率;
|
|
145
152
|
return (
|
|
146
153
|
<>
|
|
147
154
|
{node.eyebrow && plan.肩書 && (
|
|
148
155
|
<text x={cx} y={plan.肩書.y} fontSize={plan.肩書.size * 肩書倍率} fontWeight={700} letterSpacing={2 * 肩書倍率} textAnchor="middle" fill="var(--cdl-tone-accent, #2d6a8f)">
|
|
149
|
-
{
|
|
156
|
+
{肩.text}
|
|
150
157
|
</text>
|
|
151
158
|
)}
|
|
152
159
|
<text data-cdl-role="node-label" x={cx} y={plan.名前.y} fontSize={plan.名前.size * 名前倍率} fontWeight={700} textAnchor="middle" fill="var(--cdl-text, #1a1f2a)">
|
|
153
|
-
{
|
|
160
|
+
{名.text}
|
|
154
161
|
</text>
|
|
155
162
|
{node.subtitle && plan.説明 && (
|
|
156
163
|
<text x={cx} y={plan.説明.y} fontSize={plan.説明.size * 説明倍率} textAnchor="middle" fill="var(--cdl-text-dim, #5a6270)">
|
|
157
|
-
{
|
|
164
|
+
{説.text}
|
|
158
165
|
</text>
|
|
159
166
|
)}
|
|
160
167
|
</>
|
|
@@ -6,7 +6,9 @@ import {
|
|
|
6
6
|
textWidth,
|
|
7
7
|
打ち消しの倍率,
|
|
8
8
|
type 打ち消す字,
|
|
9
|
+
切って収まる下限,
|
|
9
10
|
} from "./shape-region";
|
|
11
|
+
import { 幅で切る } from "./box-lines";
|
|
10
12
|
import { MONO_ADVANCE_RATIO, displayWidth } from "./row-align";
|
|
11
13
|
import type { LaidNode } from "../types";
|
|
12
14
|
import { 字引きを作る, type CdlLang } from "../lang";
|
|
@@ -101,8 +103,15 @@ function 字幅(文字列: string, 大きさ: number, 字間: number): number {
|
|
|
101
103
|
*/
|
|
102
104
|
function 幅に収める(文字列: string, 大きさ: number, 字間: number, 箱幅: number) {
|
|
103
105
|
const 幅 = 字幅(文字列, 大きさ, 字間);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
+
// 縮めは読める下限で止める (#953)。 下限が無いと長い名前が点にしか見えない大きさになる。
|
|
107
|
+
// 止めた行は箱に入らないので、入るところまで切って `…` を付ける (#433 の決まりを保つ)
|
|
108
|
+
const 倍率 =
|
|
109
|
+
幅 > 0 && 箱幅 > 0
|
|
110
|
+
? Math.min(1, Math.max(箱幅 / 幅, 切って収まる下限(文字列, 大きさ, 字間, 箱幅, 字幅)))
|
|
111
|
+
: 1;
|
|
112
|
+
const size = 大きさ * 倍率;
|
|
113
|
+
const spacing = 字間 * 倍率;
|
|
114
|
+
return { size, spacing, text: 幅で切る(文字列, size, 箱幅, (u, f) => 字幅(u, f, spacing)) };
|
|
106
115
|
}
|
|
107
116
|
|
|
108
117
|
/**
|
|
@@ -117,16 +126,16 @@ function bottomLabel(node: LaidNode, cx: number, plan: ReturnType<typeof labelPl
|
|
|
117
126
|
<>
|
|
118
127
|
{node.eyebrow && plan.肩書 && (
|
|
119
128
|
<text x={cx} y={plan.肩書.y} fontSize={肩.size} fontWeight={700} letterSpacing={肩.spacing} textAnchor="middle" fill="var(--cdl-tone-accent, #2d6a8f)">
|
|
120
|
-
{
|
|
129
|
+
{肩.text}
|
|
121
130
|
</text>
|
|
122
131
|
)}
|
|
123
132
|
<text data-cdl-role="node-label" x={cx} y={plan.名前.y} fontSize={名.size} fontWeight={700} textAnchor="middle" fill="var(--cdl-text, #1a1f2a)">
|
|
124
|
-
{
|
|
133
|
+
{名.text}
|
|
125
134
|
</text>
|
|
126
135
|
{node.subtitle && plan.説明 && (
|
|
127
136
|
<text x={cx} y={plan.説明.y} fontSize={説.size} textAnchor="middle" fill="var(--cdl-text-dim, #5a6270)">
|
|
128
|
-
{
|
|
129
|
-
|
|
137
|
+
{説.text}
|
|
138
|
+
</text>
|
|
130
139
|
)}
|
|
131
140
|
</>
|
|
132
141
|
);
|
|
@@ -5,7 +5,9 @@ import {
|
|
|
5
5
|
fitArt,
|
|
6
6
|
打ち消しの倍率,
|
|
7
7
|
type 打ち消す字,
|
|
8
|
+
切って収まる下限,
|
|
8
9
|
} from "./shape-region";
|
|
10
|
+
import { 幅で切る } from "./box-lines";
|
|
9
11
|
import type { LaidNode } from "../types";
|
|
10
12
|
import { 字引きを作る, type CdlLang } from "../lang";
|
|
11
13
|
|
|
@@ -100,8 +102,15 @@ function 字幅(文字列: string, 大きさ: number, 字間: number): number {
|
|
|
100
102
|
*/
|
|
101
103
|
function 幅に収める(文字列: string, 大きさ: number, 字間: number, 箱幅: number) {
|
|
102
104
|
const 幅 = 字幅(文字列, 大きさ, 字間);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
+
// 縮めは読める下限で止める (#953)。 下限が無いと長い名前が点にしか見えない大きさになる。
|
|
106
|
+
// 止めた行は箱に入らないので、入るところまで切って `…` を付ける (#433 の決まりを保つ)
|
|
107
|
+
const 倍率 =
|
|
108
|
+
幅 > 0 && 箱幅 > 0
|
|
109
|
+
? Math.min(1, Math.max(箱幅 / 幅, 切って収まる下限(文字列, 大きさ, 字間, 箱幅, 字幅)))
|
|
110
|
+
: 1;
|
|
111
|
+
const size = 大きさ * 倍率;
|
|
112
|
+
const spacing = 字間 * 倍率;
|
|
113
|
+
return { size, spacing, text: 幅で切る(文字列, size, 箱幅, (u, f) => 字幅(u, f, spacing)) };
|
|
105
114
|
}
|
|
106
115
|
|
|
107
116
|
/**
|
|
@@ -116,16 +125,16 @@ function bottomLabel(node: LaidNode, cx: number, plan: ReturnType<typeof labelPl
|
|
|
116
125
|
<>
|
|
117
126
|
{node.eyebrow && plan.肩書 && (
|
|
118
127
|
<text x={cx} y={plan.肩書.y} fontSize={肩.size} fontWeight={700} letterSpacing={肩.spacing} textAnchor="middle" fill="var(--cdl-tone-accent, #2d6a8f)">
|
|
119
|
-
{
|
|
128
|
+
{肩.text}
|
|
120
129
|
</text>
|
|
121
130
|
)}
|
|
122
131
|
<text data-cdl-role="node-label" x={cx} y={plan.名前.y} fontSize={名.size} fontWeight={700} textAnchor="middle" fill="var(--cdl-text, #1a1f2a)">
|
|
123
|
-
{
|
|
132
|
+
{名.text}
|
|
124
133
|
</text>
|
|
125
134
|
{node.subtitle && plan.説明 && (
|
|
126
135
|
<text x={cx} y={plan.説明.y} fontSize={説.size} textAnchor="middle" fill="var(--cdl-text-dim, #5a6270)">
|
|
127
|
-
{
|
|
128
|
-
|
|
136
|
+
{説.text}
|
|
137
|
+
</text>
|
|
129
138
|
)}
|
|
130
139
|
</>
|
|
131
140
|
);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { JSX } from "react";
|
|
2
|
-
import { shapeRegion, labelPlan, fitArt, textWidth, type ArtExtent } from "./shape-region";
|
|
2
|
+
import { shapeRegion, labelPlan, fitArt, textWidth, 切って収まる下限, type ArtExtent } from "./shape-region";
|
|
3
|
+
import { 幅で切る } from "./box-lines";
|
|
3
4
|
import type { LaidNode } from "../types";
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -106,8 +107,15 @@ const LABEL_EYEBROW_SPACING = 2;
|
|
|
106
107
|
*/
|
|
107
108
|
function 幅に収める(文字列: string, 大きさ: number, 字間: number, 箱幅: number) {
|
|
108
109
|
const 幅 = textWidth(文字列, 大きさ, 字間);
|
|
109
|
-
|
|
110
|
-
|
|
110
|
+
// 縮めは読める下限で止める (#953)。 下限が無いと長い名前が点にしか見えない大きさになる。
|
|
111
|
+
// 止めた行は箱に入らないので、入るところまで切って `…` を付ける (#433 の決まりを保つ)
|
|
112
|
+
const 倍率 =
|
|
113
|
+
幅 > 0 && 箱幅 > 0
|
|
114
|
+
? Math.min(1, Math.max(箱幅 / 幅, 切って収まる下限(文字列, 大きさ, 字間, 箱幅, textWidth)))
|
|
115
|
+
: 1;
|
|
116
|
+
const size = 大きさ * 倍率;
|
|
117
|
+
const spacing = 字間 * 倍率;
|
|
118
|
+
return { size, spacing, text: 幅で切る(文字列, size, 箱幅, (u, f) => textWidth(u, f, spacing)) };
|
|
111
119
|
}
|
|
112
120
|
|
|
113
121
|
/**
|
|
@@ -124,16 +132,16 @@ function bottomLabel(node: LaidNode, cx: number, plan: ReturnType<typeof labelPl
|
|
|
124
132
|
<>
|
|
125
133
|
{node.eyebrow && plan.肩書 && (
|
|
126
134
|
<text x={cx} y={plan.肩書.y} fontSize={肩.size} fontWeight={700} letterSpacing={肩.spacing} textAnchor="middle" fill="var(--cdl-tone-accent, #2d6a8f)">
|
|
127
|
-
{
|
|
135
|
+
{肩.text}
|
|
128
136
|
</text>
|
|
129
137
|
)}
|
|
130
138
|
<text data-cdl-role="node-label" x={cx} y={plan.名前.y} fontSize={名.size} fontWeight={700} textAnchor="middle" fill="var(--cdl-text, #1a1f2a)">
|
|
131
|
-
{
|
|
139
|
+
{名.text}
|
|
132
140
|
</text>
|
|
133
141
|
{node.subtitle && plan.説明 && (
|
|
134
142
|
<text x={cx} y={plan.説明.y} fontSize={説.size} textAnchor="middle" fill="var(--cdl-text-dim, #5a6270)">
|
|
135
|
-
{
|
|
136
|
-
|
|
143
|
+
{説.text}
|
|
144
|
+
</text>
|
|
137
145
|
)}
|
|
138
146
|
</>
|
|
139
147
|
);
|
|
@@ -23,6 +23,8 @@ import type { JSX } from "react";
|
|
|
23
23
|
import type { LaidNode, NodeKind } from "../types";
|
|
24
24
|
import { MONO_ADVANCE_RATIO, displayWidth } from "./row-align";
|
|
25
25
|
import { textWidth } from "./text-width";
|
|
26
|
+
import { 幅で切る } from "./box-lines";
|
|
27
|
+
import { MIN_TITLE_FONT_SIZE } from "../layout/spec";
|
|
26
28
|
|
|
27
29
|
// 既存の呼出 (検査を含む) がここから取っているため、 そのまま出し直す。
|
|
28
30
|
export { textWidth } from "./text-width";
|
|
@@ -61,6 +63,45 @@ export function 幅の倍率(
|
|
|
61
63
|
return 実幅 > 幅 ? 幅 / 実幅 : 1;
|
|
62
64
|
}
|
|
63
65
|
|
|
66
|
+
/**
|
|
67
|
+
* 横で縮める時に、これ以上は縮めないという倍率 (#953)。
|
|
68
|
+
*
|
|
69
|
+
* 縮めに下限が無いと、長い名前が点にしか見えない大きさで描かれる
|
|
70
|
+
* (実測 = 幅 300 の箱に 120 字で 2.4px)。 読めない字は、書いた名前が画面から消えたのと変わらない。
|
|
71
|
+
* 箱の種別 (`actor` 等 5 種) は `kinds/compact-title.ts` の `fitTextSize` が同じ値で止める。
|
|
72
|
+
*
|
|
73
|
+
* **素の大きさが下限以下なら 0 を返す** = 下限を当てると縮めが 1 度も効かず、横へ大きく出る。
|
|
74
|
+
* その箱は高さの側で既に読めない大きさになっており、`labelPlan` が `読める: false` で報告する。
|
|
75
|
+
*/
|
|
76
|
+
export function 読める下限の倍率(大きさ: number): number {
|
|
77
|
+
return 大きさ > MIN_TITLE_FONT_SIZE ? MIN_TITLE_FONT_SIZE / 大きさ : 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 幅で縮める時に許す下限の倍率 (#953)。 下限を当てられない幅では 0 を返す。
|
|
82
|
+
*
|
|
83
|
+
* 下限で止めた行は帯より広くなるので、入るところまで切って `…` を付ける
|
|
84
|
+
* (箱の中に収める決まりは #433 が持ち、検査が全種別で見ている)。
|
|
85
|
+
*
|
|
86
|
+
* **`…` すら入らない幅では止めない**。 そこで切ると名前が 1 字も残らない =
|
|
87
|
+
* 読めない大きさより悪い。 その幅では今までどおり、小さくても全部描く。
|
|
88
|
+
*
|
|
89
|
+
* 測り方は呼ぶ側から渡す。 種別ごとに字幅の見積りが違い、ここで 1 つに決めると
|
|
90
|
+
* 縮める時と切る時で別の幅を使うことになる。
|
|
91
|
+
*/
|
|
92
|
+
export function 切って収まる下限(
|
|
93
|
+
題: string,
|
|
94
|
+
大きさ: number,
|
|
95
|
+
字間: number,
|
|
96
|
+
幅上限: number,
|
|
97
|
+
測る: (t: string, size: number, ls: number) => number,
|
|
98
|
+
): number {
|
|
99
|
+
const 下限 = 読める下限の倍率(大きさ);
|
|
100
|
+
if (!(下限 > 0) || !(幅上限 > 0)) return 0;
|
|
101
|
+
return 幅で切る(題, 大きさ * 下限, 幅上限, (u, f) => 測る(u, f, 字間 * 下限)) === "" ? 0 : 下限;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
64
105
|
/** 名前の字の大きさの上限。 大きい箱ではここに当たり、 今までの見え方を保つ。 */
|
|
65
106
|
const TITLE_MAX = 18;
|
|
66
107
|
|
|
@@ -433,24 +474,44 @@ export function bodyBand(node: LaidNode, opt: { 影ry: number; 外?: number }):
|
|
|
433
474
|
*
|
|
434
475
|
* `上` / `下` は倍率 1 の時に baseline から上下へ張り出す量、 `幅` は一番広い行の横幅。
|
|
435
476
|
* 呼ぶ側が自分の書式から出す。
|
|
477
|
+
*
|
|
478
|
+
* ## 横は読める下限で止める (#953)
|
|
479
|
+
*
|
|
480
|
+
* 横だけで倍率を決めると **下限が無い**。 幅 300 の箱に 120 字の題を渡すと 2.4 で描き、
|
|
481
|
+
* 点にしか見えない = 書いた名前が画面から消えたのと変わらない。
|
|
482
|
+
* 箱の種別 (`actor` 等 5 種) は `kinds/compact-title.ts` の `fitTextSize` が 11 で止める。
|
|
483
|
+
*
|
|
484
|
+
* `横の下限` を渡すと、横が決める倍率をそこで止める (`切って収まる下限` が算出する)。
|
|
485
|
+
* 止めた塊は帯から左右へ出るので、呼ぶ側が同じ下限で行を切って収める。
|
|
486
|
+
*
|
|
487
|
+
* **縦には入れない**。 縦にはみ出すと絵と重なり、字も絵もどちらも読めなくなる。
|
|
488
|
+
* 縦が決めた倍率のほうが小さければそちらが勝つので、下限を渡しても縦の張り出しは起きない。
|
|
436
489
|
*/
|
|
437
490
|
export function fitLabel(
|
|
438
491
|
band: { x: number; y: number; w: number; h: number },
|
|
439
492
|
block: { 上: number; 下: number; 幅: number },
|
|
440
493
|
pos: { x: number; y: number },
|
|
494
|
+
横の下限 = 0,
|
|
441
495
|
): { k: number; x: number; y: number } {
|
|
442
496
|
const 縦 = block.上 + block.下;
|
|
443
497
|
const 横 = Math.max(0, block.幅);
|
|
444
498
|
if (!(縦 > 0)) return { k: 1, x: pos.x, y: pos.y };
|
|
445
499
|
// 帯が潰れた箱では寄せ先が無い。 倍率 0 で描けば箱の外は汚さない
|
|
446
500
|
if (!(band.h > 0) || !(band.w > 0)) return { k: 0, x: band.x + band.w / 2, y: band.y };
|
|
447
|
-
|
|
501
|
+
// 横が決める倍率は読める下限で止める (#953)。 縦はそのまま = 縦にはみ出すと絵と重なる
|
|
502
|
+
const 横で = 横 > 0 ? Math.max(band.w / 横, 横の下限) : 1;
|
|
503
|
+
const k = Math.min(1, band.h / 縦, 横で);
|
|
448
504
|
// 下端と右端を帯に入れてから、 上端と左端が出る分だけ押し戻す。 倍率が帯に入るまで
|
|
449
505
|
// 落ちているので、 2 つの寄せは両立する
|
|
450
506
|
const 半 = (横 * k) / 2;
|
|
451
507
|
return {
|
|
452
508
|
k,
|
|
453
|
-
|
|
509
|
+
// 下限で止めた塊は帯より広い。 その時は帯の中央に置いて左右へ均等に出す =
|
|
510
|
+
// 寄せの式をそのまま当てると片側だけに出る (`compact-title.ts` と同じ判断)
|
|
511
|
+
x:
|
|
512
|
+
半 * 2 > band.w
|
|
513
|
+
? band.x + band.w / 2
|
|
514
|
+
: Math.max(band.x + 半, Math.min(pos.x, band.x + band.w - 半)),
|
|
454
515
|
y: Math.max(band.y + block.上 * k, Math.min(pos.y, band.y + band.h - block.下 * k)),
|
|
455
516
|
};
|
|
456
517
|
}
|
package/src/layout/spec.ts
CHANGED
|
@@ -272,6 +272,87 @@ export function titleFontSize(kind: string | undefined): number {
|
|
|
272
272
|
*/
|
|
273
273
|
export const TITLE_BOX_PADDING = 52;
|
|
274
274
|
|
|
275
|
+
/**
|
|
276
|
+
* 低い箱の時だけ題を箱に収まるまで小さくする種別。
|
|
277
|
+
*
|
|
278
|
+
* この 4 種は `kinds/compact-title.ts` の `titlePlacement` を通す。 低い箱 (`h` が
|
|
279
|
+
* `COMPACT_TITLE_MAX_H` 未満) では `fitTextSize` が字を 1 ずつ落として収めるが、
|
|
280
|
+
* それ以外の高さでは種別ごとの大きさをそのまま描くため、長い題は箱を超える。
|
|
281
|
+
*/
|
|
282
|
+
const KINDS_SCALING_TITLE_WHEN_LOW: ReadonlySet<string> = new Set(["actor", "event", "function", "storage"]);
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* 高さに関わらず題を箱に収まるまで小さくする種別。
|
|
286
|
+
*
|
|
287
|
+
* `card` は `fitTextSize` を直に呼び、残る 34 種は絵の中の字として箱の幅に収まる倍率を掛ける。
|
|
288
|
+
* 一覧は `test/title-fit-kinds.test.tsx` が `NODE_KINDS` を全件描いて突き合わせる。
|
|
289
|
+
*/
|
|
290
|
+
const KINDS_SCALING_TITLE_ALWAYS: ReadonlySet<string> = new Set([
|
|
291
|
+
"card",
|
|
292
|
+
"shape-file",
|
|
293
|
+
"shape-folder",
|
|
294
|
+
"shape-cloud",
|
|
295
|
+
"shape-cylinder",
|
|
296
|
+
"shape-hexagon",
|
|
297
|
+
"shape-diamond",
|
|
298
|
+
"shape-stack",
|
|
299
|
+
"shape-person",
|
|
300
|
+
"shape-server-rack",
|
|
301
|
+
"shape-network-node",
|
|
302
|
+
"shape-mobile-device",
|
|
303
|
+
"shape-satellite",
|
|
304
|
+
"shape-smart-contract",
|
|
305
|
+
"shape-rpc-node",
|
|
306
|
+
"shape-wallet",
|
|
307
|
+
"shape-nft",
|
|
308
|
+
"shape-token",
|
|
309
|
+
"shape-blockchain",
|
|
310
|
+
"shape-bitcoin-chain",
|
|
311
|
+
"shape-ethereum-chain",
|
|
312
|
+
"shape-blockchain-node",
|
|
313
|
+
"shape-bank",
|
|
314
|
+
"shape-trust-bank",
|
|
315
|
+
"shape-payment-provider",
|
|
316
|
+
"shape-credit-card",
|
|
317
|
+
"shape-brokerage",
|
|
318
|
+
"shape-exchange",
|
|
319
|
+
"shape-atm",
|
|
320
|
+
"shape-website",
|
|
321
|
+
"shape-storefront",
|
|
322
|
+
"shape-warehouse",
|
|
323
|
+
"shape-online-shop",
|
|
324
|
+
"shape-cdn-edge",
|
|
325
|
+
"shape-api-gateway",
|
|
326
|
+
]);
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* これより低い箱を「低い箱」 とみなす。 `kinds/compact-title.ts` の `COMPACT_MAX_H` と同じ値。
|
|
330
|
+
*
|
|
331
|
+
* 写しているのは向きが逆だから = `compact-title.ts` は描く側の module で、`layout/` から
|
|
332
|
+
* 引くと組み方が描画に依存する。 2 つがずれないことは `test/title-fit-kinds.test.tsx` が見る。
|
|
333
|
+
*/
|
|
334
|
+
export const COMPACT_TITLE_MAX_H = 100;
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* その箱が題を収まるまで小さくするか (SSOT)。
|
|
338
|
+
*
|
|
339
|
+
* 小さくする種別は題がはみ出さない代わりに、字が読めない大きさまで落ちうる。
|
|
340
|
+
* 軸 3 はこの 2 群で見るものを変える = 小さくしない種別は「箱の幅を超えるか」、
|
|
341
|
+
* 小さくする種別は「読める下限 (`MIN_TITLE_FONT_SIZE`) を割るか」 を見る。
|
|
342
|
+
*/
|
|
343
|
+
export function titleScalesToFit(kind: string | undefined, h: number): boolean {
|
|
344
|
+
const k = kind ?? "";
|
|
345
|
+
if (KINDS_SCALING_TITLE_ALWAYS.has(k)) return true;
|
|
346
|
+
return KINDS_SCALING_TITLE_WHEN_LOW.has(k) && h < COMPACT_TITLE_MAX_H;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* 題を小さくする時の下限 (world)。 `kinds/compact-title.ts` の `MIN_TEXT_SIZE` と同じ値。
|
|
351
|
+
*
|
|
352
|
+
* 写している理由は `COMPACT_TITLE_MAX_H` と同じ。
|
|
353
|
+
*/
|
|
354
|
+
export const MIN_TITLE_FONT_SIZE = 11;
|
|
355
|
+
|
|
275
356
|
/**
|
|
276
357
|
* `rows` を文字として描く node の種別。
|
|
277
358
|
*
|
package/src/layout/text-width.ts
CHANGED
|
@@ -21,11 +21,24 @@
|
|
|
21
21
|
|
|
22
22
|
type FontFamily = "sans" | "mono";
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* 見積りをどちらへ外すか。
|
|
26
|
+
*
|
|
27
|
+
* `layout` (既定) は **広い側** へ外す。 箱の大きさと線の置き場を決める側は、狭く見積もると
|
|
28
|
+
* 字が枠から出るため、外すなら広い側でなければならない。 表の値はこの向きに寄せてある。
|
|
29
|
+
*
|
|
30
|
+
* `drawn` は **実描画に合わせる**。 「はみ出すか」 を判定する側は、広く見積もると収まっている
|
|
31
|
+
* 字を挙げてしまう。 見本帳の題 407 か所を `getComputedTextLength()` で測り、和文は 1.08 倍 /
|
|
32
|
+
* 欧文は 1.16 倍の上振れだと判った (`DRAWN_*` の説明)。 その分を割り戻す。
|
|
33
|
+
*/
|
|
34
|
+
type WidthBound = "layout" | "drawn";
|
|
35
|
+
|
|
24
36
|
interface MeasureOptions {
|
|
25
37
|
fontSize?: number;
|
|
26
38
|
fontWeight?: number;
|
|
27
39
|
letterSpacing?: number;
|
|
28
40
|
fontFamily?: FontFamily;
|
|
41
|
+
bound?: WidthBound;
|
|
29
42
|
}
|
|
30
43
|
|
|
31
44
|
/**
|
|
@@ -91,6 +104,28 @@ const EMOJI_ADVANCE_EM = 1.05;
|
|
|
91
104
|
/** mono text から fallback する絵文字の advance (em)。 fontSize 19 の実測 22.0 px。 */
|
|
92
105
|
const MONO_EMOJI_ADVANCE_EM = 1.16;
|
|
93
106
|
|
|
107
|
+
/**
|
|
108
|
+
* 和文の上振れ幅。 `bound: "drawn"` で割り戻す。
|
|
109
|
+
*
|
|
110
|
+
* 実測 = 見本帳の題のうち和文だけの 247 件が **例外なく** 見積りの 1/1.08 で描かれていた
|
|
111
|
+
* (最小 1.080 / 中央 1.080)。 和文の字は全角 1.0 em で、表の 1.08 はその上振れぶんになる。
|
|
112
|
+
*/
|
|
113
|
+
const DRAWN_CJK_RATIO = 1.08;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 欧文の上振れ幅。 `bound: "drawn"` で割り戻す。
|
|
117
|
+
*
|
|
118
|
+
* 実測 = 欧文だけの 119 件で見積りが実描画の 1.16 から 1.82 倍 (中央 1.27 倍) だった。
|
|
119
|
+
* **最小の 1.16 を採る** = 割り戻した値が実描画を下回ると、はみ出す題を見落とす。
|
|
120
|
+
* 407 件すべてで下回らないことを `title-drawn-width.test.ts` が突き合わせる。
|
|
121
|
+
*/
|
|
122
|
+
const DRAWN_ASCII_RATIO = 1.16;
|
|
123
|
+
|
|
124
|
+
/** ASCII か。 `bound: "drawn"` で割り戻す係数を選ぶためだけに使う。 */
|
|
125
|
+
function isAscii(code: number): boolean {
|
|
126
|
+
return code < 0x80;
|
|
127
|
+
}
|
|
128
|
+
|
|
94
129
|
/**
|
|
95
130
|
* 絵文字ではない非 ASCII の記号 1 つ分の advance (em)。 実ブラウザで測った値 (`#481`)。
|
|
96
131
|
*
|
|
@@ -189,6 +224,7 @@ export function measureTextWidth(text: string, opts: MeasureOptions = {}): numbe
|
|
|
189
224
|
const fontSize = opts.fontSize ?? 22;
|
|
190
225
|
const letterSpacing = opts.letterSpacing ?? 0;
|
|
191
226
|
const fontFamily = opts.fontFamily ?? "sans";
|
|
227
|
+
const bound = opts.bound ?? "layout";
|
|
192
228
|
let advanceEmSum = 0;
|
|
193
229
|
let charCount = 0;
|
|
194
230
|
// 1 つの絵に畳まれる並びがある時だけ書記素で数える (`CLUSTER_JOINER_RE` の説明)
|
|
@@ -196,13 +232,23 @@ export function measureTextWidth(text: string, opts: MeasureOptions = {}): numbe
|
|
|
196
232
|
for (const ch of 文字列) {
|
|
197
233
|
charCount++;
|
|
198
234
|
const code = ch.codePointAt(0) ?? 0;
|
|
235
|
+
// 実描画に合わせる時は、字種ごとの上振れぶんを割り戻す。 絵文字と記号は実測が無いので
|
|
236
|
+
// 割り戻さない = 測っていない字を狭く見積もると、はみ出す題を見落とす
|
|
237
|
+
const 割り戻し =
|
|
238
|
+
bound === "drawn" && !isEmojiGrapheme(ch, code)
|
|
239
|
+
? isAscii(code)
|
|
240
|
+
? DRAWN_ASCII_RATIO
|
|
241
|
+
: isCJK(code)
|
|
242
|
+
? DRAWN_CJK_RATIO
|
|
243
|
+
: 1
|
|
244
|
+
: 1;
|
|
199
245
|
if (isEmojiGrapheme(ch, code)) {
|
|
200
246
|
// mono を指定しても絵文字は JetBrains Mono ではなく emoji font へ fallback する。
|
|
201
247
|
advanceEmSum += fontFamily === "mono" ? MONO_EMOJI_ADVANCE_EM : EMOJI_ADVANCE_EM;
|
|
202
248
|
continue;
|
|
203
249
|
}
|
|
204
250
|
if (fontFamily === "mono") {
|
|
205
|
-
advanceEmSum += isCJK(code) ? 1.08 : JETBRAINS_MONO_ADVANCE_EM
|
|
251
|
+
advanceEmSum += (isCJK(code) ? 1.08 : JETBRAINS_MONO_ADVANCE_EM) / 割り戻し;
|
|
206
252
|
continue;
|
|
207
253
|
}
|
|
208
254
|
if (isCJK(code)) {
|
|
@@ -210,14 +256,16 @@ export function measureTextWidth(text: string, opts: MeasureOptions = {}): numbe
|
|
|
210
256
|
// Inter fontSize 22 baseline に対し 1.08 em 前後で描画される (実測係数化)。
|
|
211
257
|
// 1.08 が cookbook / presets / text-dsl の合計 G5 warn 最小 (10 件)、
|
|
212
258
|
// 1.15 以上に上げると flow-demo が過大化して +2-6 件増える tradeoff。
|
|
213
|
-
advanceEmSum += 1.08
|
|
259
|
+
advanceEmSum += 1.08 / 割り戻し;
|
|
214
260
|
continue;
|
|
215
261
|
}
|
|
216
262
|
const table = ch.length === 1 ? INTER_BOLD_ADVANCE_EM[ch] : undefined;
|
|
217
|
-
advanceEmSum += table !== undefined ? table : defaultAdvanceEm(ch, code)
|
|
263
|
+
advanceEmSum += (table !== undefined ? table : defaultAdvanceEm(ch, code)) / 割り戻し;
|
|
218
264
|
}
|
|
219
265
|
const w = advanceEmSum * fontSize + letterSpacing * charCount;
|
|
220
|
-
|
|
266
|
+
// 最低幅は組み方のための下駄 (短い札で bbox が細くなり判定が揺れるのを防ぐ)。
|
|
267
|
+
// 実描画に合わせる時は当てない = 1 字の題が 1.2 em で描かれることは無い
|
|
268
|
+
return bound === "drawn" ? w : Math.max(w, fontSize * 1.2);
|
|
221
269
|
}
|
|
222
270
|
|
|
223
271
|
/**
|
package/src/visual-validate.ts
CHANGED
|
@@ -144,6 +144,8 @@ import {
|
|
|
144
144
|
rendersTitleText,
|
|
145
145
|
fitsOwnTitleWidth,
|
|
146
146
|
drawsTitleOutsideBox,
|
|
147
|
+
titleScalesToFit,
|
|
148
|
+
MIN_TITLE_FONT_SIZE,
|
|
147
149
|
titleFontSize,
|
|
148
150
|
TITLE_BOX_PADDING,
|
|
149
151
|
rowBaselineY,
|
|
@@ -2010,16 +2012,34 @@ function runAxes(
|
|
|
2010
2012
|
// 1 字の大きさは **その種別が題を描く大きさ** から渡す (#946)。 22 の定数で持っていたが、
|
|
2011
2013
|
// 22 で描く種別は 1 つも無く、見積りが実物の 1.2 倍から 1.7 倍になっていた。
|
|
2012
2014
|
//
|
|
2015
|
+
// 見積りは **実描画に合わせた向き** で取る (#951)。 既定の値は組み方のために広い側へ
|
|
2016
|
+
// 寄せてあり (和文 1.08 倍 / 欧文 1.16 倍以上)、そのまま「はみ出すか」 を判定すると
|
|
2017
|
+
// 収まっている題を挙げる。 見本帳の題 407 か所の実測で、広い側の値では 9 枚が挙がり、
|
|
2018
|
+
// 実測ではその 9 枚とも箱の中に 20px 以上の余白を残していた。
|
|
2019
|
+
//
|
|
2013
2020
|
// 箱の内側の余白 (52) は、題を箱の中に描く種別にだけ足す。 箱の外に描く種別 (`dyn-*` の
|
|
2014
2021
|
// 名前と図表の題) は内側の余白に縛られないので、足すと収まっている題まで挙がる。
|
|
2015
|
-
const
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
if (
|
|
2022
|
+
const 余白 = drawsTitleOutsideBox(n.kind) ? 0 : TITLE_BOX_PADDING;
|
|
2023
|
+
const 使える幅 = n.w - 余白;
|
|
2024
|
+
const 実幅 = measureTextWidth(titleTrimmed, { fontSize: titleFontSize(n.kind), bound: "drawn" });
|
|
2025
|
+
if (titleScalesToFit(n.kind, n.h)) {
|
|
2026
|
+
// 収まるまで縮める種別は、題がはみ出すことが起きない。 代わりに字が読めない大きさまで
|
|
2027
|
+
// 落ちるので、**読める下限を割るか** を見る (#951)。 下限まで縮めても収まらない時
|
|
2028
|
+
// (`actor` 等の 4 種) と、下限を無視して縮め続ける時 (絵の中の字) の両方がここに入る
|
|
2029
|
+
const 縮めた大きさ =
|
|
2030
|
+
実幅 > 0 ? titleFontSize(n.kind) * Math.min(1, 使える幅 / 実幅) : titleFontSize(n.kind);
|
|
2031
|
+
if (縮めた大きさ < MIN_TITLE_FONT_SIZE) {
|
|
2032
|
+
push(
|
|
2033
|
+
"text-readability",
|
|
2034
|
+
`node "${n.id}" title "${n.title}" が node 幅 ${n.w}px に収まらず、読める下限 ${MIN_TITLE_FONT_SIZE}px を割る (縮めて ${縮めた大きさ.toFixed(1)}px)`,
|
|
2035
|
+
"warn",
|
|
2036
|
+
);
|
|
2037
|
+
}
|
|
2038
|
+
} else if (実幅 > 使える幅 + 8) {
|
|
2019
2039
|
push(
|
|
2020
2040
|
"text-readability",
|
|
2021
2041
|
// 字ごとの実幅は小数になるので、読む人に出す時だけ丸める (判定は丸めない)
|
|
2022
|
-
`node "${n.id}" title "${n.title}" が node 幅 ${n.w}px を超過する可能性 (期待 ${Math.round(
|
|
2042
|
+
`node "${n.id}" title "${n.title}" が node 幅 ${n.w}px を超過する可能性 (期待 ${Math.round(実幅 + 余白)}px)`,
|
|
2023
2043
|
"warn",
|
|
2024
2044
|
);
|
|
2025
2045
|
}
|