@cardenelabs/cdl 0.5.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/LICENSE +21 -0
- package/README.md +343 -0
- package/SPEC.md +374 -0
- package/dist/index.cjs +28085 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +3415 -0
- package/dist/index.d.ts +3415 -0
- package/dist/index.js +27962 -0
- package/dist/index.js.map +1 -0
- package/dist/react.cjs +23043 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +2 -0
- package/dist/react.d.ts +2 -0
- package/dist/react.js +23041 -0
- package/dist/react.js.map +1 -0
- package/dist/render-C78lIXeC.d.cts +2449 -0
- package/dist/render-C78lIXeC.d.ts +2449 -0
- package/examples/quick-start.md +88 -0
- package/package.json +79 -0
- package/src/anim/core/easing.ts +77 -0
- package/src/anim/core/timeline.ts +335 -0
- package/src/anim/core/types.ts +42 -0
- package/src/anim/index.ts +20 -0
- package/src/anim/react/index.ts +3 -0
- package/src/anim/react/useReducedMotion.ts +27 -0
- package/src/anim/react/useTimeline.ts +48 -0
- package/src/assert-never.ts +16 -0
- package/src/author-intent-verify.ts +587 -0
- package/src/builder.ts +3740 -0
- package/src/compile.ts +12 -0
- package/src/dom-verify-core.ts +121 -0
- package/src/dom-verify-types.ts +21 -0
- package/src/dom-verify.ts +948 -0
- package/src/event-handler/index.ts +184 -0
- package/src/formula/ast.ts +46 -0
- package/src/formula/evaluator.ts +163 -0
- package/src/formula/formula-computeds.ts +83 -0
- package/src/formula/index.ts +5 -0
- package/src/formula/parser.ts +399 -0
- package/src/index.ts +284 -0
- package/src/input-signals.ts +74 -0
- package/src/kinds/actor.tsx +79 -0
- package/src/kinds/card.tsx +61 -0
- package/src/kinds/chart-bar.tsx +121 -0
- package/src/kinds/chart-line.tsx +163 -0
- package/src/kinds/chart-pie.tsx +107 -0
- package/src/kinds/compact-title.ts +164 -0
- package/src/kinds/dyn-shape.tsx +394 -0
- package/src/kinds/event.tsx +70 -0
- package/src/kinds/function.tsx +53 -0
- package/src/kinds/funnel.tsx +122 -0
- package/src/kinds/gantt.tsx +193 -0
- package/src/kinds/generic.tsx +368 -0
- package/src/kinds/mind-map.tsx +367 -0
- package/src/kinds/mind-radial.tsx +209 -0
- package/src/kinds/node-tone.ts +18 -0
- package/src/kinds/quadrant.tsx +164 -0
- package/src/kinds/row-align.ts +179 -0
- package/src/kinds/shape-basement.tsx +683 -0
- package/src/kinds/shape-blockchain.tsx +750 -0
- package/src/kinds/shape-commerce.tsx +553 -0
- package/src/kinds/shape-finance.tsx +706 -0
- package/src/kinds/shape-hardware.tsx +862 -0
- package/src/kinds/shape-people.tsx +439 -0
- package/src/kinds/shape-region.tsx +383 -0
- package/src/kinds/shape-software.tsx +859 -0
- package/src/kinds/storage.tsx +180 -0
- package/src/kinds/text-width.ts +73 -0
- package/src/kinds/tree.tsx +275 -0
- package/src/kinds/user-journey.tsx +240 -0
- package/src/label-text.ts +71 -0
- package/src/layout/clearance-constants.ts +47 -0
- package/src/layout/collisions.ts +2078 -0
- package/src/layout/edges.ts +2498 -0
- package/src/layout/footer-shape.ts +97 -0
- package/src/layout/geometry.ts +135 -0
- package/src/layout/label-shift.ts +28 -0
- package/src/layout/lanes.ts +328 -0
- package/src/layout/nodes.ts +190 -0
- package/src/layout/predict-bbox.ts +76 -0
- package/src/layout/px-projection.ts +176 -0
- package/src/layout/spec.ts +872 -0
- package/src/layout/text-width.ts +133 -0
- package/src/layout/tokens.ts +181 -0
- package/src/layout/viewbox.ts +47 -0
- package/src/layout-with-validation.ts +175 -0
- package/src/layout.ts +381 -0
- package/src/presets.ts +2108 -0
- package/src/reactive/batch.ts +48 -0
- package/src/reactive/computed.ts +85 -0
- package/src/reactive/effect.ts +145 -0
- package/src/reactive/index.ts +6 -0
- package/src/reactive/internal.ts +109 -0
- package/src/reactive/signal.ts +113 -0
- package/src/render/edges.tsx +318 -0
- package/src/render/header.tsx +146 -0
- package/src/render/interactive-panel.tsx +9620 -0
- package/src/render/nodes.tsx +319 -0
- package/src/render/stage.tsx +377 -0
- package/src/render/tone.ts +64 -0
- package/src/render/utils.ts +238 -0
- package/src/render.tsx +221 -0
- package/src/scroll-trigger/index.ts +109 -0
- package/src/scroll-trigger/progress.ts +49 -0
- package/src/thumbnail.tsx +114 -0
- package/src/types.ts +2371 -0
- package/src/validate.ts +268 -0
- package/src/visual-validate.ts +4381 -0
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
import type { JSX } from "react";
|
|
2
|
+
import type { LaidNode } from "../types";
|
|
3
|
+
import {
|
|
4
|
+
ASCENT,
|
|
5
|
+
DESCENT,
|
|
6
|
+
FRAME_HALF,
|
|
7
|
+
SHADOW_GAP,
|
|
8
|
+
bodyBand,
|
|
9
|
+
fitArt,
|
|
10
|
+
fitLabel,
|
|
11
|
+
labelPlan,
|
|
12
|
+
shapeRegion,
|
|
13
|
+
textWidth,
|
|
14
|
+
type ArtExtent,
|
|
15
|
+
} from "./shape-region";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Shape-driven basement 8 kinds (CAR-1099、 CAR-1130 people ベース統一版)。
|
|
19
|
+
*
|
|
20
|
+
* 共通 base = drop shadow ellipse + thick outline (active=3 / idle=1.5) + rounded corner。
|
|
21
|
+
* person は people の personBase() (Material Person 型) を複製、 badge なし generic actor 表現。
|
|
22
|
+
* 他 7 shape は幾何形状として品格 up (drop shadow + refined outline)。
|
|
23
|
+
*
|
|
24
|
+
* ## 絵を箱に収める作り (#433)
|
|
25
|
+
*
|
|
26
|
+
* 影は本体の下端から `SHADOW_GAP` 下がった中心に `ry` で描くため、 **下へ出る量は箱の高さに
|
|
27
|
+
* 依らない**。 高さ 72 の名札で実測すると 7 種すべてが下へ出ていた (`shape-person` は 176.2、
|
|
28
|
+
* `shape-stack` は 36、 `shape-file` は 18)。
|
|
29
|
+
*
|
|
30
|
+
* 直し方は 2 系統に分かれる。
|
|
31
|
+
*
|
|
32
|
+
* | 系統 | 種別 | 作り |
|
|
33
|
+
* |---|---|---|
|
|
34
|
+
* | 帯に描く | file / folder / cloud / cylinder / hexagon / diamond / stack | `bodyBand` で影と枠を先に引き、 残った帯いっぱいに描く。 固定寸法 (折れ角 / tab / 蓋の膨らみ / ずらし幅) は帯に比例させて上限で止める |
|
|
35
|
+
* | 比で縮める | person | 絵が固定寸法の人型なので `shapeRegion` + `fitArt` + `labelPlan` (`shape-people` と同じ形) |
|
|
36
|
+
*
|
|
37
|
+
* 帯に描く系は **箱の縁と絵の縁の隙間が箱の高さに依らない定数** になる。 比で縮める直し方を
|
|
38
|
+
* 誤ってこちらに使うと定数性が崩れるので、 検査がそれを捕まえる。
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
type ShapeProps = {
|
|
42
|
+
node: LaidNode;
|
|
43
|
+
active: boolean;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// basement は shape 全体が body なので、 純白ではなく accent 青系薄 tint (#dde8f0) で
|
|
47
|
+
// 他 shape (server-rack / smart-contract 等 = accent #2d6a8f と統一) と視覚的統一を取る。
|
|
48
|
+
const BASEMENT_FILL = "#dde8f0";
|
|
49
|
+
const commonFrame = (active: boolean) => ({
|
|
50
|
+
fill: BASEMENT_FILL,
|
|
51
|
+
stroke: active ? "var(--cdl-tone-accent, #2d6a8f)" : "var(--cdl-divider, #d4d4d8)",
|
|
52
|
+
strokeWidth: active ? 3 : 1.5,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
/** 影の楕円の縦半径。 帯が下に空ける量と `dropShadow` の既定を 1 つの値で持つ。 */
|
|
56
|
+
const SHADOW_RY = 4;
|
|
57
|
+
|
|
58
|
+
/** 円柱だけ影を薄くする (底が丸く、 濃いと沈んで見える)。 */
|
|
59
|
+
const CYLINDER_SHADOW_RY = 3;
|
|
60
|
+
|
|
61
|
+
/** 名前の塊の書式。 描く側 (`commonLabel`) と測る側 (`labelBlock`) が同じ値を使う。 */
|
|
62
|
+
const EYEBROW_SIZE = 12;
|
|
63
|
+
const EYEBROW_GAP = 8;
|
|
64
|
+
const EYEBROW_TRACKING = 2;
|
|
65
|
+
const SUB_SIZE = 11;
|
|
66
|
+
const SUB_GAP = 4;
|
|
67
|
+
|
|
68
|
+
/** 名前を置いてよい帯。 種別ごとに `bodyBand` より狭い形を渡す (紙の上 / 前面の板の中 等)。 */
|
|
69
|
+
type LabelBand = { x: number; y: number; w: number; h: number };
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 名前の塊を帯に収めた形。 `commonLabel` が描き、 装飾線を引く側が「線を引かない場所」 に使う。
|
|
73
|
+
*
|
|
74
|
+
* 塊の張り出しは baseline からの上下 2 つと、 一番広い行の横幅で表す。 3 つとも倍率に比例
|
|
75
|
+
* するので、 `fitLabel` が返す倍率を全行の大きさと行間と字送りに一様に掛ければ塊ごと帯に入る。
|
|
76
|
+
*/
|
|
77
|
+
function labelBlock(
|
|
78
|
+
node: LaidNode,
|
|
79
|
+
pos: { x: number; y: number },
|
|
80
|
+
size: number,
|
|
81
|
+
band: LabelBand,
|
|
82
|
+
): { k: number; x: number; baseline: number; 上端: number; 下端: number } {
|
|
83
|
+
const 上 = node.eyebrow ? size + EYEBROW_GAP + EYEBROW_SIZE * ASCENT : size * ASCENT;
|
|
84
|
+
const 下 = node.subtitle ? size + SUB_GAP + SUB_SIZE * DESCENT : size * DESCENT;
|
|
85
|
+
// 3 行のうち一番広い行が横の制約になる
|
|
86
|
+
const 幅 = Math.max(
|
|
87
|
+
textWidth(node.title ?? "", size),
|
|
88
|
+
node.eyebrow ? textWidth(node.eyebrow, EYEBROW_SIZE, EYEBROW_TRACKING) : 0,
|
|
89
|
+
node.subtitle ? textWidth(node.subtitle, SUB_SIZE) : 0,
|
|
90
|
+
);
|
|
91
|
+
const { k, x, y: baseline } = fitLabel(band, { 上, 下, 幅 }, pos);
|
|
92
|
+
return { k, x, baseline, 上端: baseline - 上 * k, 下端: baseline + 下 * k };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function commonLabel(node: LaidNode, pos: { x: number; y: number }, size: number, band: LabelBand) {
|
|
96
|
+
const 塊 = labelBlock(node, pos, size, band);
|
|
97
|
+
const k = 塊.k;
|
|
98
|
+
const x = 塊.x;
|
|
99
|
+
return (
|
|
100
|
+
<>
|
|
101
|
+
{node.eyebrow && (
|
|
102
|
+
<text
|
|
103
|
+
x={x}
|
|
104
|
+
y={塊.baseline - (size + EYEBROW_GAP) * k}
|
|
105
|
+
fontSize={EYEBROW_SIZE * k}
|
|
106
|
+
fontWeight={700}
|
|
107
|
+
letterSpacing={EYEBROW_TRACKING * k}
|
|
108
|
+
textAnchor="middle"
|
|
109
|
+
fill="var(--cdl-tone-accent, #2d6a8f)"
|
|
110
|
+
>
|
|
111
|
+
{node.eyebrow}
|
|
112
|
+
</text>
|
|
113
|
+
)}
|
|
114
|
+
<text
|
|
115
|
+
data-cdl-role="node-label"
|
|
116
|
+
x={x}
|
|
117
|
+
y={塊.baseline}
|
|
118
|
+
fontSize={size * k}
|
|
119
|
+
fontWeight={700}
|
|
120
|
+
textAnchor="middle"
|
|
121
|
+
fill="var(--cdl-text, #1a1f2a)"
|
|
122
|
+
>
|
|
123
|
+
{node.title}
|
|
124
|
+
</text>
|
|
125
|
+
{node.subtitle && (
|
|
126
|
+
<text
|
|
127
|
+
x={x}
|
|
128
|
+
y={塊.baseline + (size + SUB_GAP) * k}
|
|
129
|
+
fontSize={SUB_SIZE * k}
|
|
130
|
+
textAnchor="middle"
|
|
131
|
+
fill="var(--cdl-text-dim, #5a6270)"
|
|
132
|
+
>
|
|
133
|
+
{node.subtitle}
|
|
134
|
+
</text>
|
|
135
|
+
)}
|
|
136
|
+
</>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** drop shadow (shape 下部の薄い ellipse、 立体感付与) */
|
|
141
|
+
function dropShadow(cx: number, bottomY: number, rx: number, ry = SHADOW_RY) {
|
|
142
|
+
return (
|
|
143
|
+
<ellipse cx={cx} cy={bottomY + SHADOW_GAP} rx={rx} ry={ry} fill="#000" opacity={0.08} stroke="none" />
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** 折れ角の上限と、 紙の短辺に対する割合。 */
|
|
148
|
+
const FOLD_MAX = 26;
|
|
149
|
+
const FOLD_RATIO = 0.18;
|
|
150
|
+
|
|
151
|
+
/** 書類線の間隔の上限と、 紙の高さに対する割合。 */
|
|
152
|
+
const RULE_GAP_MAX = 20;
|
|
153
|
+
const RULE_GAP_RATIO = 0.1;
|
|
154
|
+
|
|
155
|
+
/** 書類線の左右余白の上限と、 紙の幅に対する割合。 1 本ごとに短くする割合も紙に合わせる。 */
|
|
156
|
+
const RULE_PAD_MAX = 20;
|
|
157
|
+
const RULE_PAD_RATIO = 0.14;
|
|
158
|
+
const RULE_SHRINK_RATIO = 0.055;
|
|
159
|
+
|
|
160
|
+
/** shape-file = 縦長 A4 型 file (right-top folded corner、 body に fold 内側 corner 統合) + drop shadow + text lines */
|
|
161
|
+
export function ShapeFileNode({ node, active }: ShapeProps): JSX.Element {
|
|
162
|
+
const 帯 = bodyBand(node, { 影ry: SHADOW_RY });
|
|
163
|
+
const 紙h = 帯.h;
|
|
164
|
+
const 紙w = Math.min(帯.w, 紙h * 0.72);
|
|
165
|
+
const x = 帯.cx - 紙w / 2;
|
|
166
|
+
const y = 帯.y;
|
|
167
|
+
// 折れ角は紙に比例させる。 固定の 26 は高さ 72 の紙 (幅 52) の半分を占めて絵が読めない
|
|
168
|
+
const fold = Math.min(FOLD_MAX, 紙w * FOLD_RATIO, 紙h * FOLD_RATIO);
|
|
169
|
+
const frame = commonFrame(active);
|
|
170
|
+
// 本体 path = 右上折れ角の内側 L 字 corner (紙w-fold, y+fold) を含めることで body stroke と fold triangle stroke が
|
|
171
|
+
// corner 座標を共有 = T 字 gap 解消。 Path chain = 左上 → 折れ角上端 → 折れ角内側 (下がって右) → body 右下 → 左下 → close。
|
|
172
|
+
const bodyPath = [
|
|
173
|
+
`M ${x} ${y}`,
|
|
174
|
+
`L ${x + 紙w - fold} ${y}`,
|
|
175
|
+
`L ${x + 紙w - fold} ${y + fold}`,
|
|
176
|
+
`L ${x + 紙w} ${y + fold}`,
|
|
177
|
+
`L ${x + 紙w} ${y + 紙h}`,
|
|
178
|
+
`L ${x} ${y + 紙h}`,
|
|
179
|
+
"Z",
|
|
180
|
+
].join(" ");
|
|
181
|
+
// 折れ三角 fill = 背景色で切れ込み表現 (stroke なし、 stroke は bodyPath 内で一体化)
|
|
182
|
+
const foldTriangle = `M ${x + 紙w - fold} ${y} L ${x + 紙w} ${y + fold} L ${x + 紙w - fold} ${y + fold} Z`;
|
|
183
|
+
// 名前は紙の上に置く。 帯より紙が狭い高さがあるので、 帯をそのまま渡すと名前が紙から出る
|
|
184
|
+
const 名前帯 = { x, y, w: 紙w, h: 紙h };
|
|
185
|
+
const 名前位置 = { x: 帯.cx, y: node.cy + node.h * 0.12 };
|
|
186
|
+
// 名前の塊を先に測る。 3 本目が紙頭から固定 90 の位置に出ていたので、 塊に食い込む線は引かない
|
|
187
|
+
const 名前 = labelBlock(node, 名前位置, 16, 名前帯);
|
|
188
|
+
const 間隔 = Math.min(RULE_GAP_MAX, 紙h * RULE_GAP_RATIO);
|
|
189
|
+
const 線余白 = Math.min(RULE_PAD_MAX, 紙w * RULE_PAD_RATIO);
|
|
190
|
+
const textLines = [0, 1, 2]
|
|
191
|
+
.map((i) => ({ i, ly: y + fold + 間隔 * (i + 1) }))
|
|
192
|
+
.filter(({ ly }) => ly < 名前.上端);
|
|
193
|
+
return (
|
|
194
|
+
<g>
|
|
195
|
+
{/* 影の基準は紙の下端。 帯の下端を直に書くと紙を動かした時に影だけ取り残される */}
|
|
196
|
+
{dropShadow(帯.cx, y + 紙h, 紙w * 0.42)}
|
|
197
|
+
<path d={bodyPath} data-cdl-role="node-body" {...frame} strokeLinejoin="round" />
|
|
198
|
+
<path
|
|
199
|
+
d={foldTriangle}
|
|
200
|
+
fill="var(--cdl-canvas-bg, #f5f7fa)"
|
|
201
|
+
stroke={frame.stroke}
|
|
202
|
+
strokeWidth={frame.strokeWidth}
|
|
203
|
+
strokeLinejoin="round"
|
|
204
|
+
/>
|
|
205
|
+
{/* text lines (書類感、 title 上部に配置) */}
|
|
206
|
+
{textLines.map(({ i, ly }) => {
|
|
207
|
+
const lineW = Math.max(0, 紙w - 線余白 * 2 - i * 紙w * RULE_SHRINK_RATIO);
|
|
208
|
+
return (
|
|
209
|
+
<line
|
|
210
|
+
key={i}
|
|
211
|
+
x1={x + 線余白}
|
|
212
|
+
y1={ly}
|
|
213
|
+
x2={x + 線余白 + lineW}
|
|
214
|
+
y2={ly}
|
|
215
|
+
stroke="var(--cdl-divider, #d4d4d8)"
|
|
216
|
+
strokeWidth={1}
|
|
217
|
+
/>
|
|
218
|
+
);
|
|
219
|
+
})}
|
|
220
|
+
{/* label = 中央 (text lines の下、 title 大、 subtitle 小、 A4 下半分) */}
|
|
221
|
+
{commonLabel(node, 名前位置, 16, 名前帯)}
|
|
222
|
+
</g>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** tab の高さの上限と、 帯に対する割合。 */
|
|
227
|
+
const TAB_H_MAX = 22;
|
|
228
|
+
const TAB_H_RATIO = 0.3;
|
|
229
|
+
|
|
230
|
+
/** shape-folder = 左上 tab を body 左端に完全張り付け (tab 左 0、 tab 右 35%、 slope 42%) + drop shadow */
|
|
231
|
+
export function ShapeFolderNode({ node, active }: ShapeProps): JSX.Element {
|
|
232
|
+
const 帯 = bodyBand(node, { 影ry: SHADOW_RY });
|
|
233
|
+
const x = 帯.x;
|
|
234
|
+
const y = 帯.y;
|
|
235
|
+
// tab は帯に比例させる。 固定の 22 は高さ 40 の箱で本体をほぼ埋める
|
|
236
|
+
const tabH = Math.min(TAB_H_MAX, 帯.h * TAB_H_RATIO);
|
|
237
|
+
// tab は body 左端と面一 (x = 0)、 tab 右端が slope で body 上端に降りる
|
|
238
|
+
const tabTopR = 帯.w * 0.35;
|
|
239
|
+
const tabBottomR = 帯.w * 0.42;
|
|
240
|
+
const frame = commonFrame(active);
|
|
241
|
+
// Path chain = body 左中段 (x, y+tabH) → tab 左上 (x, y) → tab 右上 (tabTopR, y) → slope 下 (tabBottomR, y+tabH) → body 右上 (w, y+tabH) → 右下 → 左下 → close
|
|
242
|
+
const bodyPath = [
|
|
243
|
+
`M ${x} ${y + tabH}`,
|
|
244
|
+
`L ${x} ${y}`,
|
|
245
|
+
`L ${x + tabTopR} ${y}`,
|
|
246
|
+
`L ${x + tabBottomR} ${y + tabH}`,
|
|
247
|
+
`L ${帯.右} ${y + tabH}`,
|
|
248
|
+
`L ${帯.右} ${帯.底}`,
|
|
249
|
+
`L ${x} ${帯.底}`,
|
|
250
|
+
"Z",
|
|
251
|
+
].join(" ");
|
|
252
|
+
// 境界線も帯に比例させる。 固定の 8 と 6 は狭い箱で本体の外へ出る
|
|
253
|
+
const 境界y = y + tabH + Math.min(8, (帯.h - tabH) * 0.2);
|
|
254
|
+
const 端 = Math.min(6, 帯.w * 0.1);
|
|
255
|
+
const 名前位置 = { x: 帯.cx, y: node.cy + 18 };
|
|
256
|
+
const 名前 = labelBlock(node, 名前位置, 16, 帯);
|
|
257
|
+
return (
|
|
258
|
+
<g>
|
|
259
|
+
{dropShadow(帯.cx, 帯.底, 帯.w * 0.44)}
|
|
260
|
+
<path data-cdl-role="node-body" d={bodyPath} {...frame} strokeLinejoin="round" />
|
|
261
|
+
{/* 本体上部 highlight line (tab の下部と body の境界を明示)。
|
|
262
|
+
名前にかかるなら引かない = 字面を線が横切ると読めなくなる (高さ 72 で横切っていた) */}
|
|
263
|
+
{境界y < 名前.上端 && (
|
|
264
|
+
<line
|
|
265
|
+
x1={x + 端}
|
|
266
|
+
y1={境界y}
|
|
267
|
+
x2={帯.右 - 端}
|
|
268
|
+
y2={境界y}
|
|
269
|
+
stroke={frame.stroke}
|
|
270
|
+
strokeWidth={1}
|
|
271
|
+
opacity={0.35}
|
|
272
|
+
/>
|
|
273
|
+
)}
|
|
274
|
+
{commonLabel(node, 名前位置, 16, 帯)}
|
|
275
|
+
</g>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** 雲の元 icon の viewBox。 この比を保つため帯を埋め切らない。 */
|
|
280
|
+
const CLOUD_VB_W = 640;
|
|
281
|
+
const CLOUD_VB_H = 512;
|
|
282
|
+
|
|
283
|
+
/** 雲の幅を帯のどこまで使うか。 左右に余韻を残す。 */
|
|
284
|
+
const CLOUD_W_RATIO = 0.9;
|
|
285
|
+
|
|
286
|
+
/** shape-cloud = Font Awesome cloud icon path を scale + translate + drop shadow */
|
|
287
|
+
export function ShapeCloudNode({ node, active }: ShapeProps): JSX.Element {
|
|
288
|
+
const 帯 = bodyBand(node, { 影ry: SHADOW_RY });
|
|
289
|
+
const frame = commonFrame(active);
|
|
290
|
+
// Font Awesome "cloud" icon の path (viewBox 640 x 512) を帯に scale + center。
|
|
291
|
+
// 元 path = M 384 64 c 88 0 160 72 160 160 ... (top-left origin)
|
|
292
|
+
const 素の倍率 = Math.min(帯.h / CLOUD_VB_H, (帯.w * CLOUD_W_RATIO) / CLOUD_VB_W);
|
|
293
|
+
// 帯が潰れた箱では倍率が 0 以下になる。 `strokeWidth / scale` が `Infinity` になるのを塞ぐ
|
|
294
|
+
const scale = Number.isFinite(素の倍率) && 素の倍率 > 0 ? 素の倍率 : 0;
|
|
295
|
+
const scaledW = CLOUD_VB_W * scale;
|
|
296
|
+
const scaledH = CLOUD_VB_H * scale;
|
|
297
|
+
const tx = 帯.cx - scaledW / 2;
|
|
298
|
+
// 雲は比が固定なので帯を縦に埋め切らない。 **下端を帯に合わせる** = 影が箱の底に着く位置に
|
|
299
|
+
// 落ち着き、 箱の高さを変えても下の隙間が変わらない
|
|
300
|
+
const ty = 帯.底 - scaledH;
|
|
301
|
+
// Font Awesome 5 solid cloud icon (正規版 path、 実測 v5.15.4)
|
|
302
|
+
const cloudPath = "M 537.6 226.6 c 4.1 -10.7 6.4 -22.4 6.4 -34.6 c 0 -53 -43 -96 -96 -96 c -19.7 0 -38.1 6 -53.3 16.2 C 367 64.2 315.3 32 256 32 C 167.6 32 96 103.6 96 192 c 0 2.7 0.1 5.4 0.2 8.1 C 40.2 219.8 0 273.2 0 336 c 0 79.5 64.5 144 144 144 h 368 c 70.7 0 128 -57.3 128 -128 c 0 -61.9 -44 -113.6 -102.4 -125.4 Z";
|
|
303
|
+
return (
|
|
304
|
+
<g>
|
|
305
|
+
{/* 影の基準は雲の下端。 帯の下端を直に書くと雲を動かした時に影だけ取り残される */}
|
|
306
|
+
{dropShadow(帯.cx, ty + scaledH, scaledW * 0.42)}
|
|
307
|
+
<g transform={`translate(${tx}, ${ty}) scale(${scale})`}>
|
|
308
|
+
<path
|
|
309
|
+
data-cdl-role="node-body"
|
|
310
|
+
d={cloudPath}
|
|
311
|
+
fill={frame.fill}
|
|
312
|
+
stroke={frame.stroke}
|
|
313
|
+
strokeWidth={scale > 0 ? frame.strokeWidth / scale : 0}
|
|
314
|
+
strokeLinejoin="round"
|
|
315
|
+
/>
|
|
316
|
+
</g>
|
|
317
|
+
{/* label = cloud 内側の中央 */}
|
|
318
|
+
{commonLabel(node, { x: 帯.cx, y: node.cy + node.h * 0.05 }, 16, 帯)}
|
|
319
|
+
</g>
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** 蓋の膨らみの上限と、 帯に対する割合。 段の弧も同じ量だけ下に膨らむ。 */
|
|
324
|
+
const CYLINDER_RY_MAX = 18;
|
|
325
|
+
const CYLINDER_RY_RATIO = 0.1;
|
|
326
|
+
|
|
327
|
+
/** 段の弧を置く高さ (帯に対する割合)。 一番下は 0.8 で、 膨らみ 0.1 を足しても帯に収まる。 */
|
|
328
|
+
const CYLINDER_DISKS = [0.3, 0.55, 0.8];
|
|
329
|
+
|
|
330
|
+
/** shape-cylinder = 3D 円柱 (上面 ellipse + 側面 + 底面 arc 下半分のみ) + drop shadow */
|
|
331
|
+
export function ShapeCylinderNode({ node, active }: ShapeProps): JSX.Element {
|
|
332
|
+
const 帯 = bodyBand(node, { 影ry: CYLINDER_SHADOW_RY });
|
|
333
|
+
const x = 帯.x;
|
|
334
|
+
const y = 帯.y;
|
|
335
|
+
const rx = 帯.w / 2;
|
|
336
|
+
// 蓋の膨らみを帯に比例させる。 固定の 18 だと 0.8 の高さに置いた段の弧が帯を突き抜ける
|
|
337
|
+
// (高さ 72 で下へ 3.6)。 割合を 0.1 に抑えると 0.8 + 0.1 < 1 で必ず収まる
|
|
338
|
+
const ry = Math.min(CYLINDER_RY_MAX, 帯.h * CYLINDER_RY_RATIO);
|
|
339
|
+
const frame = commonFrame(active);
|
|
340
|
+
const bodyPath = [
|
|
341
|
+
`M ${x} ${y + ry}`,
|
|
342
|
+
`A ${rx} ${ry} 0 0 1 ${帯.右} ${y + ry}`,
|
|
343
|
+
`L ${帯.右} ${帯.底 - ry}`,
|
|
344
|
+
`A ${rx} ${ry} 0 0 1 ${x} ${帯.底 - ry}`,
|
|
345
|
+
"Z",
|
|
346
|
+
].join(" ");
|
|
347
|
+
// 蓋の弧は円柱の形そのものなので消せない。 名前を蓋の下に置く
|
|
348
|
+
const 蓋底 = y + ry * 2;
|
|
349
|
+
const 名前位置 = { x: 帯.cx, y: node.cy + 6 };
|
|
350
|
+
const 名前帯 = { x: 帯.x, y: 蓋底, w: 帯.w, h: Math.max(0, 帯.底 - 蓋底) };
|
|
351
|
+
const 名前 = labelBlock(node, 名前位置, 15, 名前帯);
|
|
352
|
+
return (
|
|
353
|
+
<g>
|
|
354
|
+
{dropShadow(帯.cx, 帯.底, rx * 0.85, CYLINDER_SHADOW_RY)}
|
|
355
|
+
<path data-cdl-role="node-body" d={bodyPath} {...frame} strokeLinejoin="round" />
|
|
356
|
+
{/* 上面 ellipse 下半分 arc (DB icon caps line) */}
|
|
357
|
+
<path
|
|
358
|
+
d={`M ${x} ${y + ry} A ${rx} ${ry} 0 0 0 ${帯.右} ${y + ry}`}
|
|
359
|
+
fill="none"
|
|
360
|
+
stroke={frame.stroke}
|
|
361
|
+
strokeWidth={frame.strokeWidth}
|
|
362
|
+
/>
|
|
363
|
+
{/* 中央 3 layer disk lines (DB 段層表現)。 段の弧は装飾なので、 名前にかかるなら引かない */}
|
|
364
|
+
{CYLINDER_DISKS.map((r, i) => ({ i, ly: y + 帯.h * r }))
|
|
365
|
+
.filter(({ ly }) => ly + ry < 名前.上端)
|
|
366
|
+
.map(({ i, ly }) => (
|
|
367
|
+
<path
|
|
368
|
+
key={i}
|
|
369
|
+
d={`M ${x} ${ly} A ${rx} ${ry} 0 0 0 ${帯.右} ${ly}`}
|
|
370
|
+
fill="none"
|
|
371
|
+
stroke={frame.stroke}
|
|
372
|
+
strokeWidth={0.8}
|
|
373
|
+
opacity={0.3}
|
|
374
|
+
/>
|
|
375
|
+
))}
|
|
376
|
+
{commonLabel(node, 名前位置, 15, 名前帯)}
|
|
377
|
+
</g>
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** 頂点の点の半径。 帯の外へこの分だけ出るので、 帯の外余白に数える。 */
|
|
382
|
+
const HEX_DOT_R = 2.5;
|
|
383
|
+
|
|
384
|
+
/** shape-hexagon = 六角形 (component / service 表現、 accent 頂点) + drop shadow */
|
|
385
|
+
export function ShapeHexagonNode({ node, active }: ShapeProps): JSX.Element {
|
|
386
|
+
// 点は多角形の外へ出るので、 枠より点の方が広ければ点に合わせて帯を縮める
|
|
387
|
+
const 帯 = bodyBand(node, { 影ry: SHADOW_RY, 外: Math.max(HEX_DOT_R, FRAME_HALF) });
|
|
388
|
+
const cx = 帯.cx;
|
|
389
|
+
const cy = 帯.cy;
|
|
390
|
+
const cut = 帯.w * 0.2;
|
|
391
|
+
const frame = commonFrame(active);
|
|
392
|
+
const 頂点: [number, number][] = [
|
|
393
|
+
[帯.x + cut, 帯.y],
|
|
394
|
+
[帯.右 - cut, 帯.y],
|
|
395
|
+
[帯.右, cy],
|
|
396
|
+
[帯.右 - cut, 帯.底],
|
|
397
|
+
[帯.x + cut, 帯.底],
|
|
398
|
+
[帯.x, cy],
|
|
399
|
+
];
|
|
400
|
+
const points = 頂点.map(([px, py]) => `${px},${py}`).join(" ");
|
|
401
|
+
return (
|
|
402
|
+
<g>
|
|
403
|
+
{dropShadow(cx, 帯.底, 帯.w * 0.42)}
|
|
404
|
+
<polygon data-cdl-role="node-body" points={points} {...frame} strokeLinejoin="round" />
|
|
405
|
+
{/* 頂点 dot (component の accent 表現) */}
|
|
406
|
+
{頂点.map(([px, py], i) => (
|
|
407
|
+
<circle key={i} cx={px} cy={py} r={HEX_DOT_R} fill={frame.stroke} stroke="none" />
|
|
408
|
+
))}
|
|
409
|
+
{/* 六角形の内側に必ず入る矩形は上下の辺の幅 (帯.w - cut × 2)。 帯をそのまま渡すと
|
|
410
|
+
長い名前が斜辺の外へ出て背景の上に乗る */}
|
|
411
|
+
{commonLabel(
|
|
412
|
+
node,
|
|
413
|
+
{ x: cx, y: node.cy + 6 },
|
|
414
|
+
16,
|
|
415
|
+
{ x: 帯.x + cut, y: 帯.y, w: Math.max(0, 帯.w - cut * 2), h: 帯.h },
|
|
416
|
+
)}
|
|
417
|
+
</g>
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** shape-diamond = ひし形 (decision / 判定) + drop shadow + inner border */
|
|
422
|
+
export function ShapeDiamondNode({ node, active }: ShapeProps): JSX.Element {
|
|
423
|
+
const 帯 = bodyBand(node, { 影ry: SHADOW_RY });
|
|
424
|
+
const cx = 帯.cx;
|
|
425
|
+
const cy = 帯.cy;
|
|
426
|
+
const frame = commonFrame(active);
|
|
427
|
+
const points = [
|
|
428
|
+
`${cx},${帯.y}`,
|
|
429
|
+
`${帯.右},${cy}`,
|
|
430
|
+
`${cx},${帯.底}`,
|
|
431
|
+
`${帯.x},${cy}`,
|
|
432
|
+
].join(" ");
|
|
433
|
+
// inner diamond (subtle border)
|
|
434
|
+
const innerScale = 0.88;
|
|
435
|
+
const innerPoints = [
|
|
436
|
+
`${cx},${cy - (帯.h / 2) * innerScale}`,
|
|
437
|
+
`${cx + (帯.w / 2) * innerScale},${cy}`,
|
|
438
|
+
`${cx},${cy + (帯.h / 2) * innerScale}`,
|
|
439
|
+
`${cx - (帯.w / 2) * innerScale},${cy}`,
|
|
440
|
+
].join(" ");
|
|
441
|
+
return (
|
|
442
|
+
<g>
|
|
443
|
+
{dropShadow(cx, 帯.底, 帯.w * 0.4)}
|
|
444
|
+
<polygon data-cdl-role="node-body" points={points} {...frame} strokeLinejoin="round" />
|
|
445
|
+
<polygon
|
|
446
|
+
points={innerPoints}
|
|
447
|
+
fill="none"
|
|
448
|
+
stroke={frame.stroke}
|
|
449
|
+
strokeWidth={0.8}
|
|
450
|
+
opacity={0.3}
|
|
451
|
+
strokeLinejoin="round"
|
|
452
|
+
/>
|
|
453
|
+
{/* ひし形の内側に必ず入る矩形は半分の大きさ (|x|/a + |y|/b <= 1 に a/2 × b/2 が収まる)。
|
|
454
|
+
帯をそのまま渡すと長い名前が内側のひし形の線を跨いで読めなくなる */}
|
|
455
|
+
{commonLabel(
|
|
456
|
+
node,
|
|
457
|
+
{ x: cx, y: node.cy + 6 },
|
|
458
|
+
15,
|
|
459
|
+
{
|
|
460
|
+
x: cx - (帯.w * innerScale) / 4,
|
|
461
|
+
y: cy - (帯.h * innerScale) / 4,
|
|
462
|
+
w: (帯.w * innerScale) / 2,
|
|
463
|
+
h: (帯.h * innerScale) / 2,
|
|
464
|
+
},
|
|
465
|
+
)}
|
|
466
|
+
</g>
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** 板のずらし幅の上限と、 帯に対する割合。 板 2 枚ぶんずらすので割合は 0.5 未満に抑える。 */
|
|
471
|
+
const STACK_OFFSET_MAX = 12;
|
|
472
|
+
const STACK_OFFSET_RATIO = 0.15;
|
|
473
|
+
|
|
474
|
+
/** shape-stack = 重ね rect 3 枚 (layer / history) + drop shadow */
|
|
475
|
+
export function ShapeStackNode({ node, active }: ShapeProps): JSX.Element {
|
|
476
|
+
const 帯 = bodyBand(node, { 影ry: SHADOW_RY });
|
|
477
|
+
const x = 帯.x;
|
|
478
|
+
const y = 帯.y;
|
|
479
|
+
// ずらし幅を帯に比例させる。 固定の 12 は狭い箱で板の大きさを食い潰す
|
|
480
|
+
const ずれ = Math.min(STACK_OFFSET_MAX, 帯.h * STACK_OFFSET_RATIO, 帯.w * STACK_OFFSET_RATIO);
|
|
481
|
+
const frame = commonFrame(active);
|
|
482
|
+
const cardW = 帯.w - ずれ * 2;
|
|
483
|
+
const cardH = 帯.h - ずれ * 2;
|
|
484
|
+
// 一番手前の板の中心と下端。 影と名前はここに合わせる
|
|
485
|
+
const 手前cx = 帯.cx - ずれ;
|
|
486
|
+
const 手前底 = y + ずれ * 2 + cardH;
|
|
487
|
+
return (
|
|
488
|
+
<g>
|
|
489
|
+
{/* 影の基準は一番手前の板の下端。 板 2 枚ぶん下げると箱の外に落ちる (高さ 72 で 36) */}
|
|
490
|
+
{dropShadow(手前cx, 手前底, cardW * 0.42)}
|
|
491
|
+
{/* 背後 rect (offset 右上) */}
|
|
492
|
+
<rect
|
|
493
|
+
x={x + ずれ * 2}
|
|
494
|
+
y={y}
|
|
495
|
+
width={cardW}
|
|
496
|
+
height={cardH}
|
|
497
|
+
rx={8}
|
|
498
|
+
fill={BASEMENT_FILL}
|
|
499
|
+
stroke={frame.stroke}
|
|
500
|
+
strokeWidth={frame.strokeWidth}
|
|
501
|
+
/>
|
|
502
|
+
{/* 中間 rect */}
|
|
503
|
+
<rect
|
|
504
|
+
x={x + ずれ}
|
|
505
|
+
y={y + ずれ}
|
|
506
|
+
width={cardW}
|
|
507
|
+
height={cardH}
|
|
508
|
+
rx={8}
|
|
509
|
+
fill={BASEMENT_FILL}
|
|
510
|
+
stroke={frame.stroke}
|
|
511
|
+
strokeWidth={frame.strokeWidth}
|
|
512
|
+
/>
|
|
513
|
+
{/* 前面 rect (label 描画側) */}
|
|
514
|
+
<rect
|
|
515
|
+
data-cdl-role="node-body"
|
|
516
|
+
x={x}
|
|
517
|
+
y={y + ずれ * 2}
|
|
518
|
+
width={cardW}
|
|
519
|
+
height={cardH}
|
|
520
|
+
rx={8}
|
|
521
|
+
{...frame}
|
|
522
|
+
/>
|
|
523
|
+
{/* 名前は一番手前の板の中に置く。 帯をそのまま渡すと背後の板の上へはみ出す */}
|
|
524
|
+
{commonLabel(
|
|
525
|
+
node,
|
|
526
|
+
{ x: 手前cx, y: node.cy + ずれ + 6 },
|
|
527
|
+
15,
|
|
528
|
+
{ x, y: y + ずれ * 2, w: cardW, h: cardH },
|
|
529
|
+
)}
|
|
530
|
+
</g>
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* 人型の寸法 (`shape-people` の `personBase` と同じ)。 badge は付けない。
|
|
536
|
+
* 頭 (outline circle r=30) + 肩 (cubic Bezier curve、 slim body 幅 102、 頭 : 肩 = 1 : 1.7)。
|
|
537
|
+
*/
|
|
538
|
+
const PERSON_HEAD_R = 30;
|
|
539
|
+
const PERSON_HEAD_DY = 40;
|
|
540
|
+
const PERSON_SHOULDER_R = 51;
|
|
541
|
+
const PERSON_NECK_GAP = 4;
|
|
542
|
+
const PERSON_BODY_LEN = 90;
|
|
543
|
+
const PERSON_SHADOW_DY = 20;
|
|
544
|
+
const PERSON_SHADOW_RY = 4;
|
|
545
|
+
const PERSON_SHADOW_RX = 55;
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* 名前の塊を `labelPlan` の位置に描く (`shape-people` の `bottomLabel` と同じ形)。
|
|
549
|
+
*
|
|
550
|
+
* `labelPlan` は箱の高さだけで字の大きさを決めるので、 長い名前は左右へ出る。 baseline は
|
|
551
|
+
* 動かさず **字の大きさだけ横に入る倍率まで下げる** = 各行の張り出しは縮むだけなので、
|
|
552
|
+
* `labelPlan` が保証した縦の収まりは崩れない。
|
|
553
|
+
*/
|
|
554
|
+
function planLabel(node: LaidNode, cx: number, 幅上限: number, plan: ReturnType<typeof labelPlan>) {
|
|
555
|
+
const 幅 = Math.max(
|
|
556
|
+
textWidth(node.title ?? "", plan.名前.size),
|
|
557
|
+
node.eyebrow && plan.肩書 ? textWidth(node.eyebrow, plan.肩書.size, EYEBROW_TRACKING) : 0,
|
|
558
|
+
node.subtitle && plan.説明 ? textWidth(node.subtitle, plan.説明.size) : 0,
|
|
559
|
+
);
|
|
560
|
+
const k = 幅 > 0 && 幅上限 > 0 ? Math.min(1, 幅上限 / 幅) : 1;
|
|
561
|
+
return (
|
|
562
|
+
<>
|
|
563
|
+
{node.eyebrow && plan.肩書 && (
|
|
564
|
+
<text
|
|
565
|
+
x={cx}
|
|
566
|
+
y={plan.肩書.y}
|
|
567
|
+
fontSize={plan.肩書.size * k}
|
|
568
|
+
fontWeight={700}
|
|
569
|
+
letterSpacing={EYEBROW_TRACKING * k}
|
|
570
|
+
textAnchor="middle"
|
|
571
|
+
fill="var(--cdl-tone-accent, #2d6a8f)"
|
|
572
|
+
>
|
|
573
|
+
{node.eyebrow}
|
|
574
|
+
</text>
|
|
575
|
+
)}
|
|
576
|
+
<text
|
|
577
|
+
data-cdl-role="node-label"
|
|
578
|
+
x={cx}
|
|
579
|
+
y={plan.名前.y}
|
|
580
|
+
fontSize={plan.名前.size * k}
|
|
581
|
+
fontWeight={700}
|
|
582
|
+
textAnchor="middle"
|
|
583
|
+
fill="var(--cdl-text, #1a1f2a)"
|
|
584
|
+
>
|
|
585
|
+
{node.title}
|
|
586
|
+
</text>
|
|
587
|
+
{node.subtitle && plan.説明 && (
|
|
588
|
+
<text
|
|
589
|
+
x={cx}
|
|
590
|
+
y={plan.説明.y}
|
|
591
|
+
fontSize={plan.説明.size * k}
|
|
592
|
+
textAnchor="middle"
|
|
593
|
+
fill="var(--cdl-text-dim, #5a6270)"
|
|
594
|
+
>
|
|
595
|
+
{node.subtitle}
|
|
596
|
+
</text>
|
|
597
|
+
)}
|
|
598
|
+
</>
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* shape-person = 人型 figure (people の personBase() 型を複製、 badge なし generic actor 表現)。
|
|
604
|
+
* 顔なし = 表情 dot / 口 arc は描かない。
|
|
605
|
+
*
|
|
606
|
+
* 他 7 種と違い絵が固定寸法なので帯には広げられない。 `shape-people` と同じく絵の帯を
|
|
607
|
+
* `shapeRegion` で取り、 入らなければ `fitArt` が比を保ったまま縮める。 名前は箱の下端から
|
|
608
|
+
* 積み上げる (`labelPlan`) = 固定位置に置くと箱の下 176.2 まで伸びていた。
|
|
609
|
+
*/
|
|
610
|
+
export function ShapePersonNode({ node, active }: ShapeProps): JSX.Element {
|
|
611
|
+
const r = shapeRegion(node);
|
|
612
|
+
const cx = r.cx;
|
|
613
|
+
const strokeColor = active ? "var(--cdl-tone-accent, #2d6a8f)" : "var(--cdl-divider, #d4d4d8)";
|
|
614
|
+
const strokeWidth = active ? 3 : 1.5;
|
|
615
|
+
// 広がりは **描く側の定数から** 出す。 表に書き写すと絵を変えた時に片方だけ古くなる
|
|
616
|
+
const 線 = strokeWidth / 2;
|
|
617
|
+
const 上ずれ = PERSON_HEAD_DY + PERSON_HEAD_R + 線;
|
|
618
|
+
const 下ずれ =
|
|
619
|
+
PERSON_HEAD_R +
|
|
620
|
+
PERSON_NECK_GAP +
|
|
621
|
+
PERSON_BODY_LEN +
|
|
622
|
+
PERSON_SHADOW_DY +
|
|
623
|
+
PERSON_SHADOW_RY -
|
|
624
|
+
PERSON_HEAD_DY;
|
|
625
|
+
// 絵の外接矩形の中心を帯の中心に置く。 `fitArt` は縮めない大きい箱では中心を合わせずに
|
|
626
|
+
// そのまま返すので、 上下非対称のまま置くと絵が名前の側へ落ちる (高さ 240 で 22.5 食い込んだ)
|
|
627
|
+
const cy = r.cy + (上ずれ - 下ずれ) / 2;
|
|
628
|
+
const headCy = cy - PERSON_HEAD_DY;
|
|
629
|
+
const bodyTopY = headCy + PERSON_HEAD_R + PERSON_NECK_GAP;
|
|
630
|
+
const bodyBottomY = bodyTopY + PERSON_BODY_LEN;
|
|
631
|
+
const bodyPath =
|
|
632
|
+
`M ${cx - PERSON_SHOULDER_R} ${bodyBottomY} ` +
|
|
633
|
+
`C ${cx - PERSON_SHOULDER_R} ${bodyTopY + 40}, ${cx - 28} ${bodyTopY}, ${cx} ${bodyTopY} ` +
|
|
634
|
+
`C ${cx + 28} ${bodyTopY}, ${cx + PERSON_SHOULDER_R} ${bodyTopY + 40}, ${cx + PERSON_SHOULDER_R} ${bodyBottomY}`;
|
|
635
|
+
// 中心を合わせた結果、 上下の広がりは等しくなる
|
|
636
|
+
const 半 = (上ずれ + 下ずれ) / 2;
|
|
637
|
+
const extent: ArtExtent = {
|
|
638
|
+
上: 半,
|
|
639
|
+
下: 半,
|
|
640
|
+
// 影は肩より広い (rx 55 対 肩 51)。 広い方を採る
|
|
641
|
+
左: Math.max(PERSON_SHOULDER_R + 線, PERSON_SHADOW_RX),
|
|
642
|
+
右: Math.max(PERSON_SHOULDER_R + 線, PERSON_SHADOW_RX),
|
|
643
|
+
};
|
|
644
|
+
return (
|
|
645
|
+
<g>
|
|
646
|
+
{fitArt(
|
|
647
|
+
r,
|
|
648
|
+
extent,
|
|
649
|
+
<g>
|
|
650
|
+
{/* drop shadow */}
|
|
651
|
+
<ellipse
|
|
652
|
+
cx={cx}
|
|
653
|
+
cy={bodyBottomY + PERSON_SHADOW_DY}
|
|
654
|
+
rx={PERSON_SHADOW_RX}
|
|
655
|
+
ry={PERSON_SHADOW_RY}
|
|
656
|
+
fill="#000"
|
|
657
|
+
opacity={0.08}
|
|
658
|
+
stroke="none"
|
|
659
|
+
/>
|
|
660
|
+
{/* 頭 (outline circle) */}
|
|
661
|
+
<circle
|
|
662
|
+
data-cdl-role="node-body"
|
|
663
|
+
cx={cx}
|
|
664
|
+
cy={headCy}
|
|
665
|
+
r={PERSON_HEAD_R}
|
|
666
|
+
fill="none"
|
|
667
|
+
stroke={strokeColor}
|
|
668
|
+
strokeWidth={strokeWidth}
|
|
669
|
+
/>
|
|
670
|
+
{/* body (cubic Bezier curve、 open bottom = U 字ではなく肩から下ろす line のみ) */}
|
|
671
|
+
<path
|
|
672
|
+
d={bodyPath}
|
|
673
|
+
fill="none"
|
|
674
|
+
stroke={strokeColor}
|
|
675
|
+
strokeWidth={strokeWidth}
|
|
676
|
+
strokeLinecap="round"
|
|
677
|
+
/>
|
|
678
|
+
</g>,
|
|
679
|
+
)}
|
|
680
|
+
{planLabel(node, r.cx, Math.max(0, node.w), r.label)}
|
|
681
|
+
</g>
|
|
682
|
+
);
|
|
683
|
+
}
|