@cardenelabs/cdl 0.124.0 → 0.125.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 +43 -1
- package/README.md +8 -0
- package/dist/index.cjs +873 -142
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +873 -143
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +858 -142
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +858 -142
- package/dist/react.js.map +1 -1
- package/dist/{render-U6omSNH2.d.cts → render-7yEHIUK1.d.cts} +57 -3
- package/dist/{render-U6omSNH2.d.ts → render-7yEHIUK1.d.ts} +57 -3
- package/package.json +12 -9
- package/src/builder.ts +6 -0
- package/src/index.ts +4 -0
- package/src/kinds/card.tsx +12 -4
- package/src/kinds/generic.tsx +19 -9
- package/src/kinds/station.tsx +105 -19
- package/src/kinds/terminal-mark.tsx +36 -9
- package/src/layout/edges.ts +77 -24
- package/src/layout/lanes.ts +69 -5
- package/src/layout/legend.ts +68 -0
- package/src/layout/metro-path.ts +123 -30
- package/src/layout/nodes.ts +13 -2
- package/src/layout/spec.ts +17 -1
- package/src/layout/tokens.ts +8 -1
- package/src/layout.ts +20 -2
- package/src/render/edge-head.ts +6 -0
- package/src/render/edges.tsx +27 -6
- package/src/render/legend.tsx +120 -0
- package/src/render/nodes.tsx +21 -5
- package/src/render/stage.tsx +255 -10
- package/src/render/template-fields.ts +2 -0
- package/src/types.ts +106 -5
- package/src/validate.ts +5 -0
- package/src/visual-validate.ts +7 -7
package/src/layout/nodes.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { requiredRowsWidth } from "../kinds/row-align";
|
|
2
2
|
import type { CdlDiagram, CdlTextMetrics, LaidLane, LaidNode } from "../types";
|
|
3
3
|
import { 端の字に要る線の長さ } from "./head-label";
|
|
4
|
+
import { METRO_DECISION_HEIGHT, METRO_DECISION_WIDTH } from "./metro-path";
|
|
4
5
|
import {
|
|
5
6
|
hasRenderedLabel,
|
|
6
7
|
labelPillH,
|
|
@@ -181,6 +182,16 @@ export function layoutNodes(
|
|
|
181
182
|
textMetrics: CdlTextMetrics,
|
|
182
183
|
): LaidNode[] {
|
|
183
184
|
const laneMap = new Map(lanes.map((l) => [l.id, l]));
|
|
185
|
+
const metroDecisionIds = new Set(
|
|
186
|
+
diag.edges
|
|
187
|
+
.filter((edge) => edge.routing === "metro")
|
|
188
|
+
.flatMap((edge) => [edge.from, edge.to])
|
|
189
|
+
.filter((id) => diag.nodes.some((node) => node.id === id && node.kind === "decision")),
|
|
190
|
+
);
|
|
191
|
+
const nodeSize = (node: CdlDiagram["nodes"][number]) =>
|
|
192
|
+
metroDecisionIds.has(node.id)
|
|
193
|
+
? { w: METRO_DECISION_WIDTH, h: METRO_DECISION_HEIGHT }
|
|
194
|
+
: NODE_SIZE[node.kind];
|
|
184
195
|
|
|
185
196
|
// 全 lane を跨いで stack 番号で row を共有する。
|
|
186
197
|
// 同 stack 番号の node は全 lane で同じ y 中心になり、 lane 間 edge が水平で揃う。
|
|
@@ -189,7 +200,7 @@ export function layoutNodes(
|
|
|
189
200
|
const rowH = new Map<number, number>();
|
|
190
201
|
for (const stack of allStacks) {
|
|
191
202
|
const nodesAtStack = diag.nodes.filter((n) => n.stack === stack);
|
|
192
|
-
const h = Math.max(...nodesAtStack.map((n) => 箱の高さ(n,
|
|
203
|
+
const h = Math.max(...nodesAtStack.map((n) => 箱の高さ(n, nodeSize(n).h)));
|
|
193
204
|
rowH.set(stack, h);
|
|
194
205
|
}
|
|
195
206
|
const rowCy = new Map<number, number>();
|
|
@@ -238,7 +249,7 @@ export function layoutNodes(
|
|
|
238
249
|
if (!lane) {
|
|
239
250
|
throw new Error(`cdl layout: node "${n.id}" の lane "${n.lane}" が定義されていない`);
|
|
240
251
|
}
|
|
241
|
-
const size =
|
|
252
|
+
const size = nodeSize(n);
|
|
242
253
|
if (n.posX !== undefined && n.posY !== undefined) {
|
|
243
254
|
const w = n.posW ?? n.w ?? autoRowsWidth(n, size.w, textMetrics);
|
|
244
255
|
const h = n.posH ?? n.h ?? rowH.get(n.stack) ?? size.h;
|
package/src/layout/spec.ts
CHANGED
|
@@ -200,7 +200,7 @@ export function drawsTitleOutsideBox(kind: string | undefined): boolean {
|
|
|
200
200
|
*/
|
|
201
201
|
const TITLE_FONT_SIZE_DEFAULT = 18;
|
|
202
202
|
const TITLE_FONT_SIZE: ReadonlyMap<string, number> = new Map<string, number>([
|
|
203
|
-
...(["dyn-rect", "dyn-circle", "dyn-arc", "dyn-wave", "dyn-polygon"
|
|
203
|
+
...(["dyn-rect", "dyn-circle", "dyn-arc", "dyn-wave", "dyn-polygon"] as const).map(
|
|
204
204
|
(k) => [k, 13] as const,
|
|
205
205
|
),
|
|
206
206
|
...(
|
|
@@ -251,8 +251,10 @@ const TITLE_FONT_SIZE: ReadonlyMap<string, number> = new Map<string, number>([
|
|
|
251
251
|
] as const
|
|
252
252
|
).map((k) => [k, 20] as const),
|
|
253
253
|
...(["card", "event", "function"] as const).map((k) => [k, 24] as const),
|
|
254
|
+
["station", 25],
|
|
254
255
|
["actor", 32],
|
|
255
256
|
["storage", 34],
|
|
257
|
+
["timeline-number", 18],
|
|
256
258
|
]);
|
|
257
259
|
|
|
258
260
|
/**
|
|
@@ -286,6 +288,17 @@ export function skipsNodeTitleWidthCheck(kind: string | undefined): boolean {
|
|
|
286
288
|
*/
|
|
287
289
|
export const TITLE_BOX_PADDING = 52;
|
|
288
290
|
|
|
291
|
+
/** 時間軸の番号と軸に共通する縁の太さ。active でも変えない。 */
|
|
292
|
+
export const TIMELINE_NUMBER_STROKE = 4;
|
|
293
|
+
|
|
294
|
+
/** 題を描ける幅。丸い番号は縁の内側、それ以外は既存の箱内外の余白を使う。 */
|
|
295
|
+
export function titleUsableWidth(kind: string | undefined, w: number, h: number): number {
|
|
296
|
+
if (kind === "timeline-number") {
|
|
297
|
+
return Math.min(w, h) - 2 * TIMELINE_NUMBER_STROKE;
|
|
298
|
+
}
|
|
299
|
+
return w - (drawsTitleOutsideBox(kind) ? 0 : TITLE_BOX_PADDING);
|
|
300
|
+
}
|
|
301
|
+
|
|
289
302
|
/**
|
|
290
303
|
* 低い箱の時だけ題を箱に収まるまで小さくする種別。
|
|
291
304
|
*
|
|
@@ -303,6 +316,7 @@ const KINDS_SCALING_TITLE_WHEN_LOW: ReadonlySet<string> = new Set(["actor", "eve
|
|
|
303
316
|
*/
|
|
304
317
|
const KINDS_SCALING_TITLE_ALWAYS: ReadonlySet<string> = new Set([
|
|
305
318
|
"card",
|
|
319
|
+
"timeline-number",
|
|
306
320
|
"shape-file",
|
|
307
321
|
"shape-folder",
|
|
308
322
|
"shape-cloud",
|
|
@@ -470,6 +484,7 @@ const KINDS_DRAWING_ONLY_SHAPE: readonly string[] = [
|
|
|
470
484
|
const KINDS_WITHOUT_EYEBROW_TEXT: ReadonlySet<string> = new Set([
|
|
471
485
|
...KINDS_DRAWING_ONLY_SHAPE,
|
|
472
486
|
...KINDS_WITHOUT_TITLE_TEXT,
|
|
487
|
+
"timeline-number",
|
|
473
488
|
]);
|
|
474
489
|
|
|
475
490
|
/**
|
|
@@ -499,6 +514,7 @@ export function rendersEyebrow(kind: string | undefined): boolean {
|
|
|
499
514
|
const KINDS_WITHOUT_SUBTITLE_TEXT: ReadonlySet<string> = new Set([
|
|
500
515
|
...KINDS_DRAWING_OWN_PAYLOAD,
|
|
501
516
|
...KINDS_WITHOUT_TITLE_TEXT,
|
|
517
|
+
"timeline-number",
|
|
502
518
|
]);
|
|
503
519
|
|
|
504
520
|
/** その node が `subtitle` を文字として描くか (SSOT)。 破綻の扱いは `rendersEyebrow` と同じ */
|
package/src/layout/tokens.ts
CHANGED
|
@@ -14,6 +14,8 @@ export const TOKENS = {
|
|
|
14
14
|
lane_label_h: 40,
|
|
15
15
|
lane_top_pad: 100,
|
|
16
16
|
contain_pad: 28,
|
|
17
|
+
stage_column_gap: 48,
|
|
18
|
+
stage_card_text_inset: 18,
|
|
17
19
|
|
|
18
20
|
// node sizes (kind ごと、 unified row h は engine が max() で算出)
|
|
19
21
|
node: {
|
|
@@ -141,7 +143,10 @@ export const TOKENS = {
|
|
|
141
143
|
"mark-start": { w: 96, h: 96 },
|
|
142
144
|
"mark-end": { w: 96, h: 96 },
|
|
143
145
|
// 路線図の駅 (#996) ... 正方形にして小さな丸が縦横どちらにも歪まないようにする。
|
|
144
|
-
|
|
146
|
+
// 半径 16 と縁 6 の外径を、線路との接点を含めて収める。
|
|
147
|
+
station: { w: 38, h: 38 },
|
|
148
|
+
// 時間軸の番号 (#1017) ... 半径 24 の丸と太さ 4 の縁を収める。
|
|
149
|
+
"timeline-number": { w: 52, h: 52 },
|
|
145
150
|
// 時系列のやり取り (#578)。 実寸は中身から決まる (`sequenceBoardMetrics`) ので、
|
|
146
151
|
// ここは書かなかった時の受け皿
|
|
147
152
|
"sequence-board": { w: 960, h: 640 },
|
|
@@ -176,6 +181,8 @@ export const LANE_TOP_PAD = TOKENS.lane_top_pad;
|
|
|
176
181
|
export const STACK_GAP = TOKENS.row_gap;
|
|
177
182
|
export const NODE_SIZE = TOKENS.node;
|
|
178
183
|
export const CONTAIN_PAD = TOKENS.contain_pad;
|
|
184
|
+
export const STAGE_COLUMN_GAP = TOKENS.stage_column_gap;
|
|
185
|
+
export const STAGE_CARD_TEXT_INSET = TOKENS.stage_card_text_inset;
|
|
179
186
|
export const VIEW_PAD = TOKENS.view_pad;
|
|
180
187
|
export const HEADER_RESERVE = TOKENS.header_reserve;
|
|
181
188
|
export const LANE_AUTO_GAP = 80;
|
package/src/layout.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
slideLabelsAlongOwnPath,
|
|
26
26
|
} from "./layout/edges";
|
|
27
27
|
import { applyFooterShapeDrops } from "./layout/footer-shape";
|
|
28
|
+
import { layoutLegend } from "./layout/legend";
|
|
28
29
|
import {
|
|
29
30
|
expandLaneGapsForEdgeLabels,
|
|
30
31
|
expandLanesForNodes,
|
|
@@ -181,7 +182,7 @@ function dropUnsafeLaneDx(
|
|
|
181
182
|
*/
|
|
182
183
|
export function applyRowLevelNodeShifts<
|
|
183
184
|
N extends { id: string; stack: number; lane: string; cx: number; cy: number },
|
|
184
|
-
L extends { id: string; x: number },
|
|
185
|
+
L extends { id: string; x: number; stage?: unknown },
|
|
185
186
|
>(
|
|
186
187
|
nodes: readonly N[],
|
|
187
188
|
lanes: readonly L[],
|
|
@@ -189,6 +190,11 @@ export function applyRowLevelNodeShifts<
|
|
|
189
190
|
): { nodes: N[]; lanes: L[] } | null {
|
|
190
191
|
const dyByStack = aggregateRowDy(nodes, nodeShifts);
|
|
191
192
|
const dxByLane = dropUnsafeLaneDx(aggregateLaneDx(nodes, nodeShifts), nodes);
|
|
193
|
+
// 段階列は隣り合う列面の間を線の回廊として使うため、札回避の横 shift で列だけを動かさない。
|
|
194
|
+
// 縦 shift は stack の整列に必要なので、従来どおり適用する。
|
|
195
|
+
for (const lane of lanes) {
|
|
196
|
+
if (lane.stage !== undefined) dxByLane.delete(lane.id);
|
|
197
|
+
}
|
|
192
198
|
let moved = false;
|
|
193
199
|
const outNodes = nodes.map((n) => {
|
|
194
200
|
const dx = dxByLane.get(n.lane) ?? 0;
|
|
@@ -434,7 +440,18 @@ export function layout(diag: CdlDiagram): LaidDiagram {
|
|
|
434
440
|
}
|
|
435
441
|
}
|
|
436
442
|
|
|
437
|
-
const
|
|
443
|
+
const contentViewBox = computeViewBox(
|
|
444
|
+
containedLanesFinal,
|
|
445
|
+
nodesFinal,
|
|
446
|
+
edges,
|
|
447
|
+
bboxes,
|
|
448
|
+
diag.viewport,
|
|
449
|
+
);
|
|
450
|
+
// 凡例なしの経路では関数を呼ばず、既存の viewBox と配置結果の形を保つ。
|
|
451
|
+
const legendLayout = diag.legend?.length
|
|
452
|
+
? layoutLegend(diag.legend, contentViewBox, textMetrics, nodesFinal)
|
|
453
|
+
: undefined;
|
|
454
|
+
const viewBox = legendLayout?.viewBox ?? contentViewBox;
|
|
438
455
|
|
|
439
456
|
return {
|
|
440
457
|
id: diag.id,
|
|
@@ -462,6 +479,7 @@ export function layout(diag: CdlDiagram): LaidDiagram {
|
|
|
462
479
|
...(textMetrics === "monospace" ? { textMetrics: "monospace" as const } : {}),
|
|
463
480
|
// 種類も配色と同じく空の目印を出さない。 遮断をここに揃えると手書きの図にも効く
|
|
464
481
|
...(diag.type ? { type: diag.type } : {}),
|
|
482
|
+
...(legendLayout ? { legend: legendLayout.legend } : {}),
|
|
465
483
|
bboxes,
|
|
466
484
|
collisions,
|
|
467
485
|
nearCollisions,
|
package/src/render/edge-head.ts
CHANGED
|
@@ -7,6 +7,12 @@ import {
|
|
|
7
7
|
import { hasTone } from "./tone";
|
|
8
8
|
|
|
9
9
|
export const EDGE_HEAD_MUTED_MARKER_ID = "cdl-arrow-muted";
|
|
10
|
+
export const METRO_RETURN_ARROW_D = "M 21 0 L 4 9.5 L 4 -9.5 Z";
|
|
11
|
+
|
|
12
|
+
/** 戻る metro 線専用の矢印。通常の edge marker と寸法・線端からの距離が違う。 */
|
|
13
|
+
export function metroReturnArrowMarkerId(tone: string): string {
|
|
14
|
+
return `cdl-metro-return-arrow-${tone}`;
|
|
15
|
+
}
|
|
10
16
|
|
|
11
17
|
/**
|
|
12
18
|
* 関係の端の形の **描き方と marker の名前** (SSOT、 #560)。
|
package/src/render/edges.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { 等幅の字 } from "./mono-text";
|
|
|
2
2
|
import type { JSX } from "react";
|
|
3
3
|
import { computeLabelBBoxWorld, hasRenderedLabel, LABEL_PILL_PAD_X } from "../layout/spec";
|
|
4
4
|
import { 端の字の大きさ, 端の字を置く, type 線の端 } from "../layout/head-label";
|
|
5
|
+
import { METRO_LINE_WIDTH, METRO_LINE_WIDTH_CSS } from "../layout/metro-path";
|
|
5
6
|
import {
|
|
6
7
|
EDGE_HEAD_MARKER_SIZE,
|
|
7
8
|
type CdlTextMetrics,
|
|
@@ -9,9 +10,12 @@ import {
|
|
|
9
10
|
type LaidEdge,
|
|
10
11
|
type Tone,
|
|
11
12
|
} from "../types";
|
|
12
|
-
import { edgeHeadMarkerName } from "./edge-head";
|
|
13
|
+
import { edgeHeadMarkerName, metroReturnArrowMarkerId } from "./edge-head";
|
|
13
14
|
import { toneColor } from "./tone";
|
|
14
15
|
|
|
16
|
+
/** 段がまだ通っていない metro 区間を、意匠側が選ぶための属性名。 */
|
|
17
|
+
export const METRO_PENDING_ATTRIBUTE = "data-cdl-pending";
|
|
18
|
+
|
|
15
19
|
/**
|
|
16
20
|
* 辺の色 (#618)。 役目を書いた辺だけ「いま」 の色で引く。
|
|
17
21
|
*
|
|
@@ -62,6 +66,7 @@ export function CdlEdgeView({
|
|
|
62
66
|
progress,
|
|
63
67
|
stateValues,
|
|
64
68
|
flowing = false,
|
|
69
|
+
pending = false,
|
|
65
70
|
}: {
|
|
66
71
|
edge: LaidEdge;
|
|
67
72
|
active: boolean;
|
|
@@ -81,6 +86,8 @@ export function CdlEdgeView({
|
|
|
81
86
|
* 2 つの動きが同時に走って線がどちらに進んでいるか読めなくなる。
|
|
82
87
|
*/
|
|
83
88
|
flowing?: boolean;
|
|
89
|
+
/** metro 線の初出より前の段だけに付く。非 metro 線では常に偽。 */
|
|
90
|
+
pending?: boolean;
|
|
84
91
|
}): JSX.Element {
|
|
85
92
|
const color = edgeColor(edge);
|
|
86
93
|
// 光っていない edge も tone の色で描く。 以前は一律の灰色に落としていたため、 何も光っていない
|
|
@@ -96,7 +103,11 @@ export function CdlEdgeView({
|
|
|
96
103
|
// その色なら 0.9 で 4.51-4.92 と 4.5:1 を満たす (0.8 では 3.69-4.04 で届かない)。
|
|
97
104
|
// 実線と点線で同じ組を使い、 光らせた側が薄くなる逆転を作らない。
|
|
98
105
|
// 端の形は線の種類と別の軸 (#560)。 書かなければ既定の塗った三角で、名前も従来のまま
|
|
99
|
-
const
|
|
106
|
+
const style = edge.style ?? "solid";
|
|
107
|
+
const isMetroReturn = edge.metroReturn === true;
|
|
108
|
+
const marker = isMetroReturn
|
|
109
|
+
? metroReturnArrowMarkerId(edge.tone)
|
|
110
|
+
: edgeHeadMarkerName(edge.tone, edge.head, !active, edge.headFill);
|
|
100
111
|
// 出どころ側の印 (#578)。 marker は経路の向きに従って回るので、始点に置いた印は
|
|
101
112
|
// 自動で外向きになる (`orient="auto-start-reverse"`)。 書かない図では端が無い
|
|
102
113
|
const tailMarker =
|
|
@@ -104,7 +115,6 @@ export function CdlEdgeView({
|
|
|
104
115
|
? undefined
|
|
105
116
|
: edgeHeadMarkerName(edge.tone, edge.tailHead, !active, edge.tailHeadFill);
|
|
106
117
|
|
|
107
|
-
const style = edge.style ?? "solid";
|
|
108
118
|
const isDotted = style === "dotted-flow";
|
|
109
119
|
const isPassThrough = isDotted && Boolean(edge.dThrough);
|
|
110
120
|
|
|
@@ -124,6 +134,7 @@ export function CdlEdgeView({
|
|
|
124
134
|
|
|
125
135
|
return (
|
|
126
136
|
<g
|
|
137
|
+
{...(pending ? { [METRO_PENDING_ATTRIBUTE]: "true" } : {})}
|
|
127
138
|
data-cdl-edge={edge.id}
|
|
128
139
|
data-cdl-routing={edge.routing === "metro" ? "metro" : undefined}
|
|
129
140
|
data-cdl-from={edge.from}
|
|
@@ -148,11 +159,12 @@ export function CdlEdgeView({
|
|
|
148
159
|
fill="none"
|
|
149
160
|
stroke={color}
|
|
150
161
|
strokeOpacity={active ? 0.95 : 0.9}
|
|
151
|
-
strokeWidth={active ? 5 : 4}
|
|
162
|
+
strokeWidth={edge.routing === "metro" ? METRO_LINE_WIDTH : active ? 5 : 4}
|
|
152
163
|
strokeDasharray="0 18"
|
|
153
164
|
strokeLinecap="round"
|
|
154
165
|
style={{
|
|
155
166
|
transition: "stroke 280ms ease, stroke-opacity 280ms ease, stroke-width 280ms ease",
|
|
167
|
+
strokeWidth: edge.routing === "metro" ? METRO_LINE_WIDTH_CSS : undefined,
|
|
156
168
|
}}
|
|
157
169
|
/>
|
|
158
170
|
{active && !isPassThrough && <EdgeGlowCircles color={color} edgeId={edge.id} />}
|
|
@@ -237,12 +249,20 @@ function SolidEdgePath({
|
|
|
237
249
|
const boundDashOffset = edge.dashOffsetBind && stateValues
|
|
238
250
|
? resolveNumericAttr(edge.dashOffsetBind, stateValues, 0)
|
|
239
251
|
: undefined;
|
|
252
|
+
const strokeWidth = edge.routing === "metro" && edge.widthBind === undefined
|
|
253
|
+
? edge.metroReturn ? 6 : METRO_LINE_WIDTH
|
|
254
|
+
: boundWidth;
|
|
255
|
+
const strokeWidthCss = edge.routing === "metro" && !edge.metroReturn
|
|
256
|
+
? edge.widthBind === undefined
|
|
257
|
+
? METRO_LINE_WIDTH_CSS
|
|
258
|
+
: `var(--cdl-metro-line-width, ${strokeWidth}px)`
|
|
259
|
+
: undefined;
|
|
240
260
|
// dashoffset は dasharray 前提、 signal 経由の流れる線を出したい時は "6 6" 等
|
|
241
261
|
// 刻みは 2 つの役目を持つ。 光っていないことの合図 (`"solid"` の光っていない側) と、
|
|
242
262
|
// 線の種類そのもの (`"dashed"`)。 **後者は光っても刻みのまま**にする = 意味を持つ刻みが
|
|
243
263
|
// 光った瞬間に消えると、その関係だけ種類が変わって見える (#578)
|
|
244
264
|
const dashArray = dashed
|
|
245
|
-
? "10 8"
|
|
265
|
+
? edge.metroReturn ? "0 9.6" : "10 8"
|
|
246
266
|
: active
|
|
247
267
|
? boundDashOffset !== undefined ? "8 6" : undefined
|
|
248
268
|
: "4 5";
|
|
@@ -253,7 +273,7 @@ function SolidEdgePath({
|
|
|
253
273
|
fill="none"
|
|
254
274
|
stroke={boundStroke}
|
|
255
275
|
strokeOpacity={active ? 0.95 : 0.9}
|
|
256
|
-
strokeWidth={
|
|
276
|
+
strokeWidth={strokeWidth}
|
|
257
277
|
strokeDasharray={dashArray}
|
|
258
278
|
strokeDashoffset={boundDashOffset}
|
|
259
279
|
strokeLinecap="round"
|
|
@@ -261,6 +281,7 @@ function SolidEdgePath({
|
|
|
261
281
|
markerStart={tailMarker === undefined ? undefined : `url(#${tailMarker})`}
|
|
262
282
|
style={{
|
|
263
283
|
transition: "stroke 280ms ease, stroke-opacity 280ms ease, stroke-width 280ms ease",
|
|
284
|
+
strokeWidth: strokeWidthCss,
|
|
264
285
|
}}
|
|
265
286
|
/>
|
|
266
287
|
);
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { JSX } from "react";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
LEGEND_FONT_SIZE,
|
|
5
|
+
LEGEND_MARK_REFERENCE_WIDTH,
|
|
6
|
+
LEGEND_MARK_SCALE,
|
|
7
|
+
LEGEND_MARK_TEXT_GAP,
|
|
8
|
+
LEGEND_MARK_WIDTH,
|
|
9
|
+
} from "../layout/legend";
|
|
10
|
+
import type { LaidLegendItem, LegendMark } from "../types";
|
|
11
|
+
|
|
12
|
+
const ink = "var(--cdl-text-accent, #143a52)";
|
|
13
|
+
const paper = "var(--cdl-stage-bg, #f8fafc)";
|
|
14
|
+
|
|
15
|
+
const LEGEND_MARK_RENDERERS = {
|
|
16
|
+
diamond: () => (
|
|
17
|
+
<path d="M 23 -26 L 46 -8 L 23 10 L 0 -8 Z" fill="none" stroke={ink} strokeWidth={2} />
|
|
18
|
+
),
|
|
19
|
+
"filled-circle": () => <circle cx={23} cy={-8} r={11} fill={ink} />,
|
|
20
|
+
"double-circle": () => (
|
|
21
|
+
<>
|
|
22
|
+
<circle cx={23} cy={-8} r={12} fill="none" stroke={ink} strokeWidth={2.5} />
|
|
23
|
+
<circle cx={23} cy={-8} r={6} fill={ink} />
|
|
24
|
+
</>
|
|
25
|
+
),
|
|
26
|
+
"dotted-line": () => (
|
|
27
|
+
<path
|
|
28
|
+
d={`M 0 -8 H ${LEGEND_MARK_REFERENCE_WIDTH}`}
|
|
29
|
+
fill="none"
|
|
30
|
+
stroke={ink}
|
|
31
|
+
strokeWidth={6}
|
|
32
|
+
strokeLinecap="round"
|
|
33
|
+
strokeDasharray="0 9.6"
|
|
34
|
+
/>
|
|
35
|
+
),
|
|
36
|
+
"solid-line": () => (
|
|
37
|
+
<path
|
|
38
|
+
d={`M 0 -8 H ${LEGEND_MARK_REFERENCE_WIDTH}`}
|
|
39
|
+
fill="none"
|
|
40
|
+
stroke={ink}
|
|
41
|
+
strokeWidth={4}
|
|
42
|
+
strokeLinecap="round"
|
|
43
|
+
/>
|
|
44
|
+
),
|
|
45
|
+
"curved-line": () => (
|
|
46
|
+
<path
|
|
47
|
+
d={`M 0 6 C 12 -22, 34 -22, ${LEGEND_MARK_REFERENCE_WIDTH} 6`}
|
|
48
|
+
fill="none"
|
|
49
|
+
stroke={ink}
|
|
50
|
+
strokeWidth={4}
|
|
51
|
+
strokeLinecap="round"
|
|
52
|
+
/>
|
|
53
|
+
),
|
|
54
|
+
"rounded-label": () => (
|
|
55
|
+
<rect
|
|
56
|
+
x={0}
|
|
57
|
+
y={-23}
|
|
58
|
+
width={LEGEND_MARK_REFERENCE_WIDTH}
|
|
59
|
+
height={30}
|
|
60
|
+
rx={9}
|
|
61
|
+
fill={paper}
|
|
62
|
+
stroke={ink}
|
|
63
|
+
strokeWidth={2}
|
|
64
|
+
/>
|
|
65
|
+
),
|
|
66
|
+
"numbered-circle": () => (
|
|
67
|
+
<>
|
|
68
|
+
<circle cx={23} cy={-8} r={13} fill={paper} stroke={ink} strokeWidth={2.5} />
|
|
69
|
+
<text x={23} y={-2} textAnchor="middle" fontSize={17} fontWeight={700} fill={ink}>
|
|
70
|
+
1
|
|
71
|
+
</text>
|
|
72
|
+
</>
|
|
73
|
+
),
|
|
74
|
+
station: () => <circle cx={23} cy={-8} r={11} fill={paper} stroke={ink} strokeWidth={4} />,
|
|
75
|
+
arrow: () => (
|
|
76
|
+
<path
|
|
77
|
+
d={`M 0 -8 H 42 M 32 -18 L ${LEGEND_MARK_REFERENCE_WIDTH} -8 L 32 2`}
|
|
78
|
+
fill="none"
|
|
79
|
+
stroke={ink}
|
|
80
|
+
strokeWidth={3}
|
|
81
|
+
strokeLinecap="round"
|
|
82
|
+
strokeLinejoin="round"
|
|
83
|
+
/>
|
|
84
|
+
),
|
|
85
|
+
} satisfies Record<LegendMark, () => JSX.Element>;
|
|
86
|
+
|
|
87
|
+
export function CdlLegend({ items }: { items: readonly LaidLegendItem[] }): JSX.Element {
|
|
88
|
+
return (
|
|
89
|
+
<g data-cdl-role="legend">
|
|
90
|
+
{items.map((item, index) => {
|
|
91
|
+
const Mark = LEGEND_MARK_RENDERERS[item.mark];
|
|
92
|
+
return (
|
|
93
|
+
<g
|
|
94
|
+
key={`${item.mark}-${index}`}
|
|
95
|
+
data-cdl-role="legend-item"
|
|
96
|
+
data-cdl-y={item.y}
|
|
97
|
+
transform={`translate(${item.x} ${item.y})`}
|
|
98
|
+
>
|
|
99
|
+
<g
|
|
100
|
+
data-cdl-role="legend-mark"
|
|
101
|
+
data-cdl-legend-mark={item.mark}
|
|
102
|
+
transform={`scale(${LEGEND_MARK_SCALE})`}
|
|
103
|
+
>
|
|
104
|
+
<Mark />
|
|
105
|
+
</g>
|
|
106
|
+
<text
|
|
107
|
+
data-cdl-role="legend-text"
|
|
108
|
+
x={LEGEND_MARK_WIDTH + LEGEND_MARK_TEXT_GAP}
|
|
109
|
+
y={0}
|
|
110
|
+
fontSize={LEGEND_FONT_SIZE}
|
|
111
|
+
fill="var(--cdl-text-mute, #4d7187)"
|
|
112
|
+
>
|
|
113
|
+
{item.text}
|
|
114
|
+
</text>
|
|
115
|
+
</g>
|
|
116
|
+
);
|
|
117
|
+
})}
|
|
118
|
+
</g>
|
|
119
|
+
);
|
|
120
|
+
}
|
package/src/render/nodes.tsx
CHANGED
|
@@ -7,7 +7,7 @@ import { FunctionNode } from "../kinds/function";
|
|
|
7
7
|
import { StorageNode } from "../kinds/storage";
|
|
8
8
|
import { GenericNode } from "../kinds/generic";
|
|
9
9
|
import { MarkStartNode, MarkEndNode } from "../kinds/terminal-mark";
|
|
10
|
-
import { StationNode } from "../kinds/station";
|
|
10
|
+
import { StationNode, TimelineNumberNode } from "../kinds/station";
|
|
11
11
|
import { DynShapeNode } from "../kinds/dyn-shape";
|
|
12
12
|
import { ChartLineNode, type ChartLineScale } from "../kinds/chart-line";
|
|
13
13
|
import { ChartPieNode } from "../kinds/chart-pie";
|
|
@@ -119,10 +119,14 @@ export function CdlNodeView({
|
|
|
119
119
|
stateValues,
|
|
120
120
|
currentPhaseId,
|
|
121
121
|
chartLineScale,
|
|
122
|
+
compactTitleInset,
|
|
122
123
|
関係の受け口,
|
|
123
124
|
lang,
|
|
124
125
|
textMetrics,
|
|
125
126
|
値の名前,
|
|
127
|
+
metroDiagram = false,
|
|
128
|
+
metroDecision = false,
|
|
129
|
+
stationNameRight = false,
|
|
126
130
|
}: {
|
|
127
131
|
node: LaidNode;
|
|
128
132
|
active: boolean;
|
|
@@ -144,8 +148,16 @@ export function CdlNodeView({
|
|
|
144
148
|
stateValues: Record<string, string>;
|
|
145
149
|
currentPhaseId: string | undefined;
|
|
146
150
|
chartLineScale?: ChartLineScale;
|
|
151
|
+
/** 小型 card の名前を左寄せにする時の左端からの距離。段階列の札だけが渡す。 */
|
|
152
|
+
compactTitleInset?: number;
|
|
147
153
|
/** 触れた箱の関係を光らせる時の受け口 (#806)。 書かない図では `undefined` */
|
|
148
154
|
関係の受け口?: 関係の受け口;
|
|
155
|
+
/** metro 線と駅を持つ図だけが担当の名札と端の印を路線図向けに描く。 */
|
|
156
|
+
metroDiagram?: boolean;
|
|
157
|
+
/** `routing: "metro"` の線が出入りする分かれ道だけを路線図向けに描く。 */
|
|
158
|
+
metroDecision?: boolean;
|
|
159
|
+
/** 真上から線路が入る駅の名前を右へ置く。 */
|
|
160
|
+
stationNameRight?: boolean;
|
|
149
161
|
}): JSX.Element {
|
|
150
162
|
const value = node.value ? interpolate(node.value, stateValues, 値の名前) : undefined;
|
|
151
163
|
const rows = (node.rows ?? []).map((r) => interpolate(r, stateValues, 値の名前));
|
|
@@ -201,7 +213,7 @@ export function CdlNodeView({
|
|
|
201
213
|
case "event":
|
|
202
214
|
return <EventNode node={resolvedNode} active={active} progress={progress} />;
|
|
203
215
|
case "card":
|
|
204
|
-
return <CardNode node={resolvedNode} active={active} />;
|
|
216
|
+
return <CardNode node={resolvedNode} active={active} compactTitleInset={compactTitleInset} />;
|
|
205
217
|
// 時系列のやり取り (#578)。 1 つの箱が図を丸ごと描く = 縦が時間で、参加者は箱を持たない
|
|
206
218
|
case "sequence-board":
|
|
207
219
|
return (
|
|
@@ -479,11 +491,13 @@ export function CdlNodeView({
|
|
|
479
491
|
case "shape-customer-service":
|
|
480
492
|
return <ShapeCustomerServiceNode node={resolvedNode} active={active} />;
|
|
481
493
|
case "mark-start":
|
|
482
|
-
return <MarkStartNode node={resolvedNode} active={active} />;
|
|
494
|
+
return <MarkStartNode node={resolvedNode} active={active} metro={metroDiagram} />;
|
|
483
495
|
case "mark-end":
|
|
484
|
-
return <MarkEndNode node={resolvedNode} active={active} />;
|
|
496
|
+
return <MarkEndNode node={resolvedNode} active={active} metro={metroDiagram} />;
|
|
485
497
|
case "station":
|
|
486
|
-
return <StationNode node={resolvedNode} active={active} />;
|
|
498
|
+
return <StationNode node={resolvedNode} active={active} nameRight={stationNameRight} />;
|
|
499
|
+
case "timeline-number":
|
|
500
|
+
return <TimelineNumberNode node={resolvedNode} />;
|
|
487
501
|
default:
|
|
488
502
|
// 新 NodeKind 25 種は GenericNode で汎用描画 (kind 別 icon / color / shape)
|
|
489
503
|
return (
|
|
@@ -494,6 +508,8 @@ export function CdlNodeView({
|
|
|
494
508
|
stateValues={stateValues}
|
|
495
509
|
値の名前={値の名前}
|
|
496
510
|
textMetrics={textMetrics}
|
|
511
|
+
showKindIcon={!(metroDiagram && node.kind === "decision")}
|
|
512
|
+
metroDecision={metroDecision}
|
|
497
513
|
/>
|
|
498
514
|
);
|
|
499
515
|
}
|