@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.
@@ -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, NODE_SIZE[n.kind].h)));
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 = NODE_SIZE[n.kind];
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;
@@ -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", "station"] as const).map(
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` と同じ */
@@ -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
- station: { w: 24, h: 24 },
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 viewBox = computeViewBox(containedLanesFinal, nodesFinal, edges, bboxes, diag.viewport);
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,
@@ -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)。
@@ -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 marker = edgeHeadMarkerName(edge.tone, edge.head, !active, edge.headFill);
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={boundWidth}
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
+ }
@@ -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
  }