@cardenelabs/cdl 0.16.0 → 0.17.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 +49 -1
- package/dist/index.cjs +736 -193
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +222 -10
- package/dist/index.d.ts +222 -10
- package/dist/index.js +728 -194
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +458 -60
- 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 +458 -60
- package/dist/react.js.map +1 -1
- package/dist/{render-BS1E6Mlo.d.cts → render-Dc5Pdf7e.d.cts} +175 -4
- package/dist/{render-BS1E6Mlo.d.ts → render-Dc5Pdf7e.d.ts} +175 -4
- package/package.json +1 -1
- package/src/builder.ts +19 -0
- package/src/index.ts +21 -1
- package/src/kinds/card.tsx +15 -1
- package/src/kinds/sequence-board-metrics.ts +118 -0
- package/src/kinds/sequence-board.tsx +289 -0
- package/src/kinds/storage.tsx +137 -11
- package/src/layout/nodes.ts +32 -6
- package/src/layout/spec.ts +14 -1
- package/src/layout/tokens.ts +3 -0
- package/src/presets.ts +563 -183
- package/src/render/edge-head.ts +72 -8
- package/src/render/edges.tsx +30 -9
- package/src/render/nodes.tsx +4 -0
- package/src/render/stage.tsx +40 -29
- package/src/render/template-fields.ts +6 -2
- package/src/types.ts +156 -3
- package/src/visual-validate.ts +29 -2
package/src/render/edge-head.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EDGE_HEAD_DEFAULT, type EdgeHead } from "../types";
|
|
1
|
+
import { EDGE_HEAD_DEFAULT, EDGE_HEAD_FILL_DEFAULT, type EdgeHead, type EdgeHeadFill } from "../types";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* 関係の端の形の **描き方と marker の名前** (SSOT、 #560)。
|
|
@@ -9,10 +9,25 @@ import { EDGE_HEAD_DEFAULT, type EdgeHead } from "../types";
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
export type EdgeHeadShape = {
|
|
12
|
-
/**
|
|
12
|
+
/** viewBox に描く path。 空文字は「何も描かない」 (`"none"`) */
|
|
13
13
|
d: string;
|
|
14
|
+
/**
|
|
15
|
+
* 描く枠 (既定 `10 × 10`)。 ER の端は 2 つの記号が並ぶので横長になる (#578)。
|
|
16
|
+
*
|
|
17
|
+
* 枠の縦を 10 に揃えてあるので、 marker の大きさは横だけが `w / 10` 倍で伸びる =
|
|
18
|
+
* 線の太さに対する印の高さが形によって変わらない。
|
|
19
|
+
*/
|
|
20
|
+
w: number;
|
|
21
|
+
h: number;
|
|
14
22
|
/** path の端のどこを線の先端に合わせるか */
|
|
15
23
|
refX: number;
|
|
24
|
+
/**
|
|
25
|
+
* 印の大きさの倍率 (既定 1)。
|
|
26
|
+
*
|
|
27
|
+
* ER の端は 2 つの記号が並ぶので、他の形と同じ高さだと 1 つあたりが半分になって
|
|
28
|
+
* 棒と丸が見分けられない。 形ごとに読める大きさが違うので、形が倍率を持つ。
|
|
29
|
+
*/
|
|
30
|
+
scale?: number;
|
|
16
31
|
/** 塗るか。 塗らない形は線で描く */
|
|
17
32
|
filled: boolean;
|
|
18
33
|
};
|
|
@@ -27,10 +42,29 @@ export type EdgeHeadShape = {
|
|
|
27
42
|
* 9 にすると開いた側が箱から 1 だけ浮く。
|
|
28
43
|
*/
|
|
29
44
|
export const EDGE_HEAD_SHAPE: Record<EdgeHead, EdgeHeadShape> = {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
45
|
+
// 何も描かない端。 marker 自体を出さないので `d` は読まれない
|
|
46
|
+
none: { d: "", w: 10, h: 10, refX: 0, filled: false },
|
|
47
|
+
triangle: { d: "M 0 0 L 10 5 L 0 10 z", w: 10, h: 10, refX: 9, filled: true },
|
|
48
|
+
diamond: { d: "M 0 5 L 5 0 L 10 5 L 5 10 z", w: 10, h: 10, refX: 9, filled: true },
|
|
49
|
+
open: { d: "M 1 0 L 9 5 L 1 10", w: 10, h: 10, refX: 9, filled: false },
|
|
50
|
+
crow: { d: "M 10 0 L 0 5 M 10 5 L 0 5 M 10 10 L 0 5", w: 10, h: 10, refX: 10, filled: false },
|
|
51
|
+
// ER の端。 **箱に近い側 (x が大きい側) が個数、その外側が任意か** (#578)。
|
|
52
|
+
//
|
|
53
|
+
// 枠は 24 × 10 で、線の先端に合わせるのは x=24 = 箱の縁。 marker は経路の向きに回るので、
|
|
54
|
+
// 行き先側でも出どころ側でも「箱に近い側」 が同じ端になる。
|
|
55
|
+
//
|
|
56
|
+
// 三又の真ん中の枝は **関係の線そのもの** が兼ねる。 別に引くと同じ所に 2 本重なって太く見える
|
|
57
|
+
one: { d: "M 16 0 L 16 10 M 4 0 L 4 10", w: 24, h: 10, refX: 24, scale: 1.8, filled: false },
|
|
58
|
+
"zero-one": { d: "M 16 0 L 16 10 M 7 5 a 3 3 0 1 0 -6 0 a 3 3 0 1 0 6 0", w: 24, h: 10, refX: 24, scale: 1.8, filled: false },
|
|
59
|
+
many: { d: "M 24 0 L 13 5 L 24 10 M 4 0 L 4 10", w: 24, h: 10, refX: 24, scale: 1.8, filled: false },
|
|
60
|
+
"zero-many": {
|
|
61
|
+
d: "M 24 0 L 13 5 L 24 10 M 7 5 a 3 3 0 1 0 -6 0 a 3 3 0 1 0 6 0",
|
|
62
|
+
w: 24,
|
|
63
|
+
h: 10,
|
|
64
|
+
refX: 24,
|
|
65
|
+
scale: 1.8,
|
|
66
|
+
filled: false,
|
|
67
|
+
},
|
|
34
68
|
};
|
|
35
69
|
|
|
36
70
|
/**
|
|
@@ -39,7 +73,37 @@ export const EDGE_HEAD_SHAPE: Record<EdgeHead, EdgeHeadShape> = {
|
|
|
39
73
|
* **既定の形は名前を変えない** (`cdl-arrow-<tone>` / `cdl-arrow-<tone>-sm`)。
|
|
40
74
|
* 端の形を書いていない図はすべて既定なので、名前が変わると全図の矢頭が差し替わる。
|
|
41
75
|
*/
|
|
42
|
-
export function edgeHeadMarkerId(
|
|
76
|
+
export function edgeHeadMarkerId(
|
|
77
|
+
tone: string,
|
|
78
|
+
head: EdgeHead,
|
|
79
|
+
small: boolean,
|
|
80
|
+
fill: EdgeHeadFill = EDGE_HEAD_FILL_DEFAULT,
|
|
81
|
+
): string {
|
|
43
82
|
const 小 = small ? "-sm" : "";
|
|
44
|
-
|
|
83
|
+
// 塗り方は **効く形だけ** 名前に出す。 元から輪郭しか無い形 (開いた矢 / 三又) で名前を
|
|
84
|
+
// 分けると、中身の同じ marker が 2 つできる
|
|
85
|
+
const 塗 = fill === EDGE_HEAD_FILL_DEFAULT || !EDGE_HEAD_SHAPE[head].filled ? "" : `-${fill}`;
|
|
86
|
+
return head === EDGE_HEAD_DEFAULT && 塗 === ""
|
|
87
|
+
? `cdl-arrow-${tone}${小}`
|
|
88
|
+
: `cdl-arrow-${tone}-${head}${塗}${小}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* marker を 1 つ描くのに要る 4 つ。 定義側 (`render/stage.tsx`) が引く。
|
|
93
|
+
*
|
|
94
|
+
* **塗り方で色の当て方が入れ替わる**。 塗る形は面に色を置いて線を持たず、白抜きは面を紙の色に
|
|
95
|
+
* して輪郭に色を置く。 面を `"none"` にすると線が印の中を素通りして見えるので、白抜きでも
|
|
96
|
+
* 面は塗る (色が紙になるだけ)。
|
|
97
|
+
*/
|
|
98
|
+
export function edgeHeadPaint(
|
|
99
|
+
head: EdgeHead,
|
|
100
|
+
fill: EdgeHeadFill,
|
|
101
|
+
color: string,
|
|
102
|
+
): { fill: string; stroke: string; strokeWidth?: number } {
|
|
103
|
+
const 形 = EDGE_HEAD_SHAPE[head];
|
|
104
|
+
if (!形.filled) return { fill: "none", stroke: color, strokeWidth: 1.6 };
|
|
105
|
+
if (fill === "hollow") {
|
|
106
|
+
return { fill: "var(--cdl-node-fill, #ffffff)", stroke: color, strokeWidth: 1.4 };
|
|
107
|
+
}
|
|
108
|
+
return { fill: color, stroke: "none" };
|
|
45
109
|
}
|
package/src/render/edges.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { JSX } from "react";
|
|
2
|
-
import { EDGE_HEAD_DEFAULT, type LaidEdge } from "../types";
|
|
2
|
+
import { EDGE_HEAD_DEFAULT, EDGE_HEAD_FILL_DEFAULT, type LaidEdge } from "../types";
|
|
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";
|
|
@@ -45,9 +45,18 @@ export function CdlEdgeView({
|
|
|
45
45
|
// その色なら 0.9 で 4.51-4.92 と 4.5:1 を満たす (0.8 では 3.69-4.04 で届かない)。
|
|
46
46
|
// 実線と点線で同じ組を使い、 光らせた側が薄くなる逆転を作らない。
|
|
47
47
|
// 端の形は線の種類と別の軸 (#560)。 書かなければ既定の塗った三角で、名前も従来のまま
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
const 端の名前 = (h: typeof edge.head, f: typeof edge.headFill): string | undefined => {
|
|
49
|
+
const 形 = h ?? EDGE_HEAD_DEFAULT;
|
|
50
|
+
// 何も描かない端は marker を持たない (#578)。 参照すると解決できない url が残る
|
|
51
|
+
if (形 === "none") return undefined;
|
|
52
|
+
return hasTone(edge.tone)
|
|
53
|
+
? edgeHeadMarkerId(edge.tone, 形, !active, f ?? EDGE_HEAD_FILL_DEFAULT)
|
|
54
|
+
: "cdl-arrow-muted";
|
|
55
|
+
};
|
|
56
|
+
const marker = 端の名前(edge.head, edge.headFill);
|
|
57
|
+
// 出どころ側の印 (#578)。 marker は経路の向きに従って回るので、始点に置いた印は
|
|
58
|
+
// 自動で外向きになる (`orient="auto-start-reverse"`)。 書かない図では端が無い
|
|
59
|
+
const tailMarker = edge.tailHead === undefined ? undefined : 端の名前(edge.tailHead, edge.tailHeadFill);
|
|
51
60
|
|
|
52
61
|
const style = edge.style ?? "solid";
|
|
53
62
|
const isDotted = style === "dotted-flow";
|
|
@@ -100,6 +109,8 @@ export function CdlEdgeView({
|
|
|
100
109
|
progress={progress}
|
|
101
110
|
color={color}
|
|
102
111
|
marker={marker}
|
|
112
|
+
tailMarker={tailMarker}
|
|
113
|
+
dashed={style === "dashed"}
|
|
103
114
|
stateValues={stateValues}
|
|
104
115
|
/>
|
|
105
116
|
)}
|
|
@@ -129,13 +140,17 @@ function SolidEdgePath({
|
|
|
129
140
|
progress,
|
|
130
141
|
color,
|
|
131
142
|
marker,
|
|
143
|
+
tailMarker,
|
|
144
|
+
dashed,
|
|
132
145
|
stateValues,
|
|
133
146
|
}: {
|
|
134
147
|
edge: LaidEdge;
|
|
135
148
|
active: boolean;
|
|
136
149
|
progress: number;
|
|
137
150
|
color: string;
|
|
138
|
-
marker: string;
|
|
151
|
+
marker: string | undefined;
|
|
152
|
+
tailMarker: string | undefined;
|
|
153
|
+
dashed: boolean;
|
|
139
154
|
stateValues?: Record<string, string>;
|
|
140
155
|
}): JSX.Element {
|
|
141
156
|
const visibleD = active ? pathSubpath(edge.d, progress) : edge.d;
|
|
@@ -151,9 +166,14 @@ function SolidEdgePath({
|
|
|
151
166
|
? resolveNumericAttr(edge.dashOffsetBind, stateValues, 0)
|
|
152
167
|
: undefined;
|
|
153
168
|
// dashoffset は dasharray 前提、 signal 経由の流れる線を出したい時は "6 6" 等
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
169
|
+
// 刻みは 2 つの役目を持つ。 光っていないことの合図 (`"solid"` の光っていない側) と、
|
|
170
|
+
// 線の種類そのもの (`"dashed"`)。 **後者は光っても刻みのまま**にする = 意味を持つ刻みが
|
|
171
|
+
// 光った瞬間に消えると、その関係だけ種類が変わって見える (#578)
|
|
172
|
+
const dashArray = dashed
|
|
173
|
+
? "10 8"
|
|
174
|
+
: active
|
|
175
|
+
? boundDashOffset !== undefined ? "8 6" : undefined
|
|
176
|
+
: "4 5";
|
|
157
177
|
return (
|
|
158
178
|
<path
|
|
159
179
|
data-cdl-role="edge-line"
|
|
@@ -165,7 +185,8 @@ function SolidEdgePath({
|
|
|
165
185
|
strokeDasharray={dashArray}
|
|
166
186
|
strokeDashoffset={boundDashOffset}
|
|
167
187
|
strokeLinecap="round"
|
|
168
|
-
markerEnd={`url(#${marker})`}
|
|
188
|
+
markerEnd={marker === undefined ? undefined : `url(#${marker})`}
|
|
189
|
+
markerStart={tailMarker === undefined ? undefined : `url(#${tailMarker})`}
|
|
169
190
|
style={{
|
|
170
191
|
transition: "stroke 280ms ease, stroke-opacity 280ms ease, stroke-width 280ms ease",
|
|
171
192
|
}}
|
package/src/render/nodes.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { JSX } from "react";
|
|
2
2
|
import { ActorNode } from "../kinds/actor";
|
|
3
3
|
import { CardNode } from "../kinds/card";
|
|
4
|
+
import { SequenceBoardNode } from "../kinds/sequence-board";
|
|
4
5
|
import { EventNode } from "../kinds/event";
|
|
5
6
|
import { FunctionNode } from "../kinds/function";
|
|
6
7
|
import { StorageNode } from "../kinds/storage";
|
|
@@ -161,6 +162,9 @@ export function CdlNodeView({
|
|
|
161
162
|
return <EventNode node={resolvedNode} active={active} progress={progress} />;
|
|
162
163
|
case "card":
|
|
163
164
|
return <CardNode node={resolvedNode} active={active} />;
|
|
165
|
+
// 時系列のやり取り (#578)。 1 つの箱が図を丸ごと描く = 縦が時間で、参加者は箱を持たない
|
|
166
|
+
case "sequence-board":
|
|
167
|
+
return <SequenceBoardNode node={resolvedNode} stateValues={stateValues} />;
|
|
164
168
|
// 図表 9 種は payload が `{signal}` を取れるので stateValues を渡す
|
|
165
169
|
// (`render/payload-binding.ts` が描画直前に解決する)。 取れる欄は種別で違い、
|
|
166
170
|
// 数と語を取る 7 種に対し、mind-map / tree-hierarchy は **名前だけ** を取る (#467)。
|
package/src/render/stage.tsx
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { useMemo, type JSX } from "react";
|
|
2
|
-
import { EDGE_HEADS, type CdlPhase, type LaidDiagram } from "../types";
|
|
2
|
+
import { EDGE_HEADS, EDGE_HEAD_FILLS, EDGE_HEAD_FILL_DEFAULT, type CdlPhase, type LaidDiagram } from "../types";
|
|
3
3
|
import { CdlEdgeView, CdlEdgeGlowView, CdlEdgeLabelView } from "./edges";
|
|
4
4
|
import { CdlNodeView } from "./nodes";
|
|
5
5
|
import { TONE, TONE_HEX } from "./tone";
|
|
6
|
-
import { EDGE_HEAD_SHAPE, edgeHeadMarkerId } from "./edge-head";
|
|
6
|
+
import { EDGE_HEAD_SHAPE, edgeHeadMarkerId, edgeHeadPaint } from "./edge-head";
|
|
7
7
|
import { buildDiagramAltText } from "../layout/spec";
|
|
8
8
|
import { lifelineStartYs, uniformLifelineEndY } from "../layout/lifeline";
|
|
9
9
|
import { 描く進みに直す } from "../kinds/draw-ratio";
|
|
@@ -103,33 +103,44 @@ export function CdlStage({
|
|
|
103
103
|
(Issue の制約 = 色だけを足す)。 */}
|
|
104
104
|
{Object.entries(TONE).flatMap(([tone, color]) =>
|
|
105
105
|
([["", 14], ["-sm", 10]] as const).flatMap(([suffix, size]) =>
|
|
106
|
-
EDGE_HEADS.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
106
|
+
EDGE_HEADS.flatMap((head) =>
|
|
107
|
+
// 塗り方は形と別の軸 (#578)。 塗らない形では 2 つが同じ名前に畳まれるので、
|
|
108
|
+
// `edgeHeadMarkerId` が返す名前で重複を外す
|
|
109
|
+
EDGE_HEAD_FILLS.map((fill) => {
|
|
110
|
+
const 形 = EDGE_HEAD_SHAPE[head];
|
|
111
|
+
const id = edgeHeadMarkerId(tone, head, suffix === "-sm", fill);
|
|
112
|
+
// 何も描かない端は marker を定義しない (#578)。 線側も参照しない
|
|
113
|
+
if (形.d === "") return null;
|
|
114
|
+
if (fill !== EDGE_HEAD_FILL_DEFAULT && !形.filled) return null;
|
|
115
|
+
const 塗 = edgeHeadPaint(head, fill, color);
|
|
116
|
+
return (
|
|
117
|
+
<marker
|
|
118
|
+
key={id}
|
|
119
|
+
id={id}
|
|
120
|
+
viewBox={`0 0 ${形.w} ${形.h}`}
|
|
121
|
+
refX={形.refX}
|
|
122
|
+
refY={形.h / 2}
|
|
123
|
+
// 枠の縦を 10 に揃えてあるので、横だけが形の比で伸びる
|
|
124
|
+
markerWidth={(size * (形.scale ?? 1) * 形.w) / 形.h}
|
|
125
|
+
markerHeight={size * (形.scale ?? 1)}
|
|
126
|
+
markerUnits="userSpaceOnUse"
|
|
127
|
+
orient="auto-start-reverse"
|
|
128
|
+
>
|
|
129
|
+
<path
|
|
130
|
+
data-cdl-role="edge-arrowhead"
|
|
131
|
+
data-cdl-edge-head={head}
|
|
132
|
+
data-cdl-edge-head-fill={fill}
|
|
133
|
+
d={形.d}
|
|
134
|
+
fill={塗.fill}
|
|
135
|
+
stroke={塗.stroke}
|
|
136
|
+
strokeWidth={塗.strokeWidth}
|
|
137
|
+
strokeLinecap={塗.strokeWidth === undefined ? undefined : "round"}
|
|
138
|
+
strokeLinejoin={塗.strokeWidth === undefined ? undefined : "round"}
|
|
139
|
+
/>
|
|
140
|
+
</marker>
|
|
141
|
+
);
|
|
142
|
+
}),
|
|
143
|
+
),
|
|
133
144
|
),
|
|
134
145
|
)}
|
|
135
146
|
{/* tone が TONE 定義に無い edge の受け皿。 visual-validate の tone-marker-ref 軸が
|
|
@@ -12,7 +12,7 @@ import type { CdlNode } from "../types";
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/** `{名前}` を読む文字の欄 */
|
|
15
|
-
const TEXT_FIELDS = ["title", "eyebrow", "subtitle", "value", "wBind", "hBind", "visibleIf"] as const;
|
|
15
|
+
const TEXT_FIELDS = ["title", "eyebrow", "subtitle", "value", "wBind", "hBind", "visibleIf", "sequenceStep"] as const;
|
|
16
16
|
|
|
17
17
|
/** `{名前}` を読む文字の並びの欄 */
|
|
18
18
|
const LIST_FIELDS = ["rows"] as const;
|
|
@@ -58,7 +58,11 @@ type 読まない欄 =
|
|
|
58
58
|
| "funnelData"
|
|
59
59
|
| "quadrantData"
|
|
60
60
|
| "treeData"
|
|
61
|
-
| "journeyData"
|
|
61
|
+
| "journeyData"
|
|
62
|
+
// 行頭の印 (#578)。 字ではなく形なので、状態から値を差し込む対象にならない
|
|
63
|
+
| "rowMarks"
|
|
64
|
+
// 時系列のやり取りの中身 (#578)。 種別が内側で解決するので、ここでは差し込まない
|
|
65
|
+
| "sequenceData";
|
|
62
66
|
|
|
63
67
|
type 列挙した欄 =
|
|
64
68
|
| (typeof TEXT_FIELDS)[number]
|
package/src/types.ts
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SequenceBoardActor,
|
|
3
|
+
SequenceBoardBand,
|
|
4
|
+
SequenceBoardData,
|
|
5
|
+
SequenceBoardMessage,
|
|
6
|
+
SequenceBoardMessageKind,
|
|
7
|
+
} from "./kinds/sequence-board-metrics";
|
|
8
|
+
|
|
9
|
+
export type {
|
|
10
|
+
SequenceBoardActor,
|
|
11
|
+
SequenceBoardBand,
|
|
12
|
+
SequenceBoardData,
|
|
13
|
+
SequenceBoardMessage,
|
|
14
|
+
SequenceBoardMessageKind,
|
|
15
|
+
};
|
|
16
|
+
|
|
1
17
|
/** 意味付きの色の名前。 検査・描画・test はこの 1 つの列を出所にする。 */
|
|
2
18
|
export const TONES = ["accent", "teal", "success", "error", "warning", "info"] as const;
|
|
3
19
|
|
|
@@ -97,6 +113,8 @@ export const NODE_KINDS = [
|
|
|
97
113
|
// 流れの始まりと終わりの印 2 種 (#560) ... 塗った丸と輪で囲んだ丸。
|
|
98
114
|
// 状態遷移図が使う。 印そのものが意味を持つので文字は描かない
|
|
99
115
|
"mark-start", "mark-end",
|
|
116
|
+
// 時系列のやり取り (#578) ... 1 つの箱が図を丸ごと描く。 縦が時間で、参加者は箱を持たない
|
|
117
|
+
"sequence-board",
|
|
100
118
|
] as const;
|
|
101
119
|
|
|
102
120
|
export type NodeKind = (typeof NODE_KINDS)[number];
|
|
@@ -108,28 +126,125 @@ export type Side = "top" | "right" | "bottom" | "left";
|
|
|
108
126
|
* - "solid" (default) ... 実線 + 矢頭、 inactive は dashed grey
|
|
109
127
|
* - "dotted-flow" ... 点線 + active phase で粒子が path 沿いに流れる。
|
|
110
128
|
* path が経由 node を貫通する場合は自動で粒子が node 中心を貫通する動きに切替。
|
|
129
|
+
* - "dashed" ... 刻んだ線。 **光っていても刻みのまま**。
|
|
130
|
+
*
|
|
131
|
+
* `"dashed"` が要るのは、`"solid"` の刻みが「光っていない」 を表す合図に使われているため。
|
|
132
|
+
* 光ると実線に戻るので、線の種類として刻みを持たせられない。 UML の「満たす」 と「使う」、
|
|
133
|
+
* ER の「識別しない」 のように **刻みが意味を持つ関係** はこちらを使う。
|
|
111
134
|
*/
|
|
112
|
-
export type EdgeStyle = "solid" | "dotted-flow";
|
|
135
|
+
export type EdgeStyle = "solid" | "dotted-flow" | "dashed";
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 線の種類の全種 (#578)。 **型だけでなく実体を持つ**。
|
|
139
|
+
*
|
|
140
|
+
* 利用側 (dragon の記法) は受理する語を手で並べており、種類を足した時に取り残されていた
|
|
141
|
+
* (実測 = `dashed` を足した後、記法に書くと「線種が読めません」 で落ちた)。 一覧をここから
|
|
142
|
+
* 導けば、足した時点で書けるようになる。
|
|
143
|
+
*/
|
|
144
|
+
export const EDGE_STYLES: readonly EdgeStyle[] = ["solid", "dotted-flow", "dashed"];
|
|
113
145
|
|
|
114
146
|
/**
|
|
115
147
|
* 関係の **端の形** (#560)。 線の種類 (`EdgeStyle`) とは別の軸で、線の先端に何を置くかを決める。
|
|
116
148
|
*
|
|
149
|
+
* - `"none"` ... 何も描かない。 **端の 1 つに印を置かない関係** に使う (#578)
|
|
117
150
|
* - `"triangle"` ... 塗った三角。 **既定**。 書かなければこれになる
|
|
118
151
|
* - `"diamond"` ... 塗った菱。 「持っている」 側を指す
|
|
119
152
|
* - `"open"` ... 閉じない 2 本の腕。 向きだけを示し、待たない
|
|
120
153
|
* - `"crow"` ... 三又。 「多」 を示す (ER の鳥の足)
|
|
121
154
|
*
|
|
155
|
+
* ER の端は **2 つの記号が重なってできている** (#578)。 箱に近い側が個数 (棒 = 1 /
|
|
156
|
+
* 三又 = 多)、その外側が任意か (棒 = 必須 / 丸 = 任意)。 2 × 2 の 4 通りを 1 つの形として持つ。
|
|
157
|
+
*
|
|
158
|
+
* - `"one"` ... 棒が 2 本。 ちょうど 1 つ
|
|
159
|
+
* - `"zero-one"` ... 棒に丸。 1 つ、または無い
|
|
160
|
+
* - `"many"` ... 三又に棒。 少なくとも 1 つ
|
|
161
|
+
* - `"zero-many"` ... 三又に丸。 いくつでも、無くてもよい
|
|
162
|
+
*
|
|
163
|
+
* 4 通りを 2 つの欄に分けないのは、**端の印は 1 つの記号として読まれる**ため。 欄を分けると
|
|
164
|
+
* 片方だけ書いた形 (個数はあるが任意かが無い) を許すことになり、描けない組み合わせが増える。
|
|
165
|
+
*
|
|
122
166
|
* 線の種類と分けて持つのは、両者が独立に組み合わさるため。 実線 × 三角 (継ぐ) と
|
|
123
167
|
* 破線 × 三角 (満たす) は線種で分かれ、実線 × 三角 (継ぐ) と実線 × 菱 (持つ) は端で分かれる。
|
|
124
168
|
* 1 つの欄に畳むと 2 軸の積を名前で列挙することになり、組合せが増えるたびに語が増える。
|
|
125
169
|
*/
|
|
126
|
-
export type EdgeHead =
|
|
170
|
+
export type EdgeHead =
|
|
171
|
+
| "none"
|
|
172
|
+
| "triangle"
|
|
173
|
+
| "diamond"
|
|
174
|
+
| "open"
|
|
175
|
+
| "crow"
|
|
176
|
+
| "one"
|
|
177
|
+
| "zero-one"
|
|
178
|
+
| "many"
|
|
179
|
+
| "zero-many";
|
|
127
180
|
|
|
128
181
|
/** 書かなかった時の端の形。 既存の図はすべてこれで描かれている */
|
|
129
182
|
export const EDGE_HEAD_DEFAULT: EdgeHead = "triangle";
|
|
130
183
|
|
|
131
184
|
/** 端の形の全種。 marker の定義と検査の走査がこの並びを引く (書き写すとずれる) */
|
|
132
|
-
export const EDGE_HEADS: readonly EdgeHead[] = [
|
|
185
|
+
export const EDGE_HEADS: readonly EdgeHead[] = [
|
|
186
|
+
"none",
|
|
187
|
+
"triangle",
|
|
188
|
+
"diamond",
|
|
189
|
+
"open",
|
|
190
|
+
"crow",
|
|
191
|
+
"one",
|
|
192
|
+
"zero-one",
|
|
193
|
+
"many",
|
|
194
|
+
"zero-many",
|
|
195
|
+
];
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* 端の印の **塗り方** (#578)。 形 (`EdgeHead`) とは別の軸。
|
|
199
|
+
*
|
|
200
|
+
* - `"solid"` ... 墨で塗る。 **既定**。 書かなければ形ごとの既定 (`EDGE_HEAD_SHAPE.filled`) に従う
|
|
201
|
+
* - `"hollow"` ... 紙の色で塗り、輪郭だけを線で描く
|
|
202
|
+
*
|
|
203
|
+
* UML は同じ形を塗りで分ける = 白抜きの菱が「持つ」、塗った菱が「抱える」 (命が同じ)。
|
|
204
|
+
* 形を増やして表そうとすると、塗りの軸を持つ全ての形について語が倍になる。
|
|
205
|
+
*
|
|
206
|
+
* 塗らない形 (`"open"` / `"crow"`) には効かない。 元から輪郭しか無いため。
|
|
207
|
+
*/
|
|
208
|
+
export type EdgeHeadFill = "solid" | "hollow";
|
|
209
|
+
|
|
210
|
+
/** 書かなかった時の塗り方 */
|
|
211
|
+
export const EDGE_HEAD_FILL_DEFAULT: EdgeHeadFill = "solid";
|
|
212
|
+
|
|
213
|
+
/** 塗り方の全種。 marker の定義と検査の走査がこの並びを引く */
|
|
214
|
+
export const EDGE_HEAD_FILLS: readonly EdgeHeadFill[] = ["solid", "hollow"];
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* 行き先と出どころの **両端に印を置ける** (#578)。
|
|
218
|
+
*
|
|
219
|
+
* `head` が行き先、`tailHead` が出どころ。 どちらも `"none"` を書けばその端には何も描かない。
|
|
220
|
+
*
|
|
221
|
+
* 側が種類で割れるのが要点。 三角は「この先が親」 を指すので行き先に立ち、菱は
|
|
222
|
+
* 「この箱が持っている」 を指すので出どころに立つ。 ER は端ごとに違う個数を示すので両端に要る。
|
|
223
|
+
* 1 つの欄で「どちら側か」 を持つ形にすると、両端に置く関係が書けない。
|
|
224
|
+
*/
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* 行頭の印 (#578)。 **形 × 塗り の 2 軸**で 4 通りを分ける。
|
|
228
|
+
*
|
|
229
|
+
* 軸の意味は図の種類が決める = クラス図は 持ち物/振る舞い × 公開/非公開、
|
|
230
|
+
* ER 図は 値/外部キー × 必須/任意、状態遷移図は 状態の中/出入り × 続く/瞬間。
|
|
231
|
+
* 描画側が持つのは形だけで、意味は持たない。
|
|
232
|
+
*
|
|
233
|
+
* 印を持つ行は **区切り線を引かない**。 印が群を分けるので、線と印で二重に分けることになる。
|
|
234
|
+
*/
|
|
235
|
+
export type RowMark = {
|
|
236
|
+
/** 四角 (`"square"`) か山形 (`"chevron"`) か */
|
|
237
|
+
shape: "square" | "chevron";
|
|
238
|
+
/** 塗るか。 塗らない印は輪郭だけを描く */
|
|
239
|
+
filled: boolean;
|
|
240
|
+
/**
|
|
241
|
+
* 名前に下線を引くか (#578)。 ER の主キーに使う。
|
|
242
|
+
*
|
|
243
|
+
* **印の 2 軸とは別の段**に置く。 3 つ目の軸を印に足すと 4 通りが 8 通りになって
|
|
244
|
+
* 見分けが付かなくなるので、下線を重ねる形にした = 外部キーの山形と重ねられる。
|
|
245
|
+
*/
|
|
246
|
+
underline?: boolean;
|
|
247
|
+
};
|
|
133
248
|
|
|
134
249
|
export type CdlLane = {
|
|
135
250
|
id: string;
|
|
@@ -215,6 +330,29 @@ export type CdlNode = {
|
|
|
215
330
|
value?: string;
|
|
216
331
|
/** kind=storage 時の行 (各 mono、 state 参照可) */
|
|
217
332
|
rows?: string[];
|
|
333
|
+
/**
|
|
334
|
+
* 行頭の印 (#578)。 `rows` と同じ並びで、`null` の位置には印を描かない。
|
|
335
|
+
*
|
|
336
|
+
* **印を持つ行は書き方そのものが変わる**。 区切り線 2 本を引かず、名前を印の右に置き、
|
|
337
|
+
* 型を箱の右端へ薄く右揃えで置く。 線で分けるのをやめて印で分ける形なので、両方は要らない。
|
|
338
|
+
*
|
|
339
|
+
* 空文字の行と `null` の印を組にすると **群の間** になる (持ち物と振る舞いの間など)。
|
|
340
|
+
*/
|
|
341
|
+
rowMarks?: readonly (RowMark | null)[];
|
|
342
|
+
/**
|
|
343
|
+
* 時系列のやり取りの中身 (#578)。 `sequence-board` 種別が読む。
|
|
344
|
+
*
|
|
345
|
+
* 面々と言づてと帯を 1 つの箱に持たせ、内側の配置は種別が決める。 図表の種別
|
|
346
|
+
* (`gantt-timeline` 等) と同じ持ち方。
|
|
347
|
+
*/
|
|
348
|
+
sequenceData?: SequenceBoardData;
|
|
349
|
+
/**
|
|
350
|
+
* いま何番目の言づてを見ているか (#578)。 `"{seq}"` のように状態から取る。
|
|
351
|
+
*
|
|
352
|
+
* 書かない図では全部を濃く出す = 段を持たない使い方 (縮小見本 / 静止画) で薄い絵にすると、
|
|
353
|
+
* 読み手には壊れて見える。
|
|
354
|
+
*/
|
|
355
|
+
sequenceStep?: string;
|
|
218
356
|
/** width override (default は NodeKind 別 NODE_SIZE)、 catalog thumbnail 用に縮小可 */
|
|
219
357
|
w?: number;
|
|
220
358
|
/** height override */
|
|
@@ -492,6 +630,21 @@ export type CdlEdge = {
|
|
|
492
630
|
style?: EdgeStyle;
|
|
493
631
|
/** 端の形、 default "triangle" (#560)。 線の種類とは別の軸 */
|
|
494
632
|
head?: EdgeHead;
|
|
633
|
+
/**
|
|
634
|
+
* 端の印の塗り方 (#578)。 書かなければ形ごとの既定。 塗らない形には効かない。
|
|
635
|
+
*
|
|
636
|
+
* 形と分けて持つのは、UML が同じ形を塗りで分けるため (白抜きの菱 = 持つ /
|
|
637
|
+
* 塗った菱 = 抱える)。
|
|
638
|
+
*/
|
|
639
|
+
headFill?: EdgeHeadFill;
|
|
640
|
+
/**
|
|
641
|
+
* **出どころ側**の端の印 (#578)。 書かなければ何も描かない。
|
|
642
|
+
*
|
|
643
|
+
* 行き先側は `head`。 両方に置けるので、ER のように端ごとに違う個数を示す関係が書ける。
|
|
644
|
+
*/
|
|
645
|
+
tailHead?: EdgeHead;
|
|
646
|
+
/** 出どころ側の印の塗り (#578)。 書かなければ形ごとの既定 */
|
|
647
|
+
tailHeadFill?: EdgeHeadFill;
|
|
495
648
|
/**
|
|
496
649
|
* FSM transition の guard 条件 (例 "if validated")。
|
|
497
650
|
* stateMachine preset では sub に併合される、 text-dsl v0.5 では guard 単独で渡せる。
|
package/src/visual-validate.ts
CHANGED
|
@@ -1921,6 +1921,14 @@ function runAxes(
|
|
|
1921
1921
|
if (!isRenderedNode(n)) continue;
|
|
1922
1922
|
for (const row of n.rows) {
|
|
1923
1923
|
if (DIVIDER_ONLY.test(row)) continue;
|
|
1924
|
+
/*
|
|
1925
|
+
* **行頭の印を持つ箱では形式を求めない** (#578 の設計)。
|
|
1926
|
+
*
|
|
1927
|
+
* 印を出す形では、行が何を指すかは字ではなく印が示す。 群の区切りは空の行で置き、
|
|
1928
|
+
* ふるまいの行 (`在庫を押さえる`) は名前だけで値を持たない = `key: value` に当てはまらない
|
|
1929
|
+
* 行が正しい出力になる。 印の有無で分けるのは、印を持つ箱がその形で描かれるため。
|
|
1930
|
+
*/
|
|
1931
|
+
if (n.rowMarks !== undefined) continue;
|
|
1924
1932
|
if (!ROW_FORMAT.test(row)) {
|
|
1925
1933
|
push("row-format", `node "${n.id}" row "${row}" が "key: value" 形式でない`);
|
|
1926
1934
|
}
|
|
@@ -2485,9 +2493,18 @@ function runAxes(
|
|
|
2485
2493
|
for (const [stack, group] of byStack) {
|
|
2486
2494
|
if (group.length < 2) continue;
|
|
2487
2495
|
const cy0 = group[0]!.cy;
|
|
2496
|
+
/*
|
|
2497
|
+
* **上端で揃える形も認める** (#578 の設計)。
|
|
2498
|
+
*
|
|
2499
|
+
* 高さの違う箱を横に並べる図 (表 / クラス / 状態) では、中心ではなく上端を揃える。
|
|
2500
|
+
* 中心だけを見ると、正しく上端で揃っている行が高さの差の半分だけずれて見える
|
|
2501
|
+
* (実測 = 行数の違う表で 28 の差)。 どちらかで揃っていれば揃っているとみなす。
|
|
2502
|
+
*/
|
|
2503
|
+
const top0 = cy0 - (group[0]!.h ?? 0) / 2;
|
|
2488
2504
|
for (const n of group.slice(1)) {
|
|
2489
2505
|
const diff = Math.abs(n.cy - cy0);
|
|
2490
|
-
|
|
2506
|
+
const topDiff = Math.abs(n.cy - (n.h ?? 0) / 2 - top0);
|
|
2507
|
+
if (diff > MIN_ROW_ALIGNMENT_TOLERANCE && topDiff > MIN_ROW_ALIGNMENT_TOLERANCE) {
|
|
2491
2508
|
push(
|
|
2492
2509
|
"row-alignment",
|
|
2493
2510
|
`stack=${stack} row 内 node "${n.id}" cy=${n.cy.toFixed(1)} が row 基準 cy=${cy0.toFixed(1)} と ${diff.toFixed(1)} world 差 (許容 ${MIN_ROW_ALIGNMENT_TOLERANCE})`,
|
|
@@ -4221,15 +4238,25 @@ function runAxes(
|
|
|
4221
4238
|
if (group.length < 3) continue;
|
|
4222
4239
|
const sorted = [...group].sort((a, b) => a.cy - b.cy);
|
|
4223
4240
|
const gaps: number[] = [];
|
|
4241
|
+
/*
|
|
4242
|
+
* **格子に置く形では上端どうしの間隔で測る** (#578 の設計)。
|
|
4243
|
+
*
|
|
4244
|
+
* 段の位置を著者が決める図 (クラス / 表 / 状態) では、engine が制御しているのは
|
|
4245
|
+
* 段の基準線であって端の間隔ではない。 箱の高さが段ごとに違うと端の間隔は必ずばらつく
|
|
4246
|
+
* (実測 = クラス図の 2 列目で 132 と 300)。 どちらかが揃っていれば揃っているとみなす。
|
|
4247
|
+
*/
|
|
4248
|
+
const tops: number[] = [];
|
|
4224
4249
|
for (let i = 1; i < sorted.length; i++) {
|
|
4225
4250
|
const prev = sorted[i - 1]!;
|
|
4226
4251
|
const cur = sorted[i]!;
|
|
4227
4252
|
gaps.push(cur.cy - cur.h / 2 - (prev.cy + prev.h / 2));
|
|
4253
|
+
tops.push(cur.cy - cur.h / 2 - (prev.cy - prev.h / 2));
|
|
4228
4254
|
}
|
|
4229
4255
|
const maxGap = Math.max(...gaps);
|
|
4230
4256
|
const minGap = Math.min(...gaps);
|
|
4231
4257
|
const variance = maxGap - minGap;
|
|
4232
|
-
|
|
4258
|
+
const topVariance = Math.max(...tops) - Math.min(...tops);
|
|
4259
|
+
if (variance > COLUMN_GAP_VARIANCE_TOL && topVariance > COLUMN_GAP_VARIANCE_TOL) {
|
|
4233
4260
|
push(
|
|
4234
4261
|
"column-gap-uniform",
|
|
4235
4262
|
`lane "${laneId}" 内 node 間の端間 gap variance ${variance.toFixed(1)} world が spec ${COLUMN_GAP_VARIANCE_TOL} 超過 (min=${minGap.toFixed(0)} max=${maxGap.toFixed(0)})`,
|