@cardenelabs/cdl 0.22.0 → 0.24.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.
@@ -0,0 +1,201 @@
1
+ /**
2
+ * 関係から lane と stack の並びを決める (#599)。
3
+ *
4
+ * cdl は交差を `countEdgeCrossings` で数えるだけで、減らす手段を持っていなかった。
5
+ * 書き手が lane / stack を手で指定する仕様のため、交差が出ても engine 側に打つ手がない。
6
+ * 6 表 5 関係の図を人が並べた実測で交差 3 箇所、本関数の並びでは 0 箇所になる。
7
+ *
8
+ * ## 3 段で決める
9
+ *
10
+ * | 段 | やること |
11
+ * |---|---|
12
+ * | 1 | 親からの深さで lane 番号を決める (最長路) |
13
+ * | 2 | lane を跨ぐ関係に中継点を挟み、通り道を確保する |
14
+ * | 3 | 各 lane の並びを中央値法で往復させ、交差を減らす |
15
+ *
16
+ * ## `compile` からは呼ばない
17
+ *
18
+ * 書き手が明示した lane / stack を engine が黙って動かすと、既存の全図の配置が変わる。
19
+ * 本関数は入力を受けて割当を返すだけで、呼ぶかどうかは上流が決める。
20
+ *
21
+ * ## 中継点を挟む理由
22
+ *
23
+ * lane を 2 つ以上跨ぐ関係は、途中の lane で場所を取らないと他の節点と重なる経路になる。
24
+ * 中継点を置くと、その lane の並べ替えに参加して通り道が確保される。
25
+ * 返す結果に中継点は含まない (呼出側が知る必要のない内部の足場)。
26
+ */
27
+
28
+ /** 並べたい節点。 大きさは見ないので id だけでよい */
29
+ export type OrderNode = { id: string };
30
+
31
+ /** 親から子への関係。 向きが lane の深さを決める */
32
+ export type OrderEdge = { from: string; to: string };
33
+
34
+ /** 決まった並び。 lane が列、 stack が列の中の位置 */
35
+ export type OrderResult = { id: string; lane: number; stack: number };
36
+
37
+ /** 中央値法を往復させる回数。 4 往復で並びが落ち着くことを test で確かめている */
38
+ const SWEEPS = 4;
39
+
40
+ /** 中継点かどうかは id の接頭辞で分ける。 呼出側の id と衝突しない形にする */
41
+ const RELAY = "__relay:";
42
+
43
+ /**
44
+ * 親からの深さを決める。
45
+ *
46
+ * 環状の関係 (a→b→c→a) があると最長路は定義できないため、節点数を上限に打ち切る。
47
+ * 打ち切った時点の深さをそのまま使う = 環の中では順序が付かないが、結果は返る。
48
+ */
49
+ function ranks(nodes: readonly OrderNode[], edges: readonly OrderEdge[]): Map<string, number> {
50
+ const rank = new Map<string, number>();
51
+ for (const n of nodes) rank.set(n.id, 0);
52
+ for (let pass = 0; pass < nodes.length; pass += 1) {
53
+ let 動いた = false;
54
+ for (const e of edges) {
55
+ if (e.from === e.to) continue;
56
+ const a = rank.get(e.from);
57
+ const b = rank.get(e.to);
58
+ if (a === undefined || b === undefined) continue;
59
+ if (b < a + 1) {
60
+ rank.set(e.to, a + 1);
61
+ 動いた = true;
62
+ }
63
+ }
64
+ if (!動いた) break;
65
+ }
66
+ return rank;
67
+ }
68
+
69
+ /** 中央値。 参照先が無い節点は -1 を返し、並べ替えの対象から外す */
70
+ function median(values: readonly number[]): number {
71
+ if (values.length === 0) return -1;
72
+ const v = [...values].sort((a, b) => a - b);
73
+ return v[Math.floor((v.length - 1) / 2)] ?? -1;
74
+ }
75
+
76
+ /**
77
+ * 関係から lane と stack を決める。
78
+ *
79
+ * 同じ入力からは常に同じ並びが返る (乱数を使わず、同点は入力順で解く)。
80
+ *
81
+ * @param nodes 並べたい節点
82
+ * @param edges 親から子への関係。 自己参照は深さに影響しない
83
+ * @returns 節点ごとの lane と stack。 入力に無い id は返らない
84
+ */
85
+ export function orderNodes(nodes: readonly OrderNode[], edges: readonly OrderEdge[]): OrderResult[] {
86
+ if (nodes.length === 0) return [];
87
+ const ある = new Set(nodes.map((n) => n.id));
88
+ const 有効 = edges.filter((e) => ある.has(e.from) && ある.has(e.to));
89
+
90
+ const rank = ranks(nodes, 有効);
91
+
92
+ // 中継点を挟む。 lane を 2 つ以上跨ぐ関係だけが対象
93
+ type 節 = { id: string; lane: number };
94
+ const 節点: 節[] = nodes.map((n) => ({ id: n.id, lane: rank.get(n.id) ?? 0 }));
95
+ const 辺: OrderEdge[] = [];
96
+ 有効.forEach((e, i) => {
97
+ if (e.from === e.to) return;
98
+ const 始 = rank.get(e.from) ?? 0;
99
+ const 終 = rank.get(e.to) ?? 0;
100
+ let 前 = e.from;
101
+ for (let s = 始 + 1; s < 終; s += 1) {
102
+ const id = `${RELAY}${i}:${s}`;
103
+ 節点.push({ id, lane: s });
104
+ 辺.push({ from: 前, to: id });
105
+ 前 = id;
106
+ }
107
+ 辺.push({ from: 前, to: e.to });
108
+ });
109
+
110
+ // lane ごとに並べる。 初期の並びは入力順
111
+ const 深さ = Math.max(...節点.map((n) => n.lane)) + 1;
112
+ const 列: 節[][] = Array.from({ length: 深さ }, () => []);
113
+ for (const n of 節点) 列[n.lane]?.push(n);
114
+
115
+ const 位置 = (): Map<string, number> => {
116
+ const m = new Map<string, number>();
117
+ for (const 並 of 列) 並.forEach((n, i) => m.set(n.id, i));
118
+ return m;
119
+ };
120
+ const 親 = new Map<string, string[]>();
121
+ const 子 = new Map<string, string[]>();
122
+ const 足す = (m: Map<string, string[]>, key: string, value: string): void => {
123
+ const 既 = m.get(key);
124
+ if (既) 既.push(value);
125
+ else m.set(key, [value]);
126
+ };
127
+ for (const e of 辺) {
128
+ 足す(親, e.to, e.from);
129
+ 足す(子, e.from, e.to);
130
+ }
131
+
132
+ for (let pass = 0; pass < SWEEPS; pass += 1) {
133
+ const pos = 位置();
134
+ const 下向き = pass % 2 === 0;
135
+ const 順 = 下向き ? [...列.keys()] : [...列.keys()].reverse();
136
+ for (const s of 順) {
137
+ if ((下向き && s === 0) || (!下向き && s === 列.length - 1)) continue;
138
+ const 並 = 列[s];
139
+ if (!並) continue;
140
+ const 元順 = new Map(並.map((n, i) => [n.id, i]));
141
+ const 重み = new Map(
142
+ 並.map((n) => {
143
+ const 相手 = (下向き ? 親.get(n.id) : 子.get(n.id)) ?? [];
144
+ const m = median(相手.map((id) => pos.get(id) ?? -1).filter((v) => v >= 0));
145
+ return [n.id, m];
146
+ }),
147
+ );
148
+ 並.sort((a, b) => {
149
+ const A = 重み.get(a.id) ?? -1;
150
+ const B = 重み.get(b.id) ?? -1;
151
+ // 参照先を持たない節点は動かさない (元の位置を保つ)
152
+ if (A < 0 && B < 0) return (元順.get(a.id) ?? 0) - (元順.get(b.id) ?? 0);
153
+ if (A < 0) return 1;
154
+ if (B < 0) return -1;
155
+ if (A !== B) return A - B;
156
+ return (元順.get(a.id) ?? 0) - (元順.get(b.id) ?? 0);
157
+ });
158
+ }
159
+ }
160
+
161
+ // 中継点を落とし、残りに stack を振り直す
162
+ const 結果: OrderResult[] = [];
163
+ 列.forEach((並, lane) => {
164
+ let stack = 0;
165
+ for (const n of 並) {
166
+ if (n.id.startsWith(RELAY)) continue;
167
+ 結果.push({ id: n.id, lane, stack });
168
+ stack += 1;
169
+ }
170
+ });
171
+ return 結果;
172
+ }
173
+
174
+ /**
175
+ * 並びの上で交差の数を数える。
176
+ *
177
+ * 節点の実寸を見ず、lane と stack だけで数える近似。
178
+ * 同じ lane 間を渡る 2 本が、始点と終点の上下関係を入れ替えていれば 1 交差と数える。
179
+ * 配置を比べる用途にはこれで足り、実寸を要する判定は `countEdgeCrossings` が担う。
180
+ */
181
+ export function countOrderCrossings(order: readonly OrderResult[], edges: readonly OrderEdge[]): number {
182
+ const 場所 = new Map(order.map((o) => [o.id, o]));
183
+ const 渡り = edges
184
+ .filter((e) => e.from !== e.to)
185
+ .map((e) => ({ a: 場所.get(e.from), b: 場所.get(e.to) }))
186
+ .filter((e): e is { a: OrderResult; b: OrderResult } => e.a !== undefined && e.b !== undefined);
187
+
188
+ let 数 = 0;
189
+ for (let i = 0; i < 渡り.length; i += 1) {
190
+ for (let j = i + 1; j < 渡り.length; j += 1) {
191
+ const x = 渡り[i];
192
+ const y = 渡り[j];
193
+ if (!x || !y) continue;
194
+ // 同じ lane から同じ lane へ渡る 2 本だけを比べる
195
+ if (x.a.lane !== y.a.lane || x.b.lane !== y.b.lane) continue;
196
+ const 上下 = (x.a.stack - y.a.stack) * (x.b.stack - y.b.stack);
197
+ if (上下 < 0) 数 += 1;
198
+ }
199
+ }
200
+ return 数;
201
+ }
@@ -251,6 +251,7 @@ export function genericTitleBaselineY(kind: string | undefined, h: number, hasRo
251
251
  export const ROW_TOP_STORAGE = 152;
252
252
  export const ROW_PITCH_STORAGE = 56;
253
253
 
254
+
254
255
  /** `kinds/generic.tsx` の 1 行目 baseline と行送り。 */
255
256
  export const ROW_TOP_GENERIC = 100;
256
257
  export const ROW_PITCH_GENERIC = 28;
package/src/layout.ts CHANGED
@@ -10,7 +10,15 @@ import {
10
10
  resolveEdgeLabelOverlapsWithChainAndPropagate,
11
11
  resolveOverlaps,
12
12
  } from "./layout/collisions";
13
- import { bowSharedStraightPaths, layoutEdges, repositionParallelLabels } from "./layout/edges";
13
+ import {
14
+ bowSharedStraightPaths,
15
+ fanIncomingEndpoints,
16
+ hopEdgeCrossings,
17
+ layoutEdges,
18
+ repositionParallelLabels,
19
+ separateSharedStraightRuns,
20
+ slideLabelsAlongOwnPath,
21
+ } from "./layout/edges";
14
22
  import { applyFooterShapeDrops } from "./layout/footer-shape";
15
23
  import {
16
24
  expandLaneGapsForEdgeLabels,
@@ -338,7 +346,23 @@ export function layout(diag: CdlDiagram): LaidDiagram {
338
346
  // 弧が確定してから、 同じ節の組を結ぶ辺の label を「兄弟の線を跨がない位置」 へ置き直す (#376)。
339
347
  // 順序が逆だと、 直線を前提に置いた label を弧が貫く。 label を先に線から離すと、 今度は弧を
340
348
  // 膨らませる判定が label を障害物と見なして弧が消える。
349
+ // 同じ辺に入る線の終点を辺に沿って離す (#611)。 経路を引き終えてから当てる = 終点を先に
350
+ // 動かすと引き方の分岐が変わり、 迂回していた線が箱を貫通する (実測で 0 件が 7 件になった)。
351
+ fanIncomingEndpoints(edges, nodesFinal);
352
+ // 同じ通り道を並んで走る区間をずらす (#610)。 弓の後に当てる = 弓で分かれた線を数え直さない。
353
+ // 弧より先に当てる = ずらした後の形で交点を数えないと、 消えた交点に弧が残る。
354
+ //
355
+ // 終点を離した **後** に当てる。 先に当てると、 終点を離したことで新しくできた並走が
356
+ // 残る (実測で ER 4 表に長さ 95 の並走が戻った)。
357
+ separateSharedStraightRuns(edges, nodesFinal);
358
+ // 交わる所で上を通る側に弧を差し込む (#608)。 経路が確定してから当てる = 弓や迂回で
359
+ // 形が変わった後の交点を見る。 札を置く前に当てるので、 札は弧を含んだ経路の上に載る。
360
+ hopEdgeCrossings(edges);
341
361
  repositionParallelLabels(edges, nodesFinal);
362
+ // 縦にずらすだけでは逃げ場が無い札を、 自分の線に沿って動かす (#601)。 ここまでの
363
+ // 置き直しは全て縦方向で、 線に沿って動かす経路が無かった。 いま被っている札だけを
364
+ // 対象にするので、 落ち着いている図の座標は動かない。
365
+ slideLabelsAlongOwnPath(edges, nodesFinal);
342
366
 
343
367
  const bboxes = collectBBoxes(containedLanesFinal, nodesFinal, edges);
344
368
  const collisions = detectCollisions(bboxes, edges);
package/src/presets.ts CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  type SequenceBoardMessage,
12
12
  type SequenceBoardMessageKind,
13
13
  } from "./kinds/sequence-board-metrics";
14
- import type { NodeKind, Tone, EdgeStyle, EdgeHead, EdgeHeadFill, RowMark, JourneyEmotion, QuadrantKey, BoundNumber } from "./types";
14
+ import type { NodeKind, Tone, EdgeStyle, EdgeHead, EdgeHeadFill, RowMark, JourneyEmotion, QuadrantKey, BoundNumber, EdgeRole } from "./types";
15
15
 
16
16
  /**
17
17
  * 状態を読む書き方 (`{名前}`)。
@@ -794,8 +794,9 @@ export function er(preset: ErPreset): ErBuilder {
794
794
  const api: ErBuilder = {
795
795
  entity(e) {
796
796
  const laneId = `lane-${e.id}`;
797
- // 鍵の群を上にまとめ、間を空の行 1 つで開ける (#578)。 区切りの線を引く代わりに余白で
798
- // 分けるので、`rowMarks` を持つ箱では描き手が線を引かない
797
+ // 鍵の群を上にまとめる (#578)。 群の区切りは **行頭の印** が持つ = 鍵は名前に下線が付く。
798
+ // 空の行で開ける形は #606 でやめた = 下線と重複した 2 つ目の手掛かりで、
799
+ // 代わりに行の間隔を不揃いにしていた (鍵と値を持つ箱だけ境目が広がる)
799
800
  const 鍵 = (e.columns ?? []).filter((c) => c.pk === true);
800
801
  const 値 = (e.columns ?? []).filter((c) => c.pk !== true);
801
802
  const 行: string[] = [];
@@ -809,10 +810,6 @@ export function er(preset: ErPreset): ErBuilder {
809
810
  });
810
811
  };
811
812
  鍵.forEach(足す);
812
- if (鍵.length > 0 && 値.length > 0) {
813
- 行.push("");
814
- 印.push(null);
815
- }
816
813
  値.forEach(足す);
817
814
  const 設計の形 = 行.length > 0;
818
815
  const rows = 設計の形 ? 行 : (e.rows ?? []);
@@ -1170,6 +1167,10 @@ export type InfraConnection = {
1170
1167
  sub?: string;
1171
1168
  tone?: Tone;
1172
1169
  style?: EdgeStyle;
1170
+ /** 辺の役目 (#618)。 `main` を書いた辺だけ「いま」 の色で引く */
1171
+ role?: EdgeRole;
1172
+ /** 名前の下地を敷くか (#618)。 書かなければ敷く */
1173
+ labelPlate?: boolean;
1173
1174
  /** label の水平方向 fine offset (default 0)、 preset 交互 ±60 を override */
1174
1175
  labelOffsetX?: number;
1175
1176
  /** label の垂直方向 fine offset (default 0、 preset 交互 ±60 を override) */
@@ -1258,6 +1259,8 @@ export function infrastructure(preset: InfrastructurePreset): InfrastructureBuil
1258
1259
  ...(c.sub ? { sub: c.sub } : {}),
1259
1260
  tone: c.tone ?? tone,
1260
1261
  style: c.style ?? style,
1262
+ ...(c.role ? { role: c.role } : {}),
1263
+ ...(c.labelPlate === undefined ? {} : { labelPlate: c.labelPlate }),
1261
1264
  labelOffsetX: c.labelOffsetX ?? 0,
1262
1265
  labelOffsetY: c.labelOffsetY ?? 0,
1263
1266
  });
