@zenuml/core 3.47.9 → 3.48.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.
Files changed (205) hide show
  1. package/dist/cli/zenuml.mjs +13529 -0
  2. package/dist/cli/zenuml.mjs.map +1 -0
  3. package/dist/cloud-icons-eHuugVSv.js.map +1 -0
  4. package/dist/zenuml.esm.mjs +2153 -2156
  5. package/dist/zenuml.esm.mjs.map +1 -0
  6. package/dist/zenuml.js +82 -82
  7. package/dist/zenuml.js.map +1 -0
  8. package/package.json +18 -5
  9. package/.agents/skills/babysit-pr/SKILL.md +0 -223
  10. package/.agents/skills/babysit-pr/agents/openai.yaml +0 -7
  11. package/.agents/skills/dia-scoring/SKILL.md +0 -139
  12. package/.agents/skills/dia-scoring/agents/openai.yaml +0 -7
  13. package/.agents/skills/dia-scoring/references/selectors-and-keys.md +0 -253
  14. package/.agents/skills/land-pr/SKILL.md +0 -120
  15. package/.agents/skills/propagate-core-release/SKILL.md +0 -205
  16. package/.agents/skills/propagate-core-release/agents/openai.yaml +0 -7
  17. package/.agents/skills/propagate-core-release/references/downstreams.md +0 -42
  18. package/.agents/skills/ship-branch/SKILL.md +0 -105
  19. package/.agents/skills/submit-branch/SKILL.md +0 -76
  20. package/.agents/skills/validate-branch/SKILL.md +0 -72
  21. package/.claude/commands/README.md +0 -162
  22. package/.claude/commands/analyze.md +0 -101
  23. package/.claude/commands/clarify.md +0 -158
  24. package/.claude/commands/code-review.md +0 -322
  25. package/.claude/commands/constitution.md +0 -73
  26. package/.claude/commands/create-docs.md +0 -309
  27. package/.claude/commands/full-context.md +0 -121
  28. package/.claude/commands/gemini-consult.md +0 -164
  29. package/.claude/commands/handoff.md +0 -146
  30. package/.claude/commands/implement.md +0 -56
  31. package/.claude/commands/plan.md +0 -43
  32. package/.claude/commands/refactor.md +0 -188
  33. package/.claude/commands/specify.md +0 -21
  34. package/.claude/commands/tasks.md +0 -62
  35. package/.claude/commands/update-docs.md +0 -314
  36. package/.claude/hooks/README.md +0 -270
  37. package/.claude/hooks/config/sensitive-patterns.json +0 -86
  38. package/.claude/hooks/gemini-context-injector.sh +0 -129
  39. package/.claude/hooks/mcp-security-scan.sh +0 -147
  40. package/.claude/hooks/notify.sh +0 -103
  41. package/.claude/hooks/setup/hook-setup.md +0 -96
  42. package/.claude/hooks/setup/settings.json.template +0 -63
  43. package/.claude/hooks/sounds/complete.wav +0 -0
  44. package/.claude/hooks/sounds/input-needed.wav +0 -0
  45. package/.claude/hooks/subagent-context-injector.sh +0 -65
  46. package/.claude/skills/babysit-pr/SKILL.md +0 -223
  47. package/.claude/skills/babysit-pr/agents/openai.yaml +0 -7
  48. package/.claude/skills/dia-scoring/SKILL.md +0 -139
  49. package/.claude/skills/dia-scoring/agents/openai.yaml +0 -7
  50. package/.claude/skills/dia-scoring/references/selectors-and-keys.md +0 -253
  51. package/.claude/skills/emoji-eval/SKILL.md +0 -187
  52. package/.claude/skills/land-pr/SKILL.md +0 -120
  53. package/.claude/skills/propagate-core-release/SKILL.md +0 -205
  54. package/.claude/skills/propagate-core-release/agents/openai.yaml +0 -7
  55. package/.claude/skills/propagate-core-release/references/downstreams.md +0 -42
  56. package/.claude/skills/ship-branch/SKILL.md +0 -105
  57. package/.claude/skills/submit-branch/SKILL.md +0 -76
  58. package/.claude/skills/validate-branch/SKILL.md +0 -72
  59. package/.claude/skills/zenuml-ux-research/SKILL.md +0 -183
  60. package/.claude/skills/zenuml-ux-research/references/assertion-catalog.md +0 -261
  61. package/.claude/skills/zenuml-ux-research/references/best-practices-overview.md +0 -56
  62. package/.claude/skills/zenuml-ux-research/references/report-template.md +0 -89
  63. package/.claude/skills/zenuml-ux-research/references/scenarios/edit-message-label.md +0 -37
  64. package/.claude/skills/zenuml-ux-research/references/scenarios/insert-message.md +0 -36
  65. package/.claude/skills/zenuml-ux-research/references/scenarios/insert-participant.md +0 -31
  66. package/.claude/skills/zenuml-ux-research/references/scenarios/rename-participant.md +0 -33
  67. package/.claude/skills/zenuml-ux-research/references/scenarios/undo-insert.md +0 -35
  68. package/.devcontainer/devcontainer.json +0 -21
  69. package/.dockerignore +0 -19
  70. package/.eslintrc.js +0 -39
  71. package/.git-blame-ignore-revs +0 -6
  72. package/.kiro/hooks/README.md +0 -38
  73. package/.kiro/hooks/session-sound-notification.js +0 -44
  74. package/.kiro/hooks/session-sound-notification.json +0 -23
  75. package/.mcp.json.example +0 -17
  76. package/.nvmrc +0 -1
  77. package/.prettierignore +0 -4
  78. package/.prettierrc +0 -1
  79. package/.specify/memory/constitution.md +0 -33
  80. package/.specify/scripts/bash/check-prerequisites.sh +0 -166
  81. package/.specify/scripts/bash/common.sh +0 -113
  82. package/.specify/scripts/bash/create-new-feature.sh +0 -97
  83. package/.specify/scripts/bash/setup-plan.sh +0 -60
  84. package/.specify/scripts/bash/update-agent-context.sh +0 -728
  85. package/.specify/templates/agent-file-template.md +0 -23
  86. package/.specify/templates/plan-template.md +0 -219
  87. package/.specify/templates/spec-template.md +0 -116
  88. package/.specify/templates/tasks-template.md +0 -127
  89. package/.storybook/main.ts +0 -25
  90. package/.storybook/preview.ts +0 -29
  91. package/.watchmanconfig +0 -3
  92. package/AGENTS.md +0 -26
  93. package/CLAUDE.md +0 -124
  94. package/DEPLOYMENT.md +0 -62
  95. package/Dockerfile +0 -36
  96. package/IMPLEMENTATION_PLAN.md +0 -163
  97. package/Integration/vanilla-js/index.html +0 -42
  98. package/MCP-ASSISTANT-RULES.md +0 -85
  99. package/README_CN.md +0 -15
  100. package/TUTORIAL.md +0 -116
  101. package/antlr/antlr-4.11.1-complete.jar +0 -0
  102. package/bun.lock +0 -1544
  103. package/bunfig.toml +0 -52
  104. package/docs/UNICODE_SUPPORT.md +0 -179
  105. package/docs/ai-context/deployment-infrastructure.md +0 -21
  106. package/docs/ai-context/docs-overview.md +0 -89
  107. package/docs/ai-context/handoff.md +0 -174
  108. package/docs/ai-context/project-structure.md +0 -160
  109. package/docs/ai-context/system-integration.md +0 -21
  110. package/docs/asciidoc/contributor.adoc +0 -54
  111. package/docs/asciidoc/create-my-own-theme.adoc +0 -149
  112. package/docs/asciidoc/images/creation-component.png +0 -0
  113. package/docs/asciidoc/images/creation-rtl.png +0 -0
  114. package/docs/asciidoc/images/message-arrow-rtl.png +0 -0
  115. package/docs/asciidoc/images/occurrence.png +0 -0
  116. package/docs/asciidoc/images/return-message-conflict.png +0 -0
  117. package/docs/asciidoc/images/shift-up-half-the-height.png +0 -0
  118. package/docs/asciidoc/images/three-layer-info-arch.png +0 -0
  119. package/docs/asciidoc/images/vertical-alignment.svg +0 -1
  120. package/docs/asciidoc/images/vertically-aligning.png +0 -0
  121. package/docs/asciidoc/index.adoc +0 -277
  122. package/docs/asciidoc/theme-debug-web-app.png +0 -0
  123. package/docs/asciidoc/tutorial.adoc +0 -22
  124. package/docs/asciidoc/user-css.png +0 -0
  125. package/docs/async-vs-sync-parser-rules.md +0 -81
  126. package/docs/divider-parser-allow-spaces.md +0 -38
  127. package/docs/highlighting-messages.md +0 -52
  128. package/docs/images/editor-sample.png +0 -0
  129. package/docs/inherited-vs-provided-from.md +0 -64
  130. package/docs/parser/Assignment.md +0 -8
  131. package/docs/parser/PARSER_IMPROVEMENTS_CC.md +0 -425
  132. package/docs/parser/grammar_review_gemini.md +0 -116
  133. package/docs/participants-function.md +0 -25
  134. package/docs/responsive-participant-margin.md +0 -52
  135. package/docs/starter.md +0 -9
  136. package/docs/superpowers/plans/2026-03-27-e2e-test-reorg.md +0 -698
  137. package/docs/superpowers/plans/2026-03-30-emoji-support.md +0 -1220
  138. package/docs/superpowers/plans/2026-03-30-self-correcting-scoring.md +0 -206
  139. package/docs/superpowers/plans/2026-04-15-keyboard-editing-on-diagram.md +0 -1992
  140. package/docs/superpowers/plans/2026-04-15-zenuml-ux-research-skill.md +0 -1452
  141. package/docs/ux-research/.gitkeep +0 -0
  142. package/docs/ux-research/2026-04-15-rename-participant.md +0 -156
  143. package/docs/ux-research/2026-04-18-insert-participant.md +0 -151
  144. package/docs/width-translate-and-offsets.md +0 -62
  145. package/docs/xss.md +0 -59
  146. package/e2e/data/compare-cases.js +0 -1090
  147. package/e2e/data/diff-algorithm.js +0 -199
  148. package/e2e/fixtures/create-message.html +0 -26
  149. package/e2e/fixtures/editable-label.html +0 -35
  150. package/e2e/fixtures/editable-span.html +0 -122
  151. package/e2e/fixtures/empty-diagram.html +0 -23
  152. package/e2e/fixtures/fixture.html +0 -31
  153. package/e2e/fixtures/insert-participant.html +0 -23
  154. package/e2e/fixtures/reorder-cross-fragment.html +0 -31
  155. package/e2e/fixtures/reorder-fragment.html +0 -29
  156. package/e2e/fixtures/reorder-message.html +0 -27
  157. package/e2e/fixtures/svg-test.html +0 -21
  158. package/e2e/fixtures/type-switch.html +0 -29
  159. package/e2e/tools/canonical-history.html +0 -908
  160. package/e2e/tools/compare-case.html +0 -371
  161. package/e2e/tools/compare.html +0 -35
  162. package/e2e/tools/native-diff-ext/background.js +0 -60
  163. package/e2e/tools/native-diff-ext/bridge.js +0 -26
  164. package/e2e/tools/native-diff-ext/content.js +0 -194
  165. package/e2e/tools/svg-preview.html +0 -56
  166. package/embed.html +0 -193
  167. package/eslint.config.mjs +0 -35
  168. package/firebase-debug.log +0 -108
  169. package/iframe-container-demo/diagram.html +0 -124
  170. package/iframe-container-demo/host.html +0 -817
  171. package/index.html +0 -771
  172. package/mermaid-zenuml-async-spa-auth.png +0 -0
  173. package/mermaid-zenuml-async-spa-auth.snapshot.md +0 -96
  174. package/newsletter/unicode-support-announcement.md +0 -134
  175. package/playground/creation.html +0 -53
  176. package/playground/message.html +0 -63
  177. package/playwright.config.ts +0 -40
  178. package/renderer.html +0 -366
  179. package/scripts/analyze-compare-case/collect-data.mjs +0 -1134
  180. package/scripts/analyze-compare-case/config.mjs +0 -102
  181. package/scripts/analyze-compare-case/geometry.mjs +0 -101
  182. package/scripts/analyze-compare-case/native-diff.mjs +0 -224
  183. package/scripts/analyze-compare-case/output.mjs +0 -74
  184. package/scripts/analyze-compare-case/panel-diff.mjs +0 -114
  185. package/scripts/analyze-compare-case/report.mjs +0 -162
  186. package/scripts/analyze-compare-case/residual-scopes.mjs +0 -347
  187. package/scripts/analyze-compare-case/scoring.mjs +0 -829
  188. package/scripts/analyze-compare-case.mjs +0 -149
  189. package/scripts/bump-version.js +0 -117
  190. package/scripts/snapshot-dual.js +0 -173
  191. package/scripts/update-snapshots.js +0 -70
  192. package/skills/dia-scoring/SKILL.md +0 -129
  193. package/skills/dia-scoring/agents/openai.yaml +0 -7
  194. package/skills/dia-scoring/references/selectors-and-keys.md +0 -253
  195. package/tailwind.config.js +0 -126
  196. package/test-compression.html +0 -274
  197. package/test-mermaid-zenuml.html +0 -57
  198. package/test-setup.ts +0 -124
  199. package/test-url-params.html +0 -192
  200. package/tsconfig.app.json +0 -31
  201. package/tsconfig.node.json +0 -24
  202. package/tsconfig.test.json +0 -9
  203. package/vite.config.lib.ts +0 -93
  204. package/vite.config.ts +0 -84
  205. package/wrangler.toml +0 -18
