@zenuml/core 3.46.0 → 3.46.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/dia-scoring/SKILL.md +139 -0
- package/.claude/skills/dia-scoring/agents/openai.yaml +7 -0
- package/.claude/skills/dia-scoring/references/selectors-and-keys.md +253 -0
- package/CLAUDE.md +1 -1
- package/bun.lock +25 -11
- package/cy/canonical-history.html +908 -0
- package/cy/compare-case.html +357 -0
- package/cy/compare-cases.js +824 -0
- package/cy/compare.html +35 -0
- package/cy/diff-algorithm.js +199 -0
- package/cy/element-report.html +705 -0
- package/cy/icons-test.html +29 -0
- package/cy/legacy-vs-html.html +291 -0
- package/cy/native-diff-ext/background.js +60 -0
- package/cy/native-diff-ext/bridge.js +26 -0
- package/cy/native-diff-ext/content.js +194 -0
- package/cy/parity-test.html +122 -0
- package/cy/return-in-nested-if.html +29 -0
- package/cy/svg-preview.html +56 -0
- package/cy/svg-test.html +21 -0
- package/cy/theme-default-test.html +28 -0
- package/dist/stats.html +1 -1
- package/dist/zenuml.esm.mjs +16352 -15223
- package/dist/zenuml.js +701 -575
- package/docs/superpowers/plans/2026-03-23-svg-parity-features.md +283 -0
- package/index.html +568 -73
- package/package.json +15 -4
- package/scripts/analyze-compare-case/collect-data.mjs +991 -0
- package/scripts/analyze-compare-case/config.mjs +102 -0
- package/scripts/analyze-compare-case/geometry.mjs +101 -0
- package/scripts/analyze-compare-case/native-diff.mjs +224 -0
- package/scripts/analyze-compare-case/output.mjs +74 -0
- package/scripts/analyze-compare-case/panel-diff.mjs +114 -0
- package/scripts/analyze-compare-case/report.mjs +157 -0
- package/scripts/analyze-compare-case/residual-scopes.mjs +325 -0
- package/scripts/analyze-compare-case/scoring.mjs +816 -0
- package/scripts/analyze-compare-case.mjs +149 -0
- package/scripts/snapshot-dual.js +34 -34
- package/skills/dia-scoring/SKILL.md +129 -0
- package/skills/dia-scoring/agents/openai.yaml +7 -0
- package/skills/dia-scoring/references/selectors-and-keys.md +253 -0
- package/test-setup.ts +8 -0
- package/types/index.d.ts +56 -0
- package/vite.config.ts +4 -0
- package/dist/10029-icon-service-Function-Apps-ObflOLuF.js +0 -5
- package/dist/Res_AWS-Identity-Access-Management_IAM-Access-Analyzer_48-BPq60XMY.js +0 -11
- package/dist/Res_AWS-Lambda_Lambda-Function_48-Co38UB_2.js +0 -12
- package/dist/Res_Amazon-EC2_Instance_48-CRaqbNUl.js +0 -12
- package/dist/Res_Amazon-Simple-Notification-Service_Topic_48-q13mxUeM.js +0 -11
- package/dist/Res_Amazon-Simple-Queue-Service_Queue_48-D2-8gbFw.js +0 -11
- package/dist/Robustness_Diagram_Boundary-nYnmTPs8.js +0 -10
- package/dist/Robustness_Diagram_Control-DLNLoMxd.js +0 -11
- package/dist/Robustness_Diagram_Entity-Be3kcbIE.js +0 -11
- package/dist/actor-BMj_HFpo.js +0 -11
- package/dist/database-BKHQQWQK.js +0 -8
|
@@ -0,0 +1,816 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* What this file does:
|
|
3
|
+
* Compares extracted HTML and SVG geometry and converts it into scored sections.
|
|
4
|
+
*
|
|
5
|
+
* High-level flow:
|
|
6
|
+
* - Pairs equivalent items across HTML and SVG by semantic keys.
|
|
7
|
+
* - Scores per-letter drift, arrow geometry, participant labels, icons, and boxes.
|
|
8
|
+
* - Uses the analyzer's local diff only as supporting evidence when deciding
|
|
9
|
+
* whether a measured offset is strong enough to report.
|
|
10
|
+
* - Returns structured sections ready for report assembly.
|
|
11
|
+
*
|
|
12
|
+
* This module answers "how far apart are the matched elements?" but does not
|
|
13
|
+
* decide how to print results or where residual hotspots belong.
|
|
14
|
+
*
|
|
15
|
+
* Example input:
|
|
16
|
+
* Extracted HTML/SVG geometry plus a local diff image.
|
|
17
|
+
*
|
|
18
|
+
* Example output:
|
|
19
|
+
* `{ labels, numbers, arrows, participantLabels, participantIcons, participantBoxes }`
|
|
20
|
+
* where each section contains status, deltas, evidence, and normalized boxes.
|
|
21
|
+
*/
|
|
22
|
+
import { analyzeDiffSlot } from "./native-diff.mjs";
|
|
23
|
+
import {
|
|
24
|
+
iou,
|
|
25
|
+
keyForLabel,
|
|
26
|
+
normalizeOffset,
|
|
27
|
+
rectBottom,
|
|
28
|
+
rectCenter,
|
|
29
|
+
rectRight,
|
|
30
|
+
round,
|
|
31
|
+
} from "./geometry.mjs";
|
|
32
|
+
|
|
33
|
+
function enrichOrdering(labels) {
|
|
34
|
+
const byKind = new Map();
|
|
35
|
+
const byText = new Map();
|
|
36
|
+
const ordered = [...labels].sort((a, b) => (a.box.y - b.box.y) || (a.box.x - b.box.x));
|
|
37
|
+
|
|
38
|
+
for (const label of ordered) {
|
|
39
|
+
const kindCount = byKind.get(label.kind) || 0;
|
|
40
|
+
byKind.set(label.kind, kindCount + 1);
|
|
41
|
+
label.yOrder = kindCount;
|
|
42
|
+
|
|
43
|
+
const textKey = `${label.kind}\u0000${label.pairText ?? label.text}`;
|
|
44
|
+
const textCount = byText.get(textKey) || 0;
|
|
45
|
+
byText.set(textKey, textCount + 1);
|
|
46
|
+
label.textOrder = textCount;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return ordered;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function pairLabels(htmlLabels, svgLabels) {
|
|
53
|
+
const htmlOrdered = enrichOrdering(htmlLabels);
|
|
54
|
+
const svgOrdered = enrichOrdering(svgLabels);
|
|
55
|
+
|
|
56
|
+
const htmlMap = new Map(htmlOrdered.map((label) => [keyForLabel(label), label]));
|
|
57
|
+
const svgMap = new Map(svgOrdered.map((label) => [keyForLabel(label), label]));
|
|
58
|
+
const allKeys = Array.from(new Set([...htmlMap.keys(), ...svgMap.keys()]));
|
|
59
|
+
|
|
60
|
+
return allKeys
|
|
61
|
+
.map((key) => ({ key, html: htmlMap.get(key) || null, svg: svgMap.get(key) || null }))
|
|
62
|
+
.sort((a, b) => {
|
|
63
|
+
const ay = a.html?.box.y ?? a.svg?.box.y ?? 0;
|
|
64
|
+
const by = b.html?.box.y ?? b.svg?.box.y ?? 0;
|
|
65
|
+
return ay - by;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function scoreLetter(htmlLetter, svgLetter, diffImage) {
|
|
70
|
+
const directDx = svgLetter.box.x - htmlLetter.box.x;
|
|
71
|
+
const directDy = svgLetter.box.y - htmlLetter.box.y;
|
|
72
|
+
const slot = {
|
|
73
|
+
x: Math.min(htmlLetter.box.x, svgLetter.box.x) - 2,
|
|
74
|
+
y: Math.min(htmlLetter.box.y, svgLetter.box.y) - 2,
|
|
75
|
+
w: Math.max(rectRight(htmlLetter.box), rectRight(svgLetter.box)) - Math.min(htmlLetter.box.x, svgLetter.box.x) + 4,
|
|
76
|
+
h: Math.max(rectBottom(htmlLetter.box), rectBottom(svgLetter.box)) - Math.min(htmlLetter.box.y, svgLetter.box.y) + 4,
|
|
77
|
+
};
|
|
78
|
+
const diff = analyzeDiffSlot(diffImage, slot);
|
|
79
|
+
const centroidDx = diff.redCentroid && diff.blueCentroid ? diff.blueCentroid.x - diff.redCentroid.x : null;
|
|
80
|
+
const centroidDy = diff.redCentroid && diff.blueCentroid ? diff.blueCentroid.y - diff.redCentroid.y : null;
|
|
81
|
+
const nearZero = Math.abs(directDx) < 0.75 && Math.abs(directDy) < 0.75;
|
|
82
|
+
const enoughDiffPixels = diff.redCount >= 6 && diff.blueCount >= 6;
|
|
83
|
+
const xConsistent = centroidDx === null || Math.abs(directDx) < 0.75 || Math.sign(centroidDx) === Math.sign(directDx);
|
|
84
|
+
const yConsistent = centroidDy === null || Math.abs(directDy) < 0.75 || Math.sign(centroidDy) === Math.sign(directDy);
|
|
85
|
+
const overlap = iou(htmlLetter.box, svgLetter.box);
|
|
86
|
+
|
|
87
|
+
let status = "ambiguous";
|
|
88
|
+
if (nearZero) {
|
|
89
|
+
status = overlap >= 0.35 ? "ok" : "ambiguous";
|
|
90
|
+
} else if (enoughDiffPixels && xConsistent && yConsistent) {
|
|
91
|
+
status = "ok";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const diffConfidence = nearZero
|
|
95
|
+
? overlap
|
|
96
|
+
: enoughDiffPixels
|
|
97
|
+
? 0.5 + Math.min(0.5, ((diff.redCount + diff.blueCount) / 80))
|
|
98
|
+
: 0.15;
|
|
99
|
+
const confidence = round(Math.min(1, overlap * 0.45 + diffConfidence * 0.55), 3);
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
index: htmlLetter.index,
|
|
103
|
+
grapheme: htmlLetter.grapheme,
|
|
104
|
+
status,
|
|
105
|
+
dx: normalizeOffset(directDx),
|
|
106
|
+
dy: normalizeOffset(directDy),
|
|
107
|
+
confidence,
|
|
108
|
+
html_box: {
|
|
109
|
+
x: round(htmlLetter.box.x),
|
|
110
|
+
y: round(htmlLetter.box.y),
|
|
111
|
+
w: round(htmlLetter.box.w),
|
|
112
|
+
h: round(htmlLetter.box.h),
|
|
113
|
+
},
|
|
114
|
+
svg_box: {
|
|
115
|
+
x: round(svgLetter.box.x),
|
|
116
|
+
y: round(svgLetter.box.y),
|
|
117
|
+
w: round(svgLetter.box.w),
|
|
118
|
+
h: round(svgLetter.box.h),
|
|
119
|
+
},
|
|
120
|
+
evidence: {
|
|
121
|
+
direct_dx: normalizeOffset(directDx),
|
|
122
|
+
direct_dy: normalizeOffset(directDy),
|
|
123
|
+
overlap: round(overlap, 3),
|
|
124
|
+
diff_red: diff.redCount,
|
|
125
|
+
diff_blue: diff.blueCount,
|
|
126
|
+
diff_centroid_dx: centroidDx === null ? null : round(centroidDx),
|
|
127
|
+
diff_centroid_dy: centroidDy === null ? null : round(centroidDy),
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function buildSection(htmlItems, svgItems, diffImage) {
|
|
133
|
+
const pairs = pairLabels(htmlItems, svgItems);
|
|
134
|
+
const section = [];
|
|
135
|
+
|
|
136
|
+
for (const pair of pairs) {
|
|
137
|
+
const base = pair.html || pair.svg;
|
|
138
|
+
const key = {
|
|
139
|
+
kind: base?.kind ?? "message",
|
|
140
|
+
text: base?.text ?? "",
|
|
141
|
+
y_order: base?.yOrder ?? 0,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
if (!pair.html || !pair.svg) {
|
|
145
|
+
section.push({
|
|
146
|
+
key,
|
|
147
|
+
status: "ambiguous",
|
|
148
|
+
html_text: pair.html?.text ?? null,
|
|
149
|
+
svg_text: pair.svg?.text ?? null,
|
|
150
|
+
owner_text: pair.html?.ownerText ?? pair.svg?.ownerText ?? null,
|
|
151
|
+
html_box: pair.html ? pair.html.box : null,
|
|
152
|
+
svg_box: pair.svg ? pair.svg.box : null,
|
|
153
|
+
font: {
|
|
154
|
+
html: pair.html?.font ?? null,
|
|
155
|
+
svg: pair.svg?.font ?? null,
|
|
156
|
+
},
|
|
157
|
+
letters: [],
|
|
158
|
+
reason: "item missing on one side",
|
|
159
|
+
});
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const letterCount = Math.max(pair.html.letters.length, pair.svg.letters.length);
|
|
164
|
+
const letters = [];
|
|
165
|
+
for (let index = 0; index < letterCount; index++) {
|
|
166
|
+
const htmlLetter = pair.html.letters[index];
|
|
167
|
+
const svgLetter = pair.svg.letters[index];
|
|
168
|
+
if (!htmlLetter || !svgLetter || htmlLetter.grapheme !== svgLetter.grapheme) {
|
|
169
|
+
letters.push({
|
|
170
|
+
index,
|
|
171
|
+
grapheme: htmlLetter?.grapheme ?? svgLetter?.grapheme ?? "",
|
|
172
|
+
status: "ambiguous",
|
|
173
|
+
dx: null,
|
|
174
|
+
dy: null,
|
|
175
|
+
confidence: 0,
|
|
176
|
+
html_box: htmlLetter ? htmlLetter.box : null,
|
|
177
|
+
svg_box: svgLetter ? svgLetter.box : null,
|
|
178
|
+
evidence: { reason: "letter mismatch or missing" },
|
|
179
|
+
});
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
letters.push(scoreLetter(htmlLetter, svgLetter, diffImage));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const okCount = letters.filter((letter) => letter.status === "ok").length;
|
|
186
|
+
const status = okCount === letters.length ? "ok" : okCount > 0 ? "mixed" : "ambiguous";
|
|
187
|
+
section.push({
|
|
188
|
+
key,
|
|
189
|
+
status,
|
|
190
|
+
html_text: pair.html.text,
|
|
191
|
+
svg_text: pair.svg.text,
|
|
192
|
+
owner_text: pair.html.ownerText ?? pair.svg.ownerText ?? null,
|
|
193
|
+
html_box: {
|
|
194
|
+
x: round(pair.html.box.x),
|
|
195
|
+
y: round(pair.html.box.y),
|
|
196
|
+
w: round(pair.html.box.w),
|
|
197
|
+
h: round(pair.html.box.h),
|
|
198
|
+
},
|
|
199
|
+
svg_box: {
|
|
200
|
+
x: round(pair.svg.box.x),
|
|
201
|
+
y: round(pair.svg.box.y),
|
|
202
|
+
w: round(pair.svg.box.w),
|
|
203
|
+
h: round(pair.svg.box.h),
|
|
204
|
+
},
|
|
205
|
+
font: {
|
|
206
|
+
html: pair.html.font,
|
|
207
|
+
svg: pair.svg.font,
|
|
208
|
+
},
|
|
209
|
+
letters,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return section;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function scoreArrowGeometry(htmlArrow, svgArrow, diffImage, kind = htmlArrow.kind ?? svgArrow.kind ?? "message") {
|
|
217
|
+
const leftDx = svgArrow.left_x - htmlArrow.left_x;
|
|
218
|
+
const rightDx = svgArrow.right_x - htmlArrow.right_x;
|
|
219
|
+
const widthDx = svgArrow.width - htmlArrow.width;
|
|
220
|
+
const topDy = svgArrow.box.y - htmlArrow.box.y;
|
|
221
|
+
const bottomDy = rectBottom(svgArrow.box) - rectBottom(htmlArrow.box);
|
|
222
|
+
const heightDy = svgArrow.box.h - htmlArrow.box.h;
|
|
223
|
+
const slot = {
|
|
224
|
+
x: Math.min(htmlArrow.box.x, svgArrow.box.x) - 2,
|
|
225
|
+
y: Math.min(htmlArrow.box.y, svgArrow.box.y) - 2,
|
|
226
|
+
w: Math.max(rectRight(htmlArrow.box), rectRight(svgArrow.box)) - Math.min(htmlArrow.box.x, svgArrow.box.x) + 4,
|
|
227
|
+
h: Math.max(rectBottom(htmlArrow.box), rectBottom(svgArrow.box)) - Math.min(htmlArrow.box.y, svgArrow.box.y) + 4,
|
|
228
|
+
};
|
|
229
|
+
const diff = analyzeDiffSlot(diffImage, slot);
|
|
230
|
+
const centroidDx = diff.redCentroid && diff.blueCentroid ? diff.blueCentroid.x - diff.redCentroid.x : null;
|
|
231
|
+
const centroidDy = diff.redCentroid && diff.blueCentroid ? diff.blueCentroid.y - diff.redCentroid.y : null;
|
|
232
|
+
const nearZero = Math.abs(leftDx) < 0.75 && Math.abs(rightDx) < 0.75;
|
|
233
|
+
const nearZeroSelf = nearZero && Math.abs(topDy) < 0.75 && Math.abs(bottomDy) < 0.75 && Math.abs(heightDy) < 0.75;
|
|
234
|
+
const enoughDiffPixels = diff.redCount >= 6 && diff.blueCount >= 6;
|
|
235
|
+
const dominantDx = Math.abs(rightDx) >= Math.abs(leftDx) ? rightDx : leftDx;
|
|
236
|
+
const dominantDy = [topDy, bottomDy, heightDy].reduce((dominant, value) => (
|
|
237
|
+
Math.abs(value) > Math.abs(dominant) ? value : dominant
|
|
238
|
+
), 0);
|
|
239
|
+
const xConsistent = centroidDx === null || Math.abs(dominantDx) < 0.75 || Math.sign(centroidDx) === Math.sign(dominantDx);
|
|
240
|
+
const yConsistent = centroidDy === null || Math.abs(dominantDy) < 0.75 || Math.sign(centroidDy) === Math.sign(dominantDy);
|
|
241
|
+
const overlap = iou(htmlArrow.box, svgArrow.box);
|
|
242
|
+
const status = kind === "self"
|
|
243
|
+
? (nearZeroSelf || (enoughDiffPixels && xConsistent && yConsistent) || Math.abs(dominantDx) >= 0.75 || Math.abs(dominantDy) >= 0.75 ? "ok" : "ambiguous")
|
|
244
|
+
: (nearZero || (enoughDiffPixels && xConsistent) || Math.abs(dominantDx) >= 0.75 ? "ok" : "ambiguous");
|
|
245
|
+
const confidence = round(Math.min(1, overlap * 0.45 + (enoughDiffPixels ? 0.55 : 0.2)), 3);
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
status,
|
|
249
|
+
left_dx: status === "ok" ? normalizeOffset(leftDx) : null,
|
|
250
|
+
right_dx: status === "ok" ? normalizeOffset(rightDx) : null,
|
|
251
|
+
width_dx: status === "ok" ? normalizeOffset(widthDx) : null,
|
|
252
|
+
top_dy: kind === "self" && status === "ok" ? normalizeOffset(topDy) : null,
|
|
253
|
+
bottom_dy: kind === "self" && status === "ok" ? normalizeOffset(bottomDy) : null,
|
|
254
|
+
height_dy: kind === "self" && status === "ok" ? normalizeOffset(heightDy) : null,
|
|
255
|
+
confidence,
|
|
256
|
+
html_box: {
|
|
257
|
+
x: round(htmlArrow.box.x),
|
|
258
|
+
y: round(htmlArrow.box.y),
|
|
259
|
+
w: round(htmlArrow.box.w),
|
|
260
|
+
h: round(htmlArrow.box.h),
|
|
261
|
+
},
|
|
262
|
+
svg_box: {
|
|
263
|
+
x: round(svgArrow.box.x),
|
|
264
|
+
y: round(svgArrow.box.y),
|
|
265
|
+
w: round(svgArrow.box.w),
|
|
266
|
+
h: round(svgArrow.box.h),
|
|
267
|
+
},
|
|
268
|
+
evidence: {
|
|
269
|
+
left_dx: normalizeOffset(leftDx),
|
|
270
|
+
right_dx: normalizeOffset(rightDx),
|
|
271
|
+
width_dx: normalizeOffset(widthDx),
|
|
272
|
+
top_dy: normalizeOffset(topDy),
|
|
273
|
+
bottom_dy: normalizeOffset(bottomDy),
|
|
274
|
+
height_dy: normalizeOffset(heightDy),
|
|
275
|
+
overlap: round(overlap, 3),
|
|
276
|
+
diff_red: diff.redCount,
|
|
277
|
+
diff_blue: diff.blueCount,
|
|
278
|
+
diff_centroid_dx: centroidDx === null ? null : round(centroidDx),
|
|
279
|
+
diff_centroid_dy: centroidDy === null ? null : round(centroidDy),
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function buildArrowSection(htmlItems, svgItems, diffImage) {
|
|
285
|
+
const htmlOrdered = enrichOrdering(htmlItems);
|
|
286
|
+
const svgOrdered = enrichOrdering(svgItems);
|
|
287
|
+
const htmlMap = new Map(htmlOrdered.map((item) => [keyForLabel(item), item]));
|
|
288
|
+
const svgMap = new Map(svgOrdered.map((item) => [keyForLabel(item), item]));
|
|
289
|
+
const allKeys = Array.from(new Set([...htmlMap.keys(), ...svgMap.keys()]));
|
|
290
|
+
const arrows = [];
|
|
291
|
+
|
|
292
|
+
for (const key of allKeys) {
|
|
293
|
+
const html = htmlMap.get(key) || null;
|
|
294
|
+
const svg = svgMap.get(key) || null;
|
|
295
|
+
const base = html || svg;
|
|
296
|
+
const arrow = {
|
|
297
|
+
key: {
|
|
298
|
+
kind: base?.kind ?? "message",
|
|
299
|
+
text: base?.text ?? "",
|
|
300
|
+
y_order: base?.yOrder ?? 0,
|
|
301
|
+
},
|
|
302
|
+
status: "ambiguous",
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
if (!html || !svg) {
|
|
306
|
+
arrow.reason = "arrow missing on one side";
|
|
307
|
+
arrows.push(arrow);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const scored = scoreArrowGeometry(html, svg, diffImage, base?.kind);
|
|
312
|
+
arrows.push({
|
|
313
|
+
...arrow,
|
|
314
|
+
...scored,
|
|
315
|
+
label_text: base?.labelText ?? null,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return arrows;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function participantsWithIcons(htmlParticipants, svgParticipants) {
|
|
323
|
+
const htmlMap = new Map(htmlParticipants.map((participant) => [participant.name, participant]));
|
|
324
|
+
const svgMap = new Map(svgParticipants.map((participant) => [participant.name, participant]));
|
|
325
|
+
const byName = new Map();
|
|
326
|
+
for (const participant of [...htmlParticipants, ...svgParticipants]) {
|
|
327
|
+
if (!participant.name || !participant.iconBox) {
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
const html = htmlMap.get(participant.name) || null;
|
|
331
|
+
const svg = svgMap.get(participant.name) || null;
|
|
332
|
+
const hasLabel = Boolean(html?.labelText || svg?.labelText);
|
|
333
|
+
if (!hasLabel && participant.name === "_STARTER_") {
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
byName.set(participant.name, true);
|
|
337
|
+
}
|
|
338
|
+
return Array.from(byName.keys()).sort((a, b) => a.localeCompare(b));
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function buildParticipantLabelItems(participants, iconNames) {
|
|
342
|
+
const include = new Set(iconNames);
|
|
343
|
+
return participants
|
|
344
|
+
.filter((participant) => include.has(participant.name) && participant.labelText && participant.labelBox)
|
|
345
|
+
.map((participant) => ({
|
|
346
|
+
side: participant.side,
|
|
347
|
+
kind: "participant",
|
|
348
|
+
text: participant.labelText,
|
|
349
|
+
pairText: participant.name,
|
|
350
|
+
ownerText: participant.name,
|
|
351
|
+
box: participant.labelBox,
|
|
352
|
+
font: participant.labelFont,
|
|
353
|
+
letters: participant.labelLetters,
|
|
354
|
+
}));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function buildParticipantStereotypeItems(participants) {
|
|
358
|
+
return participants
|
|
359
|
+
.filter((participant) => participant.stereotypeText && participant.stereotypeBox)
|
|
360
|
+
.map((participant) => ({
|
|
361
|
+
side: participant.side,
|
|
362
|
+
kind: "participant-stereotype",
|
|
363
|
+
text: participant.stereotypeText,
|
|
364
|
+
pairText: participant.name,
|
|
365
|
+
ownerText: participant.name,
|
|
366
|
+
box: participant.stereotypeBox,
|
|
367
|
+
font: participant.stereotypeFont,
|
|
368
|
+
letters: participant.stereotypeLetters,
|
|
369
|
+
}));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function scoreParticipantIcon(htmlParticipant, svgParticipant, diffImage) {
|
|
373
|
+
const iconPresentHtml = Boolean(htmlParticipant?.iconBox);
|
|
374
|
+
const iconPresentSvg = Boolean(svgParticipant?.iconBox);
|
|
375
|
+
const base = htmlParticipant || svgParticipant;
|
|
376
|
+
const participant = {
|
|
377
|
+
name: base?.name ?? "",
|
|
378
|
+
label_text: htmlParticipant?.labelText || svgParticipant?.labelText || null,
|
|
379
|
+
presence: {
|
|
380
|
+
html: iconPresentHtml,
|
|
381
|
+
svg: iconPresentSvg,
|
|
382
|
+
},
|
|
383
|
+
anchor_kind: htmlParticipant?.anchorKind || svgParticipant?.anchorKind || null,
|
|
384
|
+
status: "ambiguous",
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
if (!iconPresentHtml || !iconPresentSvg) {
|
|
388
|
+
participant.reason = "icon missing on one side";
|
|
389
|
+
return participant;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const htmlIconCenter = rectCenter(htmlParticipant.iconBox);
|
|
393
|
+
const svgIconCenter = rectCenter(svgParticipant.iconBox);
|
|
394
|
+
const htmlAnchorCenter = rectCenter(htmlParticipant.anchorBox);
|
|
395
|
+
const svgAnchorCenter = rectCenter(svgParticipant.anchorBox);
|
|
396
|
+
const directDx = svgIconCenter.x - htmlIconCenter.x;
|
|
397
|
+
const directDy = svgIconCenter.y - htmlIconCenter.y;
|
|
398
|
+
const relativeDx = (svgIconCenter.x - svgAnchorCenter.x) - (htmlIconCenter.x - htmlAnchorCenter.x);
|
|
399
|
+
const relativeDy = (svgIconCenter.y - svgAnchorCenter.y) - (htmlIconCenter.y - htmlAnchorCenter.y);
|
|
400
|
+
const slot = {
|
|
401
|
+
x: Math.min(htmlParticipant.iconBox.x, svgParticipant.iconBox.x) - 2,
|
|
402
|
+
y: Math.min(htmlParticipant.iconBox.y, svgParticipant.iconBox.y) - 2,
|
|
403
|
+
w: Math.max(rectRight(htmlParticipant.iconBox), rectRight(svgParticipant.iconBox)) - Math.min(htmlParticipant.iconBox.x, svgParticipant.iconBox.x) + 4,
|
|
404
|
+
h: Math.max(rectBottom(htmlParticipant.iconBox), rectBottom(svgParticipant.iconBox)) - Math.min(htmlParticipant.iconBox.y, svgParticipant.iconBox.y) + 4,
|
|
405
|
+
};
|
|
406
|
+
const diff = analyzeDiffSlot(diffImage, slot);
|
|
407
|
+
const centroidDx = diff.redCentroid && diff.blueCentroid ? diff.blueCentroid.x - diff.redCentroid.x : null;
|
|
408
|
+
const centroidDy = diff.redCentroid && diff.blueCentroid ? diff.blueCentroid.y - diff.redCentroid.y : null;
|
|
409
|
+
const nearZero = Math.abs(directDx) < 0.75
|
|
410
|
+
&& Math.abs(directDy) < 0.75
|
|
411
|
+
&& Math.abs(relativeDx) < 0.75
|
|
412
|
+
&& Math.abs(relativeDy) < 0.75;
|
|
413
|
+
const enoughDiffPixels = diff.redCount >= 6 && diff.blueCount >= 6;
|
|
414
|
+
const xConsistent = centroidDx === null || Math.abs(directDx) < 0.75 || Math.sign(centroidDx) === Math.sign(directDx);
|
|
415
|
+
const yConsistent = centroidDy === null || Math.abs(directDy) < 0.75 || Math.sign(centroidDy) === Math.sign(directDy);
|
|
416
|
+
const overlap = iou(htmlParticipant.iconBox, svgParticipant.iconBox);
|
|
417
|
+
const status = nearZero
|
|
418
|
+
? (overlap >= 0.15 ? "ok" : "ambiguous")
|
|
419
|
+
: ((enoughDiffPixels && xConsistent && yConsistent)
|
|
420
|
+
|| Math.abs(directDx) >= 0.75
|
|
421
|
+
|| Math.abs(directDy) >= 0.75
|
|
422
|
+
|| Math.abs(relativeDx) >= 0.75
|
|
423
|
+
|| Math.abs(relativeDy) >= 0.75
|
|
424
|
+
? "ok"
|
|
425
|
+
: "ambiguous");
|
|
426
|
+
const confidence = round(Math.min(1, overlap * 0.45 + (enoughDiffPixels ? 0.55 : 0.2)), 3);
|
|
427
|
+
|
|
428
|
+
return {
|
|
429
|
+
...participant,
|
|
430
|
+
status,
|
|
431
|
+
icon_dx: status === "ok" ? normalizeOffset(directDx) : null,
|
|
432
|
+
icon_dy: status === "ok" ? normalizeOffset(directDy) : null,
|
|
433
|
+
relative_dx: status === "ok" ? normalizeOffset(relativeDx) : null,
|
|
434
|
+
relative_dy: status === "ok" ? normalizeOffset(relativeDy) : null,
|
|
435
|
+
confidence,
|
|
436
|
+
html_icon_box: {
|
|
437
|
+
x: round(htmlParticipant.iconBox.x),
|
|
438
|
+
y: round(htmlParticipant.iconBox.y),
|
|
439
|
+
w: round(htmlParticipant.iconBox.w),
|
|
440
|
+
h: round(htmlParticipant.iconBox.h),
|
|
441
|
+
},
|
|
442
|
+
svg_icon_box: {
|
|
443
|
+
x: round(svgParticipant.iconBox.x),
|
|
444
|
+
y: round(svgParticipant.iconBox.y),
|
|
445
|
+
w: round(svgParticipant.iconBox.w),
|
|
446
|
+
h: round(svgParticipant.iconBox.h),
|
|
447
|
+
},
|
|
448
|
+
html_anchor_box: {
|
|
449
|
+
x: round(htmlParticipant.anchorBox.x),
|
|
450
|
+
y: round(htmlParticipant.anchorBox.y),
|
|
451
|
+
w: round(htmlParticipant.anchorBox.w),
|
|
452
|
+
h: round(htmlParticipant.anchorBox.h),
|
|
453
|
+
},
|
|
454
|
+
svg_anchor_box: {
|
|
455
|
+
x: round(svgParticipant.anchorBox.x),
|
|
456
|
+
y: round(svgParticipant.anchorBox.y),
|
|
457
|
+
w: round(svgParticipant.anchorBox.w),
|
|
458
|
+
h: round(svgParticipant.anchorBox.h),
|
|
459
|
+
},
|
|
460
|
+
evidence: {
|
|
461
|
+
icon_dx: normalizeOffset(directDx),
|
|
462
|
+
icon_dy: normalizeOffset(directDy),
|
|
463
|
+
relative_dx: normalizeOffset(relativeDx),
|
|
464
|
+
relative_dy: normalizeOffset(relativeDy),
|
|
465
|
+
overlap: round(overlap, 3),
|
|
466
|
+
diff_red: diff.redCount,
|
|
467
|
+
diff_blue: diff.blueCount,
|
|
468
|
+
diff_centroid_dx: centroidDx === null ? null : round(centroidDx),
|
|
469
|
+
diff_centroid_dy: centroidDy === null ? null : round(centroidDy),
|
|
470
|
+
},
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function buildParticipantIconSection(htmlParticipants, svgParticipants, diffImage) {
|
|
475
|
+
const names = participantsWithIcons(htmlParticipants, svgParticipants);
|
|
476
|
+
const htmlMap = new Map(htmlParticipants.map((participant) => [participant.name, participant]));
|
|
477
|
+
const svgMap = new Map(svgParticipants.map((participant) => [participant.name, participant]));
|
|
478
|
+
return names.map((name) => scoreParticipantIcon(htmlMap.get(name) || null, svgMap.get(name) || null, diffImage));
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function participantNames(htmlParticipants, svgParticipants) {
|
|
482
|
+
return Array.from(
|
|
483
|
+
new Set(
|
|
484
|
+
[...htmlParticipants, ...svgParticipants]
|
|
485
|
+
.map((participant) => participant.name)
|
|
486
|
+
.filter(Boolean),
|
|
487
|
+
),
|
|
488
|
+
).sort((a, b) => a.localeCompare(b));
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function scoreParticipantBox(htmlParticipant, svgParticipant) {
|
|
492
|
+
const base = htmlParticipant || svgParticipant;
|
|
493
|
+
const item = {
|
|
494
|
+
name: base?.name ?? "",
|
|
495
|
+
status: "ambiguous",
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
if (!htmlParticipant?.participantBox || !svgParticipant?.participantBox) {
|
|
499
|
+
item.reason = "participant box missing on one side";
|
|
500
|
+
return item;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const dx = svgParticipant.participantBox.x - htmlParticipant.participantBox.x;
|
|
504
|
+
const dy = svgParticipant.participantBox.y - htmlParticipant.participantBox.y;
|
|
505
|
+
const dw = svgParticipant.participantBox.w - htmlParticipant.participantBox.w;
|
|
506
|
+
const dh = svgParticipant.participantBox.h - htmlParticipant.participantBox.h;
|
|
507
|
+
|
|
508
|
+
return {
|
|
509
|
+
...item,
|
|
510
|
+
status: "ok",
|
|
511
|
+
dx: normalizeOffset(dx),
|
|
512
|
+
dy: normalizeOffset(dy),
|
|
513
|
+
dw: normalizeOffset(dw),
|
|
514
|
+
dh: normalizeOffset(dh),
|
|
515
|
+
html_box: {
|
|
516
|
+
x: round(htmlParticipant.participantBox.x),
|
|
517
|
+
y: round(htmlParticipant.participantBox.y),
|
|
518
|
+
w: round(htmlParticipant.participantBox.w),
|
|
519
|
+
h: round(htmlParticipant.participantBox.h),
|
|
520
|
+
},
|
|
521
|
+
svg_box: {
|
|
522
|
+
x: round(svgParticipant.participantBox.x),
|
|
523
|
+
y: round(svgParticipant.participantBox.y),
|
|
524
|
+
w: round(svgParticipant.participantBox.w),
|
|
525
|
+
h: round(svgParticipant.participantBox.h),
|
|
526
|
+
},
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function buildParticipantBoxSection(htmlParticipants, svgParticipants) {
|
|
531
|
+
const names = participantNames(htmlParticipants, svgParticipants);
|
|
532
|
+
const htmlMap = new Map(htmlParticipants.map((participant) => [participant.name, participant]));
|
|
533
|
+
const svgMap = new Map(svgParticipants.map((participant) => [participant.name, participant]));
|
|
534
|
+
return names.map((name) => scoreParticipantBox(htmlMap.get(name) || null, svgMap.get(name) || null));
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function scoreParticipantColor(htmlParticipant, svgParticipant) {
|
|
538
|
+
const base = htmlParticipant || svgParticipant;
|
|
539
|
+
const item = {
|
|
540
|
+
name: base?.name ?? "",
|
|
541
|
+
status: "ambiguous",
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
if (!htmlParticipant || !svgParticipant) {
|
|
545
|
+
item.reason = "participant missing on one side";
|
|
546
|
+
return item;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return {
|
|
550
|
+
...item,
|
|
551
|
+
status: "ok",
|
|
552
|
+
html_background_color: htmlParticipant.backgroundColor ?? null,
|
|
553
|
+
svg_background_color: svgParticipant.backgroundColor ?? null,
|
|
554
|
+
html_text_color: htmlParticipant.textColor ?? null,
|
|
555
|
+
svg_text_color: svgParticipant.textColor ?? null,
|
|
556
|
+
html_stereotype_color: htmlParticipant.stereotypeColor ?? null,
|
|
557
|
+
svg_stereotype_color: svgParticipant.stereotypeColor ?? null,
|
|
558
|
+
background_match: (htmlParticipant.backgroundColor ?? null) === (svgParticipant.backgroundColor ?? null),
|
|
559
|
+
text_match: (htmlParticipant.textColor ?? null) === (svgParticipant.textColor ?? null),
|
|
560
|
+
stereotype_text_match: (htmlParticipant.stereotypeColor ?? null) === (svgParticipant.stereotypeColor ?? null),
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function buildParticipantColorSection(htmlParticipants, svgParticipants) {
|
|
565
|
+
const names = participantNames(htmlParticipants, svgParticipants);
|
|
566
|
+
const htmlMap = new Map(htmlParticipants.map((participant) => [participant.name, participant]));
|
|
567
|
+
const svgMap = new Map(svgParticipants.map((participant) => [participant.name, participant]));
|
|
568
|
+
return names.map((name) => scoreParticipantColor(htmlMap.get(name) || null, svgMap.get(name) || null));
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function groupNames(htmlGroups, svgGroups) {
|
|
572
|
+
return Array.from(
|
|
573
|
+
new Set(
|
|
574
|
+
[...htmlGroups, ...svgGroups]
|
|
575
|
+
.map((group) => group.name)
|
|
576
|
+
.filter(Boolean),
|
|
577
|
+
),
|
|
578
|
+
).sort((a, b) => a.localeCompare(b));
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function scoreGroup(htmlGroup, svgGroup) {
|
|
582
|
+
const base = htmlGroup || svgGroup;
|
|
583
|
+
const item = {
|
|
584
|
+
name: base?.name ?? "",
|
|
585
|
+
status: "ambiguous",
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
if (!htmlGroup?.box || !svgGroup?.box) {
|
|
589
|
+
item.reason = "group missing on one side";
|
|
590
|
+
return item;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const dx = svgGroup.box.x - htmlGroup.box.x;
|
|
594
|
+
const dy = svgGroup.box.y - htmlGroup.box.y;
|
|
595
|
+
const dw = svgGroup.box.w - htmlGroup.box.w;
|
|
596
|
+
const dh = svgGroup.box.h - htmlGroup.box.h;
|
|
597
|
+
const nameDx = htmlGroup.nameBox && svgGroup.nameBox
|
|
598
|
+
? svgGroup.nameBox.x - htmlGroup.nameBox.x
|
|
599
|
+
: null;
|
|
600
|
+
const nameDy = htmlGroup.nameBox && svgGroup.nameBox
|
|
601
|
+
? svgGroup.nameBox.y - htmlGroup.nameBox.y
|
|
602
|
+
: null;
|
|
603
|
+
|
|
604
|
+
return {
|
|
605
|
+
...item,
|
|
606
|
+
status: "ok",
|
|
607
|
+
html_name: htmlGroup.name,
|
|
608
|
+
svg_name: svgGroup.name,
|
|
609
|
+
dx: normalizeOffset(dx),
|
|
610
|
+
dy: normalizeOffset(dy),
|
|
611
|
+
dw: normalizeOffset(dw),
|
|
612
|
+
dh: normalizeOffset(dh),
|
|
613
|
+
name_dx: nameDx === null ? null : normalizeOffset(nameDx),
|
|
614
|
+
name_dy: nameDy === null ? null : normalizeOffset(nameDy),
|
|
615
|
+
html_box: {
|
|
616
|
+
x: round(htmlGroup.box.x),
|
|
617
|
+
y: round(htmlGroup.box.y),
|
|
618
|
+
w: round(htmlGroup.box.w),
|
|
619
|
+
h: round(htmlGroup.box.h),
|
|
620
|
+
},
|
|
621
|
+
svg_box: {
|
|
622
|
+
x: round(svgGroup.box.x),
|
|
623
|
+
y: round(svgGroup.box.y),
|
|
624
|
+
w: round(svgGroup.box.w),
|
|
625
|
+
h: round(svgGroup.box.h),
|
|
626
|
+
},
|
|
627
|
+
html_name_box: htmlGroup.nameBox ? {
|
|
628
|
+
x: round(htmlGroup.nameBox.x),
|
|
629
|
+
y: round(htmlGroup.nameBox.y),
|
|
630
|
+
w: round(htmlGroup.nameBox.w),
|
|
631
|
+
h: round(htmlGroup.nameBox.h),
|
|
632
|
+
} : null,
|
|
633
|
+
svg_name_box: svgGroup.nameBox ? {
|
|
634
|
+
x: round(svgGroup.nameBox.x),
|
|
635
|
+
y: round(svgGroup.nameBox.y),
|
|
636
|
+
w: round(svgGroup.nameBox.w),
|
|
637
|
+
h: round(svgGroup.nameBox.h),
|
|
638
|
+
} : null,
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function buildGroupSection(htmlGroups, svgGroups) {
|
|
643
|
+
const names = groupNames(htmlGroups, svgGroups);
|
|
644
|
+
const htmlMap = new Map(htmlGroups.map((group) => [group.name, group]));
|
|
645
|
+
const svgMap = new Map(svgGroups.map((group) => [group.name, group]));
|
|
646
|
+
return names.map((name) => scoreGroup(htmlMap.get(name) || null, svgMap.get(name) || null));
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function scoreOccurrence(htmlOcc, svgOcc) {
|
|
650
|
+
const base = htmlOcc || svgOcc;
|
|
651
|
+
const item = {
|
|
652
|
+
participant: base?.participant ?? "",
|
|
653
|
+
idx: base?.idx ?? 0,
|
|
654
|
+
status: "ambiguous",
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
if (!htmlOcc?.box || !svgOcc?.box) {
|
|
658
|
+
item.reason = `occurrence missing on ${!htmlOcc ? "html" : "svg"} side`;
|
|
659
|
+
return item;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const dx = svgOcc.box.x - htmlOcc.box.x;
|
|
663
|
+
const dy = svgOcc.box.y - htmlOcc.box.y;
|
|
664
|
+
const dw = svgOcc.box.w - htmlOcc.box.w;
|
|
665
|
+
const dh = svgOcc.box.h - htmlOcc.box.h;
|
|
666
|
+
|
|
667
|
+
return {
|
|
668
|
+
...item,
|
|
669
|
+
status: "ok",
|
|
670
|
+
dx: normalizeOffset(dx),
|
|
671
|
+
dy: normalizeOffset(dy),
|
|
672
|
+
dw: normalizeOffset(dw),
|
|
673
|
+
dh: normalizeOffset(dh),
|
|
674
|
+
html_box: {
|
|
675
|
+
x: round(htmlOcc.box.x),
|
|
676
|
+
y: round(htmlOcc.box.y),
|
|
677
|
+
w: round(htmlOcc.box.w),
|
|
678
|
+
h: round(htmlOcc.box.h),
|
|
679
|
+
},
|
|
680
|
+
svg_box: {
|
|
681
|
+
x: round(svgOcc.box.x),
|
|
682
|
+
y: round(svgOcc.box.y),
|
|
683
|
+
w: round(svgOcc.box.w),
|
|
684
|
+
h: round(svgOcc.box.h),
|
|
685
|
+
},
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function buildOccurrenceSection(htmlOccurrences, svgOccurrences) {
|
|
690
|
+
const maxLen = Math.max(htmlOccurrences.length, svgOccurrences.length);
|
|
691
|
+
const results = [];
|
|
692
|
+
for (let i = 0; i < maxLen; i++) {
|
|
693
|
+
results.push(scoreOccurrence(htmlOccurrences[i] || null, svgOccurrences[i] || null));
|
|
694
|
+
}
|
|
695
|
+
return results;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function scoreFragmentDivider(htmlDiv, svgDiv) {
|
|
699
|
+
const base = htmlDiv || svgDiv;
|
|
700
|
+
const item = {
|
|
701
|
+
idx: base?.idx ?? 0,
|
|
702
|
+
label: base?.label ?? "",
|
|
703
|
+
status: "ambiguous",
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
if (!htmlDiv || !svgDiv) {
|
|
707
|
+
item.reason = `divider missing on ${!htmlDiv ? "html" : "svg"} side`;
|
|
708
|
+
return item;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const dx = svgDiv.x - htmlDiv.x;
|
|
712
|
+
const dy = svgDiv.y - htmlDiv.y;
|
|
713
|
+
const dw = svgDiv.width - htmlDiv.width;
|
|
714
|
+
|
|
715
|
+
return {
|
|
716
|
+
...item,
|
|
717
|
+
status: "ok",
|
|
718
|
+
dx: normalizeOffset(dx),
|
|
719
|
+
dy: normalizeOffset(dy),
|
|
720
|
+
dw: normalizeOffset(dw),
|
|
721
|
+
html_y: round(htmlDiv.y),
|
|
722
|
+
svg_y: round(svgDiv.y),
|
|
723
|
+
html_x: round(htmlDiv.x),
|
|
724
|
+
svg_x: round(svgDiv.x),
|
|
725
|
+
html_width: round(htmlDiv.width),
|
|
726
|
+
svg_width: round(svgDiv.width),
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function scoreDivider(htmlDiv, svgDiv) {
|
|
731
|
+
const base = htmlDiv || svgDiv;
|
|
732
|
+
const item = {
|
|
733
|
+
idx: base?.idx ?? 0,
|
|
734
|
+
label: base?.label ?? "",
|
|
735
|
+
status: "ambiguous",
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
if (!htmlDiv || !svgDiv) {
|
|
739
|
+
item.reason = `divider missing on ${!htmlDiv ? "html" : "svg"} side`;
|
|
740
|
+
return item;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const dy = round(svgDiv.y - htmlDiv.y);
|
|
744
|
+
return {
|
|
745
|
+
...item,
|
|
746
|
+
status: dy === 0 ? "ok" : "ambiguous",
|
|
747
|
+
dy,
|
|
748
|
+
html_box: htmlDiv.box,
|
|
749
|
+
svg_box: svgDiv.box,
|
|
750
|
+
html_label_box: htmlDiv.label_box,
|
|
751
|
+
svg_label_box: svgDiv.label_box,
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function buildDividerSection(htmlDividers, svgDividers) {
|
|
756
|
+
const maxLen = Math.max(htmlDividers.length, svgDividers.length);
|
|
757
|
+
const results = [];
|
|
758
|
+
for (let i = 0; i < maxLen; i++) {
|
|
759
|
+
results.push(scoreDivider(htmlDividers[i] || null, svgDividers[i] || null));
|
|
760
|
+
}
|
|
761
|
+
return results;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function buildFragmentDividerSection(htmlDividers, svgDividers) {
|
|
765
|
+
const maxLen = Math.max(htmlDividers.length, svgDividers.length);
|
|
766
|
+
const results = [];
|
|
767
|
+
for (let i = 0; i < maxLen; i++) {
|
|
768
|
+
results.push(scoreFragmentDivider(htmlDividers[i] || null, svgDividers[i] || null));
|
|
769
|
+
}
|
|
770
|
+
return results;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
export function buildScoredSections(extracted, diffImage) {
|
|
774
|
+
const {
|
|
775
|
+
htmlLabels,
|
|
776
|
+
svgLabels,
|
|
777
|
+
htmlNumbers,
|
|
778
|
+
svgNumbers,
|
|
779
|
+
htmlArrows,
|
|
780
|
+
svgArrows,
|
|
781
|
+
htmlParticipants,
|
|
782
|
+
svgParticipants,
|
|
783
|
+
htmlComments,
|
|
784
|
+
svgComments,
|
|
785
|
+
htmlGroups,
|
|
786
|
+
svgGroups,
|
|
787
|
+
htmlOccurrences,
|
|
788
|
+
svgOccurrences,
|
|
789
|
+
htmlFragmentDividers,
|
|
790
|
+
svgFragmentDividers,
|
|
791
|
+
htmlDividers,
|
|
792
|
+
svgDividers,
|
|
793
|
+
} = extracted;
|
|
794
|
+
|
|
795
|
+
const iconNames = participantsWithIcons(htmlParticipants, svgParticipants);
|
|
796
|
+
const htmlParticipantLabels = buildParticipantLabelItems(htmlParticipants, iconNames);
|
|
797
|
+
const svgParticipantLabels = buildParticipantLabelItems(svgParticipants, iconNames);
|
|
798
|
+
const htmlParticipantStereotypes = buildParticipantStereotypeItems(htmlParticipants);
|
|
799
|
+
const svgParticipantStereotypes = buildParticipantStereotypeItems(svgParticipants);
|
|
800
|
+
|
|
801
|
+
return {
|
|
802
|
+
labels: buildSection(htmlLabels, svgLabels, diffImage),
|
|
803
|
+
numbers: buildSection(htmlNumbers, svgNumbers, diffImage),
|
|
804
|
+
arrows: buildArrowSection(htmlArrows, svgArrows, diffImage),
|
|
805
|
+
participantLabels: buildSection(htmlParticipantLabels, svgParticipantLabels, diffImage),
|
|
806
|
+
participantStereotypes: buildSection(htmlParticipantStereotypes, svgParticipantStereotypes, diffImage),
|
|
807
|
+
participantIcons: buildParticipantIconSection(htmlParticipants, svgParticipants, diffImage),
|
|
808
|
+
participantBoxes: buildParticipantBoxSection(htmlParticipants, svgParticipants),
|
|
809
|
+
participantColors: buildParticipantColorSection(htmlParticipants, svgParticipants),
|
|
810
|
+
comments: buildSection(htmlComments, svgComments, diffImage),
|
|
811
|
+
groups: buildGroupSection(htmlGroups, svgGroups),
|
|
812
|
+
occurrences: buildOccurrenceSection(htmlOccurrences || [], svgOccurrences || []),
|
|
813
|
+
fragmentDividers: buildFragmentDividerSection(htmlFragmentDividers || [], svgFragmentDividers || []),
|
|
814
|
+
dividers: buildDividerSection(htmlDividers || [], svgDividers || []),
|
|
815
|
+
};
|
|
816
|
+
}
|