@coldtea/pr-lens-renderer 0.1.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.
Files changed (133) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +91 -0
  3. package/dist/bounds.d.ts +23 -0
  4. package/dist/bounds.d.ts.map +1 -0
  5. package/dist/bounds.js +39 -0
  6. package/dist/bounds.js.map +1 -0
  7. package/dist/corrections.d.ts +12 -0
  8. package/dist/corrections.d.ts.map +1 -0
  9. package/dist/corrections.js +122 -0
  10. package/dist/corrections.js.map +1 -0
  11. package/dist/design.d.ts +112 -0
  12. package/dist/design.d.ts.map +1 -0
  13. package/dist/design.js +112 -0
  14. package/dist/design.js.map +1 -0
  15. package/dist/errors.d.ts +11 -0
  16. package/dist/errors.d.ts.map +1 -0
  17. package/dist/errors.js +9 -0
  18. package/dist/errors.js.map +1 -0
  19. package/dist/geometry.d.ts +20 -0
  20. package/dist/geometry.d.ts.map +1 -0
  21. package/dist/geometry.js +15 -0
  22. package/dist/geometry.js.map +1 -0
  23. package/dist/glob.d.ts +7 -0
  24. package/dist/glob.d.ts.map +1 -0
  25. package/dist/glob.js +17 -0
  26. package/dist/glob.js.map +1 -0
  27. package/dist/index.d.ts +8 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +8 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/layout/architecture.d.ts +78 -0
  32. package/dist/layout/architecture.d.ts.map +1 -0
  33. package/dist/layout/architecture.js +207 -0
  34. package/dist/layout/architecture.js.map +1 -0
  35. package/dist/layout/congestion.d.ts +18 -0
  36. package/dist/layout/congestion.d.ts.map +1 -0
  37. package/dist/layout/congestion.js +50 -0
  38. package/dist/layout/congestion.js.map +1 -0
  39. package/dist/layout/dataflow.d.ts +85 -0
  40. package/dist/layout/dataflow.d.ts.map +1 -0
  41. package/dist/layout/dataflow.js +184 -0
  42. package/dist/layout/dataflow.js.map +1 -0
  43. package/dist/layout/edges.d.ts +48 -0
  44. package/dist/layout/edges.d.ts.map +1 -0
  45. package/dist/layout/edges.js +843 -0
  46. package/dist/layout/edges.js.map +1 -0
  47. package/dist/layout/labels.d.ts +9 -0
  48. package/dist/layout/labels.d.ts.map +1 -0
  49. package/dist/layout/labels.js +149 -0
  50. package/dist/layout/labels.js.map +1 -0
  51. package/dist/layout/rank.d.ts +16 -0
  52. package/dist/layout/rank.d.ts.map +1 -0
  53. package/dist/layout/rank.js +80 -0
  54. package/dist/layout/rank.js.map +1 -0
  55. package/dist/layout/seating.d.ts +29 -0
  56. package/dist/layout/seating.d.ts.map +1 -0
  57. package/dist/layout/seating.js +131 -0
  58. package/dist/layout/seating.js.map +1 -0
  59. package/dist/manifest.d.ts +39 -0
  60. package/dist/manifest.d.ts.map +1 -0
  61. package/dist/manifest.js +99 -0
  62. package/dist/manifest.js.map +1 -0
  63. package/dist/render.d.ts +49 -0
  64. package/dist/render.d.ts.map +1 -0
  65. package/dist/render.js +125 -0
  66. package/dist/render.js.map +1 -0
  67. package/dist/scope.d.ts +21 -0
  68. package/dist/scope.d.ts.map +1 -0
  69. package/dist/scope.js +57 -0
  70. package/dist/scope.js.map +1 -0
  71. package/dist/svg/architecture.d.ts +15 -0
  72. package/dist/svg/architecture.d.ts.map +1 -0
  73. package/dist/svg/architecture.js +197 -0
  74. package/dist/svg/architecture.js.map +1 -0
  75. package/dist/svg/dataflow.d.ts +9 -0
  76. package/dist/svg/dataflow.d.ts.map +1 -0
  77. package/dist/svg/dataflow.js +284 -0
  78. package/dist/svg/dataflow.js.map +1 -0
  79. package/dist/svg/document.d.ts +37 -0
  80. package/dist/svg/document.d.ts.map +1 -0
  81. package/dist/svg/document.js +148 -0
  82. package/dist/svg/document.js.map +1 -0
  83. package/dist/svg/icons.d.ts +3 -0
  84. package/dist/svg/icons.d.ts.map +1 -0
  85. package/dist/svg/icons.js +106 -0
  86. package/dist/svg/icons.js.map +1 -0
  87. package/dist/svg/primitives.d.ts +13 -0
  88. package/dist/svg/primitives.d.ts.map +1 -0
  89. package/dist/svg/primitives.js +34 -0
  90. package/dist/svg/primitives.js.map +1 -0
  91. package/dist/svg/pulse.d.ts +24 -0
  92. package/dist/svg/pulse.d.ts.map +1 -0
  93. package/dist/svg/pulse.js +33 -0
  94. package/dist/svg/pulse.js.map +1 -0
  95. package/dist/text.d.ts +18 -0
  96. package/dist/text.d.ts.map +1 -0
  97. package/dist/text.js +70 -0
  98. package/dist/text.js.map +1 -0
  99. package/dist/theme.d.ts +44 -0
  100. package/dist/theme.d.ts.map +1 -0
  101. package/dist/theme.js +72 -0
  102. package/dist/theme.js.map +1 -0
  103. package/dist/version.d.ts +9 -0
  104. package/dist/version.d.ts.map +1 -0
  105. package/dist/version.js +9 -0
  106. package/dist/version.js.map +1 -0
  107. package/package.json +56 -0
  108. package/src/bounds.ts +50 -0
  109. package/src/corrections.ts +166 -0
  110. package/src/design.ts +124 -0
  111. package/src/errors.ts +21 -0
  112. package/src/geometry.ts +22 -0
  113. package/src/glob.ts +20 -0
  114. package/src/index.ts +30 -0
  115. package/src/layout/architecture.ts +324 -0
  116. package/src/layout/congestion.ts +76 -0
  117. package/src/layout/dataflow.ts +307 -0
  118. package/src/layout/edges.ts +1061 -0
  119. package/src/layout/labels.ts +192 -0
  120. package/src/layout/rank.ts +95 -0
  121. package/src/layout/seating.ts +189 -0
  122. package/src/manifest.ts +118 -0
  123. package/src/render.ts +190 -0
  124. package/src/scope.ts +67 -0
  125. package/src/svg/architecture.ts +293 -0
  126. package/src/svg/dataflow.ts +408 -0
  127. package/src/svg/document.ts +198 -0
  128. package/src/svg/icons.ts +121 -0
  129. package/src/svg/primitives.ts +50 -0
  130. package/src/svg/pulse.ts +48 -0
  131. package/src/text.ts +92 -0
  132. package/src/theme.ts +107 -0
  133. package/src/version.ts +9 -0
