@cardenelabs/dragon 0.34.0 → 0.35.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/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 +39 -24
- 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,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 構成図の置き場所。
|
|
3
|
+
*
|
|
4
|
+
* 区画を上から帯に積み、帯の左に区画の名前を置く。箱は全ての帯で共通の列に並べるので、
|
|
5
|
+
* 区画をまたぐ線は縦にまっすぐ走る。箱の並びは線を辿って決める。前の区画の箱から線が来る箱を
|
|
6
|
+
* その箱の列に置き、そこから線で繋がる箱を隣の列へ (寄せの指示が左か右なら下の行へ) 置いていく。
|
|
7
|
+
* 線は縦と横の折れ線で、間にある箱を避ける。名前が箱に掛かる時は帯を広げて名前の場所を空ける。
|
|
8
|
+
*/
|
|
9
|
+
import { LOOK, TYPE } from "../look/values";
|
|
10
|
+
import type { Diagram, LabelSide, Region } from "../schema";
|
|
11
|
+
import { textWidth } from "./measure";
|
|
12
|
+
import {
|
|
13
|
+
badgeOnPath,
|
|
14
|
+
boxColors,
|
|
15
|
+
boxWidth,
|
|
16
|
+
cornerBadge,
|
|
17
|
+
edgeColor,
|
|
18
|
+
edgesOf,
|
|
19
|
+
labelStyle,
|
|
20
|
+
type Layout,
|
|
21
|
+
layoutStrip,
|
|
22
|
+
type Point,
|
|
23
|
+
placeLabel,
|
|
24
|
+
type Rect,
|
|
25
|
+
regionColors,
|
|
26
|
+
type SceneBox,
|
|
27
|
+
type SceneEdge,
|
|
28
|
+
type SceneLabel,
|
|
29
|
+
type SceneRegion,
|
|
30
|
+
shiftContent,
|
|
31
|
+
stripNeed,
|
|
32
|
+
} from "./scene";
|
|
33
|
+
|
|
34
|
+
interface Group {
|
|
35
|
+
region?: Region;
|
|
36
|
+
members: string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface Slot {
|
|
40
|
+
group: number;
|
|
41
|
+
col: number;
|
|
42
|
+
row: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface Room {
|
|
46
|
+
top: number;
|
|
47
|
+
bottom: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function layoutArchitecture(diagram: Diagram): Layout {
|
|
51
|
+
const groups = groupsOf(diagram);
|
|
52
|
+
const slots = placeBoxes(diagram, groups);
|
|
53
|
+
const rooms = groups.map(() => ({ top: 0, bottom: 0 }));
|
|
54
|
+
let layout = arrange(diagram, groups, slots, rooms);
|
|
55
|
+
// 名前を置く場所が帯の外へ出たら、その帯を広げて置き直す
|
|
56
|
+
for (let pass = 0; pass < 3 && grow(layout, groups, slots, rooms); pass++) layout = arrange(diagram, groups, slots, rooms);
|
|
57
|
+
return layout;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function groupsOf(diagram: Diagram): Group[] {
|
|
61
|
+
const regions = diagram.regions ?? [];
|
|
62
|
+
const known = new Set(regions.map((r) => r.id));
|
|
63
|
+
const groups: Group[] = regions.map((region) => ({ region, members: diagram.boxes.filter((b) => b.region === region.id).map((b) => b.id) }));
|
|
64
|
+
const loose = diagram.boxes.filter((b) => b.region === undefined || !known.has(b.region)).map((b) => b.id);
|
|
65
|
+
if (loose.length > 0) groups.push({ members: loose });
|
|
66
|
+
return groups;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const vertical = (side: LabelSide | undefined): boolean => side === "left" || side === "right";
|
|
70
|
+
|
|
71
|
+
/** 箱を列と行へ置く。列は全ての区画で共通。 */
|
|
72
|
+
function placeBoxes(diagram: Diagram, groups: Group[]): Map<string, Slot> {
|
|
73
|
+
const columns = LOOK.architecture.columns;
|
|
74
|
+
const edges = edgesOf(diagram).map((e) => e.edge);
|
|
75
|
+
const slots = new Map<string, Slot>();
|
|
76
|
+
groups.forEach((group, g) => {
|
|
77
|
+
const inGroup = new Set(group.members);
|
|
78
|
+
const taken = new Set<string>();
|
|
79
|
+
const free = (col: number, row: number): boolean => col >= 0 && col < columns && !taken.has(`${col},${row}`);
|
|
80
|
+
const near = (col: number, row: number): [number, number] => {
|
|
81
|
+
const start = Math.min(columns - 1, Math.max(0, col));
|
|
82
|
+
for (let r = row; ; r++) {
|
|
83
|
+
for (let d = 0; d < columns; d++) {
|
|
84
|
+
for (const c of [start + d, start - d]) if (free(c, r)) return [c, r];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
const put = (id: string, col: number, row: number): void => {
|
|
89
|
+
slots.set(id, { group: g, col, row });
|
|
90
|
+
taken.add(`${col},${row}`);
|
|
91
|
+
};
|
|
92
|
+
let dir = 1;
|
|
93
|
+
const visit = (id: string): void => {
|
|
94
|
+
for (const edge of edges) {
|
|
95
|
+
const partner = edge.from === id ? edge.to : edge.to === id ? edge.from : undefined;
|
|
96
|
+
if (partner === undefined || !inGroup.has(partner) || slots.has(partner)) continue;
|
|
97
|
+
const me = slots.get(id) as Slot;
|
|
98
|
+
const [c, r] = vertical(edge.labelSide) ? near(me.col, me.row + 1) : free(me.col + dir, me.row) ? [me.col + dir, me.row] : near(me.col + dir, me.row);
|
|
99
|
+
put(partner, c, r);
|
|
100
|
+
visit(partner);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
// 前の区画の箱と線で繋がる箱 (起きる順に早い線から) をその箱の列に置き、そこから辿る
|
|
104
|
+
const anchorOf = (id: string): { order: number; col: number } | undefined => {
|
|
105
|
+
for (let i = 0; i < edges.length; i++) {
|
|
106
|
+
const edge = edges[i];
|
|
107
|
+
const partner = edge?.from === id ? edge.to : edge?.to === id ? edge.from : undefined;
|
|
108
|
+
const slot = partner === undefined ? undefined : slots.get(partner);
|
|
109
|
+
if (slot && slot.group < g) return { order: i, col: slot.col };
|
|
110
|
+
}
|
|
111
|
+
return undefined;
|
|
112
|
+
};
|
|
113
|
+
let placed = 0;
|
|
114
|
+
for (;;) {
|
|
115
|
+
const rest = group.members.filter((id) => !slots.has(id));
|
|
116
|
+
if (rest.length === 0) break;
|
|
117
|
+
const anchored = rest.map((id) => ({ id, anchor: anchorOf(id) })).filter((a) => a.anchor !== undefined);
|
|
118
|
+
anchored.sort((a, b) => (a.anchor?.order ?? 0) - (b.anchor?.order ?? 0));
|
|
119
|
+
const pick = anchored[0] ?? { id: rest[0] as string, anchor: undefined };
|
|
120
|
+
const want = pick.anchor?.col ?? 0;
|
|
121
|
+
if (placed === 0) dir = want >= (columns - 1) / 2 ? -1 : 1;
|
|
122
|
+
const [c, r] = free(want, 0) ? [want, 0] : near(want, 0);
|
|
123
|
+
put(pick.id, c, r);
|
|
124
|
+
visit(pick.id);
|
|
125
|
+
placed++;
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
// 使っていない列を詰める
|
|
129
|
+
const used = [...new Set([...slots.values()].map((s) => s.col))].sort((a, b) => a - b);
|
|
130
|
+
for (const slot of slots.values()) slot.col = used.indexOf(slot.col);
|
|
131
|
+
return slots;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** 区画の名前の行。空白があれば真ん中に近い空白で 2 行に折る。 */
|
|
135
|
+
function nameLines(name: string): string[] {
|
|
136
|
+
const spaces = [...name.matchAll(/ /g)].map((m) => m.index);
|
|
137
|
+
if (spaces.length === 0) return [name];
|
|
138
|
+
const at = spaces.reduce((best, i) => (Math.abs(i - name.length / 2) < Math.abs(best - name.length / 2) ? i : best));
|
|
139
|
+
return [name.slice(0, at), name.slice(at + 1)];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function headerSize(region: Region): { w: number; h: number; lines: string[] } {
|
|
143
|
+
const { icon, nameY, line, inset } = LOOK.region;
|
|
144
|
+
const lines = nameLines(region.name);
|
|
145
|
+
const w = Math.max(icon, ...lines.map((l) => textWidth(l, TYPE.regionName)), textWidth(region.note ?? "", TYPE.regionNote));
|
|
146
|
+
// 名前の最後の行の下と、添え書きの下まで
|
|
147
|
+
const h = nameY - inset + (lines.length - 1) * line + (region.note ? line : 0) + LOOK.gap / 2;
|
|
148
|
+
return { w, h, lines };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function arrange(diagram: Diagram, groups: Group[], slots: Map<string, Slot>, rooms: Room[]): Layout {
|
|
152
|
+
const { box: B, region: R, architecture: A, margin, gap } = LOOK;
|
|
153
|
+
const colors = boxColors(diagram);
|
|
154
|
+
const regionColor = regionColors(diagram);
|
|
155
|
+
const edges = edgesOf(diagram);
|
|
156
|
+
|
|
157
|
+
// 列の幅と列の間。隣の列を横に結ぶ名前のある線は、名前が収まるまで間を広げる (広げすぎる時は脇へ寄せる)
|
|
158
|
+
const columnCount = Math.max(1, ...[...slots.values()].map((s) => s.col + 1));
|
|
159
|
+
const widths = new Map(diagram.boxes.map((b) => [b.id, boxWidth(diagram, b.id, true)]));
|
|
160
|
+
const colW = Array.from({ length: columnCount }, (_, c) => Math.max(B.minW, ...[...slots].filter(([, s]) => s.col === c).map(([id]) => widths.get(id) ?? B.minW)));
|
|
161
|
+
const between = Array.from({ length: Math.max(0, columnCount - 1) }, () => A.columnGap as number);
|
|
162
|
+
for (const { edge } of edges) {
|
|
163
|
+
const a = slots.get(edge.from);
|
|
164
|
+
const b = slots.get(edge.to);
|
|
165
|
+
if (!a || !b || edge.label === undefined || a.group !== b.group || a.row !== b.row || Math.abs(a.col - b.col) !== 1 || vertical(edge.labelSide)) continue;
|
|
166
|
+
const need = textWidth(edge.label, labelStyle(edge.label)) + LOOK.label.padX * 2 + A.labelRoom * 2;
|
|
167
|
+
const i = Math.min(a.col, b.col);
|
|
168
|
+
if (need <= A.maxColumnGap) between[i] = Math.max(between[i] ?? 0, need);
|
|
169
|
+
}
|
|
170
|
+
const headers = groups.map((g) => (g.region ? headerSize(g.region) : undefined));
|
|
171
|
+
const headerW = Math.max(0, ...headers.map((h) => (h ? h.w + R.inset * 2 : 0)));
|
|
172
|
+
const colLeft: number[] = [];
|
|
173
|
+
for (let c = 0; c < columnCount; c++) colLeft.push(c === 0 ? margin + (headerW > 0 ? headerW : R.pad) : (colLeft[c - 1] as number) + (colW[c - 1] as number) + (between[c - 1] as number));
|
|
174
|
+
const center = (c: number): number => (colLeft[c] as number) + (colW[c] as number) / 2;
|
|
175
|
+
|
|
176
|
+
// 帯を上から積む
|
|
177
|
+
const boxes: SceneBox[] = [];
|
|
178
|
+
const bands: { group: number; y: number; h: number; rows: number }[] = [];
|
|
179
|
+
let cursor = margin;
|
|
180
|
+
groups.forEach((group, g) => {
|
|
181
|
+
const rows = Math.max(1, ...group.members.map((id) => (slots.get(id)?.row ?? 0) + 1));
|
|
182
|
+
const room = rooms[g] as Room;
|
|
183
|
+
const top = cursor + R.pad + room.top;
|
|
184
|
+
for (const id of group.members) {
|
|
185
|
+
const slot = slots.get(id) as Slot;
|
|
186
|
+
const source = diagram.boxes.find((b) => b.id === id);
|
|
187
|
+
const w = widths.get(id) ?? B.minW;
|
|
188
|
+
const cx = center(slot.col);
|
|
189
|
+
const y = top + slot.row * (B.h + A.rowGap);
|
|
190
|
+
boxes.push({
|
|
191
|
+
id,
|
|
192
|
+
name: source?.name ?? id,
|
|
193
|
+
...(source?.sub !== undefined ? { sub: source.sub } : {}),
|
|
194
|
+
...(source?.icon !== undefined ? { icon: source.icon } : {}),
|
|
195
|
+
color: colors.get(id) ?? "slate",
|
|
196
|
+
x: cx - w / 2,
|
|
197
|
+
y,
|
|
198
|
+
w,
|
|
199
|
+
h: B.h,
|
|
200
|
+
cx,
|
|
201
|
+
cy: y + B.h / 2,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
const contentBottom = top + rows * B.h + (rows - 1) * A.rowGap;
|
|
205
|
+
const header = headers[g];
|
|
206
|
+
const h = Math.max(contentBottom + R.pad + room.bottom - cursor, header ? header.h + R.inset * 2 : 0, group.region ? R.minH : 0);
|
|
207
|
+
bands.push({ group: g, y: cursor, h, rows });
|
|
208
|
+
cursor += h + R.gap;
|
|
209
|
+
});
|
|
210
|
+
const bandsBottom = cursor - R.gap;
|
|
211
|
+
|
|
212
|
+
const byId = new Map(boxes.map((b) => [b.id, b]));
|
|
213
|
+
const bandOf = (id: string) => bands[slots.get(id)?.group ?? 0] as (typeof bands)[number];
|
|
214
|
+
const blocked = (points: Point[], a: string, b: string): boolean =>
|
|
215
|
+
boxes.some((box) => {
|
|
216
|
+
if (box.id === a || box.id === b) return false;
|
|
217
|
+
const pad = LOOK.label.search;
|
|
218
|
+
for (let i = 1; i < points.length; i++) {
|
|
219
|
+
const [x1, y1] = points[i - 1] as Point;
|
|
220
|
+
const [x2, y2] = points[i] as Point;
|
|
221
|
+
if (Math.min(x1, x2) <= box.x + box.w + pad && Math.max(x1, x2) >= box.x - pad && Math.min(y1, y2) <= box.y + box.h + pad && Math.max(y1, y2) >= box.y - pad) return true;
|
|
222
|
+
}
|
|
223
|
+
return false;
|
|
224
|
+
});
|
|
225
|
+
// 箱の下 (上) の、次の行か次の帯との間の真ん中
|
|
226
|
+
const laneBelow = (id: string): number => {
|
|
227
|
+
const box = byId.get(id) as SceneBox;
|
|
228
|
+
const band = bandOf(id);
|
|
229
|
+
return (slots.get(id)?.row ?? 0) < band.rows - 1 ? box.y + box.h + A.rowGap / 2 : Math.min(box.y + box.h + R.pad / 2, band.y + band.h - LOOK.edge.inset);
|
|
230
|
+
};
|
|
231
|
+
const laneAbove = (id: string): number => {
|
|
232
|
+
const box = byId.get(id) as SceneBox;
|
|
233
|
+
const band = bandOf(id);
|
|
234
|
+
return (slots.get(id)?.row ?? 0) > 0 ? box.y - A.rowGap / 2 : Math.max(box.y - R.pad / 2, band.y + LOOK.edge.inset);
|
|
235
|
+
};
|
|
236
|
+
const inset = LOOK.edge.inset;
|
|
237
|
+
const route = (from: string, to: string): Point[] => {
|
|
238
|
+
const a = byId.get(from) as SceneBox;
|
|
239
|
+
const b = byId.get(to) as SceneBox;
|
|
240
|
+
const sa = slots.get(from) as Slot;
|
|
241
|
+
const sb = slots.get(to) as Slot;
|
|
242
|
+
if (sa.group === sb.group && sa.row === sb.row) {
|
|
243
|
+
const right = b.cx > a.cx;
|
|
244
|
+
const straight: Point[] = [
|
|
245
|
+
[right ? a.x + a.w : a.x, a.cy],
|
|
246
|
+
[right ? b.x - inset : b.x + b.w + inset, b.cy],
|
|
247
|
+
];
|
|
248
|
+
if (!blocked(straight, from, to)) return straight;
|
|
249
|
+
const lane = laneBelow(from);
|
|
250
|
+
return [
|
|
251
|
+
[a.cx, a.y + a.h],
|
|
252
|
+
[a.cx, lane],
|
|
253
|
+
[b.cx, lane],
|
|
254
|
+
[b.cx, b.y + b.h + inset],
|
|
255
|
+
];
|
|
256
|
+
}
|
|
257
|
+
const down = b.cy > a.cy;
|
|
258
|
+
const leave: Point = [a.cx, down ? a.y + a.h : a.y];
|
|
259
|
+
const enter: Point = [b.cx, down ? b.y - inset : b.y + b.h + inset];
|
|
260
|
+
if (sa.col === sb.col) {
|
|
261
|
+
const straight: Point[] = [leave, enter];
|
|
262
|
+
if (!blocked(straight, from, to)) return straight;
|
|
263
|
+
const x = a.x + a.w + (between[sa.col] ?? A.columnGap) / 2;
|
|
264
|
+
return [
|
|
265
|
+
[a.x + a.w, a.cy],
|
|
266
|
+
[x, a.cy],
|
|
267
|
+
[x, b.cy],
|
|
268
|
+
[b.x + b.w + inset, b.cy],
|
|
269
|
+
];
|
|
270
|
+
}
|
|
271
|
+
const right = b.cx > a.cx;
|
|
272
|
+
const lane = down ? laneBelow(from) : laneAbove(from);
|
|
273
|
+
const candidates: Point[][] = [
|
|
274
|
+
[leave, [a.cx, b.cy], [right ? b.x - inset : b.x + b.w + inset, b.cy]],
|
|
275
|
+
[[right ? a.x + a.w : a.x, a.cy], [b.cx, a.cy], enter],
|
|
276
|
+
[leave, [a.cx, lane], [b.cx, lane], enter],
|
|
277
|
+
];
|
|
278
|
+
return candidates.find((points) => !blocked(points, from, to)) ?? (candidates[2] as Point[]);
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const sceneEdges: SceneEdge[] = edges.map(({ step, event, edge }) => {
|
|
282
|
+
const target = byId.get(edge.to) as SceneBox;
|
|
283
|
+
return {
|
|
284
|
+
step,
|
|
285
|
+
event,
|
|
286
|
+
from: edge.from,
|
|
287
|
+
to: edge.to,
|
|
288
|
+
points: route(edge.from, edge.to),
|
|
289
|
+
color: edgeColor(edge, colors),
|
|
290
|
+
carry: edge.carry,
|
|
291
|
+
settle: [target.cx, target.cy + B.iconY],
|
|
292
|
+
};
|
|
293
|
+
});
|
|
294
|
+
// 同じ 2 つの箱を行きと帰りで結ぶまっすぐな線は、少しずらして重ならないようにする
|
|
295
|
+
for (const edge of sceneEdges) {
|
|
296
|
+
if (edge.points.length !== 2) continue;
|
|
297
|
+
const back = sceneEdges.some((other) => other.from === edge.to && other.to === edge.from && other.points.length === 2);
|
|
298
|
+
if (!back) continue;
|
|
299
|
+
const [p, q] = edge.points as [Point, Point];
|
|
300
|
+
const flat = Math.abs(q[1] - p[1]) < Math.abs(q[0] - p[0]);
|
|
301
|
+
const d = (edge.from < edge.to ? 1 : -1) * LOOK.edge.parallel;
|
|
302
|
+
edge.points = edge.points.map(([x, y]) => (flat ? [x, y + d] : [x + d, y]));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const regions: SceneRegion[] = [];
|
|
306
|
+
groups.forEach((group, g) => {
|
|
307
|
+
const band = bands[g] as (typeof bands)[number];
|
|
308
|
+
const header = headers[g];
|
|
309
|
+
if (!group.region || !header) return;
|
|
310
|
+
regions.push({
|
|
311
|
+
id: group.region.id,
|
|
312
|
+
lines: header.lines,
|
|
313
|
+
...(group.region.note !== undefined ? { note: group.region.note } : {}),
|
|
314
|
+
...(group.region.icon !== undefined ? { icon: group.region.icon } : {}),
|
|
315
|
+
color: regionColor.get(group.region.id) ?? "slate",
|
|
316
|
+
x: margin,
|
|
317
|
+
y: band.y,
|
|
318
|
+
w: 0,
|
|
319
|
+
h: band.h,
|
|
320
|
+
header: { x: margin + R.inset, y: band.y + R.inset, w: header.w, h: header.h },
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const labels: SceneLabel[] = [];
|
|
325
|
+
for (const edge of sceneEdges) {
|
|
326
|
+
const source = diagram.steps[edge.step]?.events[edge.event];
|
|
327
|
+
if (source?.type !== "edge" || source.label === undefined) continue;
|
|
328
|
+
const soft: Rect[] = [...regions.map((r) => r.header), ...labels.filter((l) => l.step === edge.step)];
|
|
329
|
+
labels.push(
|
|
330
|
+
placeLabel(
|
|
331
|
+
{ step: edge.step, event: edge.event, text: source.label, points: edge.points, color: edge.color, ...(source.labelSide ? { side: source.labelSide } : {}) },
|
|
332
|
+
{ hard: boxes, soft },
|
|
333
|
+
),
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const layout: Layout = { kind: "architecture", width: 0, height: 0, regions, boxes, lifelines: [], edges: sceneEdges, labels, actions: [], badges: [] };
|
|
338
|
+
// 名前が左へ出たら、帯の中身を右へ寄せる。帯は図の幅いっぱいに引き直す
|
|
339
|
+
const shift = labels.length > 0 ? Math.max(0, margin + LOOK.label.soft - Math.min(...labels.map((l) => l.x))) : 0;
|
|
340
|
+
if (shift > 0) shiftContent(layout, shift, 0);
|
|
341
|
+
const contentRight = Math.max(...boxes.map((b) => b.x + b.w + R.pad), ...labels.map((l) => l.x + l.w + LOOK.label.soft), ...sceneEdges.flatMap((e) => e.points.map((p) => p[0] + R.pad)));
|
|
342
|
+
const width = Math.max(contentRight + margin, stripNeed(diagram) + margin * 2);
|
|
343
|
+
for (const region of regions) region.w = width - margin * 2;
|
|
344
|
+
const strip = layoutStrip(diagram, margin, bandsBottom + gap, width - margin * 2);
|
|
345
|
+
layout.width = width;
|
|
346
|
+
layout.height = (strip ? strip.y + strip.h : bandsBottom) + margin;
|
|
347
|
+
if (strip) layout.strip = strip;
|
|
348
|
+
layout.badges = diagram.steps.map((step, s) => {
|
|
349
|
+
const edge = sceneEdges.find((e) => e.step === s);
|
|
350
|
+
if (!edge) return cornerBadge(step.events, s, layout);
|
|
351
|
+
const [x, y] = badgeOnPath(edge.points);
|
|
352
|
+
return { step: s, x, y, color: edge.color };
|
|
353
|
+
});
|
|
354
|
+
return layout;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** 区画の中を結ぶ線の名前が帯の外へ出ていたら、その分だけ帯の上か下を広げる。広げたら true。 */
|
|
358
|
+
function grow(layout: Layout, groups: Group[], slots: Map<string, Slot>, rooms: Room[]): boolean {
|
|
359
|
+
let grown = false;
|
|
360
|
+
const soft = LOOK.label.soft;
|
|
361
|
+
for (const label of layout.labels) {
|
|
362
|
+
const edge = layout.edges.find((e) => e.step === label.step && e.event === label.event);
|
|
363
|
+
const a = edge && slots.get(edge.from);
|
|
364
|
+
const b = edge && slots.get(edge.to);
|
|
365
|
+
if (!a || !b || a.group !== b.group || !groups[a.group]?.region) continue;
|
|
366
|
+
const region = layout.regions.find((r) => r.id === groups[a.group]?.region?.id);
|
|
367
|
+
const room = rooms[a.group];
|
|
368
|
+
if (!region || !room) continue;
|
|
369
|
+
const over = region.y + soft - label.y;
|
|
370
|
+
const under = label.y + label.h - (region.y + region.h - soft);
|
|
371
|
+
if (over > 0) {
|
|
372
|
+
room.top += over;
|
|
373
|
+
grown = true;
|
|
374
|
+
}
|
|
375
|
+
if (under > 0) {
|
|
376
|
+
room.bottom += under;
|
|
377
|
+
grown = true;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return grown;
|
|
381
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 字の幅を、同梱した字の送り幅の表で測る。描かずに測るので、頁の無い所 (node) でも同じ値になる。
|
|
3
|
+
*/
|
|
4
|
+
import { advance, EM, type Family } from "../look/fonts";
|
|
5
|
+
import type { TextStyle } from "../look/values";
|
|
6
|
+
|
|
7
|
+
/** 和文 (CJK) の字を含むか。等幅の字は和文を持たないので、含む字は Noto Sans JP で描く。 */
|
|
8
|
+
export const hasWide = (text: string): boolean => [...text].some((ch) => (ch.codePointAt(0) ?? 0) >= 0x2e80);
|
|
9
|
+
|
|
10
|
+
/** 等幅を望む字でも、和文を含めば和文の字で描く。 */
|
|
11
|
+
export function familyFor(text: string, preferred: Family): Family {
|
|
12
|
+
return preferred === "mono" && hasWide(text) ? "sans" : preferred;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** 1 行の字の幅。 */
|
|
16
|
+
export function textWidth(text: string, style: TextStyle): number {
|
|
17
|
+
const family = familyFor(text, style.family);
|
|
18
|
+
let sum = 0;
|
|
19
|
+
for (const ch of text) sum += advance(ch, family);
|
|
20
|
+
return (sum * style.size) / EM;
|
|
21
|
+
}
|