@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,859 @@
|
|
|
1
|
+
import type { JSX } from "react";
|
|
2
|
+
import type { LaidNode } from "../types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shape-driven software 6 kinds (CAR-1111 Phase 2-B)。
|
|
6
|
+
*
|
|
7
|
+
* Phase 2-A (basement 8) と同じ「要素形状自体がパーツ」 方針で、
|
|
8
|
+
* ソフトウェア開発領域の視覚要素 = window / terminal / code-block /
|
|
9
|
+
* kanban-card / message-bubble / gear を SVG path で直接描画する。
|
|
10
|
+
*
|
|
11
|
+
* 各 kind は 1 SVG path (+ 補助 shape) + title + optional subtitle 構成、
|
|
12
|
+
* fill / stroke は theme (var(--cdl-*)) 追従、 active 時は accent stroke + width 拡張。
|
|
13
|
+
*
|
|
14
|
+
* ## 高さ 72 の箱で出ていた分
|
|
15
|
+
*
|
|
16
|
+
* 順序図の名札に使う高さ 72 の箱では、 6 種のうち 5 種が下か左右へ出ていた。
|
|
17
|
+
*
|
|
18
|
+
* | 種別 | 出ていた分 | 勘定していなかったもの |
|
|
19
|
+
* |---|---|---|
|
|
20
|
+
* | shape-code-block | 下 36.2 / 右 132 | 中に描く行の長さと本体の高さ |
|
|
21
|
+
* | shape-terminal | 下 29.3 / 右 13.2 | 同上 |
|
|
22
|
+
* | shape-gear | 下 10.8 | 歯先の下に付く名前 |
|
|
23
|
+
* | shape-window | 下 5.2 | 名前の説明の行と枠の線 |
|
|
24
|
+
* | shape-kanban-card | 下 3 / 右 3 | 右下へずらす影と枠の線 |
|
|
25
|
+
*
|
|
26
|
+
* 5 種とも、 箱の大きさから描いているのに中へ置く物の分を勘定していないことが原因だった。
|
|
27
|
+
* これは shape-region.tsx の原因 1 に当たる。
|
|
28
|
+
* 絵ごと縮める fitArt では名前の取り分だけ大きい箱でも絵が小さくなるため、 描ける範囲を先に狭める。
|
|
29
|
+
*
|
|
30
|
+
* 縮める倍率はすべて Math.min(1, 箱に入る量 / 自分の寸法) の形にしてある。
|
|
31
|
+
* 入る間は倍率が 1 なので、 大きい箱の見え方は変わらない。
|
|
32
|
+
*
|
|
33
|
+
* 上へのはみ出しは、 何ともぶつからず図の外にも出ないことを呼出側が実測済みなので残す。
|
|
34
|
+
* layout/footer-shape.ts の SHAPE_DRAW_UP_EXTENT は、 shape-gear の上への伸びを 96 として持つ。
|
|
35
|
+
* 歯車は名前だけを動かして絵に触れないため、 その表の値は変わらない。
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
type ShapeProps = {
|
|
39
|
+
node: LaidNode;
|
|
40
|
+
active: boolean;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const commonFrame = (active: boolean) => ({
|
|
44
|
+
fill: "var(--cdl-node-fill, #ffffff)",
|
|
45
|
+
stroke: active ? "var(--cdl-tone-accent, #2d6a8f)" : "var(--cdl-divider, #d4d4d8)",
|
|
46
|
+
strokeWidth: active ? 3 : 1.5,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// 字が baseline から上へ張り出す量を見積もるため、 shape-region.tsx と同じ比を使う。
|
|
50
|
+
const ASCENT = 0.72;
|
|
51
|
+
// 字が baseline から下へ張り出す量を見積もるため、 shape-region.tsx と同じ比を使う。
|
|
52
|
+
const DESCENT = 0.25;
|
|
53
|
+
// SVG の字幅は描く前に測れず、 getBBox も描いた後にしか使えないため送り幅で見積もる。
|
|
54
|
+
// 手元の等幅書体は 0.55-0.61 なので、 0.62 なら実寸を下回らず収めた行が実際にはみ出さない。
|
|
55
|
+
const MONO_ADVANCE = 0.62;
|
|
56
|
+
// 説明は名前が 16 の時に従来の 11 となり、 名前を縮めた時も同じ比で追随する。
|
|
57
|
+
const LABEL_SUB_RATIO = 11 / 16;
|
|
58
|
+
// 肩書は名前が 16 の時に従来の 12 となり、 名前を縮めた時も同じ比で追随する。
|
|
59
|
+
const LABEL_EYE_RATIO = 12 / 16;
|
|
60
|
+
// 肩書と名前の間は commonLabel の従来の 8 を保つ。
|
|
61
|
+
const LABEL_EYE_GAP = 8;
|
|
62
|
+
// 名前と説明の間は commonLabel の従来の 4 を保つ。
|
|
63
|
+
const LABEL_SUB_GAP = 4;
|
|
64
|
+
|
|
65
|
+
/** 塊が baseline から上下へ張り出す量を、 描く側の定数から渡すための型。 */
|
|
66
|
+
type LabelSpan = { 上: number; 下: number };
|
|
67
|
+
|
|
68
|
+
/** 一行だけが場所を取るので、 別の寸法表を持たず実際の字の比から張り出しを出す。 */
|
|
69
|
+
const oneLineSpan = (size: number): LabelSpan => ({
|
|
70
|
+
上: size * ASCENT,
|
|
71
|
+
下: size * DESCENT,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* commonLabel が実際に書く行だけを、 その行の比と間隔から塊の張り出しに含める。
|
|
76
|
+
* 描く側と別に寸法表を持つと、 絵を変えた時に片方だけ古くなるためここから算出する。
|
|
77
|
+
*/
|
|
78
|
+
const commonLabelSpan =
|
|
79
|
+
(node: LaidNode) =>
|
|
80
|
+
(size: number): LabelSpan => ({
|
|
81
|
+
上: node.eyebrow ? size + LABEL_EYE_GAP + size * LABEL_EYE_RATIO * ASCENT : size * ASCENT,
|
|
82
|
+
下: node.subtitle ? size + LABEL_SUB_GAP + size * LABEL_SUB_RATIO * DESCENT : size * DESCENT,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 入る間は位置と大きさを変えず、 入らない時だけ字を 1 ずつ縮めて下端に収まるまで上げる。
|
|
87
|
+
* 塊が箱より高い時は、 下の隣接領域を汚さないよう下端に合わせ、 上へ出ることを許す。
|
|
88
|
+
* 張り出しは描く側の定数から渡し、 絵を変えた時に別の寸法表だけが古くならないようにする。
|
|
89
|
+
*/
|
|
90
|
+
function fitLabel(
|
|
91
|
+
範囲: { 天: number; 底: number },
|
|
92
|
+
基準y: number,
|
|
93
|
+
基準大: number,
|
|
94
|
+
張り出し: (size: number) => LabelSpan,
|
|
95
|
+
使える高さ = 範囲.底 - 範囲.天,
|
|
96
|
+
): { y: number; size: number } {
|
|
97
|
+
let size = 基準大;
|
|
98
|
+
for (; size > 1; size--) {
|
|
99
|
+
const s = 張り出し(size);
|
|
100
|
+
if (s.上 + s.下 <= 使える高さ) break;
|
|
101
|
+
}
|
|
102
|
+
const s = 張り出し(size);
|
|
103
|
+
const 下限 = 範囲.底 - s.下;
|
|
104
|
+
const 上限 = 範囲.天 + s.上;
|
|
105
|
+
return {
|
|
106
|
+
y: 上限 > 下限 ? 下限 : Math.min(Math.max(基準y, 上限), 下限),
|
|
107
|
+
size,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const commonLabel = (node: LaidNode, x: number, y: number, size: number) => (
|
|
112
|
+
<>
|
|
113
|
+
{node.eyebrow && (
|
|
114
|
+
<text
|
|
115
|
+
x={x}
|
|
116
|
+
y={y - size - LABEL_EYE_GAP}
|
|
117
|
+
fontSize={size * LABEL_EYE_RATIO}
|
|
118
|
+
fontWeight={700}
|
|
119
|
+
letterSpacing={2}
|
|
120
|
+
textAnchor="middle"
|
|
121
|
+
fill="var(--cdl-tone-accent, #2d6a8f)"
|
|
122
|
+
>
|
|
123
|
+
{node.eyebrow}
|
|
124
|
+
</text>
|
|
125
|
+
)}
|
|
126
|
+
<text
|
|
127
|
+
data-cdl-role="node-label"
|
|
128
|
+
x={x}
|
|
129
|
+
y={y}
|
|
130
|
+
fontSize={size}
|
|
131
|
+
fontWeight={700}
|
|
132
|
+
textAnchor="middle"
|
|
133
|
+
fill="var(--cdl-text, #1a1f2a)"
|
|
134
|
+
>
|
|
135
|
+
{node.title}
|
|
136
|
+
</text>
|
|
137
|
+
{node.subtitle && (
|
|
138
|
+
<text
|
|
139
|
+
x={x}
|
|
140
|
+
y={y + size + LABEL_SUB_GAP}
|
|
141
|
+
fontSize={size * LABEL_SUB_RATIO}
|
|
142
|
+
textAnchor="middle"
|
|
143
|
+
fill="var(--cdl-text-dim, #5a6270)"
|
|
144
|
+
>
|
|
145
|
+
{node.subtitle}
|
|
146
|
+
</text>
|
|
147
|
+
)}
|
|
148
|
+
</>
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
/** shape-window = OS ウィンドウ (title bar + traffic lights + body) */
|
|
152
|
+
export function ShapeWindowNode({ node, active }: ShapeProps): JSX.Element {
|
|
153
|
+
const frame = commonFrame(active);
|
|
154
|
+
// 枠線は箱の外へ strokeWidth / 2 出るため、 描ける範囲から先に引く。
|
|
155
|
+
const 線 = frame.strokeWidth / 2;
|
|
156
|
+
const x = node.cx - node.w / 2 + 線;
|
|
157
|
+
const y = node.cy - node.h / 2 + 線;
|
|
158
|
+
const w = Math.max(0, node.w - 線 * 2);
|
|
159
|
+
const h = Math.max(0, node.h - 線 * 2);
|
|
160
|
+
// 低い箱でも名前を置く場所を残し、 十分な高さでは従来の 26 を保つ。
|
|
161
|
+
const barH = Math.min(26, h * 0.4);
|
|
162
|
+
// 信号は帯から出ない大きさにし、 帯が 26 なら従来の半径 5 を保つ。
|
|
163
|
+
const 灯 = Math.min(5, barH * 0.2);
|
|
164
|
+
// 信号の並びも灯と同じ割合で詰め、 従来の帯では等倍の位置を保つ。
|
|
165
|
+
const 灯間 = 灯 / 5;
|
|
166
|
+
// 名前が信号と重ならないよう、 信号帯の下端を使える範囲の天にする。
|
|
167
|
+
const 名前 = fitLabel(
|
|
168
|
+
{ 天: y + barH, 底: y + h },
|
|
169
|
+
node.cy + barH / 2 + 6,
|
|
170
|
+
16,
|
|
171
|
+
commonLabelSpan(node),
|
|
172
|
+
);
|
|
173
|
+
return (
|
|
174
|
+
<g>
|
|
175
|
+
<rect data-cdl-role="node-body" x={x} y={y} width={w} height={h} rx={8} {...frame} />
|
|
176
|
+
<path
|
|
177
|
+
d={`M ${x} ${y + barH} L ${x + w} ${y + barH}`}
|
|
178
|
+
stroke={frame.stroke}
|
|
179
|
+
strokeWidth={frame.strokeWidth}
|
|
180
|
+
/>
|
|
181
|
+
<circle cx={x + 14 * 灯間} cy={y + barH / 2} r={灯} fill="#ff5f56" stroke="none" />
|
|
182
|
+
<circle cx={x + 30 * 灯間} cy={y + barH / 2} r={灯} fill="#ffbd2e" stroke="none" />
|
|
183
|
+
<circle cx={x + 46 * 灯間} cy={y + barH / 2} r={灯} fill="#27c93f" stroke="none" />
|
|
184
|
+
{commonLabel(node, node.cx, 名前.y, 名前.size)}
|
|
185
|
+
</g>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** shape-terminal = 端末 (黒地 + macOS 風 top bar + prompt 3 行 + typing cursor) */
|
|
190
|
+
export function ShapeTerminalNode({ node, active }: ShapeProps): JSX.Element {
|
|
191
|
+
const frame = commonFrame(active);
|
|
192
|
+
const 線 = frame.strokeWidth / 2;
|
|
193
|
+
const x = node.cx - node.w / 2 + 線;
|
|
194
|
+
const y = node.cy - node.h / 2 + 線;
|
|
195
|
+
const w = Math.max(0, node.w - 線 * 2);
|
|
196
|
+
// 42 は従来どおり名前の行に空け、 低い箱では負の本体を作らないための取り分。
|
|
197
|
+
const bodyH = Math.max(0, node.h - 42 - 線 * 2);
|
|
198
|
+
const bodyFill = "#0f1419";
|
|
199
|
+
const barFill = "#1a1f2a";
|
|
200
|
+
const lineFont = "ui-monospace, SFMono-Regular, Menlo, monospace";
|
|
201
|
+
const 余白 = 16;
|
|
202
|
+
const 帯 = 28;
|
|
203
|
+
const 角 = 10;
|
|
204
|
+
const 先頭 = 26;
|
|
205
|
+
const 行間 = 22;
|
|
206
|
+
const カーソル = { 左: 30, 幅: 9, 高: 14, 上: 11, 下: 3 };
|
|
207
|
+
const 行 = [
|
|
208
|
+
[
|
|
209
|
+
{ 左: 16, 字: 12.5, 色: "#7cd88f", 文: "$" },
|
|
210
|
+
{ 左: 30, 字: 12.5, 色: "#e6edf3", 文: "pnpm build" },
|
|
211
|
+
],
|
|
212
|
+
[{ 左: 16, 字: 12, 色: "#8b95a5", 文: "→ 12 chunks in 1.8s" }],
|
|
213
|
+
[{ 左: 16, 字: 12.5, 色: "#7cd88f", 文: "$" }],
|
|
214
|
+
];
|
|
215
|
+
// 中身は自分の座標で描くため、 本体に入る倍率を出せるよう最長行から自然幅を求める。
|
|
216
|
+
const 自然幅 = Math.max(...行.flat().map((t) => t.左 + t.文.length * MONO_ADVANCE * t.字)) + 余白;
|
|
217
|
+
// 最終行とカーソルまで入る自然高を使い、 縦にも同じ倍率を適用できるようにする。
|
|
218
|
+
const 自然高 = 帯 + 先頭 + 行間 * (行.length - 1) + Math.max(カーソル.下, 12.5 * DESCENT);
|
|
219
|
+
// 縦横を同じ倍率で縮めて比を保ち、 幅 300・高さ 200 では 1 のまま従来値を保つ。
|
|
220
|
+
const 倍 = Math.min(1, w / 自然幅, bodyH / 自然高);
|
|
221
|
+
const barH = 帯 * 倍;
|
|
222
|
+
const 丸 = 角 * 倍;
|
|
223
|
+
const 名前 = fitLabel({ 天: y, 底: y + node.h - 線 * 2 }, y + bodyH + 22, 15, oneLineSpan);
|
|
224
|
+
return (
|
|
225
|
+
<g>
|
|
226
|
+
<rect
|
|
227
|
+
data-cdl-role="node-body"
|
|
228
|
+
x={x}
|
|
229
|
+
y={y}
|
|
230
|
+
width={w}
|
|
231
|
+
height={bodyH}
|
|
232
|
+
rx={角}
|
|
233
|
+
fill={bodyFill}
|
|
234
|
+
stroke={frame.stroke}
|
|
235
|
+
strokeWidth={frame.strokeWidth}
|
|
236
|
+
/>
|
|
237
|
+
{/* 本体が残らない箱では、 幅 0 や高さ 0 の中身を並べない。 */}
|
|
238
|
+
{倍 > 0 && (
|
|
239
|
+
<>
|
|
240
|
+
<path
|
|
241
|
+
d={`M ${x} ${y + barH + 丸} L ${x} ${y + 丸} Q ${x} ${y}, ${x + 丸} ${y} L ${x + w - 丸} ${y} Q ${x + w} ${y}, ${x + w} ${y + 丸} L ${x + w} ${y + barH + 丸} Z`}
|
|
242
|
+
fill={barFill}
|
|
243
|
+
stroke="none"
|
|
244
|
+
/>
|
|
245
|
+
<path d={`M ${x} ${y + barH} L ${x + w} ${y + barH}`} stroke="#2a3140" strokeWidth={1} />
|
|
246
|
+
<circle cx={x + 16 * 倍} cy={y + barH / 2} r={5.5 * 倍} fill="#ff5f56" stroke="none" />
|
|
247
|
+
<circle cx={x + 34 * 倍} cy={y + barH / 2} r={5.5 * 倍} fill="#ffbd2e" stroke="none" />
|
|
248
|
+
<circle cx={x + 52 * 倍} cy={y + barH / 2} r={5.5 * 倍} fill="#27c93f" stroke="none" />
|
|
249
|
+
<text
|
|
250
|
+
x={x + w / 2}
|
|
251
|
+
y={y + barH / 2 + 4 * 倍}
|
|
252
|
+
fontSize={11 * 倍}
|
|
253
|
+
fontFamily={lineFont}
|
|
254
|
+
textAnchor="middle"
|
|
255
|
+
fill="#7a8290"
|
|
256
|
+
>
|
|
257
|
+
{"— zsh —"}
|
|
258
|
+
</text>
|
|
259
|
+
{行.flatMap((texts, i) => {
|
|
260
|
+
const baseline = y + barH + (先頭 + 行間 * i) * 倍;
|
|
261
|
+
return texts.map((t) => (
|
|
262
|
+
<text
|
|
263
|
+
key={`${i}-${t.左}`}
|
|
264
|
+
x={x + t.左 * 倍}
|
|
265
|
+
y={baseline}
|
|
266
|
+
fontSize={t.字 * 倍}
|
|
267
|
+
fontFamily={lineFont}
|
|
268
|
+
fill={t.色}
|
|
269
|
+
>
|
|
270
|
+
{t.文}
|
|
271
|
+
</text>
|
|
272
|
+
));
|
|
273
|
+
})}
|
|
274
|
+
<rect
|
|
275
|
+
x={x + カーソル.左 * 倍}
|
|
276
|
+
y={y + barH + (先頭 + 行間 * (行.length - 1) - カーソル.上) * 倍}
|
|
277
|
+
width={カーソル.幅 * 倍}
|
|
278
|
+
height={カーソル.高 * 倍}
|
|
279
|
+
fill="#e6edf3"
|
|
280
|
+
opacity={0.85}
|
|
281
|
+
>
|
|
282
|
+
<animate
|
|
283
|
+
attributeName="opacity"
|
|
284
|
+
values="0.85;0;0.85"
|
|
285
|
+
dur="1s"
|
|
286
|
+
repeatCount="indefinite"
|
|
287
|
+
/>
|
|
288
|
+
</rect>
|
|
289
|
+
</>
|
|
290
|
+
)}
|
|
291
|
+
<text
|
|
292
|
+
data-cdl-role="node-label"
|
|
293
|
+
x={node.cx}
|
|
294
|
+
y={名前.y}
|
|
295
|
+
fontSize={名前.size}
|
|
296
|
+
fontWeight={700}
|
|
297
|
+
textAnchor="middle"
|
|
298
|
+
fill="var(--cdl-text, #1a1f2a)"
|
|
299
|
+
>
|
|
300
|
+
{node.title}
|
|
301
|
+
</text>
|
|
302
|
+
{node.eyebrow && (
|
|
303
|
+
<text
|
|
304
|
+
x={node.cx}
|
|
305
|
+
y={y - 8}
|
|
306
|
+
fontSize={12}
|
|
307
|
+
fontWeight={700}
|
|
308
|
+
letterSpacing={2}
|
|
309
|
+
textAnchor="middle"
|
|
310
|
+
fill="var(--cdl-tone-accent, #2d6a8f)"
|
|
311
|
+
>
|
|
312
|
+
{node.eyebrow}
|
|
313
|
+
</text>
|
|
314
|
+
)}
|
|
315
|
+
</g>
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** shape-code-block = code editor (tab + gutter + 4 syntax-colored stub lines) */
|
|
320
|
+
export function ShapeCodeBlockNode({ node, active }: ShapeProps): JSX.Element {
|
|
321
|
+
const frame = commonFrame(active);
|
|
322
|
+
const 線 = frame.strokeWidth / 2;
|
|
323
|
+
const x = node.cx - node.w / 2 + 線;
|
|
324
|
+
const y = node.cy - node.h / 2 + 線;
|
|
325
|
+
const w = Math.max(0, node.w - 線 * 2);
|
|
326
|
+
// 42 は従来どおり名前の行に空け、 低い箱では負の本体を作らないための取り分。
|
|
327
|
+
const bodyH = Math.max(0, node.h - 42 - 線 * 2);
|
|
328
|
+
const lineFont = "ui-monospace, SFMono-Regular, Menlo, monospace";
|
|
329
|
+
const タブ幅 = 100;
|
|
330
|
+
const タブ高 = 24;
|
|
331
|
+
const 角 = 10;
|
|
332
|
+
const 溝 = 32;
|
|
333
|
+
const 字下げ = 12;
|
|
334
|
+
const 余白 = 12;
|
|
335
|
+
const 行頭 = 22;
|
|
336
|
+
const 行間 = 20;
|
|
337
|
+
const 太さ = 9;
|
|
338
|
+
const 隙間 = 6;
|
|
339
|
+
const 持ち上げ = 8;
|
|
340
|
+
const lines = [
|
|
341
|
+
[
|
|
342
|
+
{ c: "#c678dd", w: 40 },
|
|
343
|
+
{ c: "#61afef", w: 60 },
|
|
344
|
+
{ c: "#e6edf3", w: 20 },
|
|
345
|
+
{ c: "#98c379", w: 90 },
|
|
346
|
+
],
|
|
347
|
+
[
|
|
348
|
+
{ c: "#e6edf3", w: 24 },
|
|
349
|
+
{ c: "#61afef", w: 55 },
|
|
350
|
+
{ c: "#d19a66", w: 30 },
|
|
351
|
+
],
|
|
352
|
+
[
|
|
353
|
+
{ c: "#c678dd", w: 40 },
|
|
354
|
+
{ c: "#e6edf3", w: 20 },
|
|
355
|
+
{ c: "#98c379", w: 120 },
|
|
356
|
+
],
|
|
357
|
+
[
|
|
358
|
+
{ c: "#e5c07b", w: 70 },
|
|
359
|
+
{ c: "#e6edf3", w: 40 },
|
|
360
|
+
],
|
|
361
|
+
];
|
|
362
|
+
const 最長 = Math.max(
|
|
363
|
+
...lines.map(
|
|
364
|
+
(tokens) => tokens.reduce((sum, token) => sum + token.w, 0) + 隙間 * (tokens.length - 1),
|
|
365
|
+
),
|
|
366
|
+
);
|
|
367
|
+
// 中身は自分の座標で描くため、 本体に入る倍率を出せるよう最長行から自然幅を求める。
|
|
368
|
+
const 自然幅 = 溝 + 字下げ + 最長 + 余白;
|
|
369
|
+
// 最終行の token と行番号まで入る自然高を使い、 縦にも同じ倍率を適用できるようにする。
|
|
370
|
+
const 自然高 =
|
|
371
|
+
タブ高 + 行頭 + 行間 * (lines.length - 1) + Math.max(太さ - 持ち上げ, 10.5 * DESCENT);
|
|
372
|
+
// 右 132 のはみ出しは、 中の行を本体へ縦横同率で収めて直す。
|
|
373
|
+
// fitArt で絵ごと縮めると tab の字と行番号が読めなくなるため使わない。
|
|
374
|
+
// 幅 300・高さ 200 では倍率が 1 のままなので、 従来値を保つ。
|
|
375
|
+
const 倍 = Math.min(1, w / 自然幅, bodyH / 自然高);
|
|
376
|
+
const tabW = タブ幅 * 倍;
|
|
377
|
+
const tabH = タブ高 * 倍;
|
|
378
|
+
const 丸 = 角 * 倍;
|
|
379
|
+
const gutterW = 溝 * 倍;
|
|
380
|
+
const codeY0 = y + tabH + 行頭 * 倍;
|
|
381
|
+
const lineH = 行間 * 倍;
|
|
382
|
+
const 名前 = fitLabel({ 天: y, 底: y + node.h - 線 * 2 }, y + bodyH + 22, 15, oneLineSpan);
|
|
383
|
+
return (
|
|
384
|
+
<g>
|
|
385
|
+
<rect
|
|
386
|
+
data-cdl-role="node-body"
|
|
387
|
+
x={x}
|
|
388
|
+
y={y}
|
|
389
|
+
width={w}
|
|
390
|
+
height={bodyH}
|
|
391
|
+
rx={角}
|
|
392
|
+
fill="#0f1419"
|
|
393
|
+
stroke={frame.stroke}
|
|
394
|
+
strokeWidth={frame.strokeWidth}
|
|
395
|
+
/>
|
|
396
|
+
{/* 本体が残らない箱では、 幅 0 や高さ 0 の中身を並べない。 */}
|
|
397
|
+
{倍 > 0 && (
|
|
398
|
+
<>
|
|
399
|
+
<path
|
|
400
|
+
d={`M ${x} ${y + tabH} L ${x} ${y + 丸} Q ${x} ${y}, ${x + 丸} ${y} L ${x + tabW - 丸} ${y} Q ${x + tabW} ${y}, ${x + tabW} ${y + 丸} L ${x + tabW} ${y + tabH} Z`}
|
|
401
|
+
fill="#1a1f2a"
|
|
402
|
+
stroke="none"
|
|
403
|
+
/>
|
|
404
|
+
<path d={`M ${x} ${y + tabH} L ${x + w} ${y + tabH}`} stroke="#2a3140" strokeWidth={1} />
|
|
405
|
+
<text
|
|
406
|
+
x={x + tabW / 2}
|
|
407
|
+
y={y + tabH / 2 + 4 * 倍}
|
|
408
|
+
fontSize={11 * 倍}
|
|
409
|
+
fontFamily={lineFont}
|
|
410
|
+
textAnchor="middle"
|
|
411
|
+
fill="#e6edf3"
|
|
412
|
+
>
|
|
413
|
+
utils.ts
|
|
414
|
+
</text>
|
|
415
|
+
<circle cx={x + tabW - 丸} cy={y + tabH / 2} r={3 * 倍} fill="#7cd88f" stroke="none" />
|
|
416
|
+
<path
|
|
417
|
+
d={`M ${x + gutterW} ${y + tabH} L ${x + gutterW} ${y + bodyH}`}
|
|
418
|
+
stroke="#2a3140"
|
|
419
|
+
strokeWidth={1}
|
|
420
|
+
/>
|
|
421
|
+
{lines.map((toks, i) => {
|
|
422
|
+
let cursor = x + gutterW + 字下げ * 倍;
|
|
423
|
+
return (
|
|
424
|
+
<g key={i}>
|
|
425
|
+
<text
|
|
426
|
+
x={x + gutterW - 持ち上げ * 倍}
|
|
427
|
+
y={codeY0 + i * lineH}
|
|
428
|
+
fontSize={10.5 * 倍}
|
|
429
|
+
fontFamily={lineFont}
|
|
430
|
+
textAnchor="end"
|
|
431
|
+
fill="#4a5266"
|
|
432
|
+
>
|
|
433
|
+
{i + 1}
|
|
434
|
+
</text>
|
|
435
|
+
{toks.map((t, j) => {
|
|
436
|
+
const rect = (
|
|
437
|
+
<rect
|
|
438
|
+
key={j}
|
|
439
|
+
x={cursor}
|
|
440
|
+
y={codeY0 + i * lineH - 持ち上げ * 倍}
|
|
441
|
+
width={t.w * 倍}
|
|
442
|
+
height={太さ * 倍}
|
|
443
|
+
rx={2 * 倍}
|
|
444
|
+
fill={t.c}
|
|
445
|
+
fillOpacity={0.85}
|
|
446
|
+
stroke="none"
|
|
447
|
+
/>
|
|
448
|
+
);
|
|
449
|
+
cursor += (t.w + 隙間) * 倍;
|
|
450
|
+
return rect;
|
|
451
|
+
})}
|
|
452
|
+
</g>
|
|
453
|
+
);
|
|
454
|
+
})}
|
|
455
|
+
</>
|
|
456
|
+
)}
|
|
457
|
+
<text
|
|
458
|
+
data-cdl-role="node-label"
|
|
459
|
+
x={node.cx}
|
|
460
|
+
y={名前.y}
|
|
461
|
+
fontSize={名前.size}
|
|
462
|
+
fontWeight={700}
|
|
463
|
+
textAnchor="middle"
|
|
464
|
+
fill="var(--cdl-text, #1a1f2a)"
|
|
465
|
+
>
|
|
466
|
+
{node.title}
|
|
467
|
+
</text>
|
|
468
|
+
{node.eyebrow && (
|
|
469
|
+
<text
|
|
470
|
+
x={node.cx}
|
|
471
|
+
y={y - 8}
|
|
472
|
+
fontSize={12}
|
|
473
|
+
fontWeight={700}
|
|
474
|
+
letterSpacing={2}
|
|
475
|
+
textAnchor="middle"
|
|
476
|
+
fill="var(--cdl-tone-accent, #2d6a8f)"
|
|
477
|
+
>
|
|
478
|
+
{node.eyebrow}
|
|
479
|
+
</text>
|
|
480
|
+
)}
|
|
481
|
+
</g>
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** shape-kanban-card = kanban ticket (top color bar + 3 stub line) */
|
|
486
|
+
export function ShapeKanbanCardNode({ node, active }: ShapeProps): JSX.Element {
|
|
487
|
+
const frame = commonFrame(active);
|
|
488
|
+
const 線 = frame.strokeWidth / 2;
|
|
489
|
+
// 影は札を右下へずらして重ねるため、 ずらす分と枠線を札の寸法から先に引く。
|
|
490
|
+
const 影 = 3;
|
|
491
|
+
const x = node.cx - node.w / 2 + 線;
|
|
492
|
+
const y = node.cy - node.h / 2 + 線;
|
|
493
|
+
const w = Math.max(0, node.w - 線 * 2 - 影);
|
|
494
|
+
const h = Math.max(0, node.h - 線 * 2 - 影);
|
|
495
|
+
const 帯間余白 = 4;
|
|
496
|
+
const 左右余白 = 14;
|
|
497
|
+
const 上段隙間 = 8;
|
|
498
|
+
const 優先帯高 = 10;
|
|
499
|
+
const ID文 = "CAR-1111";
|
|
500
|
+
const ID基準 = 30;
|
|
501
|
+
const ID字 = 11;
|
|
502
|
+
const ID字間 = 1;
|
|
503
|
+
const PILL上 = 20;
|
|
504
|
+
const PILL幅 = 54;
|
|
505
|
+
const PILL高 = 16;
|
|
506
|
+
const PILL字 = 9;
|
|
507
|
+
const FOOTER下 = 22;
|
|
508
|
+
const AVATAR中心Y = 4;
|
|
509
|
+
const AVATAR半径 = 9;
|
|
510
|
+
const AVATAR枠 = 1.5;
|
|
511
|
+
const 名前字 = 17;
|
|
512
|
+
const 説明基準比 = 20 / 17;
|
|
513
|
+
const 説明字比 = 12 / 17;
|
|
514
|
+
// 各帯は実際に描く定数から出し、 配置と収容寸法が別々に古くならないようにする。
|
|
515
|
+
const 上帯 = PILL上 + PILL高 + 帯間余白;
|
|
516
|
+
const 下帯 = FOOTER下 - AVATAR中心Y + AVATAR半径 + AVATAR枠 / 2 + 帯間余白;
|
|
517
|
+
const 中帯 = 名前字 * ASCENT + 名前字 * 説明基準比 + 名前字 * 説明字比 * DESCENT;
|
|
518
|
+
const 自然高 = 上帯 + 中帯 + 下帯;
|
|
519
|
+
const ID幅 = ID文.length * MONO_ADVANCE * ID字 + ID字間 * ID文.length;
|
|
520
|
+
const 自然幅 = 左右余白 + ID幅 + 上段隙間 + PILL幅 + 左右余白;
|
|
521
|
+
// 札の中身だけを縦横同率で縮め、 入る札では 1 のまま従来の大きさを保つ。
|
|
522
|
+
const 倍 = Math.min(1, w / 自然幅, h / 自然高);
|
|
523
|
+
const priorityBarH = 優先帯高 * 倍;
|
|
524
|
+
const priorityBarR = Math.min(10, priorityBarH);
|
|
525
|
+
const idY = y + ID基準 * 倍;
|
|
526
|
+
const pillY = y + PILL上 * 倍;
|
|
527
|
+
const footY = y + h - FOOTER下 * 倍;
|
|
528
|
+
// 名前 17 と説明 12 の baseline 間隔 20 を比で持ち、 名前を縮めた時に説明も追随させる。
|
|
529
|
+
// 中帯を渡すのは、 名前を上下の帯と分ける意図を残すため。
|
|
530
|
+
// ただし今の倍率では札の中心も名前の大きさも中帯に収まり、 札全体へ広げても描く値が変わらないため変異試験では落ちない。
|
|
531
|
+
// 倍率の作りを変えた時に効き始める防御として残す。
|
|
532
|
+
const 名前 = fitLabel(
|
|
533
|
+
{ 天: y + 上帯 * 倍, 底: y + h - 下帯 * 倍 },
|
|
534
|
+
y + h / 2 - 2 * 倍,
|
|
535
|
+
名前字 * 倍,
|
|
536
|
+
(size) => ({
|
|
537
|
+
上: size * ASCENT,
|
|
538
|
+
下: size * 説明基準比 + size * 説明字比 * DESCENT,
|
|
539
|
+
}),
|
|
540
|
+
);
|
|
541
|
+
return (
|
|
542
|
+
<g>
|
|
543
|
+
{/* shadow */}
|
|
544
|
+
<rect
|
|
545
|
+
x={x + 影}
|
|
546
|
+
y={y + 影}
|
|
547
|
+
width={w}
|
|
548
|
+
height={h}
|
|
549
|
+
rx={10}
|
|
550
|
+
fill={frame.stroke}
|
|
551
|
+
opacity={0.18}
|
|
552
|
+
stroke="none"
|
|
553
|
+
/>
|
|
554
|
+
<rect data-cdl-role="node-body" x={x} y={y} width={w} height={h} rx={10} {...frame} />
|
|
555
|
+
{/* 本体が残らない札では、 中身の各要素を 0 の大きさで重ねない。 */}
|
|
556
|
+
{倍 > 0 && (
|
|
557
|
+
<>
|
|
558
|
+
{/* priority color bar top */}
|
|
559
|
+
<path
|
|
560
|
+
d={`M ${x + priorityBarR} ${y} L ${x + w - priorityBarR} ${y} A ${priorityBarR} ${priorityBarR} 0 0 1 ${x + w} ${y + priorityBarR} L ${x + w} ${y + priorityBarH} L ${x} ${y + priorityBarH} L ${x} ${y + priorityBarR} A ${priorityBarR} ${priorityBarR} 0 0 1 ${x + priorityBarR} ${y} Z`}
|
|
561
|
+
fill="var(--cdl-tone-accent, #2d6a8f)"
|
|
562
|
+
stroke="none"
|
|
563
|
+
/>
|
|
564
|
+
{/* ID + status pill 上段 */}
|
|
565
|
+
<text
|
|
566
|
+
x={x + 左右余白 * 倍}
|
|
567
|
+
y={idY}
|
|
568
|
+
fontSize={ID字 * 倍}
|
|
569
|
+
fontFamily="ui-monospace, SFMono-Regular, Menlo, monospace"
|
|
570
|
+
fontWeight={700}
|
|
571
|
+
letterSpacing={ID字間 * 倍}
|
|
572
|
+
fill="var(--cdl-text-dim, #5a6270)"
|
|
573
|
+
>
|
|
574
|
+
{ID文}
|
|
575
|
+
</text>
|
|
576
|
+
<rect
|
|
577
|
+
x={x + w - (左右余白 + PILL幅) * 倍}
|
|
578
|
+
y={pillY}
|
|
579
|
+
width={PILL幅 * 倍}
|
|
580
|
+
height={PILL高 * 倍}
|
|
581
|
+
rx={(PILL高 / 2) * 倍}
|
|
582
|
+
fill="#e5c07b"
|
|
583
|
+
opacity={0.9}
|
|
584
|
+
stroke="var(--cdl-tone-accent, #2d6a8f)"
|
|
585
|
+
strokeWidth={1 * 倍}
|
|
586
|
+
/>
|
|
587
|
+
<text
|
|
588
|
+
x={x + w - (左右余白 + PILL幅 / 2) * 倍}
|
|
589
|
+
y={pillY + 12 * 倍}
|
|
590
|
+
fontSize={PILL字 * 倍}
|
|
591
|
+
fontWeight={700}
|
|
592
|
+
letterSpacing={1 * 倍}
|
|
593
|
+
textAnchor="middle"
|
|
594
|
+
fill="#5a4a1a"
|
|
595
|
+
fontFamily="ui-monospace, SFMono-Regular, Menlo, monospace"
|
|
596
|
+
>
|
|
597
|
+
IN PROG
|
|
598
|
+
</text>
|
|
599
|
+
{/* タスク名 = title (中央上、 大きく太字) */}
|
|
600
|
+
<text
|
|
601
|
+
data-cdl-role="node-label"
|
|
602
|
+
x={x + w / 2}
|
|
603
|
+
y={名前.y}
|
|
604
|
+
fontSize={名前.size}
|
|
605
|
+
fontWeight={700}
|
|
606
|
+
textAnchor="middle"
|
|
607
|
+
fill="var(--cdl-text, #1a1f2a)"
|
|
608
|
+
>
|
|
609
|
+
{node.title}
|
|
610
|
+
</text>
|
|
611
|
+
{/* タスク説明 (中央下、 小さめ dim) */}
|
|
612
|
+
<text
|
|
613
|
+
x={x + w / 2}
|
|
614
|
+
y={名前.y + 名前.size * 説明基準比}
|
|
615
|
+
fontSize={名前.size * 説明字比}
|
|
616
|
+
textAnchor="middle"
|
|
617
|
+
fill="var(--cdl-text-dim, #5a6270)"
|
|
618
|
+
>
|
|
619
|
+
shape-driven kind 実装
|
|
620
|
+
</text>
|
|
621
|
+
{/* footer = 2 avatars 左 + due date 右 */}
|
|
622
|
+
<g transform={`translate(${x + 左右余白 * 倍}, ${footY})`}>
|
|
623
|
+
<circle
|
|
624
|
+
cx={9 * 倍}
|
|
625
|
+
cy={AVATAR中心Y * 倍}
|
|
626
|
+
r={AVATAR半径 * 倍}
|
|
627
|
+
fill="var(--cdl-tone-accent, #2d6a8f)"
|
|
628
|
+
stroke="var(--cdl-node-fill, #ffffff)"
|
|
629
|
+
strokeWidth={AVATAR枠 * 倍}
|
|
630
|
+
/>
|
|
631
|
+
<text
|
|
632
|
+
x={9 * 倍}
|
|
633
|
+
y={8 * 倍}
|
|
634
|
+
fontSize={10 * 倍}
|
|
635
|
+
fontWeight={700}
|
|
636
|
+
textAnchor="middle"
|
|
637
|
+
fill="#ffffff"
|
|
638
|
+
fontFamily="Georgia, serif"
|
|
639
|
+
>
|
|
640
|
+
A
|
|
641
|
+
</text>
|
|
642
|
+
<circle
|
|
643
|
+
cx={23 * 倍}
|
|
644
|
+
cy={AVATAR中心Y * 倍}
|
|
645
|
+
r={AVATAR半径 * 倍}
|
|
646
|
+
fill="#c678dd"
|
|
647
|
+
stroke="var(--cdl-node-fill, #ffffff)"
|
|
648
|
+
strokeWidth={AVATAR枠 * 倍}
|
|
649
|
+
/>
|
|
650
|
+
<text
|
|
651
|
+
x={23 * 倍}
|
|
652
|
+
y={8 * 倍}
|
|
653
|
+
fontSize={10 * 倍}
|
|
654
|
+
fontWeight={700}
|
|
655
|
+
textAnchor="middle"
|
|
656
|
+
fill="#ffffff"
|
|
657
|
+
fontFamily="Georgia, serif"
|
|
658
|
+
>
|
|
659
|
+
B
|
|
660
|
+
</text>
|
|
661
|
+
</g>
|
|
662
|
+
<text
|
|
663
|
+
x={x + w - 左右余白 * 倍}
|
|
664
|
+
y={footY + 8 * 倍}
|
|
665
|
+
fontSize={11 * 倍}
|
|
666
|
+
fontFamily="ui-monospace, SFMono-Regular, Menlo, monospace"
|
|
667
|
+
fontWeight={600}
|
|
668
|
+
textAnchor="end"
|
|
669
|
+
fill="var(--cdl-text-dim, #5a6270)"
|
|
670
|
+
>
|
|
671
|
+
Jul 10
|
|
672
|
+
</text>
|
|
673
|
+
</>
|
|
674
|
+
)}
|
|
675
|
+
</g>
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/** shape-message-bubble = チャット吹き出し (rounded rect + tail) */
|
|
680
|
+
export function ShapeMessageBubbleNode({ node, active }: ShapeProps): JSX.Element {
|
|
681
|
+
// **枠線の半分だけ内へ寄せる**。 線は中央揃えで描かれるので、 輪郭を箱の縁ちょうどに置くと
|
|
682
|
+
// 半分 (`active` で 1.5) が箱の外に残る。 幾何としては収まっていても、 見えている墨は出る
|
|
683
|
+
// (実測 = 下と左右へ 0.8)。 一番太くなる `active` の側で寄せて、 どちらの状態でも収める
|
|
684
|
+
const 枠線の半分 = 3 / 2;
|
|
685
|
+
const x = node.cx - node.w / 2 + 枠線の半分;
|
|
686
|
+
const y = node.cy - node.h / 2 + 枠線の半分;
|
|
687
|
+
const w = node.w - 枠線の半分 * 2;
|
|
688
|
+
const frame = commonFrame(active);
|
|
689
|
+
const tailW = 16;
|
|
690
|
+
const tailH = 14;
|
|
691
|
+
const bodyH = node.h - 枠線の半分 * 2 - tailH;
|
|
692
|
+
const path = [
|
|
693
|
+
`M ${x + 12} ${y}`,
|
|
694
|
+
`L ${x + w - 12} ${y}`,
|
|
695
|
+
`Q ${x + w} ${y}, ${x + w} ${y + 12}`,
|
|
696
|
+
`L ${x + w} ${y + bodyH - 12}`,
|
|
697
|
+
`Q ${x + w} ${y + bodyH}, ${x + w - 12} ${y + bodyH}`,
|
|
698
|
+
`L ${x + 24 + tailW} ${y + bodyH}`,
|
|
699
|
+
`L ${x + 24} ${y + bodyH + tailH}`,
|
|
700
|
+
`L ${x + 24} ${y + bodyH}`,
|
|
701
|
+
`L ${x + 12} ${y + bodyH}`,
|
|
702
|
+
`Q ${x} ${y + bodyH}, ${x} ${y + bodyH - 12}`,
|
|
703
|
+
`L ${x} ${y + 12}`,
|
|
704
|
+
`Q ${x} ${y}, ${x + 12} ${y}`,
|
|
705
|
+
"Z",
|
|
706
|
+
].join(" ");
|
|
707
|
+
return (
|
|
708
|
+
<g>
|
|
709
|
+
<path data-cdl-role="node-body" d={path} {...frame} strokeLinejoin="round" />
|
|
710
|
+
{commonLabel(node, node.cx, y + bodyH / 2 + 6, 16)}
|
|
711
|
+
</g>
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/** shape-gear = 単一大歯車 (12 台形 teeth をひとつの polygon で描画、 spoke + hub + bolt) */
|
|
716
|
+
export function ShapeGearNode({ node, active }: ShapeProps): JSX.Element {
|
|
717
|
+
const cx = node.cx;
|
|
718
|
+
const cy = node.cy - 30;
|
|
719
|
+
const frame = commonFrame(active);
|
|
720
|
+
const bodyR = Math.min(node.w, node.h) * 0.32;
|
|
721
|
+
const tipR = bodyR + bodyR * 0.24;
|
|
722
|
+
const teeth = 12;
|
|
723
|
+
// 各 tooth = 全角度 (2π / teeth) の内、
|
|
724
|
+
// 中央 40% を tip (先端外側)、 両端 30% を root (根元 body 円周)、
|
|
725
|
+
// その間の傾斜 15% ずつが台形の斜辺 (root → tip)
|
|
726
|
+
const anglePerTooth = (Math.PI * 2) / teeth;
|
|
727
|
+
const halfBaseWidth = anglePerTooth * 0.35; // root 幅の半角 = 全体の 70%
|
|
728
|
+
const halfTipWidth = anglePerTooth * 0.2; // tip 幅の半角 = 全体の 40%
|
|
729
|
+
// path 開始点 (0 度 = 右) から時計回りに 12 tooth の base_R → tip_R → tip_L → base_L → next base_R … を chain
|
|
730
|
+
const pts: string[] = [];
|
|
731
|
+
for (let i = 0; i < teeth; i++) {
|
|
732
|
+
const center = i * anglePerTooth;
|
|
733
|
+
const a1 = center - halfBaseWidth; // 前 tooth の base_L (= 現 tooth の base_R)
|
|
734
|
+
const a2 = center - halfTipWidth; // tip_R
|
|
735
|
+
const a3 = center + halfTipWidth; // tip_L
|
|
736
|
+
const a4 = center + halfBaseWidth; // base_L
|
|
737
|
+
pts.push(`${cx + Math.cos(a1) * bodyR},${cy + Math.sin(a1) * bodyR}`);
|
|
738
|
+
pts.push(`${cx + Math.cos(a2) * tipR},${cy + Math.sin(a2) * tipR}`);
|
|
739
|
+
pts.push(`${cx + Math.cos(a3) * tipR},${cy + Math.sin(a3) * tipR}`);
|
|
740
|
+
pts.push(`${cx + Math.cos(a4) * bodyR},${cy + Math.sin(a4) * bodyR}`);
|
|
741
|
+
}
|
|
742
|
+
// 名前は歯先から 28 下に付くだけで箱の下端を見ていなかったため、 名前だけを下端へ収める。
|
|
743
|
+
// SHAPE_DRAW_UP_EXTENT が絵の上への伸びを持つので、 歯車には触らない。
|
|
744
|
+
// 使える高さを node.h / 2 にして残り半分を歯車へ空け、 低い箱で名前が歯車を覆わないようにする。
|
|
745
|
+
const 名前 = fitLabel(
|
|
746
|
+
{ 天: node.cy - node.h / 2, 底: node.cy + node.h / 2 },
|
|
747
|
+
cy + tipR + 28,
|
|
748
|
+
18,
|
|
749
|
+
(size) => ({
|
|
750
|
+
上: size * ASCENT,
|
|
751
|
+
下: node.subtitle ? size + size * (13 / 18) * DESCENT : size * DESCENT,
|
|
752
|
+
}),
|
|
753
|
+
node.h / 2,
|
|
754
|
+
);
|
|
755
|
+
return (
|
|
756
|
+
<g>
|
|
757
|
+
{/* shadow (歯先より 3px 内側、 offset なし = 同心円) */}
|
|
758
|
+
{/* 小さい箱でも負の r は SVG の不正値になるため 0 で止め、 通常の大きさでは値を変えない。 */}
|
|
759
|
+
<circle
|
|
760
|
+
cx={cx}
|
|
761
|
+
cy={cy}
|
|
762
|
+
r={Math.max(0, tipR - 3)}
|
|
763
|
+
fill={frame.stroke}
|
|
764
|
+
opacity={0.25}
|
|
765
|
+
stroke="none"
|
|
766
|
+
/>
|
|
767
|
+
{/* rotating gear group */}
|
|
768
|
+
<g>
|
|
769
|
+
<animateTransform
|
|
770
|
+
attributeName="transform"
|
|
771
|
+
type="rotate"
|
|
772
|
+
from={`0 ${cx} ${cy}`}
|
|
773
|
+
to={`360 ${cx} ${cy}`}
|
|
774
|
+
dur="20s"
|
|
775
|
+
repeatCount="indefinite"
|
|
776
|
+
/>
|
|
777
|
+
{/* gear 外形 = 台形歯を含む一体 polygon */}
|
|
778
|
+
<polygon
|
|
779
|
+
data-cdl-role="node-body"
|
|
780
|
+
points={pts.join(" ")}
|
|
781
|
+
fill="var(--cdl-tone-accent, #2d6a8f)"
|
|
782
|
+
stroke={frame.stroke}
|
|
783
|
+
strokeWidth={frame.strokeWidth}
|
|
784
|
+
strokeLinejoin="round"
|
|
785
|
+
/>
|
|
786
|
+
{/* 4 spokes (十字) */}
|
|
787
|
+
{[0, 90, 180, 270].map((deg) => (
|
|
788
|
+
<rect
|
|
789
|
+
key={deg}
|
|
790
|
+
x={cx - 9}
|
|
791
|
+
y={cy - bodyR * 0.68}
|
|
792
|
+
width={18}
|
|
793
|
+
height={bodyR * 0.58}
|
|
794
|
+
rx={3}
|
|
795
|
+
fill="var(--cdl-node-fill, #ffffff)"
|
|
796
|
+
stroke="#1a4a68"
|
|
797
|
+
strokeWidth={1.5}
|
|
798
|
+
transform={`rotate(${deg} ${cx} ${cy})`}
|
|
799
|
+
/>
|
|
800
|
+
))}
|
|
801
|
+
{/* hub */}
|
|
802
|
+
<circle
|
|
803
|
+
cx={cx}
|
|
804
|
+
cy={cy}
|
|
805
|
+
r={bodyR * 0.32}
|
|
806
|
+
fill="var(--cdl-node-fill, #ffffff)"
|
|
807
|
+
stroke="#1a4a68"
|
|
808
|
+
strokeWidth={2}
|
|
809
|
+
/>
|
|
810
|
+
<circle
|
|
811
|
+
cx={cx}
|
|
812
|
+
cy={cy}
|
|
813
|
+
r={bodyR * 0.2}
|
|
814
|
+
fill="var(--cdl-tone-accent, #2d6a8f)"
|
|
815
|
+
stroke="#1a4a68"
|
|
816
|
+
strokeWidth={1.5}
|
|
817
|
+
/>
|
|
818
|
+
{/* center hole (bolt) */}
|
|
819
|
+
<circle cx={cx} cy={cy} r={5} fill="#1a1f2a" stroke="none" />
|
|
820
|
+
</g>
|
|
821
|
+
{/* label 下部 */}
|
|
822
|
+
{node.eyebrow && (
|
|
823
|
+
<text
|
|
824
|
+
x={cx}
|
|
825
|
+
y={cy - tipR - 20}
|
|
826
|
+
fontSize={13}
|
|
827
|
+
fontWeight={700}
|
|
828
|
+
letterSpacing={2}
|
|
829
|
+
textAnchor="middle"
|
|
830
|
+
fill="var(--cdl-tone-accent, #2d6a8f)"
|
|
831
|
+
>
|
|
832
|
+
{node.eyebrow}
|
|
833
|
+
</text>
|
|
834
|
+
)}
|
|
835
|
+
<text
|
|
836
|
+
data-cdl-role="node-label"
|
|
837
|
+
x={cx}
|
|
838
|
+
y={名前.y}
|
|
839
|
+
fontSize={名前.size}
|
|
840
|
+
fontWeight={700}
|
|
841
|
+
textAnchor="middle"
|
|
842
|
+
fill="var(--cdl-text, #1a1f2a)"
|
|
843
|
+
>
|
|
844
|
+
{node.title}
|
|
845
|
+
</text>
|
|
846
|
+
{node.subtitle && (
|
|
847
|
+
<text
|
|
848
|
+
x={cx}
|
|
849
|
+
y={名前.y + 名前.size}
|
|
850
|
+
fontSize={名前.size * (13 / 18)}
|
|
851
|
+
textAnchor="middle"
|
|
852
|
+
fill="var(--cdl-text-dim, #5a6270)"
|
|
853
|
+
>
|
|
854
|
+
{node.subtitle}
|
|
855
|
+
</text>
|
|
856
|
+
)}
|
|
857
|
+
</g>
|
|
858
|
+
);
|
|
859
|
+
}
|