@@ -1442,8 +1445,8 @@ export function classDiagram(preset: ClassDiagramPreset): ClassDiagramBuilder {
1442
1445
  return api;
1443
1446
  },
1444
1447
  build() {
1445
- // 行頭の印で持ち物と振る舞いを分ける (#578)。 群の間は **空の行** で開ける = 区切りの
1446
- // 線を引く代わりに余白で分ける。 印を持つ行は描き手が線を引かない
1448
+ // 行頭の印で持ち物と振る舞いを分ける (#578)。 持ち物は四角、振る舞いは山形なので、
1449
+ // 群の区切りは行ごとに読める。 空の行で開ける形は #606 でやめた
1447
1450
  const 中身 = 箱.map((c) => {
1448
1451
  const rows: string[] = [];
1449
1452
  const rowMarks: (RowMark | null)[] = [];
@@ -1451,10 +1454,6 @@ export function classDiagram(preset: ClassDiagramPreset): ClassDiagramBuilder {
1451
1454
  rows.push(stripAccess(a));
1452
1455
  rowMarks.push({ shape: "square", filled: isPublic(a) });
1453
1456
  }
1454
- if ((c.attributes?.length ?? 0) > 0 && (c.methods?.length ?? 0) > 0) {
1455
- rows.push("");
1456
- rowMarks.push(null);
1457
- }
1458
1457
  for (const m of c.methods ?? []) {
1459
1458
  rows.push(stripAccess(m));
1460
1459
  rowMarks.push({ shape: "chevron", filled: isPublic(m) });
@@ -3,6 +3,28 @@ import { EDGE_HEAD_DEFAULT, EDGE_HEAD_FILL_DEFAULT, type LaidEdge } from "../typ
3
3
  import { edgeHeadMarkerId } from "./edge-head";
4
4
  import { computeLabelBBoxWorld, hasRenderedLabel, LABEL_PILL_PAD_X } from "../layout/spec";
5
5
  import { hasTone, toneColor } from "./tone";
6
+ import type { EdgeRole, Tone } from "../types";
7
+
8
+ /**
9
+ * 辺の色 (#618)。 役目を書いた辺だけ「いま」 の色で引く。
10
+ *
11
+ * 図の中に道が 2 種 (主となる 1 本と、 そこから枝分かれする預け先) ある時、 どちらも
12
+ * 同じ色だとどこから読むかが決まらない。 主となる道にだけ別の色を当てて順番を作る。
13
+ */
14
+ /**
15
+ * 辺の太さの既定 (#618)。
16
+ *
17
+ * 元は光って 5 / 光らず 2.5 で、 消費側 (dragon) が `!important` で 2.25 に上書きしていた。
18
+ * 実際に画面へ出る 2.25 は箱の枠 (1.75) より太く、 線が箱より前に出ていた。
19
+ * 箱の枠と釣り合う細さへ下げる。
20
+ */
21
+ export function defaultEdgeWidth(active: boolean): number {
22
+ return active ? 2.5 : 1.25;
23
+ }
24
+
25
+ export function edgeColor(edge: { tone: Tone; role?: EdgeRole }): string {
26
+ return edge.role === "main" ? "var(--cdl-now, #c0421f)" : toneColor(edge.tone);
27
+ }
6
28
  import { EDGE_LABEL_TEXT, fontFamilyAttr, type EdgeLabelLine } from "../label-text";
7
29
  import { interpolate, pathSubpath, resolveNumericAttr, shrinkPathEnd } from "./utils";
8
30
 
@@ -39,7 +61,7 @@ export function CdlEdgeView({
39
61
  progress: number;
40
62
  stateValues?: Record<string, string>;
41
63
  }): JSX.Element {
42
- const color = toneColor(edge.tone);
64
+ const color = edgeColor(edge);
43
65
  // 光っていない edge も tone の色で描く。 以前は一律の灰色に落としていたため、 何も光っていない
44
66
  // 静的な図では全 edge が同じ色になり、 因果の向き (警戒 = 赤 / 良好 = 緑) が線から読めなかった。
45
67
  // 光っている / いないの区別は、 実線は太さ (5 対 2.5) と線種 (実線対破線)、 点線は太さ (5 対 4)
@@ -171,7 +193,7 @@ function SolidEdgePath({
171
193
  // 伸ばすのは初めて出る段だけ (#584)。 光っているかで引くと、光り続ける図で毎段引き直される
172
194
  const visibleD = drawing ? pathSubpath(edge.d, progress) : edge.d;
173
195
  // signal binding: widthBind / strokeBind / dashOffsetBind (CAR edge-signal-binding)
174
- const defaultWidth = active ? 5 : 2.5;
196
+ const defaultWidth = defaultEdgeWidth(active);
175
197
  const boundWidth = edge.widthBind && stateValues
176
198
  ? resolveNumericAttr(edge.widthBind, stateValues, defaultWidth)
177
199
  : defaultWidth;
@@ -269,7 +291,7 @@ export function CdlEdgeGlowView({
269
291
  active: boolean;
270
292
  }): JSX.Element | null {
271
293
  if (!active) return null;
272
- const color = toneColor(edge.tone);
294
+ const color = edgeColor(edge);
273
295
  return <EdgeGlowCircles color={color} edgeId={edge.id} />;
274
296
  }
275
297
 
@@ -286,7 +308,7 @@ export function CdlEdgeLabelView({
286
308
  // 空 label でも rect + text で 12.5x12.5 の小 marker が残り、 dragon sweep で
287
309
  // node bbox と数十 px² の実測 overlap を生む (mind-demo 61px² 事象)。
288
310
  if (!hasRenderedLabel(edge)) return null;
289
- const color = toneColor(edge.tone);
311
+ const color = edgeColor(edge);
290
312
  // SSOT = spec.ts の computeLabelBBoxWorld (CAR-431 で render / layout / predict-bbox 3 箇所の
291
313
  // bbox 計算 drift を解消)。 pill h / w / padding / boxLeftX / boxTopY は本 helper 経由で取得、
292
314
  // <g transform="translate(labelX, labelY)"> 内 rect は相対座標なので world x/y を helper から
@@ -312,19 +334,22 @@ export function CdlEdgeLabelView({
312
334
  data-cdl-edge-label-text={edge.label}
313
335
  data-cdl-edge-sub-text={edge.sub ?? ""}
314
336
  >
315
- <rect
316
- data-cdl-role="edge-label-bg"
317
- x={boxLeftX}
318
- y={boxTopY}
319
- width={boxW}
320
- height={boxH}
321
- rx={10}
322
- fill="var(--cdl-label-bg, #ffffff)"
323
- stroke={active ? color : "var(--cdl-divider, #d4d4d8)"}
324
- strokeOpacity={active ? 0.9 : 0.55}
325
- strokeWidth={1.5}
326
- opacity={active ? 1 : 0.92}
327
- />
337
+ {/* 下地は既定で敷く。 丸い下地は箱と同じ形なので、 罫の細い図では外せるようにした (#618) */}
338
+ {edge.labelPlate !== false && (
339
+ <rect
340
+ data-cdl-role="edge-label-bg"
341
+ x={boxLeftX}
342
+ y={boxTopY}
343
+ width={boxW}
344
+ height={boxH}
345
+ rx={10}
346
+ fill="var(--cdl-label-bg, #ffffff)"
347
+ stroke={active ? color : "var(--cdl-divider, #d4d4d8)"}
348
+ strokeOpacity={active ? 0.9 : 0.55}
349
+ strokeWidth={1.5}
350
+ opacity={active ? 1 : 0.92}
351
+ />
352
+ )}
328
353
  <text
329
354
  data-cdl-role="edge-label"
330
355
  x={textX}
package/src/types.ts CHANGED
@@ -134,6 +134,9 @@ export type Side = "top" | "right" | "bottom" | "left";
134
134
  */
135
135
  export type EdgeStyle = "solid" | "dotted-flow" | "dashed";
136
136
 
137
+ /** 辺の役目 (#618)。 `main` = 図の主となる道 */
138
+ export type EdgeRole = "main";
139
+
137
140
  /**
138
141
  * 線の種類の全種 (#578)。 **型だけでなく実体を持つ**。
139
142
  *
@@ -637,6 +640,21 @@ export type CdlEdge = {
637
640
  side?: Side;
638
641
  /** visual style、 default "solid" */
639
642
  style?: EdgeStyle;
643
+ /**
644
+ * 辺の役目 (#618)。 書かなければ従来どおり tone の色で引く。
645
+ *
646
+ * `main` を書いた辺だけ「いま」 の色 (`--cdl-now`) で引く。 図の中で 1 本の道を
647
+ * 目立たせ、 どこから読むかを決めるための口。 すべての辺に書くと差が消えるので、
648
+ * 主となる 1 本 (または 1 続き) にだけ書く。
649
+ */
650
+ role?: EdgeRole;
651
+ /**
652
+ * 名前の下地を敷くか (#618)。 書かなければ敷く (従来どおり)。
653
+ *
654
+ * 丸い下地は箱と同じ形なので、 名前が小さな箱に見える。 罫の細い図では外したほうが
655
+ * 線と名前の区別が付く。
656
+ */
657
+ labelPlate?: boolean;
640
658
  /** 端の形、 default "triangle" (#560)。 線の種類とは別の軸 */
641
659
  head?: EdgeHead;
642
660
  /**