@cardenelabs/dragon 0.34.0 → 0.35.1
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/README.md +39 -720
- package/dist/cli.cjs +4529 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.js +4523 -0
- package/dist/cli.js.map +1 -0
- package/dist/export.cjs +3912 -0
- package/dist/export.cjs.map +1 -0
- package/dist/export.d.cts +36 -0
- package/dist/export.d.ts +36 -0
- package/dist/export.js +3907 -0
- package/dist/export.js.map +1 -0
- package/dist/fonts.cjs +7 -0
- package/dist/fonts.cjs.map +1 -0
- package/dist/fonts.js +4 -0
- package/dist/fonts.js.map +1 -0
- package/dist/index.cjs +4105 -20356
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +227 -5522
- package/dist/index.d.ts +227 -5522
- package/dist/index.js +4091 -20283
- package/dist/index.js.map +1 -1
- package/dist/player.global.js +2 -0
- package/dist/schema-BBWfniwH.d.cts +112 -0
- package/dist/schema-BBWfniwH.d.ts +112 -0
- package/package.json +53 -27
- package/src/cli.ts +237 -0
- package/src/export/capture.ts +277 -0
- package/src/export/fonts.ts +68 -0
- package/src/export/html.ts +32 -0
- package/src/export/index.ts +146 -0
- package/src/field-messages.ts +104 -0
- package/src/figure-text.ts +42 -0
- package/src/index.ts +30 -283
- package/src/layout/architecture.ts +381 -0
- package/src/layout/measure.ts +21 -0
- package/src/layout/scene.ts +353 -0
- package/src/layout/sequence.ts +189 -0
- package/src/load-fonts.ts +8 -0
- package/src/look/fonts.ts +33 -0
- package/src/look/palettes.json +209 -0
- package/src/look/values.ts +155 -0
- package/src/markdown.ts +35 -0
- package/src/messages.ts +122 -0
- package/src/parser.ts +342 -310
- package/src/player-global.ts +4 -0
- package/src/player.ts +271 -0
- package/src/printer.ts +76 -0
- package/src/render/icons.ts +41 -0
- package/src/render/motion.ts +106 -0
- package/src/render/parts.ts +279 -0
- package/src/render/svg.ts +341 -0
- package/src/schema.ts +161 -16
- package/src/schemas/diagram.schema.json +150 -0
- package/src/schemas/validate-diagram.d.ts +12 -0
- package/src/schemas/validate-diagram.js +8 -0
- package/src/timeline.ts +166 -0
- package/src/tokenize.ts +20 -281
- package/src/version.ts +55 -0
- package/src/view.ts +16 -0
- package/src/words.ts +79 -0
- package/examples/quick-start.md +0 -142
- package/src/canvas-bounds.ts +0 -52
- package/src/color.ts +0 -197
- package/src/compile/actor-ids.ts +0 -297
- package/src/compile/actor-option-notice.ts +0 -379
- package/src/compile/actors.ts +0 -103
- package/src/compile/c4.ts +0 -74
- package/src/compile/chain.ts +0 -93
- package/src/compile/chart-fields.ts +0 -193
- package/src/compile/deep-rewrite.ts +0 -30
- package/src/compile/derived-source.ts +0 -18
- package/src/compile/direction.ts +0 -102
- package/src/compile/edge-option-notice.ts +0 -112
- package/src/compile/er-relation.ts +0 -200
- package/src/compile/flow.ts +0 -54
- package/src/compile/flowchart.ts +0 -344
- package/src/compile/funnel.ts +0 -87
- package/src/compile/gantt.ts +0 -337
- package/src/compile/generic.ts +0 -696
- package/src/compile/hierarchy.ts +0 -92
- package/src/compile/journey.ts +0 -93
- package/src/compile/kinds.ts +0 -139
- package/src/compile/lanes.ts +0 -138
- package/src/compile/metro.ts +0 -271
- package/src/compile/mind.ts +0 -393
- package/src/compile/node-title.ts +0 -24
- package/src/compile/notice.ts +0 -141
- package/src/compile/order.ts +0 -57
- package/src/compile/parts-lookup.ts +0 -17
- package/src/compile/parts.ts +0 -2709
- package/src/compile/placement-apply.ts +0 -689
- package/src/compile/placement.ts +0 -15
- package/src/compile/quadrant.ts +0 -265
- package/src/compile/record.ts +0 -75
- package/src/compile/row-marks.ts +0 -128
- package/src/compile/rows.ts +0 -40
- package/src/compile/sequence.ts +0 -305
- package/src/compile/slug.ts +0 -37
- package/src/compile/stages.ts +0 -247
- package/src/compile/subtitle.ts +0 -35
- package/src/compile/swimlane-marks.ts +0 -87
- package/src/compile/swimlane.ts +0 -82
- package/src/compile/timeline.ts +0 -213
- package/src/compile/topology.ts +0 -44
- package/src/compile/tree.ts +0 -69
- package/src/compile/value-chart.ts +0 -337
- package/src/compile/values.ts +0 -432
- package/src/compile/word-state.ts +0 -82
- package/src/compile.ts +0 -3168
- package/src/focus.ts +0 -46
- package/src/input-size.ts +0 -181
- package/src/json-line-map.ts +0 -30
- package/src/json-parser.ts +0 -4306
- package/src/keywords.ts +0 -417
- package/src/near-name.ts +0 -42
- package/src/notation-lint.ts +0 -342
- package/src/relative-pos.ts +0 -251
- package/src/schemas/diagram.json +0 -3088
- package/src/types.ts +0 -1009
- package/src/v05/index.ts +0 -9
- package/src/v05/input-table.generated.ts +0 -138
- package/src/v05/parser-types.ts +0 -32
- package/src/v05/parser.ts +0 -6668
- package/src/v05/readout-table.generated.ts +0 -1088
- package/src/v05/removed-syntax.ts +0 -115
- package/src/value-syntax.ts +0 -313
- package/src/write-position.ts +0 -270
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 置き場所を決めた結果 (場面) の形と、図の種類に依らず使う置き方。
|
|
3
|
+
*
|
|
4
|
+
* 座標は字が原寸の時の px で、左上を (0, 0) にする。線の名前は線の脇から箱を避けて置き、
|
|
5
|
+
* 中身の帯は図の下に横いっぱいに置く。
|
|
6
|
+
*/
|
|
7
|
+
import type { Family } from "../look/fonts";
|
|
8
|
+
import { COLOR_ORDER, type ColorKey, LOOK, TYPE } from "../look/values";
|
|
9
|
+
import type { Carry, Diagram, DiagramEvent, LabelSide } from "../schema";
|
|
10
|
+
import { hasWide, textWidth } from "./measure";
|
|
11
|
+
|
|
12
|
+
export type Point = [number, number];
|
|
13
|
+
|
|
14
|
+
export interface Rect {
|
|
15
|
+
x: number;
|
|
16
|
+
y: number;
|
|
17
|
+
w: number;
|
|
18
|
+
h: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SceneBox extends Rect {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
sub?: string;
|
|
25
|
+
icon?: string;
|
|
26
|
+
color: ColorKey;
|
|
27
|
+
cx: number;
|
|
28
|
+
cy: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface SceneRegion extends Rect {
|
|
32
|
+
id: string;
|
|
33
|
+
/** 名前を折り返した行 */
|
|
34
|
+
lines: string[];
|
|
35
|
+
note?: string;
|
|
36
|
+
icon?: string;
|
|
37
|
+
color: ColorKey;
|
|
38
|
+
/** 名前と印が占める所 */
|
|
39
|
+
header: Rect;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 線。`step` と `event` は原本の `steps[step].events[event]` を指す。 */
|
|
43
|
+
export interface SceneEdge {
|
|
44
|
+
step: number;
|
|
45
|
+
event: number;
|
|
46
|
+
from: string;
|
|
47
|
+
to: string;
|
|
48
|
+
points: Point[];
|
|
49
|
+
color: ColorKey;
|
|
50
|
+
carry: Carry;
|
|
51
|
+
/** 着いた運ぶ物が留まる点。無ければ線の終わり */
|
|
52
|
+
settle?: Point;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface SceneLabel extends Rect {
|
|
56
|
+
step: number;
|
|
57
|
+
event: number;
|
|
58
|
+
text: string;
|
|
59
|
+
family: Family;
|
|
60
|
+
color: ColorKey;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 時系列図の箱の中の処理。生存線の上に札で出す。 */
|
|
64
|
+
export interface SceneAction extends Rect {
|
|
65
|
+
step: number;
|
|
66
|
+
event: number;
|
|
67
|
+
box: string;
|
|
68
|
+
text: string;
|
|
69
|
+
color: ColorKey;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface SceneBadge {
|
|
73
|
+
step: number;
|
|
74
|
+
x: number;
|
|
75
|
+
y: number;
|
|
76
|
+
color: ColorKey;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface SceneLifeline {
|
|
80
|
+
id: string;
|
|
81
|
+
x: number;
|
|
82
|
+
y1: number;
|
|
83
|
+
y2: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface SceneField {
|
|
87
|
+
name: string;
|
|
88
|
+
x: number;
|
|
89
|
+
y: number;
|
|
90
|
+
w: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface SceneStrip extends Rect {
|
|
94
|
+
title: string;
|
|
95
|
+
fields: SceneField[];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface Layout {
|
|
99
|
+
kind: Diagram["kind"];
|
|
100
|
+
width: number;
|
|
101
|
+
height: number;
|
|
102
|
+
regions: SceneRegion[];
|
|
103
|
+
boxes: SceneBox[];
|
|
104
|
+
lifelines: SceneLifeline[];
|
|
105
|
+
edges: SceneEdge[];
|
|
106
|
+
labels: SceneLabel[];
|
|
107
|
+
actions: SceneAction[];
|
|
108
|
+
badges: SceneBadge[];
|
|
109
|
+
strip?: SceneStrip;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export type EdgeEvent = Extract<DiagramEvent, { type: "edge" }>;
|
|
113
|
+
|
|
114
|
+
/** 段と線の位置を添えた線の並び。段の順、段の中は起きる順。 */
|
|
115
|
+
export function edgesOf(diagram: Diagram): { step: number; event: number; edge: EdgeEvent }[] {
|
|
116
|
+
return diagram.steps.flatMap((step, s) =>
|
|
117
|
+
step.events.flatMap((event, e) => (event.type === "edge" ? [{ step: s, event: e, edge: event }] : [])),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 箱の色。書いていなければ区画の色、区画も無ければ並んだ順に分類の色を配る。 */
|
|
122
|
+
export function boxColors(diagram: Diagram): Map<string, ColorKey> {
|
|
123
|
+
const regions = regionColors(diagram);
|
|
124
|
+
const colors = new Map<string, ColorKey>();
|
|
125
|
+
diagram.boxes.forEach((box, i) => {
|
|
126
|
+
const fromRegion = box.region === undefined ? undefined : regions.get(box.region);
|
|
127
|
+
colors.set(box.id, box.color ?? fromRegion ?? (COLOR_ORDER[i % COLOR_ORDER.length] as ColorKey));
|
|
128
|
+
});
|
|
129
|
+
return colors;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function regionColors(diagram: Diagram): Map<string, ColorKey> {
|
|
133
|
+
const colors = new Map<string, ColorKey>();
|
|
134
|
+
(diagram.regions ?? []).forEach((region, i) => colors.set(region.id, region.color ?? (COLOR_ORDER[i % COLOR_ORDER.length] as ColorKey)));
|
|
135
|
+
return colors;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** 線の色。硬貨を運ぶ線は硬貨の色、それ以外は送る側の箱の色。 */
|
|
139
|
+
export const edgeColor = (edge: EdgeEvent, colors: Map<string, ColorKey>): ColorKey =>
|
|
140
|
+
edge.carry.kind === "coin" ? "coin" : (colors.get(edge.from) ?? "slate");
|
|
141
|
+
|
|
142
|
+
/** 箱の幅。名前、下の小さな字、書き換わる字、中で出す処理のどれもが収まる幅。 */
|
|
143
|
+
export function boxWidth(diagram: Diagram, id: string, withActions: boolean): number {
|
|
144
|
+
const box = diagram.boxes.find((b) => b.id === id);
|
|
145
|
+
if (!box) return LOOK.box.minW;
|
|
146
|
+
const lines = [textWidth(box.name, TYPE.boxName), textWidth(box.sub ?? "", TYPE.boxSub)];
|
|
147
|
+
for (const step of diagram.steps) {
|
|
148
|
+
for (const event of step.events) {
|
|
149
|
+
if (event.type === "swap" && event.box === id) lines.push(textWidth(event.text, TYPE.boxSubStrong));
|
|
150
|
+
if (withActions && event.type === "action" && event.box === id) lines.push(textWidth(event.label, TYPE.boxSubStrong));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return Math.max(LOOK.box.minW, Math.max(...lines) + LOOK.box.padX * 2);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** 2 つの四角の隙間。重なる時は負。縦か横のどちらかで離れていれば、その離れ方の大きい方。 */
|
|
157
|
+
export function gapBetween(a: Rect, b: Rect): number {
|
|
158
|
+
const dx = Math.max(b.x - (a.x + a.w), a.x - (b.x + b.w));
|
|
159
|
+
const dy = Math.max(b.y - (a.y + a.h), a.y - (b.y + b.h));
|
|
160
|
+
return Math.max(dx, dy);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** 折れ線のうち一番長い区間。 */
|
|
164
|
+
export function longest(points: Point[]): { a: Point; b: Point; vertical: boolean } {
|
|
165
|
+
let best = { a: points[0] as Point, b: (points[1] ?? points[0]) as Point, length: -1 };
|
|
166
|
+
for (let i = 1; i < points.length; i++) {
|
|
167
|
+
const a = points[i - 1] as Point;
|
|
168
|
+
const b = points[i] as Point;
|
|
169
|
+
const length = Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
170
|
+
if (length > best.length) best = { a, b, length };
|
|
171
|
+
}
|
|
172
|
+
return { a: best.a, b: best.b, vertical: Math.abs(best.b[0] - best.a[0]) < Math.abs(best.b[1] - best.a[1]) };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** 折れ線の長さと、始まりから `distance` 進んだ点。 */
|
|
176
|
+
export function along(points: Point[], distance: number): Point {
|
|
177
|
+
let left = distance;
|
|
178
|
+
for (let i = 1; i < points.length; i++) {
|
|
179
|
+
const a = points[i - 1] as Point;
|
|
180
|
+
const b = points[i] as Point;
|
|
181
|
+
const length = Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
182
|
+
if (left <= length && length > 0) return [a[0] + ((b[0] - a[0]) * left) / length, a[1] + ((b[1] - a[1]) * left) / length];
|
|
183
|
+
left -= length;
|
|
184
|
+
}
|
|
185
|
+
return points[points.length - 1] as Point;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function pathLength(points: Point[]): number {
|
|
189
|
+
let sum = 0;
|
|
190
|
+
for (let i = 1; i < points.length; i++) {
|
|
191
|
+
const a = points[i - 1] as Point;
|
|
192
|
+
const b = points[i] as Point;
|
|
193
|
+
sum += Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
194
|
+
}
|
|
195
|
+
return sum;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** 段の番号を置く点。線の始まりから少し進んだ所。短い線は始まりに置くと箱と重なるので中ほど。 */
|
|
199
|
+
export function badgeOnPath(points: Point[]): Point {
|
|
200
|
+
const length = pathLength(points);
|
|
201
|
+
return along(points, length < LOOK.badge.short ? length / 2 : LOOK.badge.along);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export interface LabelRequest {
|
|
205
|
+
step: number;
|
|
206
|
+
event: number;
|
|
207
|
+
text: string;
|
|
208
|
+
points: Point[];
|
|
209
|
+
side?: LabelSide;
|
|
210
|
+
color: ColorKey;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export interface Obstacles {
|
|
214
|
+
/** 10px 以上離す物 (箱) */
|
|
215
|
+
hard: Rect[];
|
|
216
|
+
/** 重ならなければよい物 (区画の名前、同じ段の線の名前) */
|
|
217
|
+
soft: Rect[];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export const labelStyle = (text: string) => (hasWide(text) ? TYPE.labelJa : TYPE.label);
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* 線の名前を置く。線の一番長い区間の中ほどから、寄せの指示の側 (無ければ横の線は上、縦の線は右) へ
|
|
224
|
+
* 離して置く。箱や区画の名前に掛かる時は、指示の側へさらに離し、線に沿ってもずらして空きを探す。
|
|
225
|
+
*/
|
|
226
|
+
export function placeLabel(request: LabelRequest, obstacles: Obstacles): SceneLabel {
|
|
227
|
+
const w = textWidth(request.text, labelStyle(request.text)) + LOOK.label.padX * 2;
|
|
228
|
+
const h = LOOK.label.h;
|
|
229
|
+
const segment = longest(request.points);
|
|
230
|
+
const mx = (segment.a[0] + segment.b[0]) / 2;
|
|
231
|
+
const my = (segment.a[1] + segment.b[1]) / 2;
|
|
232
|
+
const side = request.side ?? (segment.vertical ? "right" : "above");
|
|
233
|
+
const { offset, side: sideGap } = LOOK.label;
|
|
234
|
+
const base: Rect =
|
|
235
|
+
side === "above"
|
|
236
|
+
? { x: mx - w / 2, y: my - offset - h / 2, w, h }
|
|
237
|
+
: side === "below"
|
|
238
|
+
? { x: mx - w / 2, y: my + offset - h / 2, w, h }
|
|
239
|
+
: side === "left"
|
|
240
|
+
? { x: mx - sideGap - w, y: my - h / 2, w, h }
|
|
241
|
+
: { x: mx + sideGap, y: my - h / 2, w, h };
|
|
242
|
+
// 離す向き (指示の側) と、ずらす向き (それに直交する向き)
|
|
243
|
+
const away: Point = side === "above" ? [0, -1] : side === "below" ? [0, 1] : side === "left" ? [-1, 0] : [1, 0];
|
|
244
|
+
const slideAxis: Point = [Math.abs(away[1]), Math.abs(away[0])];
|
|
245
|
+
const slideMax = side === "above" || side === "below" ? w : h * 4;
|
|
246
|
+
const clear = (rect: Rect): boolean =>
|
|
247
|
+
obstacles.hard.every((box) => gapBetween(rect, box) >= LOOK.gap) && obstacles.soft.every((other) => gapBetween(rect, other) >= LOOK.label.soft);
|
|
248
|
+
const { search, searchMax } = LOOK.label;
|
|
249
|
+
for (let d = 0; d <= searchMax; d += search) {
|
|
250
|
+
for (let s = 0; s <= slideMax; s += search) {
|
|
251
|
+
for (const sign of s === 0 ? [1] : [1, -1]) {
|
|
252
|
+
const rect = { x: base.x + away[0] * d + slideAxis[0] * s * sign, y: base.y + away[1] * d + slideAxis[1] * s * sign, w, h };
|
|
253
|
+
if (clear(rect)) return { ...rect, step: request.step, event: request.event, text: request.text, family: labelStyle(request.text).family, color: request.color };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return { ...base, step: request.step, event: request.event, text: request.text, family: labelStyle(request.text).family, color: request.color };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** 中身の帯の欄の幅。欄の名前と、入る値と札を並べた最も長い形が収まる幅。 */
|
|
261
|
+
function fieldWidth(diagram: Diagram, name: string): number {
|
|
262
|
+
let value = 0;
|
|
263
|
+
let tag = 0;
|
|
264
|
+
for (const step of diagram.steps) {
|
|
265
|
+
for (const event of step.events) {
|
|
266
|
+
if (event.type !== "strip" || event.field !== name) continue;
|
|
267
|
+
if (event.value !== undefined) value = Math.max(value, textWidth(event.value, hasWide(event.value) ? TYPE.fieldValueJa : TYPE.fieldValue));
|
|
268
|
+
if (event.tag !== undefined) tag = Math.max(tag, textWidth(event.tag, TYPE.tag) + LOOK.strip.tagPadX * 2);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const both = value + (tag > 0 ? (value > 0 ? LOOK.strip.tagGap : 0) + tag : 0);
|
|
272
|
+
return Math.max(textWidth(name, TYPE.fieldName), both);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** 1 行に並べた時に帯が要る幅。 */
|
|
276
|
+
export function stripNeed(diagram: Diagram): number {
|
|
277
|
+
if (!diagram.strip) return 0;
|
|
278
|
+
const { titleX, fieldGap, pad } = LOOK.strip;
|
|
279
|
+
const start = titleX + textWidth(diagram.strip.title, TYPE.stripTitle) + fieldGap;
|
|
280
|
+
return start + diagram.strip.fields.reduce((sum, name) => sum + fieldWidth(diagram, name) + fieldGap, 0) - fieldGap + pad;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** 中身の帯。欄は左から並べ、幅に収まらない欄は次の行へ送る。 */
|
|
284
|
+
export function layoutStrip(diagram: Diagram, x: number, y: number, width: number): SceneStrip | undefined {
|
|
285
|
+
if (!diagram.strip) return undefined;
|
|
286
|
+
const { titleX, fieldGap, pad, line, h } = LOOK.strip;
|
|
287
|
+
const start = titleX + textWidth(diagram.strip.title, TYPE.stripTitle) + fieldGap;
|
|
288
|
+
const fields: SceneField[] = [];
|
|
289
|
+
let cx = start;
|
|
290
|
+
let row = 0;
|
|
291
|
+
for (const name of diagram.strip.fields) {
|
|
292
|
+
const w = fieldWidth(diagram, name);
|
|
293
|
+
if (cx > start && cx + w > width - pad) {
|
|
294
|
+
cx = start;
|
|
295
|
+
row++;
|
|
296
|
+
}
|
|
297
|
+
fields.push({ name, x: x + cx, y: y + row * line, w });
|
|
298
|
+
cx += w + fieldGap;
|
|
299
|
+
}
|
|
300
|
+
return { x, y, w: width, h: h + row * line, title: diagram.strip.title, fields };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** 場面の全ての物を (dx, dy) だけ動かす。区画と帯は動かさず、横いっぱいの形を保つ。 */
|
|
304
|
+
export function shiftContent(layout: Layout, dx: number, dy: number): void {
|
|
305
|
+
const move = (rect: Rect): void => {
|
|
306
|
+
rect.x += dx;
|
|
307
|
+
rect.y += dy;
|
|
308
|
+
};
|
|
309
|
+
for (const box of layout.boxes) {
|
|
310
|
+
move(box);
|
|
311
|
+
box.cx += dx;
|
|
312
|
+
box.cy += dy;
|
|
313
|
+
}
|
|
314
|
+
for (const region of layout.regions) {
|
|
315
|
+
region.y += dy;
|
|
316
|
+
move(region.header);
|
|
317
|
+
}
|
|
318
|
+
for (const line of layout.lifelines) {
|
|
319
|
+
line.x += dx;
|
|
320
|
+
line.y1 += dy;
|
|
321
|
+
line.y2 += dy;
|
|
322
|
+
}
|
|
323
|
+
for (const edge of layout.edges) {
|
|
324
|
+
edge.points = edge.points.map(([px, py]) => [px + dx, py + dy]);
|
|
325
|
+
if (edge.settle) edge.settle = [edge.settle[0] + dx, edge.settle[1] + dy];
|
|
326
|
+
}
|
|
327
|
+
for (const label of layout.labels) move(label);
|
|
328
|
+
for (const action of layout.actions) move(action);
|
|
329
|
+
for (const badge of layout.badges) {
|
|
330
|
+
badge.x += dx;
|
|
331
|
+
badge.y += dy;
|
|
332
|
+
}
|
|
333
|
+
if (layout.strip) {
|
|
334
|
+
layout.strip.y += dy;
|
|
335
|
+
for (const field of layout.strip.fields) {
|
|
336
|
+
field.x += dx;
|
|
337
|
+
field.y += dy;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** 線も処理も無い段の番号。書き換わる箱の左上か、中身の帯の左上か、最初の箱の左上。 */
|
|
343
|
+
export function cornerBadge(events: DiagramEvent[], step: number, layout: Layout): SceneBadge {
|
|
344
|
+
const { inset } = LOOK.badge;
|
|
345
|
+
for (const event of events) {
|
|
346
|
+
const id = event.type === "swap" || event.type === "action" ? event.box : undefined;
|
|
347
|
+
const box = layout.boxes.find((b) => b.id === id);
|
|
348
|
+
if (box) return { step, x: box.x + inset, y: box.y + inset, color: box.color };
|
|
349
|
+
}
|
|
350
|
+
if (layout.strip && events.some((event) => event.type === "strip")) return { step, x: layout.strip.x + inset, y: layout.strip.y + inset, color: "slate" };
|
|
351
|
+
const box = layout.boxes[0] as SceneBox;
|
|
352
|
+
return { step, x: box.x + inset, y: box.y + inset, color: box.color };
|
|
353
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 時系列図の置き場所。
|
|
3
|
+
*
|
|
4
|
+
* 生存線の頭の箱を上に横 1 列に並べ、段で起きる線と処理を起きる順に 1 行ずつ下へ積む。
|
|
5
|
+
* 生存線の間は、箱の幅と、その間を走る線の名前が収まる幅を取る。中身の帯は一番下に置く。
|
|
6
|
+
*/
|
|
7
|
+
import { LOOK, TYPE } from "../look/values";
|
|
8
|
+
import type { Diagram } from "../schema";
|
|
9
|
+
import { textWidth } from "./measure";
|
|
10
|
+
import {
|
|
11
|
+
badgeOnPath,
|
|
12
|
+
boxColors,
|
|
13
|
+
cornerBadge,
|
|
14
|
+
boxWidth,
|
|
15
|
+
edgeColor,
|
|
16
|
+
labelStyle,
|
|
17
|
+
type Layout,
|
|
18
|
+
layoutStrip,
|
|
19
|
+
type Point,
|
|
20
|
+
placeLabel,
|
|
21
|
+
type SceneAction,
|
|
22
|
+
type SceneBadge,
|
|
23
|
+
type SceneBox,
|
|
24
|
+
type SceneEdge,
|
|
25
|
+
type SceneLabel,
|
|
26
|
+
shiftContent,
|
|
27
|
+
stripNeed,
|
|
28
|
+
} from "./scene";
|
|
29
|
+
|
|
30
|
+
export function layoutSequence(diagram: Diagram): Layout {
|
|
31
|
+
const { box: B, sequence: Q, label: L, margin, gap } = LOOK;
|
|
32
|
+
const colors = boxColors(diagram);
|
|
33
|
+
const listed = diagram.sequence?.lifelines ?? [];
|
|
34
|
+
const order = [...listed, ...diagram.boxes.map((b) => b.id).filter((id) => !listed.includes(id))];
|
|
35
|
+
const column = new Map(order.map((id, i) => [id, i]));
|
|
36
|
+
const widths = order.map((id) => boxWidth(diagram, id, false));
|
|
37
|
+
|
|
38
|
+
// 生存線の間。隣の箱どうしの隙間を取り、名前のある線が走る間は名前が収まるまで広げる
|
|
39
|
+
const spacing = widths.slice(1).map((w, i) => (widths[i] as number) / 2 + w / 2 + Q.columnGap);
|
|
40
|
+
const needs: { from: number; to: number; need: number }[] = [];
|
|
41
|
+
for (const step of diagram.steps) {
|
|
42
|
+
for (const event of step.events) {
|
|
43
|
+
if (event.type !== "edge" || event.label === undefined) continue;
|
|
44
|
+
const a = column.get(event.from) ?? 0;
|
|
45
|
+
const b = column.get(event.to) ?? 0;
|
|
46
|
+
const w = textWidth(event.label, labelStyle(event.label)) + L.padX * 2;
|
|
47
|
+
if (a === b) {
|
|
48
|
+
// 自分へ戻る線は名前を右に置くので、右の生存線までに輪と名前が収まる幅を取る
|
|
49
|
+
if (a < spacing.length) needs.push({ from: a, to: a + 1, need: Q.selfW + L.side + w + Q.labelRoom });
|
|
50
|
+
} else {
|
|
51
|
+
needs.push({ from: Math.min(a, b), to: Math.max(a, b), need: w + Q.labelRoom * 2 });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
needs.sort((x, y) => x.to - x.from - (y.to - y.from));
|
|
56
|
+
for (const { from, to, need } of needs) {
|
|
57
|
+
let have = 0;
|
|
58
|
+
for (let i = from; i < to; i++) have += spacing[i] ?? 0;
|
|
59
|
+
if (have >= need) continue;
|
|
60
|
+
const add = (need - have) / (to - from);
|
|
61
|
+
for (let i = from; i < to; i++) spacing[i] = (spacing[i] ?? 0) + add;
|
|
62
|
+
}
|
|
63
|
+
const xs: number[] = [];
|
|
64
|
+
order.forEach((_, i) => xs.push(i === 0 ? margin + (widths[0] as number) / 2 : (xs[i - 1] as number) + (spacing[i - 1] as number)));
|
|
65
|
+
|
|
66
|
+
const boxes: SceneBox[] = order.map((id, i) => {
|
|
67
|
+
const source = diagram.boxes.find((b) => b.id === id);
|
|
68
|
+
const w = widths[i] as number;
|
|
69
|
+
const cx = xs[i] as number;
|
|
70
|
+
return {
|
|
71
|
+
id,
|
|
72
|
+
name: source?.name ?? id,
|
|
73
|
+
...(source?.sub !== undefined ? { sub: source.sub } : {}),
|
|
74
|
+
...(source?.icon !== undefined ? { icon: source.icon } : {}),
|
|
75
|
+
color: colors.get(id) ?? "slate",
|
|
76
|
+
x: cx - w / 2,
|
|
77
|
+
y: margin,
|
|
78
|
+
w,
|
|
79
|
+
h: B.h,
|
|
80
|
+
cx,
|
|
81
|
+
cy: margin + B.h / 2,
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
const headBottom = margin + B.h;
|
|
85
|
+
|
|
86
|
+
// 行を積む。線は上か下に名前を、処理は札を持つので、行ごとに上下に要る高さが違う
|
|
87
|
+
const edges: SceneEdge[] = [];
|
|
88
|
+
const actions: SceneAction[] = [];
|
|
89
|
+
const requests: Parameters<typeof placeLabel>[0][] = [];
|
|
90
|
+
let y = headBottom + Q.firstRow;
|
|
91
|
+
let below = 0;
|
|
92
|
+
let first = true;
|
|
93
|
+
diagram.steps.forEach((step, s) => {
|
|
94
|
+
step.events.forEach((event, e) => {
|
|
95
|
+
if (event.type !== "edge" && event.type !== "action") return;
|
|
96
|
+
let above: number;
|
|
97
|
+
let under: number;
|
|
98
|
+
if (event.type === "edge") {
|
|
99
|
+
const side = event.labelSide ?? "above";
|
|
100
|
+
const self = event.from === event.to;
|
|
101
|
+
const named = event.label !== undefined;
|
|
102
|
+
above = named && side === "above" ? L.offset + L.h / 2 : gap;
|
|
103
|
+
under = self ? Q.selfH + gap : named && side === "below" ? L.offset + L.h / 2 : gap;
|
|
104
|
+
} else {
|
|
105
|
+
above = LOOK.action.h / 2;
|
|
106
|
+
under = LOOK.action.h / 2;
|
|
107
|
+
}
|
|
108
|
+
y = first ? y + above : y + Math.max(Q.minPitch, below + Q.rowGap + above) + (e === 0 && s > 0 ? Q.stepGap : 0);
|
|
109
|
+
first = false;
|
|
110
|
+
below = under;
|
|
111
|
+
|
|
112
|
+
if (event.type === "edge") {
|
|
113
|
+
const xf = xs[column.get(event.from) ?? 0] as number;
|
|
114
|
+
const xt = xs[column.get(event.to) ?? 0] as number;
|
|
115
|
+
const dir = Math.sign(xt - xf) || 1;
|
|
116
|
+
const points: Point[] =
|
|
117
|
+
xf === xt
|
|
118
|
+
? [
|
|
119
|
+
[xf, y],
|
|
120
|
+
[xf + Q.selfW, y],
|
|
121
|
+
[xf + Q.selfW, y + Q.selfH],
|
|
122
|
+
[xf + LOOK.edge.inset, y + Q.selfH],
|
|
123
|
+
]
|
|
124
|
+
: [
|
|
125
|
+
[xf, y],
|
|
126
|
+
[xt - dir * LOOK.edge.inset, y],
|
|
127
|
+
];
|
|
128
|
+
const color = edgeColor(event, colors);
|
|
129
|
+
edges.push({ step: s, event: e, from: event.from, to: event.to, points, color, carry: event.carry });
|
|
130
|
+
if (event.label !== undefined) {
|
|
131
|
+
requests.push({
|
|
132
|
+
step: s,
|
|
133
|
+
event: e,
|
|
134
|
+
text: event.label,
|
|
135
|
+
points,
|
|
136
|
+
color,
|
|
137
|
+
...(event.labelSide ? { side: event.labelSide } : xf === xt ? { side: "right" as const } : {}),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
// 札は生存線の上の番号の丸を隠さないよう、丸の右に出す (#3017)
|
|
142
|
+
const x = xs[column.get(event.box) ?? 0] as number;
|
|
143
|
+
const w = textWidth(event.label, TYPE.action) + LOOK.action.padX * 2;
|
|
144
|
+
actions.push({ step: s, event: e, box: event.box, text: event.label, color: colors.get(event.box) ?? "slate", x: x + LOOK.badge.r + 4, y: y - LOOK.action.h / 2, w, h: LOOK.action.h });
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
const bottom = (first ? headBottom : y + below) + Q.tail;
|
|
149
|
+
const lifelines = order.map((id, i) => ({ id, x: xs[i] as number, y1: headBottom, y2: bottom }));
|
|
150
|
+
|
|
151
|
+
const labels: SceneLabel[] = [];
|
|
152
|
+
for (const request of requests) {
|
|
153
|
+
const soft = [...labels.filter((l) => l.step === request.step), ...actions];
|
|
154
|
+
labels.push(placeLabel(request, { hard: boxes, soft }));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const layout: Layout = { kind: "sequence", width: 0, height: 0, regions: [], boxes, lifelines, edges, labels, actions, badges: [] };
|
|
158
|
+
|
|
159
|
+
// 図の幅。名前が左に出たら全体を右へ寄せ、右端は一番右の物に余白を足した所
|
|
160
|
+
const lefts = [...boxes.map((b) => b.x), ...labels.map((l) => l.x), ...actions.map((a) => a.x)];
|
|
161
|
+
const shift = Math.max(0, margin - Math.min(...lefts));
|
|
162
|
+
if (shift > 0) shiftContent(layout, shift, 0);
|
|
163
|
+
const rights = [...boxes.map((b) => b.x + b.w), ...labels.map((l) => l.x + l.w), ...actions.map((a) => a.x + a.w), ...edges.flatMap((edge) => edge.points.map((p) => p[0]))];
|
|
164
|
+
const width = Math.max(Math.max(...rights) + margin, stripNeed(diagram) + margin * 2);
|
|
165
|
+
const strip = layoutStrip(diagram, margin, bottom + gap, width - margin * 2);
|
|
166
|
+
layout.width = width;
|
|
167
|
+
layout.height = (strip ? strip.y + strip.h : bottom) + margin;
|
|
168
|
+
if (strip) layout.strip = strip;
|
|
169
|
+
layout.badges = badges(diagram, layout);
|
|
170
|
+
return layout;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 段の番号。その段の最初の線の始まりに置き、線が無ければ処理する箱の生存線の上、無ければ書き換わる箱の角。
|
|
175
|
+
* 処理の札はその段の終わりに消えるので、札に付けると止め絵で丸だけが浮く (#3017)。
|
|
176
|
+
*/
|
|
177
|
+
function badges(diagram: Diagram, layout: Layout): SceneBadge[] {
|
|
178
|
+
return diagram.steps.map((step, s) => {
|
|
179
|
+
const edge = layout.edges.find((e) => e.step === s);
|
|
180
|
+
if (edge) {
|
|
181
|
+
const [x, y] = badgeOnPath(edge.points);
|
|
182
|
+
return { step: s, x, y, color: edge.color };
|
|
183
|
+
}
|
|
184
|
+
const action = layout.actions.find((a) => a.step === s);
|
|
185
|
+
const lifeline = action && layout.lifelines.find((l) => l.id === action.box);
|
|
186
|
+
if (action && lifeline) return { step: s, x: lifeline.x, y: action.y + action.h / 2, color: action.color };
|
|
187
|
+
return cornerBadge(step.events, s, layout);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 頁に同梱の字を読み込む入口 (`@cardenelabs/dragon/fonts`)。頁で 1 度だけ import する。
|
|
3
|
+
*
|
|
4
|
+
* 字の CSS (`@font-face`) を読み込むだけで、何も出さない。束ねる道具 (Vite など) が CSS と字の file を頁へ運ぶ。
|
|
5
|
+
* 描く入口 (`@cardenelabs/dragon`) から分けているのは、node で原本を検査する時に CSS を読ませないため。
|
|
6
|
+
*/
|
|
7
|
+
import "@fontsource-variable/noto-sans-jp";
|
|
8
|
+
import "@fontsource-variable/jetbrains-mono";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 図が使う 2 つの字。どちらも package の依存として同梱する。
|
|
3
|
+
*
|
|
4
|
+
* 描いた SVG は字をこの 2 つの名前でしか名指さない。端末の字の名前を持たないので、
|
|
5
|
+
* 同梱した字が読み込まれていない時に、端末の字へ黙って落ちて幅が変わることが無い。
|
|
6
|
+
* 頁に字を読み込むのは `@cardenelabs/dragon/fonts` の入口 (`load-fonts.ts`) が持つ。
|
|
7
|
+
*/
|
|
8
|
+
export const FONTS = { sans: "Noto Sans JP Variable", mono: "JetBrains Mono Variable" } as const;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 和文の字の英数字と記号の幅 (1000 分率)。使う太さ (500 / 600 / 650 / 700) のうち一番広い値。
|
|
12
|
+
* `scripts/font-widths.mjs` が同梱の字をブラウザに読ませて測った値で、字を上げた時は測り直して貼る。
|
|
13
|
+
* 表に無い字のうち U+2E80 以降 (仮名と漢字と全角の記号) は全角 (1000) とする。
|
|
14
|
+
*/
|
|
15
|
+
export const SANS_ADVANCE: Readonly<Record<string, number>> = {"0":590,"1":590,"2":590,"3":590,"4":590,"5":590,"6":590,"7":590,"8":590,"9":590," ":228,"!":371,"\"":575,"#":590,"$":590,"%":963,"&":740,"'":325,"(":378,")":378,"*":507,"+":590,",":325,"-":371,".":325,"/":390,":":325,";":325,"<":590,"=":590,">":590,"?":514,"@":1007,"A":642,"B":682,"C":657,"D":715,"E":616,"F":586,"G":718,"H":758,"I":330,"J":568,"K":686,"L":579,"M":854,"N":750,"O":771,"P":668,"Q":771,"R":682,"S":624,"T":626,"U":748,"V":619,"W":915,"X":627,"Y":581,"Z":614,"[":378,"\\":390,"]":378,"^":590,"_":568,"`":626,"a":591,"b":644,"c":528,"d":645,"e":581,"f":372,"g":598,"h":640,"i":305,"j":306,"k":605,"l":315,"m":964,"n":641,"o":626,"p":645,"q":645,"r":437,"s":495,"t":421,"u":638,"v":576,"w":863,"x":562,"y":575,"z":512,"{":378,"|":297,"}":378,"~":590,"…":1000,"−":590,"×":1000,"→":1000,"←":1000,"↑":1000,"↓":1000,"–":545,"—":908,"‘":325,"’":325,"“":575,"”":575,"°":404,"±":1000,"·":593,"•":379};
|
|
16
|
+
|
|
17
|
+
/** 等幅の字の幅 (1000 分率)。どの字も同じ幅。 */
|
|
18
|
+
export const MONO_ADVANCE = 600;
|
|
19
|
+
|
|
20
|
+
/** 全角として数える字の始まり。仮名と漢字と全角の記号はここから後ろにある。 */
|
|
21
|
+
export const WIDE_FROM = 0x2e80;
|
|
22
|
+
|
|
23
|
+
export type Family = keyof typeof FONTS;
|
|
24
|
+
|
|
25
|
+
/** 字の大きさを 1000 とした時の幅。上の表はこの単位で持つ。 */
|
|
26
|
+
export const EM = 1000;
|
|
27
|
+
|
|
28
|
+
/** 1 字の幅 (1000 分率)。表に無い字は全角とする。 */
|
|
29
|
+
export function advance(ch: string, family: Family): number {
|
|
30
|
+
if ((ch.codePointAt(0) ?? 0) >= WIDE_FROM) return EM;
|
|
31
|
+
if (family === "mono") return MONO_ADVANCE;
|
|
32
|
+
return SANS_ADVANCE[ch] ?? EM;
|
|
33
|
+
}
|