@tienne/gestalt 0.88.0 → 0.89.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 (197) hide show
  1. package/CLAUDE.md +6 -2
  2. package/README.ko.md +2 -1
  3. package/README.md +3 -1
  4. package/dist/package.json +2 -1
  5. package/dist/plugin/skills/_shared/proactive-routing.md +1 -0
  6. package/dist/plugin/skills/architecture/SKILL.md +913 -0
  7. package/dist/schemas/architecture-ir.schema.json +596 -0
  8. package/dist/src/architecture/canvas-geometry.d.ts +40 -0
  9. package/dist/src/architecture/canvas-geometry.d.ts.map +1 -0
  10. package/dist/src/architecture/canvas-geometry.js +212 -0
  11. package/dist/src/architecture/canvas-geometry.js.map +1 -0
  12. package/dist/src/architecture/codeowners.d.ts +16 -0
  13. package/dist/src/architecture/codeowners.d.ts.map +1 -0
  14. package/dist/src/architecture/codeowners.js +58 -0
  15. package/dist/src/architecture/codeowners.js.map +1 -0
  16. package/dist/src/architecture/compare-layout.d.ts +27 -0
  17. package/dist/src/architecture/compare-layout.d.ts.map +1 -0
  18. package/dist/src/architecture/compare-layout.js +72 -0
  19. package/dist/src/architecture/compare-layout.js.map +1 -0
  20. package/dist/src/architecture/dataflow-layout.d.ts +29 -0
  21. package/dist/src/architecture/dataflow-layout.d.ts.map +1 -0
  22. package/dist/src/architecture/dataflow-layout.js +229 -0
  23. package/dist/src/architecture/dataflow-layout.js.map +1 -0
  24. package/dist/src/architecture/doc-fresh.d.ts +31 -0
  25. package/dist/src/architecture/doc-fresh.d.ts.map +1 -0
  26. package/dist/src/architecture/doc-fresh.js +84 -0
  27. package/dist/src/architecture/doc-fresh.js.map +1 -0
  28. package/dist/src/architecture/doc-link.d.ts +84 -0
  29. package/dist/src/architecture/doc-link.d.ts.map +1 -0
  30. package/dist/src/architecture/doc-link.js +396 -0
  31. package/dist/src/architecture/doc-link.js.map +1 -0
  32. package/dist/src/architecture/doc-routes.d.ts +39 -0
  33. package/dist/src/architecture/doc-routes.d.ts.map +1 -0
  34. package/dist/src/architecture/doc-routes.js +123 -0
  35. package/dist/src/architecture/doc-routes.js.map +1 -0
  36. package/dist/src/architecture/doc-scan.d.ts +107 -0
  37. package/dist/src/architecture/doc-scan.d.ts.map +1 -0
  38. package/dist/src/architecture/doc-scan.js +617 -0
  39. package/dist/src/architecture/doc-scan.js.map +1 -0
  40. package/dist/src/architecture/doc-stale.d.ts +32 -0
  41. package/dist/src/architecture/doc-stale.d.ts.map +1 -0
  42. package/dist/src/architecture/doc-stale.js +60 -0
  43. package/dist/src/architecture/doc-stale.js.map +1 -0
  44. package/dist/src/architecture/drilldown.d.ts +51 -0
  45. package/dist/src/architecture/drilldown.d.ts.map +1 -0
  46. package/dist/src/architecture/drilldown.js +1160 -0
  47. package/dist/src/architecture/drilldown.js.map +1 -0
  48. package/dist/src/architecture/endpoint-match.d.ts +43 -0
  49. package/dist/src/architecture/endpoint-match.d.ts.map +1 -0
  50. package/dist/src/architecture/endpoint-match.js +133 -0
  51. package/dist/src/architecture/endpoint-match.js.map +1 -0
  52. package/dist/src/architecture/flow-layout.d.ts +79 -0
  53. package/dist/src/architecture/flow-layout.d.ts.map +1 -0
  54. package/dist/src/architecture/flow-layout.js +519 -0
  55. package/dist/src/architecture/flow-layout.js.map +1 -0
  56. package/dist/src/architecture/focus.d.ts +14 -0
  57. package/dist/src/architecture/focus.d.ts.map +1 -0
  58. package/dist/src/architecture/focus.js +107 -0
  59. package/dist/src/architecture/focus.js.map +1 -0
  60. package/dist/src/architecture/global-context.d.ts +23 -0
  61. package/dist/src/architecture/global-context.d.ts.map +1 -0
  62. package/dist/src/architecture/global-context.js +67 -0
  63. package/dist/src/architecture/global-context.js.map +1 -0
  64. package/dist/src/architecture/html-client.d.ts +35 -0
  65. package/dist/src/architecture/html-client.d.ts.map +1 -0
  66. package/dist/src/architecture/html-client.js +1852 -0
  67. package/dist/src/architecture/html-client.js.map +1 -0
  68. package/dist/src/architecture/html-renderer.d.ts +22 -0
  69. package/dist/src/architecture/html-renderer.d.ts.map +1 -0
  70. package/dist/src/architecture/html-renderer.js +1290 -0
  71. package/dist/src/architecture/html-renderer.js.map +1 -0
  72. package/dist/src/architecture/html-theme.d.ts +25 -0
  73. package/dist/src/architecture/html-theme.d.ts.map +1 -0
  74. package/dist/src/architecture/html-theme.js +681 -0
  75. package/dist/src/architecture/html-theme.js.map +1 -0
  76. package/dist/src/architecture/index.d.ts +9 -0
  77. package/dist/src/architecture/index.d.ts.map +1 -0
  78. package/dist/src/architecture/index.js +9 -0
  79. package/dist/src/architecture/index.js.map +1 -0
  80. package/dist/src/architecture/ir-schema.d.ts +1832 -0
  81. package/dist/src/architecture/ir-schema.d.ts.map +1 -0
  82. package/dist/src/architecture/ir-schema.js +343 -0
  83. package/dist/src/architecture/ir-schema.js.map +1 -0
  84. package/dist/src/architecture/kind-text.d.ts +21 -0
  85. package/dist/src/architecture/kind-text.d.ts.map +1 -0
  86. package/dist/src/architecture/kind-text.js +42 -0
  87. package/dist/src/architecture/kind-text.js.map +1 -0
  88. package/dist/src/architecture/layout.d.ts +227 -0
  89. package/dist/src/architecture/layout.d.ts.map +1 -0
  90. package/dist/src/architecture/layout.js +627 -0
  91. package/dist/src/architecture/layout.js.map +1 -0
  92. package/dist/src/architecture/mcp-tool-match.d.ts +49 -0
  93. package/dist/src/architecture/mcp-tool-match.d.ts.map +1 -0
  94. package/dist/src/architecture/mcp-tool-match.js +53 -0
  95. package/dist/src/architecture/mcp-tool-match.js.map +1 -0
  96. package/dist/src/architecture/merge.d.ts +54 -0
  97. package/dist/src/architecture/merge.d.ts.map +1 -0
  98. package/dist/src/architecture/merge.js +681 -0
  99. package/dist/src/architecture/merge.js.map +1 -0
  100. package/dist/src/architecture/micro-app.d.ts +20 -0
  101. package/dist/src/architecture/micro-app.d.ts.map +1 -0
  102. package/dist/src/architecture/micro-app.js +41 -0
  103. package/dist/src/architecture/micro-app.js.map +1 -0
  104. package/dist/src/architecture/name-ref-match.d.ts +43 -0
  105. package/dist/src/architecture/name-ref-match.d.ts.map +1 -0
  106. package/dist/src/architecture/name-ref-match.js +127 -0
  107. package/dist/src/architecture/name-ref-match.js.map +1 -0
  108. package/dist/src/architecture/packs/data.d.ts +109 -0
  109. package/dist/src/architecture/packs/data.d.ts.map +1 -0
  110. package/dist/src/architecture/packs/data.js +64 -0
  111. package/dist/src/architecture/packs/data.js.map +1 -0
  112. package/dist/src/architecture/packs/generic.d.ts +47 -0
  113. package/dist/src/architecture/packs/generic.d.ts.map +1 -0
  114. package/dist/src/architecture/packs/generic.js +35 -0
  115. package/dist/src/architecture/packs/generic.js.map +1 -0
  116. package/dist/src/architecture/packs/harness.d.ts +96 -0
  117. package/dist/src/architecture/packs/harness.d.ts.map +1 -0
  118. package/dist/src/architecture/packs/harness.js +66 -0
  119. package/dist/src/architecture/packs/harness.js.map +1 -0
  120. package/dist/src/architecture/packs/index.d.ts +840 -0
  121. package/dist/src/architecture/packs/index.d.ts.map +1 -0
  122. package/dist/src/architecture/packs/index.js +87 -0
  123. package/dist/src/architecture/packs/index.js.map +1 -0
  124. package/dist/src/architecture/packs/infra.d.ts +91 -0
  125. package/dist/src/architecture/packs/infra.d.ts.map +1 -0
  126. package/dist/src/architecture/packs/infra.js +57 -0
  127. package/dist/src/architecture/packs/infra.js.map +1 -0
  128. package/dist/src/architecture/packs/knowledge.d.ts +72 -0
  129. package/dist/src/architecture/packs/knowledge.d.ts.map +1 -0
  130. package/dist/src/architecture/packs/knowledge.js +58 -0
  131. package/dist/src/architecture/packs/knowledge.js.map +1 -0
  132. package/dist/src/architecture/packs/process.d.ts +87 -0
  133. package/dist/src/architecture/packs/process.d.ts.map +1 -0
  134. package/dist/src/architecture/packs/process.js +52 -0
  135. package/dist/src/architecture/packs/process.js.map +1 -0
  136. package/dist/src/architecture/packs/render-classes.d.ts +11 -0
  137. package/dist/src/architecture/packs/render-classes.d.ts.map +1 -0
  138. package/dist/src/architecture/packs/render-classes.js +92 -0
  139. package/dist/src/architecture/packs/render-classes.js.map +1 -0
  140. package/dist/src/architecture/packs/types.d.ts +66 -0
  141. package/dist/src/architecture/packs/types.d.ts.map +1 -0
  142. package/dist/src/architecture/packs/types.js +18 -0
  143. package/dist/src/architecture/packs/types.js.map +1 -0
  144. package/dist/src/architecture/packs/web-product.d.ts +348 -0
  145. package/dist/src/architecture/packs/web-product.d.ts.map +1 -0
  146. package/dist/src/architecture/packs/web-product.js +226 -0
  147. package/dist/src/architecture/packs/web-product.js.map +1 -0
  148. package/dist/src/architecture/sequence-layout.d.ts +50 -0
  149. package/dist/src/architecture/sequence-layout.d.ts.map +1 -0
  150. package/dist/src/architecture/sequence-layout.js +140 -0
  151. package/dist/src/architecture/sequence-layout.js.map +1 -0
  152. package/dist/src/architecture/service-facts.d.ts +30 -0
  153. package/dist/src/architecture/service-facts.d.ts.map +1 -0
  154. package/dist/src/architecture/service-facts.js +88 -0
  155. package/dist/src/architecture/service-facts.js.map +1 -0
  156. package/dist/src/architecture/store.d.ts +50 -0
  157. package/dist/src/architecture/store.d.ts.map +1 -0
  158. package/dist/src/architecture/store.js +320 -0
  159. package/dist/src/architecture/store.js.map +1 -0
  160. package/dist/src/architecture/types.d.ts +400 -0
  161. package/dist/src/architecture/types.d.ts.map +1 -0
  162. package/dist/src/architecture/types.js +75 -0
  163. package/dist/src/architecture/types.js.map +1 -0
  164. package/dist/src/architecture/validator.d.ts +65 -0
  165. package/dist/src/architecture/validator.d.ts.map +1 -0
  166. package/dist/src/architecture/validator.js +1249 -0
  167. package/dist/src/architecture/validator.js.map +1 -0
  168. package/dist/src/core/errors.d.ts +4 -0
  169. package/dist/src/core/errors.d.ts.map +1 -1
  170. package/dist/src/core/errors.js +8 -0
  171. package/dist/src/core/errors.js.map +1 -1
  172. package/dist/src/mcp/schemas.d.ts +317 -0
  173. package/dist/src/mcp/schemas.d.ts.map +1 -1
  174. package/dist/src/mcp/schemas.js +151 -0
  175. package/dist/src/mcp/schemas.js.map +1 -1
  176. package/dist/src/mcp/server.d.ts.map +1 -1
  177. package/dist/src/mcp/server.js +7 -1
  178. package/dist/src/mcp/server.js.map +1 -1
  179. package/dist/src/mcp/tools/architecture-passthrough.d.ts +9 -0
  180. package/dist/src/mcp/tools/architecture-passthrough.d.ts.map +1 -0
  181. package/dist/src/mcp/tools/architecture-passthrough.js +587 -0
  182. package/dist/src/mcp/tools/architecture-passthrough.js.map +1 -0
  183. package/dist/src/utils/claude-projects.d.ts +19 -0
  184. package/dist/src/utils/claude-projects.d.ts.map +1 -0
  185. package/dist/src/utils/claude-projects.js +157 -0
  186. package/dist/src/utils/claude-projects.js.map +1 -0
  187. package/dist/src/utils/read-only-tools.d.ts +19 -0
  188. package/dist/src/utils/read-only-tools.d.ts.map +1 -0
  189. package/dist/src/utils/read-only-tools.js +132 -0
  190. package/dist/src/utils/read-only-tools.js.map +1 -0
  191. package/package.json +2 -1
  192. package/plugin/.codex-plugin/plugin.json +1 -1
  193. package/plugin/.mcp.json +1 -1
  194. package/plugin/mcp.json +1 -1
  195. package/plugin/skills/_shared/proactive-routing.md +1 -0
  196. package/plugin/skills/architecture/SKILL.md +913 -0
  197. package/schemas/architecture-ir.schema.json +596 -0