@@ -0,0 +1,307 @@
1
+ import type { Flow, FlowMessage, FlowParticipant, GraphNode } from "@coldtea/pr-lens-schema";
2
+ import {
3
+ CARD_HEIGHT,
4
+ CARD_HEIGHT_WITH_SUBTITLE,
5
+ CARD_PADDING_X,
6
+ DIAGRAM_MARGIN,
7
+ ICON_CHIP_GAP,
8
+ ICON_CHIP_SIZE,
9
+ SUBTITLE_SIZE,
10
+ TITLE_SIZE,
11
+ } from "../design.js";
12
+ import type { Box } from "../geometry.js";
13
+ import { measure } from "../text.js";
14
+
15
+ export const PARTICIPANT_TOP = 18;
16
+ /** The lifeline hangs just clear of the card that heads its column. */
17
+ export const LIFELINE_GAP = 2;
18
+ /** Where the first message sits below the top of the lifelines. */
19
+ export const FIRST_MESSAGE_DROP = 41;
20
+ export const MESSAGE_PITCH = 38;
21
+ export const SELF_MESSAGE_PITCH = 54;
22
+ export const FLOW_BOTTOM_PADDING = 24;
23
+ export const FLOW_GAP = 40;
24
+
25
+ export const COLUMN_MIN_WIDTH = 150;
26
+ export const COLUMN_GAP = 80;
27
+
28
+ /**
29
+ * How far a column's band reaches beyond its cards, echoing the padding a
30
+ * lane keeps around the cards it holds.
31
+ */
32
+ export const FLOW_BAND_PAD_X = 12;
33
+ export const FLOW_BAND_PAD_Y = 12;
34
+
35
+ /** Half the width of an activation bar, and how far it holds arrows off the lifeline. */
36
+ export const ACTIVATION_HALF_WIDTH = 6;
37
+ export const MARKER_INSET = 5;
38
+
39
+ export const SELF_LOOP_REACH = 52;
40
+ export const SELF_LOOP_DROP = 6;
41
+ export const SELF_LOOP_CORNER = 7;
42
+ /** How far below its own row a self-message loop reaches. */
43
+ export const SELF_LOOP_EXTENT = SELF_LOOP_DROP + SELF_LOOP_CORNER * 2;
44
+
45
+ export type ActivationBar = { top: number; bottom: number };
46
+
47
+ export type PlacedParticipant = {
48
+ /** The node this column stands for, its label swapped for the column's. */
49
+ node: GraphNode;
50
+ centreX: number;
51
+ /** The node card heading the column, in the full card design system. */
52
+ card: Box;
53
+ /** Spans where this column is working on a synchronous call it received. */
54
+ activations: ActivationBar[];
55
+ };
56
+
57
+ export type PlacedMessage = {
58
+ message: FlowMessage;
59
+ /** The label as drawn, with a repeat count folded in when there is one. */
60
+ label: string;
61
+ y: number;
62
+ fromX: number;
63
+ toX: number;
64
+ /** Which beats of the drawing's cycle belong to this message, and how many. */
65
+ slot: { start: number; count: number };
66
+ };
67
+
68
+ export type FlowLayout = {
69
+ flow: Flow;
70
+ top: number;
71
+ height: number;
72
+ /** Where the lifelines start — under the tallest card in the header row. */
73
+ lifelineTop: number;
74
+ participants: PlacedParticipant[];
75
+ messages: PlacedMessage[];
76
+ };
77
+
78
+ export type DataFlowLayout = {
79
+ width: number;
80
+ height: number;
81
+ /** One width for every column of every flow, so stacked flows line up. */
82
+ columnWidth: number;
83
+ flows: FlowLayout[];
84
+ /** Beats in the shared cycle: one per pulse the drawing draws. */
85
+ slotCount: number;
86
+ };
87
+
88
+ const messagePitch = (message: FlowMessage): number =>
89
+ message.kind === "self" ? SELF_MESSAGE_PITCH : MESSAGE_PITCH;
90
+
91
+ /**
92
+ * How many turns a message takes in the cycle: one per repeat, up to the cap,
93
+ * so a repeated step crosses its own arrow several times running rather than
94
+ * once.
95
+ *
96
+ * A step the document leaves unanimated takes none. Reserving a turn for one
97
+ * that draws nothing would put a hole in the relay — the drawing would go
98
+ * dark for the length of it, which is the very thing sharing a cycle is meant
99
+ * to avoid. This is the one place that count is decided, so the schedule and
100
+ * the painting cannot disagree about it.
101
+ */
102
+ export const pulseCount = (message: FlowMessage, cap: number): number =>
103
+ message.animated ? Math.min(message.repeat ?? 1, cap) : 0;
104
+
105
+ const labelFor = (message: FlowMessage): string =>
106
+ message.repeat === undefined || message.repeat === 1
107
+ ? message.label
108
+ : `${message.label} ×${message.repeat}`;
109
+
110
+ /**
111
+ * A synchronous call is the one kind whose sender waits, so it is the one
112
+ * kind that activates its receiver: the bar starts where the call arrives and
113
+ * runs to the return that answers it — the first later `return` back to the
114
+ * caller not already claimed by an earlier call. A call no return answers
115
+ * keeps its receiver active through the receiver's last involvement, because
116
+ * the diagram never shows the work finishing. Async and self messages
117
+ * activate nothing: neither implies anyone is waiting.
118
+ */
119
+ const activationsFor = (
120
+ node: string,
121
+ messages: readonly PlacedMessage[],
122
+ ): ActivationBar[] => {
123
+ const claimed = new Set<number>();
124
+ const bars: ActivationBar[] = [];
125
+
126
+ messages.forEach((placed, index) => {
127
+ if (placed.message.kind !== "sync" || placed.message.to !== node) return;
128
+
129
+ const answer = messages.findIndex(
130
+ (candidate, position) =>
131
+ position > index &&
132
+ !claimed.has(position) &&
133
+ candidate.message.kind === "return" &&
134
+ candidate.message.from === node &&
135
+ candidate.message.to === placed.message.from,
136
+ );
137
+
138
+ const answered = messages[answer];
139
+ if (answer !== -1 && answered !== undefined) {
140
+ claimed.add(answer);
141
+ bars.push({ top: placed.y, bottom: answered.y });
142
+ return;
143
+ }
144
+
145
+ const last = messages.reduce(
146
+ (lowest, candidate, position) =>
147
+ position >= index &&
148
+ (candidate.message.from === node || candidate.message.to === node)
149
+ ? Math.max(lowest, candidate.y)
150
+ : lowest,
151
+ placed.y,
152
+ );
153
+ bars.push({ top: placed.y, bottom: last + MESSAGE_PITCH / 3 });
154
+ });
155
+
156
+ return mergedBars(bars);
157
+ };
158
+
159
+ /**
160
+ * Nested or unanswered calls can hand one column overlapping spans; a single
161
+ * bar per busy stretch keeps the drawing readable. Bars arrive ordered by
162
+ * top, because message y only ever grows.
163
+ */
164
+ const mergedBars = (bars: readonly ActivationBar[]): ActivationBar[] => {
165
+ const merged: ActivationBar[] = [];
166
+ for (const bar of bars) {
167
+ const current = merged[merged.length - 1];
168
+ if (current !== undefined && bar.top <= current.bottom)
169
+ current.bottom = Math.max(current.bottom, bar.bottom);
170
+ else merged.push({ ...bar });
171
+ }
172
+ return merged;
173
+ };
174
+
175
+ /**
176
+ * The node a column stands for, as the card will show it: the column's own
177
+ * shorter label wins over the node's, and a participant the graph no longer
178
+ * carries still needs a card, so a placeholder node stands in for one.
179
+ */
180
+ const participantNode = (
181
+ participant: FlowParticipant,
182
+ byId: ReadonlyMap<string, GraphNode>,
183
+ ): GraphNode => {
184
+ const node = byId.get(participant.node);
185
+ if (node === undefined)
186
+ return {
187
+ id: participant.node,
188
+ label: participant.label ?? participant.node,
189
+ kind: "other",
190
+ delta: "unchanged",
191
+ lane: "",
192
+ files: [],
193
+ badges: [],
194
+ };
195
+ return participant.label === undefined ? node : { ...node, label: participant.label };
196
+ };
197
+
198
+ const cardHeight = (node: GraphNode): number =>
199
+ node.subtitle === undefined ? CARD_HEIGHT : CARD_HEIGHT_WITH_SUBTITLE;
200
+
201
+ /** The width a column's card asks for: chip, title, and mono subtitle. */
202
+ const cardContentWidth = (node: GraphNode): number =>
203
+ CARD_PADDING_X * 2 +
204
+ ICON_CHIP_SIZE +
205
+ ICON_CHIP_GAP +
206
+ Math.max(
207
+ measure(node.label, "sans-bold", TITLE_SIZE),
208
+ node.subtitle === undefined ? 0 : measure(node.subtitle, "mono", SUBTITLE_SIZE),
209
+ );
210
+
211
+ export const layoutDataFlow = (
212
+ flows: readonly Flow[],
213
+ nodes: readonly GraphNode[],
214
+ pulseCap: number,
215
+ ): DataFlowLayout => {
216
+ const byId = new Map(nodes.map((node) => [node.id, node]));
217
+
218
+ // A whole pixel: adding measured widths leaves floating-point dust, and a
219
+ // card handed back exactly the width its title measured would round into
220
+ // truncating that title.
221
+ const columnWidth = Math.ceil(
222
+ Math.max(
223
+ COLUMN_MIN_WIDTH,
224
+ ...flows.flatMap((flow) =>
225
+ flow.participants.map((participant) => cardContentWidth(participantNode(participant, byId))),
226
+ ),
227
+ ),
228
+ );
229
+
230
+ let slotCount = 0;
231
+ for (const flow of flows)
232
+ for (const message of flow.messages) slotCount += pulseCount(message, pulseCap);
233
+
234
+ let cursorY = 0;
235
+ let slotCursor = 0;
236
+ let width = 0;
237
+
238
+ const placed = flows.map((flow) => {
239
+ const columnNodes = flow.participants.map((participant) => participantNode(participant, byId));
240
+ const centres = flow.participants.map(
241
+ (_, index) => DIAGRAM_MARGIN + columnWidth / 2 + index * (columnWidth + COLUMN_GAP),
242
+ );
243
+ const centreOf = new Map(
244
+ flow.participants.map((participant, index) => [participant.node, centres[index] ?? 0]),
245
+ );
246
+
247
+ const lifelineTop =
248
+ cursorY + PARTICIPANT_TOP + Math.max(...columnNodes.map(cardHeight)) + LIFELINE_GAP;
249
+
250
+ let messageY = lifelineTop + FIRST_MESSAGE_DROP;
251
+ const messages: PlacedMessage[] = flow.messages.map((message) => {
252
+ const y = messageY;
253
+ messageY += messagePitch(message);
254
+ const count = pulseCount(message, pulseCap);
255
+ const slot = { start: slotCursor, count };
256
+ slotCursor += count;
257
+ return {
258
+ message,
259
+ label: labelFor(message),
260
+ y,
261
+ fromX: centreOf.get(message.from) ?? 0,
262
+ toX: centreOf.get(message.to) ?? 0,
263
+ slot,
264
+ };
265
+ });
266
+
267
+ const participants = columnNodes.map((node, index) => {
268
+ const centreX = centres[index] ?? 0;
269
+ return {
270
+ node,
271
+ centreX,
272
+ card: {
273
+ x: centreX - columnWidth / 2,
274
+ y: cursorY + PARTICIPANT_TOP,
275
+ width: columnWidth,
276
+ height: cardHeight(node),
277
+ },
278
+ activations: activationsFor(node.id, messages),
279
+ };
280
+ });
281
+
282
+ const drawn = messages.reduce(
283
+ (lowest, placedMessage) =>
284
+ Math.max(lowest, placedMessage.y + (placedMessage.message.kind === "self" ? SELF_LOOP_EXTENT : 0)),
285
+ lifelineTop + FIRST_MESSAGE_DROP,
286
+ );
287
+ const bottom = drawn + FLOW_BOTTOM_PADDING;
288
+ const height = bottom - cursorY;
289
+ const top = cursorY;
290
+ cursorY = bottom + FLOW_GAP;
291
+ width = Math.max(
292
+ width,
293
+ (centres[centres.length - 1] ?? 0) + columnWidth / 2 + DIAGRAM_MARGIN,
294
+ );
295
+
296
+ return { flow, top, height, lifelineTop, participants, messages };
297
+ });
298
+
299
+ return {
300
+ // Whole numbers, for the same reason the architecture canvas uses them.
301
+ width: Math.ceil(width),
302
+ height: Math.ceil(cursorY - FLOW_GAP + DIAGRAM_MARGIN),
303
+ columnWidth,
304
+ flows: placed,
305
+ slotCount,
306
+ };
307
+ };