@@ -1,162 +0,0 @@
1
- /*
2
- * What this file does:
3
- * Assembles the final report object from scored sections and residual attribution.
4
- *
5
- * High-level flow:
6
- * - Pulls in semantic scoring results and residual hotspot attribution.
7
- * - Builds the JSON report shape consumed by scripts and humans.
8
- * - Produces concise summary lines for labels, numbers, arrows, icons, boxes,
9
- * and residual scopes so the CLI can print a readable digest.
10
- *
11
- * This module is the boundary between raw analysis results and final report shape.
12
- *
13
- * Example input:
14
- * `caseName`, extracted geometry, and the analyzer's local diff image.
15
- *
16
- * Example output:
17
- * A single report object with JSON-friendly sections plus terminal summary arrays
18
- * such as `summary`, `arrow_summary`, and `residual_scope_summary`.
19
- */
20
- import { buildResidualScopes } from "./residual-scopes.mjs";
21
- import { buildScoredSections } from "./scoring.mjs";
22
-
23
- function formatLetterSummary(letter) {
24
- if (letter.dx == null || letter.dy == null) {
25
- return `${letter.grapheme}: ambiguous`;
26
- }
27
- const dx = letter.dx.toFixed(2);
28
- const dy = letter.dy.toFixed(2);
29
- return `${letter.grapheme}: dx=${dx}px dy=${dy}px`;
30
- }
31
-
32
- function formatSectionSummary(prefix, item) {
33
- const notes = [];
34
- if (item.owner_text) {
35
- notes.push(`owner=${item.owner_text}`);
36
- }
37
- if (item.html_text && item.svg_text && item.html_text !== item.svg_text) {
38
- notes.push(`text_mismatch(html=${item.html_text} svg=${item.svg_text})`);
39
- }
40
- const noteSuffix = notes.length > 0 ? ` [${notes.join("; ")}]` : "";
41
- if (!item.letters || item.letters.length === 0) {
42
- return `${prefix}:${item.key.kind}:${item.key.text}${noteSuffix} -> ambiguous`;
43
- }
44
- return `${prefix}:${item.key.kind}:${item.key.text}${noteSuffix} -> ${item.letters.map(formatLetterSummary).join(", ")}`;
45
- }
46
-
47
- function formatArrowSummary(arrow) {
48
- if (arrow.status !== "ok") {
49
- return "ambiguous";
50
- }
51
- const parts = [
52
- `left_dx=${arrow.left_dx.toFixed(2)}px`,
53
- `right_dx=${arrow.right_dx.toFixed(2)}px`,
54
- `width_dx=${arrow.width_dx.toFixed(2)}px`,
55
- ];
56
- if (arrow.key?.kind === "self") {
57
- parts.push(`top_dy=${arrow.top_dy.toFixed(2)}px`);
58
- parts.push(`bottom_dy=${arrow.bottom_dy.toFixed(2)}px`);
59
- parts.push(`height_dy=${arrow.height_dy.toFixed(2)}px`);
60
- }
61
- return parts.join(" ");
62
- }
63
-
64
- function formatParticipantIconSummary(icon) {
65
- const notes = [];
66
- if (icon.emoji) {
67
- notes.push(`emoji=${icon.emoji}`);
68
- }
69
- if (icon.label_text) {
70
- notes.push(`label=${icon.label_text}`);
71
- }
72
- if (icon.anchor_kind) {
73
- notes.push(`anchor=${icon.anchor_kind}`);
74
- }
75
- if (icon.presence && icon.presence.html !== icon.presence.svg) {
76
- notes.push(`presence_mismatch(html=${icon.presence.html} svg=${icon.presence.svg})`);
77
- }
78
- const noteSuffix = notes.length > 0 ? ` [${notes.join("; ")}]` : "";
79
- if (icon.status !== "ok") {
80
- return `icon:${icon.name}${noteSuffix} -> ambiguous`;
81
- }
82
- return `icon:${icon.name}${noteSuffix} -> icon_dx=${icon.icon_dx.toFixed(2)}px icon_dy=${icon.icon_dy.toFixed(2)}px relative_dx=${icon.relative_dx.toFixed(2)}px relative_dy=${icon.relative_dy.toFixed(2)}px`;
83
- }
84
-
85
- function formatParticipantBoxSummary(box) {
86
- if (box.status !== "ok") {
87
- return `participant-box:${box.name} -> ambiguous`;
88
- }
89
- return `participant-box:${box.name} -> dx=${box.dx.toFixed(2)}px dy=${box.dy.toFixed(2)}px dw=${box.dw.toFixed(2)}px dh=${box.dh.toFixed(2)}px`;
90
- }
91
-
92
- function formatParticipantColorSummary(color) {
93
- if (color.status !== "ok") {
94
- return `participant-color:${color.name} -> ambiguous`;
95
- }
96
- return `participant-color:${color.name} -> bg(html=${color.html_background_color} svg=${color.svg_background_color}) text(html=${color.html_text_color} svg=${color.svg_text_color}) stereotype(html=${color.html_stereotype_color} svg=${color.svg_stereotype_color})`;
97
- }
98
-
99
- function formatGroupSummary(group) {
100
- if (group.status !== "ok") {
101
- return `participant-group:${group.name} -> ambiguous`;
102
- }
103
- const namePart = group.name_dx === null || group.name_dy === null
104
- ? "name=ambiguous"
105
- : `name_dx=${group.name_dx.toFixed(2)}px name_dy=${group.name_dy.toFixed(2)}px`;
106
- return `participant-group:${group.name} -> dx=${group.dx.toFixed(2)}px dy=${group.dy.toFixed(2)}px dw=${group.dw.toFixed(2)}px dh=${group.dh.toFixed(2)}px ${namePart}`;
107
- }
108
-
109
- function formatOccurrenceSummary(occ) {
110
- if (occ.status !== "ok") {
111
- return `occurrence:${occ.participant}#${occ.idx} -> ambiguous`;
112
- }
113
- return `occurrence:${occ.participant}#${occ.idx} -> dx=${occ.dx.toFixed(2)}px dy=${occ.dy.toFixed(2)}px dw=${occ.dw.toFixed(2)}px dh=${occ.dh.toFixed(2)}px`;
114
- }
115
-
116
- function formatFragmentDividerSummary(div) {
117
- if (div.status !== "ok") {
118
- return `fragment-divider:#${div.idx} -> ambiguous`;
119
- }
120
- return `fragment-divider:#${div.idx} -> dx=${div.dx.toFixed(2)}px dy=${div.dy.toFixed(2)}px dw=${div.dw.toFixed(2)}px`;
121
- }
122
-
123
- export function buildReport(caseName, extracted, diffImage) {
124
- const sections = buildScoredSections(extracted, diffImage);
125
- const residualScopes = buildResidualScopes(extracted, diffImage);
126
-
127
- return {
128
- case: caseName,
129
- title: sections.title,
130
- labels: sections.labels,
131
- numbers: sections.numbers,
132
- arrows: sections.arrows,
133
- participant_labels: sections.participantLabels,
134
- participant_stereotypes: sections.participantStereotypes,
135
- participant_icons: sections.participantIcons,
136
- participant_boxes: sections.participantBoxes,
137
- participant_colors: sections.participantColors,
138
- comments: sections.comments,
139
- participant_groups: sections.groups,
140
- occurrences: sections.occurrences,
141
- fragment_dividers: sections.fragmentDividers,
142
- dividers: sections.dividers,
143
- residual_scopes: residualScopes.scopes,
144
- title_summary: sections.title ? formatSectionSummary("title", sections.title) : null,
145
- summary: sections.labels.map((label) => formatSectionSummary("label", label)),
146
- number_summary: sections.numbers.map((number) => formatSectionSummary("number", number)),
147
- arrow_summary: sections.arrows.map((arrow) => `arrow:${arrow.key.text} -> ${formatArrowSummary(arrow)}`),
148
- participant_label_summary: sections.participantLabels.map((label) => formatSectionSummary("participant-label", label)),
149
- participant_stereotype_summary: sections.participantStereotypes.map((label) => formatSectionSummary("participant-stereotype", label)),
150
- participant_icon_summary: sections.participantIcons.map((icon) => formatParticipantIconSummary(icon)),
151
- participant_box_summary: sections.participantBoxes.map((box) => formatParticipantBoxSummary(box)),
152
- participant_color_summary: sections.participantColors.map((color) => formatParticipantColorSummary(color)),
153
- comment_summary: sections.comments.map((comment) => formatSectionSummary("comment", comment)),
154
- participant_group_summary: sections.groups.map((group) => formatGroupSummary(group)),
155
- occurrence_summary: sections.occurrences.map((occ) => formatOccurrenceSummary(occ)),
156
- fragment_divider_summary: sections.fragmentDividers.map((div) => formatFragmentDividerSummary(div)),
157
- divider_summary: sections.dividers.map((d) => `divider:"${d.label}" dy=${d.dy ?? "?"} ${d.status}`),
158
- residual_scope_summary: residualScopes.summary,
159
- residual_scope_html_only_top: residualScopes.html_only_top,
160
- residual_scope_svg_only_top: residualScopes.svg_only_top,
161
- };
162
- }
@@ -1,347 +0,0 @@
1
- /*
2
- * What this file does:
3
- * Groups remaining diff pixels into residual hotspots and attributes them to nearby elements.
4
- *
5
- * High-level flow:
6
- * - Scans the analyzer's local diff for connected html-only and svg-only clusters.
7
- * - Builds candidate scope items from labels, arrows, participant headers, icons,
8
- * boxes, and frame-level containers.
9
- * - Picks the nearest and most specific target on each side for each cluster.
10
- * - Produces concise residual summaries for terminal and JSON output.
11
- *
12
- * This module explains "where the remaining diff seems to belong" after the
13
- * semantic scoring step.
14
- *
15
- * Example input:
16
- * Extracted geometry plus a diff image containing `html-only` and `svg-only` pixels.
17
- *
18
- * Example output:
19
- * `{ scopes, summary, html_only_top, svg_only_top }`
20
- * with each scope carrying a bbox, centroid, and nearest HTML/SVG targets.
21
- */
22
- import { area, intersectionArea, rectBottom, rectRight, round } from "./geometry.mjs";
23
-
24
- function scopePriority(category) {
25
- switch (category) {
26
- case "participant-icon":
27
- return 8;
28
- case "participant-stereotype":
29
- case "label":
30
- case "number":
31
- case "comment":
32
- case "participant-label":
33
- return 7;
34
- case "arrow":
35
- case "fragment-divider":
36
- return 6;
37
- case "occurrence":
38
- case "participant-box":
39
- case "participant-group":
40
- return 5;
41
- case "frame-border":
42
- return 2;
43
- case "diagram-root":
44
- return 1;
45
- default:
46
- return 0;
47
- }
48
- }
49
-
50
- function pointInRect(point, rect) {
51
- return point.x >= rect.x && point.x <= rectRight(rect) && point.y >= rect.y && point.y <= rectBottom(rect);
52
- }
53
-
54
- function distanceToRect(point, rect) {
55
- const dx = point.x < rect.x ? rect.x - point.x : point.x > rectRight(rect) ? point.x - rectRight(rect) : 0;
56
- const dy = point.y < rect.y ? rect.y - point.y : point.y > rectBottom(rect) ? point.y - rectBottom(rect) : 0;
57
- return Math.hypot(dx, dy);
58
- }
59
-
60
- function formatScopeName(item) {
61
- if (item.owner_text && item.text && item.owner_text !== item.text) {
62
- return `${item.owner_text}:${item.text}`;
63
- }
64
- return item.name ?? item.text ?? item.kind ?? item.category ?? "unknown";
65
- }
66
-
67
- function buildScopeItems(side, extracted) {
68
- const items = [];
69
-
70
- function push(category, name, box, extra = {}) {
71
- if (!box || box.w <= 0 || box.h <= 0) {
72
- return;
73
- }
74
- items.push({
75
- side,
76
- category,
77
- name,
78
- box,
79
- ...extra,
80
- });
81
- }
82
-
83
- const sideKey = side === "html" ? "html" : "svg";
84
- const labels = side === "html" ? extracted.htmlLabels : extracted.svgLabels;
85
- const numbers = side === "html" ? extracted.htmlNumbers : extracted.svgNumbers;
86
- const arrows = side === "html" ? extracted.htmlArrows : extracted.svgArrows;
87
- const participants = side === "html" ? extracted.htmlParticipants : extracted.svgParticipants;
88
- const comments = side === "html" ? extracted.htmlComments : extracted.svgComments;
89
- const groups = side === "html" ? extracted.htmlGroups : extracted.svgGroups;
90
- const occurrences = side === "html" ? (extracted.htmlOccurrences || []) : (extracted.svgOccurrences || []);
91
- const fragmentDividers = side === "html" ? (extracted.htmlFragmentDividers || []) : (extracted.svgFragmentDividers || []);
92
-
93
- for (const label of labels) {
94
- push("label", formatScopeName(label), label.box, {
95
- kind: label.kind,
96
- text: label.text,
97
- owner_text: label.ownerText ?? null,
98
- });
99
- }
100
- for (const number of numbers) {
101
- push("number", formatScopeName(number), number.box, {
102
- kind: number.kind,
103
- text: number.text,
104
- owner_text: number.ownerText ?? null,
105
- });
106
- }
107
- for (const arrow of arrows) {
108
- push("arrow", formatScopeName(arrow), arrow.box, {
109
- kind: arrow.kind,
110
- text: arrow.text,
111
- owner_text: arrow.labelText ?? null,
112
- });
113
- }
114
- for (const comment of comments) {
115
- push("comment", formatScopeName(comment), comment.box, {
116
- kind: comment.kind,
117
- text: comment.text,
118
- });
119
- }
120
- for (const participant of participants) {
121
- push("participant-box", participant.name, participant.participantBox, {
122
- kind: "participant",
123
- text: participant.labelText ?? participant.name,
124
- owner_text: participant.name,
125
- });
126
- push("participant-label", participant.name, participant.labelBox, {
127
- kind: "participant",
128
- text: participant.labelText ?? participant.name,
129
- owner_text: participant.name,
130
- });
131
- push("participant-stereotype", participant.name, participant.stereotypeBox, {
132
- kind: "participant",
133
- text: participant.stereotypeText ?? participant.name,
134
- owner_text: participant.name,
135
- });
136
- push("participant-icon", participant.name, participant.iconBox, {
137
- kind: "participant",
138
- text: participant.labelText ?? participant.name,
139
- owner_text: participant.name,
140
- });
141
- }
142
- for (const group of groups) {
143
- push("participant-group", group.name || "group", group.box, {
144
- kind: "group",
145
- text: group.name,
146
- });
147
- }
148
- for (const occ of occurrences) {
149
- push("occurrence", `${occ.participant}#${occ.idx}`, occ.box, {
150
- kind: "occurrence",
151
- text: occ.participant,
152
- owner_text: occ.participant,
153
- });
154
- }
155
- for (const div of fragmentDividers) {
156
- push("fragment-divider", `divider#${div.idx}`, { x: div.x, y: div.y, w: div.width, h: 1 }, {
157
- kind: "divider",
158
- text: div.label || `divider#${div.idx}`,
159
- });
160
- }
161
-
162
- if (sideKey === "html") {
163
- push("diagram-root", "html-root", extracted.htmlRootBox, { kind: "root" });
164
- } else {
165
- push("frame-border", "frame-border", extracted.svgFrameBorderBox, { kind: "frame" });
166
- push("diagram-root", "svg-root", extracted.svgRootBox, { kind: "root" });
167
- }
168
-
169
- return items;
170
- }
171
-
172
- function buildDiffClusters(diffImage, targetClass) {
173
- const visited = new Uint8Array(diffImage.width * diffImage.height);
174
- const clusters = [];
175
- const offsets = [-1, 0, 1, 0, -1];
176
-
177
- for (let index = 0; index < diffImage.classData.length; index++) {
178
- if (visited[index] || diffImage.classData[index] !== targetClass) {
179
- continue;
180
- }
181
- visited[index] = 1;
182
- const queue = [index];
183
- let head = 0;
184
- let size = 0;
185
- let sumX = 0;
186
- let sumY = 0;
187
- let left = diffImage.width;
188
- let top = diffImage.height;
189
- let right = -1;
190
- let bottom = -1;
191
-
192
- while (head < queue.length) {
193
- const current = queue[head++];
194
- const x = current % diffImage.width;
195
- const y = Math.floor(current / diffImage.width);
196
- size++;
197
- sumX += x + 0.5;
198
- sumY += y + 0.5;
199
- left = Math.min(left, x);
200
- top = Math.min(top, y);
201
- right = Math.max(right, x);
202
- bottom = Math.max(bottom, y);
203
-
204
- for (let dir = 0; dir < 4; dir++) {
205
- const nx = x + offsets[dir];
206
- const ny = y + offsets[dir + 1];
207
- if (nx < 0 || nx >= diffImage.width || ny < 0 || ny >= diffImage.height) {
208
- continue;
209
- }
210
- const nextIndex = ny * diffImage.width + nx;
211
- if (visited[nextIndex] || diffImage.classData[nextIndex] !== targetClass) {
212
- continue;
213
- }
214
- visited[nextIndex] = 1;
215
- queue.push(nextIndex);
216
- }
217
- }
218
-
219
- clusters.push({
220
- class: targetClass === 2 ? "html-only" : "svg-only",
221
- size,
222
- bbox: {
223
- x: left,
224
- y: top,
225
- w: right - left + 1,
226
- h: bottom - top + 1,
227
- },
228
- centroid: {
229
- x: sumX / size,
230
- y: sumY / size,
231
- },
232
- });
233
- }
234
-
235
- return clusters.sort((a, b) => b.size - a.size);
236
- }
237
-
238
- function normalizeClusterToFrameSpace(cluster, scaleX, scaleY) {
239
- return {
240
- ...cluster,
241
- bbox: {
242
- x: cluster.bbox.x / scaleX,
243
- y: cluster.bbox.y / scaleY,
244
- w: cluster.bbox.w / scaleX,
245
- h: cluster.bbox.h / scaleY,
246
- },
247
- centroid: {
248
- x: cluster.centroid.x / scaleX,
249
- y: cluster.centroid.y / scaleY,
250
- },
251
- };
252
- }
253
-
254
- function pickScopeTarget(cluster, items) {
255
- const centroid = cluster.centroid;
256
- let best = null;
257
- let bestScore = -Infinity;
258
-
259
- for (const item of items) {
260
- const contains = pointInRect(centroid, item.box);
261
- const distance = distanceToRect(centroid, item.box);
262
- const overlap = intersectionArea(cluster.bbox, item.box);
263
- const score = (contains ? 10000 : 0)
264
- + overlap * 10
265
- - distance * 100
266
- + scopePriority(item.category) * 1000
267
- - area(item.box) * 0.01;
268
-
269
- if (score > bestScore) {
270
- bestScore = score;
271
- best = {
272
- category: item.category,
273
- name: item.name,
274
- kind: item.kind ?? null,
275
- text: item.text ?? null,
276
- owner_text: item.owner_text ?? null,
277
- contains_centroid: contains,
278
- overlap_area: round(overlap),
279
- distance: round(distance),
280
- box: {
281
- x: round(item.box.x),
282
- y: round(item.box.y),
283
- w: round(item.box.w),
284
- h: round(item.box.h),
285
- },
286
- };
287
- }
288
- }
289
-
290
- return best;
291
- }
292
-
293
- function formatResidualScopeSummary(scope) {
294
- const htmlTarget = scope.html_target
295
- ? `${scope.html_target.category}:${scope.html_target.name}`
296
- : "none";
297
- const svgTarget = scope.svg_target
298
- ? `${scope.svg_target.category}:${scope.svg_target.name}`
299
- : "none";
300
- return `${scope.class}:${scope.size}px @ (${scope.centroid.x.toFixed(1)},${scope.centroid.y.toFixed(1)}) -> html=${htmlTarget} svg=${svgTarget}`;
301
- }
302
-
303
- export function buildResidualScopes(extracted, diffImage) {
304
- const htmlItems = buildScopeItems("html", extracted);
305
- const svgItems = buildScopeItems("svg", extracted);
306
- const frameWidth = extracted.htmlRootBox?.w || extracted.svgRootBox?.w || diffImage.width;
307
- const frameHeight = extracted.htmlRootBox?.h || extracted.svgRootBox?.h || diffImage.height;
308
- const scaleX = frameWidth > 0 ? diffImage.width / frameWidth : 1;
309
- const scaleY = frameHeight > 0 ? diffImage.height / frameHeight : 1;
310
- const clusters = [
311
- ...buildDiffClusters(diffImage, 2),
312
- ...buildDiffClusters(diffImage, 3),
313
- ]
314
- .map((cluster) => normalizeClusterToFrameSpace(cluster, scaleX, scaleY))
315
- .sort((a, b) => b.size - a.size);
316
-
317
- const residualScopes = clusters.map((cluster, index) => ({
318
- rank: index + 1,
319
- class: cluster.class,
320
- size: cluster.size,
321
- centroid: {
322
- x: round(cluster.centroid.x),
323
- y: round(cluster.centroid.y),
324
- },
325
- bbox: {
326
- x: round(cluster.bbox.x),
327
- y: round(cluster.bbox.y),
328
- w: round(cluster.bbox.w),
329
- h: round(cluster.bbox.h),
330
- },
331
- html_target: pickScopeTarget(cluster, htmlItems),
332
- svg_target: pickScopeTarget(cluster, svgItems),
333
- }));
334
-
335
- const byClass = residualScopes.reduce((acc, scope) => {
336
- acc[scope.class] = acc[scope.class] || [];
337
- acc[scope.class].push(scope);
338
- return acc;
339
- }, {});
340
-
341
- return {
342
- scopes: residualScopes,
343
- summary: residualScopes.slice(0, 20).map((scope) => formatResidualScopeSummary(scope)),
344
- html_only_top: (byClass["html-only"] || []).slice(0, 10),
345
- svg_only_top: (byClass["svg-only"] || []).slice(0, 10),
346
- };
347
- }