@@ -0,0 +1,627 @@
1
+ // 워커 없는 번들판을 쓴다. 기본 진입점은 web-worker 패키지를 찾다가 MCP stdio 서버에서 경고를 찍을 수 있다
2
+ import ELK from 'elkjs/lib/elk.bundled.js';
3
+ import { flowBadgeText, LANE_TITLES, NODE_KIND_SHORT, platformChipText } from './kind-text.js';
4
+ import { ALL_PACKS_VOCABULARY } from './packs/index.js';
5
+ import { chipTextOverride, displayKindOf, EDGE_KINDS, ENVIRONMENT_ORDER } from './types.js';
6
+ /**
7
+ * 화면에 세로 띠로 그리는 칸. 레벨마다 같은 kind도 다른 레인에 설 수 있어서 kind와 따로 둔다.
8
+ * unit은 서비스 레벨의 기능 영역 칸, external은 서버를 거쳐서만 닿는 칸이다
9
+ */
10
+ export const LANE_IDS = Object.keys(ALL_PACKS_VOCABULARY.lanes);
11
+ /** 평면 그림의 레인. rank가 같은 kind는 같은 레인에 둬야 띠가 겹치지 않는다 */
12
+ export const KIND_LANE = Object.fromEntries(Object.entries(ALL_PACKS_VOCABULARY.nodeKinds).map(([k, d]) => [k, d.lane]));
13
+ /** 노드가 기본으로 서는 레인. MCP 도구는 endpoint와 같은 열이지만 레인 이름이 달라야 API로 안 읽힌다 */
14
+ export function laneOfNode(node) {
15
+ return displayKindOf(node) === 'mcp_tool' ? 'tool' : KIND_LANE[node.kind];
16
+ }
17
+ const LANE_PADDING_X = 20;
18
+ // 화면끼리, 기능 영역끼리 잇는 화면 이동은 요청 흐름이 아니다. elk에 넘기면 같은 레인 안에서 열을 여러 개로 벌려 그림이 옆으로 늘어난다
19
+ const STACKED_LANES = new Set(LANE_IDS.filter((l) => ALL_PACKS_VOCABULARY.lanes[l].stacked === true));
20
+ const GRAPH_OPTIONS = {
21
+ 'elk.algorithm': 'layered',
22
+ 'elk.direction': 'RIGHT',
23
+ 'elk.randomSeed': '1',
24
+ 'elk.partitioning.activate': 'true',
25
+ // 레인 띠 사이 틈과 곡선이 꺾일 자리를 함께 잡는 간격이다
26
+ 'elk.layered.spacing.nodeNodeBetweenLayers': '96',
27
+ 'elk.spacing.nodeNode': '24',
28
+ // 기본값(true)이면 연결 요소마다 따로 배치해 붙이므로 partition 순위가 요소 사이에서 안 지켜진다. 끝점이 비어 고립된 노드도 제 열에 서야 한다
29
+ 'elk.separateConnectedComponents': 'false',
30
+ };
31
+ // order를 받은 노드만 그 순서대로 레이어 안에 세운다. 나머지는 elk가 교차를 줄이는 자리에 둔다
32
+ const ORDERED_GRAPH_OPTIONS = {
33
+ ...GRAPH_OPTIONS,
34
+ 'elk.layered.crossingMinimization.semiInteractive': 'true',
35
+ };
36
+ // 열 순위는 팩의 nodeKinds.rank에 있다
37
+ export const PARTITION_RANK = Object.fromEntries(Object.entries(ALL_PACKS_VOCABULARY.nodeKinds).map(([k, d]) => [k, d.rank]));
38
+ /** 평면 그림에서 레인 순서가 화살표 반대라 오른쪽에서 왼쪽으로 그리는 엣지 */
39
+ export const FLAT_BACKWARD_EDGE_KINDS = new Set(EDGE_KINDS.filter((k) => ALL_PACKS_VOCABULARY.edgeKinds[k].backward === true));
40
+ /** 환경 정렬 순위. 정한 환경이 앞이고 그 밖의 환경, 환경 없음 순이다 */
41
+ export function environmentRank(env) {
42
+ if (env === undefined)
43
+ return ENVIRONMENT_ORDER.length + 1;
44
+ const i = ENVIRONMENT_ORDER.indexOf(env);
45
+ return i === -1 ? ENVIRONMENT_ORDER.length : i;
46
+ }
47
+ /** 환경 순서로 비교하고 같은 순위면 환경 이름, 그다음 tie로 가른다 */
48
+ export function compareEnvironment(a, b, tie = 0) {
49
+ const r = environmentRank(a) - environmentRank(b);
50
+ if (r !== 0)
51
+ return r;
52
+ const x = a ?? '';
53
+ const y = b ?? '';
54
+ return x < y ? -1 : x > y ? 1 : tie;
55
+ }
56
+ const NODE_HEIGHT = 48;
57
+ // 표시 이름과 기술 이름을 두 줄로 쌓는 카드. 글자 수와 무관하게 고정해야 좌표가 입력에만 묶인다
58
+ export const NODE_HEIGHT_TWO_LINES = 60;
59
+ /** "추정" 배지가 첫 줄에서 차지하는 폭. 앞 여백과 알약 테두리, 한글 두 자 */
60
+ export const INFERRED_BADGE_UNITS = 5;
61
+ /** 카드 왼쪽 색 띠와 안쪽 여백. 종류 칩은 이 뒤에서 시작한다 */
62
+ export const NODE_TEXT_LEFT = 12;
63
+ /** 글자 뒤 여백. 들어갈 레벨 표시(›)가 오른쪽 아래에 앉을 자리를 포함한다 */
64
+ export const NODE_TEXT_RIGHT = 20;
65
+ // 종류 칩: 아이콘과 좌우 여백, 칩 뒤 틈이 고정 폭이고 글자는 칩 글꼴(10.5px) 기준으로 잰다
66
+ const CHIP_FIXED_WIDTH = 33;
67
+ const CHIP_CHAR_WIDTH = 6.5;
68
+ // 플랫폼 칩: 앞 여백, 테두리와 좌우 안쪽 여백, 아이콘과 글자 사이가 고정 폭이고 글자는 10px 글꼴 기준이다
69
+ const PLATFORM_CHIP_FIXED_WIDTH = 4 + 2 + 8 + 11 + 3;
70
+ const PLATFORM_CHIP_CHAR_WIDTH = 6;
71
+ // 흐름 배지: 앞 여백, 좌우 안쪽 여백, 아이콘과 글자 사이가 고정 폭이고 글자는 10.5px 글꼴 기준이다
72
+ const FLOW_BADGE_FIXED_WIDTH = 6 + 12 + 12 + 3;
73
+ const FLOW_BADGE_CHAR_WIDTH = 6.5;
74
+ /** 흐름 배지가 첫 줄에서 차지하는 폭(px) */
75
+ export function flowBadgeWidth(count) {
76
+ return count === undefined || count === 0
77
+ ? 0
78
+ : FLOW_BADGE_FIXED_WIDTH + textUnits(flowBadgeText(count)) * FLOW_BADGE_CHAR_WIDTH;
79
+ }
80
+ /** 플랫폼 칩들이 둘째 줄에서 차지하는 폭(px) */
81
+ export function platformChipsWidth(platforms, webHosting) {
82
+ let w = 0;
83
+ for (const p of platforms ?? []) {
84
+ w +=
85
+ PLATFORM_CHIP_FIXED_WIDTH +
86
+ textUnits(platformChipText(p, webHosting)) * PLATFORM_CHIP_CHAR_WIDTH;
87
+ }
88
+ return w;
89
+ }
90
+ /** 종류 칩이 첫 줄에서 차지하는 폭(px) */
91
+ export function chipWidth(kind, chip) {
92
+ return kind === undefined
93
+ ? 0
94
+ : CHIP_FIXED_WIDTH + textUnits(chip ?? NODE_KIND_SHORT[kind]) * CHIP_CHAR_WIDTH;
95
+ }
96
+ export const NARROW_CHAR_WIDTH = 8;
97
+ const MIN_NODE_WIDTH = 120;
98
+ const MAX_NODE_WIDTH = 320;
99
+ // 한글 같은 전각 문자는 라틴 문자 두 칸을 먹는다. 실제 폰트 측정 대신 글자 수로 재야 서버 결과가 환경에 안 흔들린다
100
+ function charUnits(ch) {
101
+ const code = ch.codePointAt(0) ?? 0;
102
+ return code >= 0x2e80 ? 2 : 1;
103
+ }
104
+ export function textUnits(text) {
105
+ let units = 0;
106
+ for (const ch of text)
107
+ units += charUnits(ch);
108
+ return units;
109
+ }
110
+ /**
111
+ * 첫 줄은 종류 칩과 이름, 둘째 줄은 secondLine이 있으면 그것, 없고 표시 이름이 있으면 기술 이름이다.
112
+ * 플랫폼 칩은 둘째 줄 끝에 붙는다. 첫 줄에 두면 칩 몫만큼 이름이 먼저 잘린다.
113
+ * 폭은 두 줄 중 긴 쪽을 따른다
114
+ */
115
+ export function measureNode(label, displayName, displayNameInferred, kind, extras = {}) {
116
+ const name = displayName ?? label;
117
+ const first = chipWidth(kind, extras.chip) +
118
+ (textUnits(name) +
119
+ (displayName !== undefined && displayNameInferred ? INFERRED_BADGE_UNITS : 0)) *
120
+ NARROW_CHAR_WIDTH +
121
+ flowBadgeWidth(extras.flows);
122
+ const second = extras.secondLine ?? (displayName !== undefined ? label : undefined);
123
+ const chips = platformChipsWidth(extras.platforms, extras.webHosting);
124
+ const twoLines = second !== undefined || chips > 0;
125
+ const inner = twoLines
126
+ ? Math.max(first, textUnits(second ?? '') * NARROW_CHAR_WIDTH + chips)
127
+ : first;
128
+ const raw = Math.ceil(NODE_TEXT_LEFT + NODE_TEXT_RIGHT + inner);
129
+ return {
130
+ width: Math.min(MAX_NODE_WIDTH, Math.max(MIN_NODE_WIDTH, raw)),
131
+ height: twoLines ? NODE_HEIGHT_TWO_LINES : NODE_HEIGHT,
132
+ };
133
+ }
134
+ function round2(v) {
135
+ return Math.round(v * 100) / 100;
136
+ }
137
+ // localeCompare는 실행 환경 로캘에 따라 순서가 달라질 수 있어 코드 유닛 비교로 고정한다
138
+ function byId(a, b) {
139
+ return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
140
+ }
141
+ // 레벨마다 덧대는 rank 보정은 다 합쳐도 이보다 작다. 구간 순번에 곱해 앞자리로 두면 구간 안에서는 원래 순서가 그대로 남는다
142
+ const STAGE_RANK_STRIDE = 100;
143
+ /** 어느 구간에도 안 맞은 노드를 모으는 맨 끝 구간 이름 */
144
+ export const UNSTAGED_LABEL = '그 밖';
145
+ /** 노드마다 구간을 정한다. 이름으로 올린 구간이 먼저다. 그다음은 kinds와 repos가 맞는 첫 구간이다 */
146
+ export function assignStages(ir) {
147
+ const stages = ir.stages ?? [];
148
+ if (stages.length === 0)
149
+ return undefined;
150
+ const named = new Map();
151
+ stages.forEach((st, i) => {
152
+ for (const id of st.nodes ?? [])
153
+ if (!named.has(id))
154
+ named.set(id, i);
155
+ });
156
+ const indexOf = new Map();
157
+ let unstaged = 0;
158
+ for (const n of ir.nodes) {
159
+ const byName = named.get(n.id);
160
+ const byRule = stages.findIndex((st) => st.kinds !== undefined &&
161
+ st.kinds.includes(n.kind) &&
162
+ (st.repos === undefined || st.repos.includes(n.repo)));
163
+ const index = byName ?? (byRule >= 0 ? byRule : stages.length);
164
+ if (index === stages.length)
165
+ unstaged += 1;
166
+ indexOf.set(n.id, index);
167
+ }
168
+ const labels = stages.map((st) => st.label);
169
+ if (unstaged > 0)
170
+ labels.push(UNSTAGED_LABEL);
171
+ return { indexOf, labels, unstaged };
172
+ }
173
+ export async function computeLayout(ir, drawableNodeIds, drawableEdgeIds) {
174
+ const staged = assignStages(ir);
175
+ const nodes = ir.nodes
176
+ .filter((n) => drawableNodeIds.has(n.id))
177
+ .map((n) => ({
178
+ id: n.id,
179
+ label: n.label,
180
+ ...(n.displayName !== undefined ? { displayName: n.displayName } : {}),
181
+ ...(n.displayNameInferred ? { displayNameInferred: true } : {}),
182
+ rank: PARTITION_RANK[n.kind],
183
+ lane: laneOfNode(n),
184
+ ...(staged !== undefined ? { stage: staged.indexOf.get(n.id) } : {}),
185
+ kind: displayKindOf(n),
186
+ ...(chipTextOverride(n) !== undefined ? { chip: chipTextOverride(n) } : {}),
187
+ ...(n.environment !== undefined ? { order: environmentRank(n.environment) } : {}),
188
+ }));
189
+ const edges = ir.edges
190
+ .filter((e) => drawableEdgeIds.has(e.id))
191
+ .map((e) => ({
192
+ id: e.id,
193
+ from: e.from,
194
+ to: e.to,
195
+ ...(FLAT_BACKWARD_EDGE_KINDS.has(e.kind) ? { backward: true } : {}),
196
+ }));
197
+ const laid = await computeGraphLayout(nodes, edges, staged?.labels);
198
+ return stackBands(laid, bandGroupsOf(ir), ir.edges);
199
+ }
200
+ /**
201
+ * IR과 무관한 노드와 엣지 목록으로 elkjs 좌표를 낸다. 드릴다운 레벨의 묶음 엣지처럼 IR에 없는 엣지도 받는다.
202
+ * 같은 입력이면 같은 좌표가 나오게 입력과 출력을 id 순으로 정렬한다.
203
+ */
204
+ export async function computeGraphLayout(nodeList, edgeList, stageLabels) {
205
+ const nodes = [...nodeList].sort(byId);
206
+ const rankOf = (n) => stageLabels !== undefined && n.stage !== undefined
207
+ ? n.stage * STAGE_RANK_STRIDE + n.rank
208
+ : n.rank;
209
+ const laneOf = new Map(nodes.map((n) => [n.id, n.lane]));
210
+ // 끝점이 하나라도 빠진 엣지를 넘기면 ELK가 예외를 던진다
211
+ const edges = edgeList
212
+ .filter((e) => laneOf.has(e.from) && laneOf.has(e.to))
213
+ .filter((e) => !(laneOf.get(e.from) === laneOf.get(e.to) && STACKED_LANES.has(laneOf.get(e.from))))
214
+ .sort(byId);
215
+ const ordered = nodes.some((n) => n.order !== undefined);
216
+ const graph = {
217
+ id: 'root',
218
+ layoutOptions: ordered ? ORDERED_GRAPH_OPTIONS : GRAPH_OPTIONS,
219
+ children: nodes.map((n) => ({
220
+ id: n.id,
221
+ ...measureNode(n.label, n.displayName, n.displayNameInferred, n.kind, {
222
+ ...(n.chip !== undefined ? { chip: n.chip } : {}),
223
+ ...(n.platforms !== undefined ? { platforms: n.platforms } : {}),
224
+ ...(n.webHosting !== undefined ? { webHosting: n.webHosting } : {}),
225
+ ...(n.secondLine !== undefined ? { secondLine: n.secondLine } : {}),
226
+ ...(n.flows !== undefined ? { flows: n.flows } : {}),
227
+ }),
228
+ // 레이어 안 카드를 왼쪽에 맞춰야 레인이 들쭉날쭉한 열이 아니라 한 줄로 읽힌다
229
+ layoutOptions: {
230
+ 'elk.partitioning.partition': String(rankOf(n)),
231
+ 'elk.alignment': 'LEFT',
232
+ ...(n.order !== undefined ? { 'elk.position': `(0,${n.order})` } : {}),
233
+ },
234
+ })),
235
+ edges: edges.map((e) => e.backward
236
+ ? { id: e.id, sources: [e.to], targets: [e.from] }
237
+ : { id: e.id, sources: [e.from], targets: [e.to] }),
238
+ };
239
+ const laid = await new ELK().layout(graph);
240
+ const outNodes = (laid.children ?? [])
241
+ .map((c) => ({
242
+ id: c.id,
243
+ x: round2(c.x ?? 0),
244
+ y: round2(c.y ?? 0),
245
+ width: round2(c.width ?? 0),
246
+ height: round2(c.height ?? 0),
247
+ }))
248
+ .sort(byId);
249
+ // 한 레이어(같은 x) 카드는 폭을 가장 넓은 카드에 맞춘다. 레이어 폭이 원래 그 값이라 옆 레이어와 겹치지 않는다
250
+ const columnWidth = new Map();
251
+ for (const n of outNodes)
252
+ columnWidth.set(n.x, Math.max(columnWidth.get(n.x) ?? 0, n.width));
253
+ for (const n of outNodes)
254
+ n.width = columnWidth.get(n.x);
255
+ const outEdges = (laid.edges ?? [])
256
+ .map((e) => ({
257
+ id: e.id,
258
+ points: (e.sections ?? []).flatMap((s) => [s.startPoint, ...(s.bendPoints ?? []), s.endPoint].map((p) => ({
259
+ x: round2(p.x),
260
+ y: round2(p.y),
261
+ }))),
262
+ }))
263
+ .sort(byId);
264
+ return {
265
+ width: round2(laid.width ?? 0),
266
+ height: round2(laid.height ?? 0),
267
+ nodes: outNodes,
268
+ edges: outEdges,
269
+ lanes: computeLanes(nodes, outNodes, stageLabels),
270
+ };
271
+ }
272
+ // 레인 경계를 서버에서 박아야 브라우저 글꼴이나 창 크기와 무관하게 같은 그림이 나온다
273
+ function computeLanes(input, placed, stageLabels) {
274
+ const titleOf = new Map();
275
+ const laneOf = new Map(input.map((n) => {
276
+ if (stageLabels !== undefined && n.stage !== undefined) {
277
+ const id = `stage:${n.stage}`;
278
+ titleOf.set(id, stageLabels[n.stage] ?? UNSTAGED_LABEL);
279
+ return [n.id, id];
280
+ }
281
+ titleOf.set(n.lane, LANE_TITLES[n.lane]);
282
+ return [n.id, n.lane];
283
+ }));
284
+ const spans = new Map();
285
+ for (const box of placed) {
286
+ const lane = laneOf.get(box.id);
287
+ if (lane === undefined)
288
+ continue;
289
+ const span = spans.get(lane);
290
+ if (span) {
291
+ span.left = Math.min(span.left, box.x);
292
+ span.right = Math.max(span.right, box.x + box.width);
293
+ span.count += 1;
294
+ }
295
+ else {
296
+ spans.set(lane, { left: box.x, right: box.x + box.width, count: 1 });
297
+ }
298
+ }
299
+ return [...spans.entries()]
300
+ .map(([id, s]) => ({
301
+ id,
302
+ title: titleOf.get(id),
303
+ x: round2(s.left - LANE_PADDING_X),
304
+ width: round2(s.right - s.left + LANE_PADDING_X * 2),
305
+ count: s.count,
306
+ }))
307
+ .sort((a, b) => a.x - b.x || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
308
+ }
309
+ const PIN_GAP = 24;
310
+ /**
311
+ * 노드 하나를 제 열 맨 위로 올린다. 그 위에 있던 카드는 올린 카드 높이만큼 내려 원래 자리에 들어간다.
312
+ * 자리가 바뀐 카드에 붙은 elk 경로는 버린다. 옛 좌표 기준이라 경유점으로 쓰면 엉뚱한 데서 꺾인다
313
+ */
314
+ export function pinToColumnTop(layout, nodeId) {
315
+ const target = layout.nodes.find((n) => n.id === nodeId);
316
+ if (!target)
317
+ return layout;
318
+ const above = layout.nodes.filter((n) => n.x === target.x && n.y < target.y);
319
+ if (above.length === 0)
320
+ return layout;
321
+ const top = Math.min(...above.map((n) => n.y));
322
+ const moved = new Set([nodeId, ...above.map((n) => n.id)]);
323
+ const shift = target.height + PIN_GAP;
324
+ const nodes = layout.nodes.map((n) => {
325
+ if (n.id === nodeId)
326
+ return { ...n, y: top };
327
+ if (moved.has(n.id))
328
+ return { ...n, y: round2(n.y + shift) };
329
+ return { ...n };
330
+ });
331
+ const bottom = Math.max(...nodes.map((n) => n.y + n.height));
332
+ return {
333
+ ...layout,
334
+ height: round2(Math.max(layout.height, bottom)),
335
+ nodes,
336
+ edges: layout.edges.map((e) => ({ ...e })),
337
+ movedNodeIds: [...moved].sort(),
338
+ };
339
+ }
340
+ /**
341
+ * 나무를 위에서부터 깊이 우선으로 쌓는다. 잎은 차례로 한 칸씩 내려가고 부모는 첫 자식과 같은 높이에 선다.
342
+ * 형제 하위 나무끼리 세로 구간이 안 겹쳐서 어느 화면이 어느 앱 것인지 블록으로 읽힌다. elk는 교차만 줄여서 앱끼리 섞어 놓는다
343
+ */
344
+ export function stackTree(layout, tree) {
345
+ const byId = new Map(layout.nodes.map((n) => [n.id, n]));
346
+ if (!byId.has(tree.root))
347
+ return layout;
348
+ const placed = new Map();
349
+ const top = byId.get(tree.root).y;
350
+ let cursor = top;
351
+ const visit = (id, seen) => {
352
+ const node = byId.get(id);
353
+ if (!node || seen.has(id))
354
+ return;
355
+ seen.add(id);
356
+ const start = cursor;
357
+ for (const c of tree.children.get(id) ?? [])
358
+ visit(c, seen);
359
+ placed.set(id, round2(start));
360
+ cursor = Math.max(cursor, start + node.height + PIN_GAP);
361
+ };
362
+ visit(tree.root, new Set());
363
+ const nodes = layout.nodes.map((n) => ({ ...n, y: placed.get(n.id) ?? n.y }));
364
+ const moved = nodes.filter((n, i) => n.y !== layout.nodes[i].y).map((n) => n.id);
365
+ const bottom = Math.max(...nodes.map((n) => n.y + n.height));
366
+ return {
367
+ ...layout,
368
+ height: round2(Math.max(layout.height, bottom)),
369
+ nodes,
370
+ edges: layout.edges.map((e) => ({ ...e })),
371
+ movedNodeIds: [...new Set([...(layout.movedNodeIds ?? []), ...moved])].sort(),
372
+ };
373
+ }
374
+ /**
375
+ * ids로만 이뤄진 열을 통째로 올리거나 내려 그 열 맨 위 카드가 기준 카드와 같은 높이에 서게 한다.
376
+ * 맨 위로 올린 소유자 카드와 그 서빙 사슬이 한 줄로 읽혀야 하는데, elk는 사슬을 소유자의 원래 자리 옆에 두기 때문이다
377
+ */
378
+ export function alignColumnsTo(layout, anchorId, nodeIds) {
379
+ const anchor = layout.nodes.find((n) => n.id === anchorId);
380
+ if (!anchor)
381
+ return layout;
382
+ const members = new Set(nodeIds);
383
+ const columns = new Map();
384
+ for (const n of layout.nodes)
385
+ columns.set(n.x, [...(columns.get(n.x) ?? []), n]);
386
+ const shift = new Map();
387
+ for (const col of columns.values()) {
388
+ if (col.some((n) => n.id === anchorId) || !col.every((n) => members.has(n.id)))
389
+ continue;
390
+ const dy = anchor.y - Math.min(...col.map((n) => n.y));
391
+ if (dy !== 0)
392
+ for (const n of col)
393
+ shift.set(n.id, dy);
394
+ }
395
+ if (shift.size === 0)
396
+ return layout;
397
+ const nodes = layout.nodes.map((n) => ({ ...n, y: round2(n.y + (shift.get(n.id) ?? 0)) }));
398
+ const bottom = Math.max(...nodes.map((n) => n.y + n.height));
399
+ return {
400
+ ...layout,
401
+ height: round2(Math.max(layout.height, bottom)),
402
+ nodes,
403
+ edges: layout.edges.map((e) => ({ ...e })),
404
+ movedNodeIds: [...new Set([...(layout.movedNodeIds ?? []), ...shift.keys()])].sort(),
405
+ };
406
+ }
407
+ /**
408
+ * 노드를 제 열 맨 아래로 내려 받은 순서대로 쌓는다. 그 열의 나머지 카드는 elk가 정한 위아래 순서대로 위에서부터 다시 잇는다.
409
+ * 기능 영역과 같은 열에 서는 앱 카드가 기능 영역 사이에 끼면 어느 카드가 한 단계 아래 앱인지 안 읽혀서 쓴다
410
+ */
411
+ export function sinkToColumnBottom(layout, nodeIds) {
412
+ const sinkOrder = new Map(nodeIds.map((id, i) => [id, i]));
413
+ const placed = new Map();
414
+ const columns = new Map();
415
+ for (const n of layout.nodes)
416
+ columns.set(n.x, [...(columns.get(n.x) ?? []), n]);
417
+ for (const col of columns.values()) {
418
+ if (!col.some((n) => sinkOrder.has(n.id)))
419
+ continue;
420
+ const byY = [...col].sort((a, b) => a.y - b.y || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
421
+ const stacked = [
422
+ ...byY.filter((n) => !sinkOrder.has(n.id)),
423
+ ...byY
424
+ .filter((n) => sinkOrder.has(n.id))
425
+ .sort((a, b) => sinkOrder.get(a.id) - sinkOrder.get(b.id)),
426
+ ];
427
+ let y = byY[0].y;
428
+ for (const n of stacked) {
429
+ placed.set(n.id, round2(y));
430
+ y += n.height + PIN_GAP;
431
+ }
432
+ }
433
+ if (placed.size === 0)
434
+ return layout;
435
+ const nodes = layout.nodes.map((n) => ({ ...n, y: placed.get(n.id) ?? n.y }));
436
+ const moved = nodes.filter((n, i) => n.y !== layout.nodes[i].y).map((n) => n.id);
437
+ const bottom = Math.max(...nodes.map((n) => n.y + n.height));
438
+ return {
439
+ ...layout,
440
+ height: round2(Math.max(layout.height, bottom)),
441
+ nodes,
442
+ edges: layout.edges.map((e) => ({ ...e })),
443
+ movedNodeIds: [...new Set([...(layout.movedNodeIds ?? []), ...moved])].sort(),
444
+ };
445
+ }
446
+ // 테두리와 카드 사이 여백. 카드 사이 간격(PIN_GAP)의 절반보다 작아야 이웃 카드와 테두리가 안 겹친다
447
+ export const FRAME_PAD = 8;
448
+ /**
449
+ * 멤버 카드를 머리 카드 열로 옮겨 머리 바로 아래에 잇는다. 그 열의 나머지 카드는 elk가 정한 위아래 순서를 지키며 아래로 민다.
450
+ * 멤버가 다른 열에 있었으면 그 열은 빈자리를 그대로 둔다. 다른 카드 자리를 건드리면 elk 경로가 더 많이 틀어진다
451
+ */
452
+ export function stackFrames(layout, frames) {
453
+ const byNode = new Map(layout.nodes.map((n) => [n.id, n]));
454
+ const live = frames
455
+ .map((f) => ({ head: f.head, members: f.members.filter((m) => byNode.has(m)) }))
456
+ .filter((f) => byNode.has(f.head) && f.members.length > 0);
457
+ if (live.length === 0)
458
+ return layout;
459
+ const memberOf = new Set(live.flatMap((f) => f.members));
460
+ const placed = new Map();
461
+ for (const x of [...new Set(live.map((f) => byNode.get(f.head).x))].sort((a, b) => a - b)) {
462
+ const column = layout.nodes
463
+ .filter((n) => n.x === x && !memberOf.has(n.id))
464
+ .sort((a, b) => a.y - b.y || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
465
+ const order = column.flatMap((n) => [
466
+ n,
467
+ ...(live.find((f) => f.head === n.id)?.members.map((m) => byNode.get(m)) ?? []),
468
+ ]);
469
+ const width = Math.max(...order.map((n) => n.width));
470
+ let y = Math.min(...order.map((n) => n.y));
471
+ for (const n of order) {
472
+ placed.set(n.id, { x, y: round2(y), width });
473
+ y += n.height + PIN_GAP;
474
+ }
475
+ }
476
+ const nodes = layout.nodes.map((n) => ({ ...n, ...placed.get(n.id) }));
477
+ const moved = nodes
478
+ .filter((n, i) => n.y !== layout.nodes[i].y || n.x !== layout.nodes[i].x)
479
+ .map((n) => n.id);
480
+ const bottom = Math.max(...nodes.map((n) => n.y + n.height));
481
+ return {
482
+ ...layout,
483
+ height: round2(Math.max(layout.height, bottom + FRAME_PAD)),
484
+ nodes,
485
+ edges: layout.edges.map((e) => ({ ...e })),
486
+ movedNodeIds: [...new Set([...(layout.movedNodeIds ?? []), ...moved])].sort(),
487
+ };
488
+ }
489
+ /** 쌓기가 다 끝난 좌표에서 테두리를 낸다. 띠로 다시 쌓으면 카드가 움직이므로 맨 마지막에 부른다 */
490
+ export function frameRects(layout, frames) {
491
+ const byNode = new Map(layout.nodes.map((n) => [n.id, n]));
492
+ const rects = [];
493
+ for (const f of [...frames].sort((a, b) => (a.head < b.head ? -1 : a.head > b.head ? 1 : 0))) {
494
+ const boxes = [f.head, ...f.members].map((id) => byNode.get(id));
495
+ if (boxes[0] === undefined || boxes.slice(1).every((b) => b === undefined))
496
+ continue;
497
+ const present = boxes.filter((b) => b !== undefined);
498
+ const left = Math.min(...present.map((n) => n.x)) - FRAME_PAD;
499
+ const top = Math.min(...present.map((n) => n.y)) - FRAME_PAD;
500
+ const right = Math.max(...present.map((n) => n.x + n.width)) + FRAME_PAD;
501
+ const bottom = Math.max(...present.map((n) => n.y + n.height)) + FRAME_PAD;
502
+ rects.push({
503
+ id: f.head,
504
+ x: round2(left),
505
+ y: round2(top),
506
+ width: round2(right - left),
507
+ height: round2(bottom - top),
508
+ });
509
+ }
510
+ return rects.length === 0 ? layout : { ...layout, frames: rects };
511
+ }
512
+ export function bandGroupsOf(ir) {
513
+ return (ir.groups ?? []).map((g) => ({ id: g.id, name: g.name, members: new Set(g.members) }));
514
+ }
515
+ function rankProductsByLinks(productsOf, groups, links) {
516
+ const ownerOf = (id) => {
517
+ const owners = groups.flatMap((g, i) => (g.members.has(id) ? [i] : []));
518
+ return owners.length === 1 ? owners[0] : undefined;
519
+ };
520
+ const weight = new Map();
521
+ const add = (shared, other) => {
522
+ if ((productsOf.get(shared)?.length ?? 0) < 2)
523
+ return;
524
+ const owner = ownerOf(other);
525
+ if (owner === undefined)
526
+ return;
527
+ const w = weight.get(shared) ?? new Map();
528
+ w.set(owner, (w.get(owner) ?? 0) + 1);
529
+ weight.set(shared, w);
530
+ };
531
+ for (const l of links) {
532
+ add(l.from, l.to);
533
+ add(l.to, l.from);
534
+ }
535
+ for (const [id, w] of weight) {
536
+ productsOf.get(id).sort((a, b) => (w.get(b) ?? 0) - (w.get(a) ?? 0) || a - b);
537
+ }
538
+ }
539
+ // 띠 머리 높이와 띠 끝 여백. 머리에는 구분선과 띠 이름, 그 아래 첫 카드의 제품 브릭이 들어간다. 포커스 화면 스크립트도 같은 값을 쓴다
540
+ export const BAND_HEAD = 40;
541
+ export const BAND_TAIL = 14;
542
+ /**
543
+ * 여러 제품을 합친 그림을 가로 띠로 다시 쌓는다. 맨 위가 둘 이상의 제품이 같이 쓰는 띠이고 그 아래로 그룹 순서대로 제품마다 전용 띠가 이어진다.
544
+ * 어느 그룹에도 없는 노드는 같이 쓰는 띠에 둔다. 한 제품 몫이라고 말할 근거가 없어서다.
545
+ * 열(x)은 elk가 정한 그대로 둔다. 같이 쓰는 띠 안에서는 쓰는 제품이 많은 카드가 위로 가고 그 밖에는 elk의 위아래 순서를 지킨다.
546
+ * 띠 높이는 모든 열에서 같게 맞춰야 구분선 한 줄로 띠가 갈린다. 카드가 있는 띠가 하나뿐이면 나눠 보일 게 없어 그대로 돌려준다.
547
+ * 같이 쓰는 카드의 제품 순서는 그 카드와 바로 이어진 그 제품 전용 카드가 많은 순이다. 게이트웨이라면 그 제품 API로 가는 선이 많은 쪽이 앞에 온다. 같으면 그룹 순서다
548
+ */
549
+ export function stackBands(layout, groups, links = []) {
550
+ if (groups.length < 2 || layout.nodes.length === 0)
551
+ return layout;
552
+ const productsOf = new Map();
553
+ for (const n of layout.nodes) {
554
+ productsOf.set(n.id, groups.flatMap((g, i) => (g.members.has(n.id) ? [i] : [])));
555
+ }
556
+ rankProductsByLinks(productsOf, groups, links);
557
+ const bandOf = (id) => {
558
+ const ps = productsOf.get(id);
559
+ return ps.length === 1 ? ps[0] + 1 : 0;
560
+ };
561
+ const bandCount = groups.length + 1;
562
+ const counts = Array.from({ length: bandCount }, () => 0);
563
+ for (const n of layout.nodes)
564
+ counts[bandOf(n.id)] += 1;
565
+ if (counts.filter((c) => c > 0).length < 2)
566
+ return layout;
567
+ const columns = new Map();
568
+ for (const n of layout.nodes)
569
+ columns.set(n.x, [...(columns.get(n.x) ?? []), n]);
570
+ const bandHeight = Array.from({ length: bandCount }, () => 0);
571
+ for (const col of columns.values()) {
572
+ for (let band = 0; band < bandCount; band++) {
573
+ const inBand = col.filter((n) => bandOf(n.id) === band);
574
+ if (inBand.length === 0)
575
+ continue;
576
+ const h = inBand.reduce((sum, n) => sum + n.height, 0) + PIN_GAP * (inBand.length - 1);
577
+ bandHeight[band] = Math.max(bandHeight[band], h);
578
+ }
579
+ }
580
+ const top = Math.min(...layout.nodes.map((n) => n.y));
581
+ const bottomMargin = layout.height - Math.max(...layout.nodes.map((n) => n.y + n.height));
582
+ const bands = [];
583
+ let cursor = top;
584
+ for (let band = 0; band < bandCount; band++) {
585
+ if (counts[band] === 0)
586
+ continue;
587
+ const height = BAND_HEAD + bandHeight[band] + BAND_TAIL;
588
+ bands.push({ band, y: round2(cursor), height: round2(height) });
589
+ cursor += height;
590
+ }
591
+ const bandTop = new Map(bands.map((b) => [b.band, b.y + BAND_HEAD]));
592
+ const share = (n) => productsOf.get(n.id).length;
593
+ const placed = new Map();
594
+ for (const col of columns.values()) {
595
+ for (const [band, start] of bandTop) {
596
+ let y = start;
597
+ const inBand = col
598
+ .filter((n) => bandOf(n.id) === band)
599
+ .sort((a, b) => (band === 0 ? share(b) - share(a) : 0) ||
600
+ a.y - b.y ||
601
+ (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
602
+ for (const n of inBand) {
603
+ placed.set(n.id, round2(y));
604
+ y += n.height + PIN_GAP;
605
+ }
606
+ }
607
+ }
608
+ const nodes = layout.nodes.map((n) => ({ ...n, y: placed.get(n.id) ?? n.y }));
609
+ const moved = nodes.filter((n, i) => n.y !== layout.nodes[i].y).map((n) => n.id);
610
+ const sortedIds = nodes.map((n) => n.id).sort();
611
+ return {
612
+ ...layout,
613
+ height: round2(cursor + bottomMargin),
614
+ nodes,
615
+ edges: layout.edges.map((e) => ({ ...e })),
616
+ movedNodeIds: [...new Set([...(layout.movedNodeIds ?? []), ...moved])].sort(),
617
+ regions: {
618
+ groups: groups.map((g) => ({ id: g.id, name: g.name })),
619
+ bands,
620
+ bandOf: Object.fromEntries(sortedIds.map((id) => [id, bandOf(id)])),
621
+ productsOf: Object.fromEntries(sortedIds
622
+ .filter((id) => productsOf.get(id).length > 1)
623
+ .map((id) => [id, productsOf.get(id)])),
624
+ },
625
+ };
626
+ }
627
+ //# sourceMappingURL=layout.js.map