@cardenelabs/cdl 0.28.0 → 0.30.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.
@@ -2453,6 +2453,17 @@ type CdlDiagram = {
2453
2453
  * 既定の図にも当たる。
2454
2454
  */
2455
2455
  palette?: string;
2456
+ /**
2457
+ * この図がどの種類か (#645)。 書かない図は種類を持たない。
2458
+ *
2459
+ * **`cdl` は種類の名前だけを出す**。 名前を `data-cdl-type` として図の根に出すだけで、
2460
+ * 太さや色などの見た目は消費側が決める。 見た目を `cdl` に持たせると、消費側の表現を
2461
+ * 変えるたびに `cdl` の更新が要る。
2462
+ *
2463
+ * 空文字は書かなかった扱いにする = 空を出すと消費側の `[data-cdl-type]` が
2464
+ * 種類を書いていない図にも当たる。
2465
+ */
2466
+ type?: string;
2456
2467
  /**
2457
2468
  * 矢印をいつ出すか (既定 = `"phase"`、 #582)。
2458
2469
  *
@@ -2652,6 +2663,8 @@ type LaidDiagram = {
2652
2663
  topic: string;
2653
2664
  /** どの配色で描くか (#1553)。 `CdlDiagram.palette` をそのまま持ち越す */
2654
2665
  palette?: string;
2666
+ /** どの種類の図か (#645)。 `CdlDiagram.type` をそのまま持ち越す */
2667
+ type?: string;
2655
2668
  viewBox: {
2656
2669
  x: number;
2657
2670
  y: number;
@@ -2453,6 +2453,17 @@ type CdlDiagram = {
2453
2453
  * 既定の図にも当たる。
2454
2454
  */
2455
2455
  palette?: string;
2456
+ /**
2457
+ * この図がどの種類か (#645)。 書かない図は種類を持たない。
2458
+ *
2459
+ * **`cdl` は種類の名前だけを出す**。 名前を `data-cdl-type` として図の根に出すだけで、
2460
+ * 太さや色などの見た目は消費側が決める。 見た目を `cdl` に持たせると、消費側の表現を
2461
+ * 変えるたびに `cdl` の更新が要る。
2462
+ *
2463
+ * 空文字は書かなかった扱いにする = 空を出すと消費側の `[data-cdl-type]` が
2464
+ * 種類を書いていない図にも当たる。
2465
+ */
2466
+ type?: string;
2456
2467
  /**
2457
2468
  * 矢印をいつ出すか (既定 = `"phase"`、 #582)。
2458
2469
  *
@@ -2652,6 +2663,8 @@ type LaidDiagram = {
2652
2663
  topic: string;
2653
2664
  /** どの配色で描くか (#1553)。 `CdlDiagram.palette` をそのまま持ち越す */
2654
2665
  palette?: string;
2666
+ /** どの種類の図か (#645)。 `CdlDiagram.type` をそのまま持ち越す */
2667
+ type?: string;
2655
2668
  viewBox: {
2656
2669
  x: number;
2657
2670
  y: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cardenelabs/cdl",
3
- "version": "0.28.0",
3
+ "version": "0.30.0",
4
4
  "description": "CDL (Chainome Diagram Language). Mermaid-like declarative DSL that compiles to animated SVG diagrams. Built for blockchain / Solidity flows, generic enough for sequence / flow / state / ER / topology diagrams.",
5
5
  "license": "MIT",
6
6
  "author": "cardene777",
package/src/builder.ts CHANGED
@@ -1642,6 +1642,10 @@ export function diagram(
1642
1642
  * この図をどの配色で描くか (#1553)。 詳細は `CdlDiagram.palette` の doc 参照。
1643
1643
  */
1644
1644
  palette?: string;
1645
+ /**
1646
+ * この図がどの種類か (#645)。 詳しくは `CdlDiagram.type` の説明を参照。
1647
+ */
1648
+ type?: string;
1645
1649
  },
1646
1650
  ): DiagramBuilder {
1647
1651
  const lanes: CdlLane[] = [];
@@ -3780,6 +3784,8 @@ export function diagram(
3780
3784
  ...(options.structuredData === "exclude" ? { structuredData: "exclude" as const } : {}),
3781
3785
  // 配色の名前は素通しする。 空文字の遮断は `layout` が持つ (後段が必ず通るため)
3782
3786
  ...(options.palette !== undefined ? { palette: options.palette } : {}),
3787
+ // 種類の名前も同じ経路で渡し、空文字の扱いを `layout` の 1 箇所に揃える
3788
+ ...(options.type !== undefined ? { type: options.type } : {}),
3783
3789
  // inputs / formulas / scrollTriggers / eventBindings は空なら省略 (static diagram の JSON 形状維持)
3784
3790
  ...(inputs.length > 0 ? { inputs: [...inputs] } : {}),
3785
3791
  ...(formulas.length > 0 ? { formulas: [...formulas] } : {}),
@@ -2999,12 +2999,110 @@ export function separateSharedStraightRuns(edges: LaidEdge[], nodes: readonly La
2999
2999
  }
3000
3000
  }
3001
3001
 
3002
+ /**
3003
+ * 同じ節の同じ辺から出る線のうち、 最長の 1 本を除いて最初の折れを手前へ引く (#651)。
3004
+ *
3005
+ * 起点は `fan-origin-single-point` のため動かさず、 最初の折れ直後の区間を平行移動する。
3006
+ * 1 本しか出ない辺は `detourStub` が決めた箱との中間位置をそのまま保つ。
3007
+ */
3008
+ export function pullOutgoingFirstCorner(edges: LaidEdge[], nodes: readonly LaidNode[]): void {
3009
+ if (edges.length < 2) return;
3010
+
3011
+ const 束 = new Map<string, LaidEdge[]>();
3012
+ const 同じ組の数 = new Map<string, number>();
3013
+ for (const e of edges) {
3014
+ const key = `${e.from}::${e.fromSide}`;
3015
+ const list = 束.get(key) ?? [];
3016
+ list.push(e);
3017
+ 束.set(key, list);
3018
+ const pair = `${e.from}->${e.to}`;
3019
+ 同じ組の数.set(pair, (同じ組の数.get(pair) ?? 0) + 1);
3020
+ }
3021
+
3022
+ let 札を旧経路へ追従済み = false;
3023
+ for (const list of 束.values()) {
3024
+ if (list.length < 2) continue;
3025
+ const 並び = list
3026
+ .flatMap((e) => {
3027
+ const pts = pathVertices(e.d);
3028
+ if (!pts || pts.length < 2) return [];
3029
+ return [{ e, pts, 最初の長さ: Math.hypot(pts[1]!.x - pts[0]!.x, pts[1]!.y - pts[0]!.y) }];
3030
+ })
3031
+ .sort((a, b) => b.最初の長さ - a.最初の長さ);
3032
+
3033
+ // 最長の線を引いても共有区間は縮まらないので、 2 本目以降だけを動かす。
3034
+ for (const { e, pts, 最初の長さ } of 並び.slice(1)) {
3035
+ // 同じ 2 節を結ぶ線の分離は `bowSharedStraightPaths` の範囲。 迂回経路は重ねて残す。
3036
+ if ((同じ組の数.get(`${e.from}->${e.to}`) ?? 0) > 1) continue;
3037
+ if (pts.length < 3 || 最初の長さ <= EDGE_STUB_OUT + VERTICAL_DETOUR_CORNER) continue;
3038
+ const 横 = e.fromSide === "left" || e.fromSide === "right";
3039
+ if (!runIsAligned(pts, 0, 横 ? 1 : 0)) continue;
3040
+
3041
+ // 角丸の半径分を折れの位置に足し、起点からの直線部を EDGE_STUB_OUT だけ残す。
3042
+ const 移動量 = 最初の長さ - (EDGE_STUB_OUT + VERTICAL_DETOUR_CORNER);
3043
+ const ux = (pts[1]!.x - pts[0]!.x) / 最初の長さ;
3044
+ const uy = (pts[1]!.y - pts[0]!.y) / 最初の長さ;
3045
+ const 試し = pts.map((p, i) =>
3046
+ i === 1 || i === 2 ? { x: p.x - ux * 移動量, y: p.y - uy * 移動量 } : p,
3047
+ );
3048
+ const 試しD = verticesToPath(試し);
3049
+ const 試し区間 = flattenPathSegments(extractPathSegments(試しD));
3050
+ if (
3051
+ nodes.some(
3052
+ (n) => n.id !== e.from && n.id !== e.to && segmentsCrossRect(試し区間, nodeRect(n)),
3053
+ )
3054
+ ) {
3055
+ continue;
3056
+ }
3057
+
3058
+ const 前 = e.d;
3059
+ const 前の交差 = countEdgeCrossings(edges).count;
3060
+ e.d = 試しD;
3061
+ if (countEdgeCrossings(edges).count > 前の交差) {
3062
+ e.d = 前;
3063
+ continue;
3064
+ }
3065
+ e.d = 前;
3066
+
3067
+ // 旧経路に重なっていた札を先に確定する。 経路を動かした後では重なりが消え、 札だけが
3068
+ // 元の位置に取り残される。
3069
+ if (!札を旧経路へ追従済み) {
3070
+ slideLabelsAlongOwnPath(edges, nodes);
3071
+ 札を旧経路へ追従済み = true;
3072
+ }
3073
+ const labelBox = computeLabelBBoxWorld(e);
3074
+ const label = { x: labelBox.x + labelBox.w / 2, y: labelBox.y + labelBox.h / 2 };
3075
+ const 線分までの距離2 = (a: Vertex, b: Vertex): number => {
3076
+ const dx = b.x - a.x;
3077
+ const dy = b.y - a.y;
3078
+ const len2 = dx * dx + dy * dy;
3079
+ const t =
3080
+ len2 === 0
3081
+ ? 0
3082
+ : Math.max(0, Math.min(1, ((label.x - a.x) * dx + (label.y - a.y) * dy) / len2));
3083
+ return (label.x - (a.x + dx * t)) ** 2 + (label.y - (a.y + dy * t)) ** 2;
3084
+ };
3085
+ const 動く区間への距離 = 線分までの距離2(pts[1]!, pts[2]!);
3086
+ const labelも動かす = pts
3087
+ .slice(0, -1)
3088
+ .every((p, i) => 線分までの距離2(p, pts[i + 1]!) >= 動く区間への距離 - 1e-6);
3089
+ e.d = 試しD;
3090
+ if (labelも動かす) {
3091
+ e.labelX -= ux * 移動量;
3092
+ e.labelY -= uy * 移動量;
3093
+ }
3094
+ }
3095
+ }
3096
+ }
3097
+
3002
3098
  /** 同じ辺に入る線を離す間隔 (world unit)。 箱の行 1 つ分。 */
3003
3099
  const ENDPOINT_FAN_STEP = 56;
3004
3100
  /** これより詰まるなら離さない (world unit)。 離しても線どうしを見分けられない。 */
3005
3101
  const ENDPOINT_FAN_MIN = 20;
3006
- /** 終点の手前で辺に沿って動くために取る助走の長さ (world unit)。 */
3102
+ /** ずらすために最後の区間へ要る長さ (world unit)。 これより短い区間は動かさない。 */
3007
3103
  const ENDPOINT_FAN_RUNUP = 40;
3104
+ /** これより短い重なりは動かさない (world unit)。 動かすとまっすぐな線に折れが増える。 */
3105
+ const MIXED_ROOT_OVERLAP_MIN = 24;
3008
3106
 
3009
3107
  /**
3010
3108
  * 同じ節の同じ辺に **入る** 線の終点を、 辺に沿って離す (#611)。
@@ -3039,6 +3137,26 @@ const ENDPOINT_FAN_RUNUP = 40;
3039
3137
  export function fanIncomingEndpoints(edges: LaidEdge[], nodes: readonly LaidNode[]): void {
3040
3138
  if (edges.length < 2) return;
3041
3139
  const nmap = new Map(nodes.map((n) => [n.id, n]));
3140
+ const 平坦な区間 = new Map(edges.map((e) => [e.id, flattenPathSegments(extractPathSegments(e.d))] as const));
3141
+
3142
+ const 軸に平行な重なり長さ = (一方: LaidEdge, 他方: LaidEdge): number => {
3143
+ let 合計 = 0;
3144
+ for (const a of 平坦な区間.get(一方.id) ?? []) {
3145
+ const aは水平 = Math.abs(a.y2 - a.y1) <= 1;
3146
+ const aは垂直 = Math.abs(a.x2 - a.x1) <= 1;
3147
+ for (const b of 平坦な区間.get(他方.id) ?? []) {
3148
+ const bは水平 = Math.abs(b.y2 - b.y1) <= 1;
3149
+ const bは垂直 = Math.abs(b.x2 - b.x1) <= 1;
3150
+ if (aは水平 && bは水平 && Math.abs(a.y1 - b.y1) <= 1) {
3151
+ 合計 += Math.max(0, Math.min(Math.max(a.x1, a.x2), Math.max(b.x1, b.x2)) - Math.max(Math.min(a.x1, a.x2), Math.min(b.x1, b.x2)));
3152
+ }
3153
+ if (aは垂直 && bは垂直 && Math.abs(a.x1 - b.x1) <= 1) {
3154
+ 合計 += Math.max(0, Math.min(Math.max(a.y1, a.y2), Math.max(b.y1, b.y2)) - Math.max(Math.min(a.y1, a.y2), Math.min(b.y1, b.y2)));
3155
+ }
3156
+ }
3157
+ }
3158
+ return 合計;
3159
+ };
3042
3160
 
3043
3161
  /** 同じ 2 点を結ぶ線の本数。 その組は弓が扱うので、 ここでは触らない。 */
3044
3162
  const 同じ組の数 = new Map<string, number>();
@@ -3046,7 +3164,14 @@ export function fanIncomingEndpoints(edges: LaidEdge[], nodes: readonly LaidNode
3046
3164
 
3047
3165
  /** 同じ節の同じ辺に入る線。 */
3048
3166
  const 束 = new Map<string, LaidEdge[]>();
3167
+ const 出る線 = new Map<string, LaidEdge[]>();
3049
3168
  for (const e of edges) {
3169
+ if (nmap.has(e.from)) {
3170
+ const key = `${e.from}::${e.fromSide}`;
3171
+ const list = 出る線.get(key) ?? [];
3172
+ list.push(e);
3173
+ 出る線.set(key, list);
3174
+ }
3050
3175
  if (!nmap.has(e.to)) continue;
3051
3176
  // 同じ 2 点を結ぶ線は `bowSharedStraightPaths` が弓で分ける範囲。 `#941` は
3052
3177
  // 「どの幅でも箱に当たるなら分離せず重なりを残す」 と決めており、 末尾で分けると
@@ -3058,36 +3183,62 @@ export function fanIncomingEndpoints(edges: LaidEdge[], nodes: readonly LaidNode
3058
3183
  束.set(key, list);
3059
3184
  }
3060
3185
 
3061
- for (const [, list] of 束) {
3062
- if (list.length < 2) continue;
3186
+ for (const [key, list] of 束) {
3187
+ const 中央が塞がっている = (出る線.get(key) ?? []).some((出線) =>
3188
+ list.some((入線) =>
3189
+ !(入線.from === 出線.to && 入線.to === 出線.from)
3190
+ && 軸に平行な重なり長さ(入線, 出線) >= MIXED_ROOT_OVERLAP_MIN
3191
+ )
3192
+ );
3193
+ if (list.length < 2 && !中央が塞がっている) continue;
3063
3194
  const n = nmap.get(list[0]!.to)!;
3064
3195
  const 横 = list[0]!.toSide === "left" || list[0]!.toSide === "right";
3065
3196
  const 辺の長さ = 横 ? n.h : n.w;
3066
3197
  // 4 隅に刺さらない範囲。 `arrow-endpoint-center` が見ているのと同じ余白を空ける。
3067
3198
  const 使える幅 = 辺の長さ - 2 * ARROW_ENDPOINT_CENTER_TOL;
3068
3199
  if (使える幅 <= 0) continue;
3069
- const 間隔 = Math.min(ENDPOINT_FAN_STEP, 使える幅 / (list.length - 1));
3200
+ const 席の間 = 中央が塞がっている ? list.length : list.length - 1;
3201
+ const 間隔 = Math.min(ENDPOINT_FAN_STEP, 使える幅 / 席の間);
3070
3202
  if (間隔 < ENDPOINT_FAN_MIN) continue;
3071
3203
 
3072
3204
  // 来た高さの順に割り当てる。 上を走ってきた線を上の端点へ入れれば、 束の中で交差しない。
3073
3205
  // 図の並び順で割り当てると、 上を走る線が下の端点へ回って必ず交差する。
3206
+ const 辺の中心 = 横 ? n.cy : n.cx;
3074
3207
  const 並び = list
3075
- .map((e) => ({ e, 深さ: 辺から離れた側の位置(e, 横) ?? 0 }))
3208
+ .map((e) => {
3209
+ const 区間 = extractPathSegments(e.d);
3210
+ const last = 区間[区間.length - 1];
3211
+ const 長い助走 = last === undefined ? 0 : Math.hypot(last.x2 - last.x1, last.y2 - last.y1);
3212
+ const 中心を補う = 中央が塞がっている || list.length === 2;
3213
+ const 深さ = 辺から離れた側の位置(e, 横) ?? (中心を補う ? 辺の中心 : 0);
3214
+ return { e, 長い助走, 深さ };
3215
+ })
3076
3216
  .sort((a, b) => a.深さ - b.深さ || (a.e.id < b.e.id ? -1 : 1));
3077
3217
 
3078
3218
  const 中心 = ((並び.length - 1) * 間隔) / 2;
3219
+ let 負側の席 = 0;
3220
+ let 正側の席 = 0;
3079
3221
  for (let k = 0; k < 並び.length; k++) {
3080
- const d = k * 間隔 - 中心;
3222
+ const d = 中央が塞がっている
3223
+ ? 並び[k]!.深さ < 辺の中心
3224
+ ? -(++負側の席 * 間隔)
3225
+ : ++正側の席 * 間隔
3226
+ : k * 間隔 - 中心;
3081
3227
  if (Math.abs(d) < 0.001) continue;
3082
3228
  const e = 並び[k]!.e;
3083
- const 差替 = 末尾を辺に沿ってずらす(e, 横, d, nodes);
3084
- if (差替 === null) continue;
3085
3229
  // 交差が増えるなら戻す。 判定は軸と同じ数え方を使う = 独自の数え方は軸と食い違い、
3086
3230
  // 軸が 3 件を数えている状況で 0 件と判定して通してしまった (実測)。
3087
3231
  const 前 = e.d;
3088
3232
  const 前の交差 = countEdgeCrossings(edges).count;
3089
- e.d = 差替;
3090
- if (countEdgeCrossings(edges).count > 前の交差) e.d = 前;
3233
+ const 長い助走 = 並び[k]!.長い助走;
3234
+ for (const 助走 of [長い助走, ENDPOINT_FAN_RUNUP]) {
3235
+ e.d = 前;
3236
+ const 差替 = 末尾を辺に沿ってずらす(e, 横, d, 助走, nodes);
3237
+ if (差替 === null) continue;
3238
+ e.d = 差替;
3239
+ if (countEdgeCrossings(edges).count <= 前の交差) break;
3240
+ e.d = 前;
3241
+ }
3091
3242
  }
3092
3243
  }
3093
3244
  }
@@ -3121,6 +3272,7 @@ function 末尾を辺に沿ってずらす(
3121
3272
  e: LaidEdge,
3122
3273
  横: boolean,
3123
3274
  d: number,
3275
+ 助走: number,
3124
3276
  nodes: readonly LaidNode[],
3125
3277
  ): string | null {
3126
3278
  if ((e.d.match(/M/g) ?? []).length !== 1) return null;
@@ -3139,10 +3291,11 @@ function 末尾を辺に沿ってずらす(
3139
3291
  const 長さ = Math.hypot(last.x2 - last.x1, last.y2 - last.y1);
3140
3292
  if (長さ < ENDPOINT_FAN_RUNUP + 1) return null;
3141
3293
 
3142
- // 助走を始める点。 終点から辺の外へ `ENDPOINT_FAN_RUNUP` 戻る。
3294
+ // 助走が最後の区間を越えると、 書き戻した直前の折れから短く戻る区間ができる。
3143
3295
  const ux = (last.x1 - last.x2) / 長さ;
3144
3296
  const uy = (last.y1 - last.y2) / 長さ;
3145
- const 折れ1 = { x: last.x2 + ux * ENDPOINT_FAN_RUNUP, y: last.y2 + uy * ENDPOINT_FAN_RUNUP };
3297
+ const 丸めた助走 = Math.min(助走, 長さ);
3298
+ const 折れ1 = { x: last.x2 + ux * 丸めた助走, y: last.y2 + uy * 丸めた助走 };
3146
3299
  const 折れ2 = 横 ? { x: 折れ1.x, y: 折れ1.y + d } : { x: 折れ1.x + d, y: 折れ1.y };
3147
3300
  const 終点 = 横 ? { x: last.x2, y: last.y2 + d } : { x: last.x2 + d, y: last.y2 };
3148
3301
 
@@ -3169,7 +3322,8 @@ function 末尾を辺に沿ってずらす(
3169
3322
  parts.push(`L ${s.x2} ${s.y2}`);
3170
3323
  }
3171
3324
  // 助走の折れは丸めない。 短い 2 折れなので、 丸めると切り口が区間からはみ出す。
3172
- parts.push(`L ${round1(折れ1.x)} ${round1(折れ1.y)}`);
3325
+ const 折れ1は直前の折れ = Math.abs(折れ1.x - last.x1) <= 0.5 && Math.abs(折れ1.y - last.y1) <= 0.5;
3326
+ if (!折れ1は直前の折れ) parts.push(`L ${round1(折れ1.x)} ${round1(折れ1.y)}`);
3173
3327
  parts.push(`L ${round1(折れ2.x)} ${round1(折れ2.y)}`);
3174
3328
  parts.push(`L ${round1(終点.x)} ${round1(終点.y)}`);
3175
3329
  return parts.join(" ");
package/src/layout.ts CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  fanIncomingEndpoints,
16
16
  hopEdgeCrossings,
17
17
  layoutEdges,
18
+ pullOutgoingFirstCorner,
18
19
  repositionParallelLabels,
19
20
  separateSharedStraightRuns,
20
21
  slideLabelsAlongOwnPath,
@@ -346,6 +347,9 @@ export function layout(diag: CdlDiagram): LaidDiagram {
346
347
  // 弧が確定してから、 同じ節の組を結ぶ辺の label を「兄弟の線を跨がない位置」 へ置き直す (#376)。
347
348
  // 順序が逆だと、 直線を前提に置いた label を弧が貫く。 label を先に線から離すと、 今度は弧を
348
349
  // 膨らませる判定が label を障害物と見なして弧が消える。
350
+ // 同じ辺から出る線は、 最長の 1 本を除いて最初の直線部を 40 world まで引く (#651)。
351
+ // 経路の引き方を変えず、 確定後の最初の折れだけを動かす。
352
+ pullOutgoingFirstCorner(edges, nodesFinal);
349
353
  // 同じ辺に入る線の終点を辺に沿って離す (#611)。 経路を引き終えてから当てる = 終点を先に
350
354
  // 動かすと引き方の分岐が変わり、 迂回していた線が箱を貫通する (実測で 0 件が 7 件になった)。
351
355
  fanIncomingEndpoints(edges, nodesFinal);
@@ -403,6 +407,8 @@ export function layout(diag: CdlDiagram): LaidDiagram {
403
407
  // **遮断はここだけに置く**。 描画は必ず組み立てを通り、組み立て API も `er` も
404
408
  // 素通しにしてある。 上流にも置くと、どの入力でも通らない分岐が残る
405
409
  ...(diag.palette ? { palette: diag.palette } : {}),
410
+ // 種類も配色と同じく空の目印を出さない。 遮断をここに揃えると手書きの図にも効く
411
+ ...(diag.type ? { type: diag.type } : {}),
406
412
  bboxes,
407
413
  collisions,
408
414
  nearCollisions,
package/src/presets.ts CHANGED
@@ -217,7 +217,7 @@ export type SwimlaneResult = DiagramBuilder & {
217
217
 
218
218
  export function swimlane(preset: SwimlanePreset): SwimlaneResult {
219
219
  const w = preset.laneWidth ?? 400;
220
- const b = diagram(preset.id, { topic: preset.topic });
220
+ const b = diagram(preset.id, { topic: preset.topic, type: "swimlane" });
221
221
  const labelToId = new Map<string, string>();
222
222
  const idsInOrder: string[] = [];
223
223
  for (const label of preset.lanes) {
@@ -297,7 +297,7 @@ export type FlowBuilder = {
297
297
  */
298
298
  export function flow(preset: FlowPreset): FlowBuilder {
299
299
  const w = preset.laneWidth ?? 400;
300
- const b = diagram(preset.id, { topic: preset.topic });
300
+ const b = diagram(preset.id, { topic: preset.topic, type: "flow" });
301
301
  b.lane("flow", { width: w, ...(preset.laneLabel ? { label: preset.laneLabel } : {}) });
302
302
  const tone: Tone = preset.defaultTone ?? "accent";
303
303
  const style: EdgeStyle = preset.defaultStyle ?? "dotted-flow";
@@ -451,7 +451,7 @@ export type SequenceBuilder = {
451
451
  */
452
452
  export function sequence(preset: SequencePreset): SequenceBuilder {
453
453
  const tone: Tone = preset.defaultTone ?? "accent";
454
- const b = diagram(preset.id, { topic: preset.topic });
454
+ const b = diagram(preset.id, { topic: preset.topic, type: "sequence" });
455
455
 
456
456
  // 名前だけの文字列と、呼び名を添えた物の両方を受ける (#578)
457
457
  const 面々: SequenceBoardActor[] = preset.actors.map((a) =>
@@ -591,7 +591,7 @@ export function topology(preset: TopologyPreset): TopologyBuilder {
591
591
  const groupW = preset.groupWidth ?? 460;
592
592
  const tone: Tone = preset.defaultTone ?? "accent";
593
593
  const style: EdgeStyle = preset.defaultStyle ?? "solid";
594
- const b = diagram(preset.id, { topic: preset.topic });
594
+ const b = diagram(preset.id, { topic: preset.topic, type: "topology" });
595
595
 
596
596
  const groupOrder: string[] = [];
597
597
  const groupStackCount = new Map<string, number>();
@@ -803,7 +803,7 @@ export function er(preset: ErPreset): ErBuilder {
803
803
  const tone: Tone = preset.defaultTone ?? "info";
804
804
  const 縞を敷く = preset.rowStripe ?? true;
805
805
  // 配色の名前は素通しする。 空文字の遮断は `diagram` が持つ = ここで重ねると到達しない分岐になる
806
- const b = diagram(preset.id, { topic: preset.topic, palette: preset.palette });
806
+ const b = diagram(preset.id, { topic: preset.topic, palette: preset.palette, type: "er" });
807
807
 
808
808
  const entities: string[] = [];
809
809
  const phaseActivate: string[] = [];
@@ -1030,7 +1030,7 @@ export type StateMachineBuilder = {
1030
1030
  export function stateMachine(preset: StateMachinePreset): StateMachineBuilder {
1031
1031
  const stateW = resolveStateW(preset.stateWidth, "stateMachine");
1032
1032
  const tone: Tone = preset.defaultTone ?? "accent";
1033
- const b = diagram(preset.id, { topic: preset.topic });
1033
+ const b = diagram(preset.id, { topic: preset.topic, type: "state" });
1034
1034
 
1035
1035
  const phaseActivate: string[] = [];
1036
1036
  const stateOrder = new Map<string, number>();
@@ -1236,7 +1236,7 @@ export function infrastructure(preset: InfrastructurePreset): InfrastructureBuil
1236
1236
  const laneW = preset.laneWidth ?? 380;
1237
1237
  const tone: Tone = preset.defaultTone ?? "accent";
1238
1238
  const style: EdgeStyle = preset.defaultStyle ?? "solid";
1239
- const b = diagram(preset.id, { topic: preset.topic });
1239
+ const b = diagram(preset.id, { topic: preset.topic, type: "infrastructure" });
1240
1240
 
1241
1241
  // col → laneId、 col の数で lane を auto 作成
1242
1242
  const colLaneIds = new Map<number, string>();
@@ -1498,7 +1498,7 @@ export function classDiagram(preset: ClassDiagramPreset): ClassDiagramBuilder {
1498
1498
  const 図の既定 = preset.defaultTone;
1499
1499
  // 縞と配色は `er()` と同じ扱い (#627)。 箱の作りが同じなので、既定も揃える
1500
1500
  const 縞を敷く = preset.rowStripe ?? true;
1501
- const b = diagram(preset.id, { topic: preset.topic, palette: preset.palette });
1501
+ const b = diagram(preset.id, { topic: preset.topic, palette: preset.palette, type: "class" });
1502
1502
 
1503
1503
  const classes: string[] = [];
1504
1504
  const phaseActivate: string[] = [];
@@ -1640,7 +1640,7 @@ export type TreeBuilder = {
1640
1640
  export function tree(preset: TreePreset): TreeBuilder {
1641
1641
  const canvasW = preset.nodeWidth ? preset.nodeWidth * 2 : 720;
1642
1642
  const canvasH = 480;
1643
- const b = diagram(preset.id, { topic: preset.topic });
1643
+ const b = diagram(preset.id, { topic: preset.topic, type: "tree" });
1644
1644
  const treeNodes: TreeNode[] = [];
1645
1645
 
1646
1646
  const api: TreeBuilder = {
@@ -1722,7 +1722,7 @@ export type UserJourneyBuilder = {
1722
1722
  export function userJourney(preset: UserJourneyPreset): UserJourneyBuilder {
1723
1723
  const canvasW = preset.stepWidth ? preset.stepWidth * 2 : 720;
1724
1724
  const canvasH = 480;
1725
- const b = diagram(preset.id, { topic: preset.topic });
1725
+ const b = diagram(preset.id, { topic: preset.topic, type: "journey" });
1726
1726
  const steps: JourneyStep[] = [];
1727
1727
 
1728
1728
  const api: UserJourneyBuilder = {
@@ -1805,7 +1805,7 @@ export type MindMapBuilder = {
1805
1805
  export function mindMap(preset: MindMapPreset): MindMapBuilder {
1806
1806
  const canvasW = preset.branchWidth ? preset.branchWidth * 2 : 720;
1807
1807
  const canvasH = 480;
1808
- const b = diagram(preset.id, { topic: preset.topic });
1808
+ const b = diagram(preset.id, { topic: preset.topic, type: "mindmap" });
1809
1809
  const branches: MindBranch[] = [];
1810
1810
 
1811
1811
  const api: MindMapBuilder = {
@@ -1887,7 +1887,7 @@ export type FunnelBuilder = {
1887
1887
  export function funnel(preset: FunnelPreset): FunnelBuilder {
1888
1888
  const canvasW = toGridSize(preset.stageWidth ?? 560);
1889
1889
  const canvasH = 480;
1890
- const b = diagram(preset.id, { topic: preset.topic });
1890
+ const b = diagram(preset.id, { topic: preset.topic, type: "funnel" });
1891
1891
  const stages: FunnelStage[] = [];
1892
1892
 
1893
1893
  const api: FunnelBuilder = {
@@ -1974,7 +1974,7 @@ export type QuadrantBuilder = {
1974
1974
  export function quadrant(preset: QuadrantPreset): QuadrantBuilder {
1975
1975
  const canvasW = 640;
1976
1976
  const canvasH = 480;
1977
- const b = diagram(preset.id, { topic: preset.topic });
1977
+ const b = diagram(preset.id, { topic: preset.topic, type: "quadrant" });
1978
1978
  const items: QuadrantItem[] = [];
1979
1979
 
1980
1980
  const api: QuadrantBuilder = {
@@ -2088,7 +2088,7 @@ export function chart(preset: ChartPreset): ChartBuilder {
2088
2088
  ? 320
2089
2089
  : 360,
2090
2090
  );
2091
- const b = diagram(preset.id, { topic: preset.topic });
2091
+ const b = diagram(preset.id, { topic: preset.topic, type: "chart" });
2092
2092
  const data: ChartDatum[] = [];
2093
2093
 
2094
2094
  const kindMap = {
@@ -2199,7 +2199,7 @@ export function gantt(preset: GanttPreset): GanttBuilder {
2199
2199
  const canvasW = preset.laneWidth ? preset.laneWidth * 2 : 720;
2200
2200
  const canvasH = 360;
2201
2201
  const tone: Tone = preset.defaultTone ?? "teal";
2202
- const b = diagram(preset.id, { topic: preset.topic });
2202
+ const b = diagram(preset.id, { topic: preset.topic, type: "gantt" });
2203
2203
  const tasks: GanttTask[] = [];
2204
2204
 
2205
2205
  const api: GanttBuilder = {
@@ -2326,7 +2326,7 @@ const SHAPE_EYEBROW: Record<FlowchartNodeShape, string> = {
2326
2326
  export function flowchart(preset: FlowchartPreset): FlowchartBuilder {
2327
2327
  const laneW = preset.laneWidth ?? 380;
2328
2328
  const tone: Tone = preset.defaultTone ?? "accent";
2329
- const b = diagram(preset.id, { topic: preset.topic });
2329
+ const b = diagram(preset.id, { topic: preset.topic, type: "flowchart" });
2330
2330
 
2331
2331
  const laneToId = new Map<string, string>();
2332
2332
  const laneStackCount = new Map<string, number>();
@@ -2443,7 +2443,7 @@ const NET_KIND_MAP: Record<NetworkDeviceKind, NodeKind> = {
2443
2443
  export function network(preset: NetworkPreset): NetworkBuilder {
2444
2444
  const laneW = preset.laneWidth ?? 320;
2445
2445
  const tone: Tone = preset.defaultTone ?? "info";
2446
- const b = diagram(preset.id, { topic: preset.topic });
2446
+ const b = diagram(preset.id, { topic: preset.topic, type: "network" });
2447
2447
 
2448
2448
  const colLaneIds = new Map<number, string>();
2449
2449
  const phaseActivate: string[] = [];
@@ -2560,7 +2560,7 @@ export function stateMachine2(preset: StateMachine2Preset): StateMachine2Builder
2560
2560
  const stateW = resolveStateW(preset.stateWidth, "stateMachine2");
2561
2561
  const laneW = boxLaneW(stateW);
2562
2562
  const tone: Tone = preset.defaultTone ?? "accent";
2563
- const b = diagram(preset.id, { topic: preset.topic });
2563
+ const b = diagram(preset.id, { topic: preset.topic, type: "state" });
2564
2564
 
2565
2565
  const stateOrder = new Map<string, number>();
2566
2566
  const phaseActivate: string[] = [];
@@ -151,6 +151,7 @@ export function CdlStage({
151
151
  // 通るため、空文字は `layout` が既に落としている。 ここにも守りを置くと、
152
152
  // どの入力でも通らない分岐が残る
153
153
  data-cdl-palette={laid.palette}
154
+ data-cdl-type={laid.type}
154
155
  >
155
156
  <defs>
156
157
  {/* CAR-643 = cdl は geometry only。 shadow / border effect の filter は cdl SVG 内に
package/src/types.ts CHANGED
@@ -2528,6 +2528,17 @@ export type CdlDiagram = {
2528
2528
  * 既定の図にも当たる。
2529
2529
  */
2530
2530
  palette?: string;
2531
+ /**
2532
+ * この図がどの種類か (#645)。 書かない図は種類を持たない。
2533
+ *
2534
+ * **`cdl` は種類の名前だけを出す**。 名前を `data-cdl-type` として図の根に出すだけで、
2535
+ * 太さや色などの見た目は消費側が決める。 見た目を `cdl` に持たせると、消費側の表現を
2536
+ * 変えるたびに `cdl` の更新が要る。
2537
+ *
2538
+ * 空文字は書かなかった扱いにする = 空を出すと消費側の `[data-cdl-type]` が
2539
+ * 種類を書いていない図にも当たる。
2540
+ */
2541
+ type?: string;
2531
2542
  /**
2532
2543
  * 矢印をいつ出すか (既定 = `"phase"`、 #582)。
2533
2544
  *
@@ -2734,6 +2745,8 @@ export type LaidDiagram = {
2734
2745
  topic: string;
2735
2746
  /** どの配色で描くか (#1553)。 `CdlDiagram.palette` をそのまま持ち越す */
2736
2747
  palette?: string;
2748
+ /** どの種類の図か (#645)。 `CdlDiagram.type` をそのまま持ち越す */
2749
+ type?: string;
2737
2750
  viewBox: { x: number; y: number; w: number; h: number };
2738
2751
  /**
2739
2752
  * 図全体の倍率 (`CdlViewport.scale` を正規化した値)。