@zenuml/core 3.47.0 → 3.47.2
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/.agents/skills/babysit-pr/SKILL.md +223 -0
- package/.agents/skills/babysit-pr/agents/openai.yaml +7 -0
- package/.agents/skills/dia-scoring/SKILL.md +139 -0
- package/.agents/skills/dia-scoring/agents/openai.yaml +7 -0
- package/.agents/skills/dia-scoring/references/selectors-and-keys.md +253 -0
- package/.agents/skills/land-pr/SKILL.md +120 -0
- package/.agents/skills/propagate-core-release/SKILL.md +205 -0
- package/.agents/skills/propagate-core-release/agents/openai.yaml +7 -0
- package/.agents/skills/propagate-core-release/references/downstreams.md +42 -0
- package/.agents/skills/ship-branch/SKILL.md +105 -0
- package/.agents/skills/submit-branch/SKILL.md +76 -0
- package/.agents/skills/validate-branch/SKILL.md +72 -0
- package/.claude/skills/emoji-eval/SKILL.md +187 -0
- package/.claude/skills/propagate-core-release/SKILL.md +81 -76
- package/.claude/skills/propagate-core-release/agents/openai.yaml +2 -2
- package/AGENTS.md +1 -1
- package/dist/stats.html +1 -1
- package/dist/zenuml.esm.mjs +16210 -15460
- package/dist/zenuml.js +540 -535
- package/docs/superpowers/plans/2026-03-30-emoji-support.md +1220 -0
- package/docs/superpowers/plans/2026-03-30-self-correcting-scoring.md +206 -0
- package/e2e/data/compare-cases.js +233 -0
- package/e2e/tools/compare-case.html +17 -3
- package/package.json +3 -3
- package/playwright.config.ts +1 -1
- package/scripts/analyze-compare-case/collect-data.mjs +159 -16
- package/scripts/analyze-compare-case/config.mjs +1 -1
- package/scripts/analyze-compare-case/report.mjs +5 -0
- package/scripts/analyze-compare-case/residual-scopes.mjs +23 -1
- package/scripts/analyze-compare-case/scoring.mjs +13 -0
|
@@ -273,6 +273,24 @@ export async function collectLabelData(page) {
|
|
|
273
273
|
return Array.from(byName.values());
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
+
function collectHtmlTitle(root, rootRect) {
|
|
277
|
+
const titleEl = root.querySelector(".title");
|
|
278
|
+
if (!titleEl) return null;
|
|
279
|
+
const text = (titleEl.textContent ?? "").trim();
|
|
280
|
+
if (!text) return null;
|
|
281
|
+
const measured = measureTextEntry(titleEl, rootRect);
|
|
282
|
+
return { side: "html", kind: "title", text, box: measured.box, font: measured.font, letters: measured.letters };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function collectSvgTitle(root, rootRect) {
|
|
286
|
+
const titleEl = root.querySelector("text.frame-title");
|
|
287
|
+
if (!titleEl) return null;
|
|
288
|
+
const text = (titleEl.textContent ?? "").trim();
|
|
289
|
+
if (!text) return null;
|
|
290
|
+
const measured = measureTextEntry(titleEl, rootRect);
|
|
291
|
+
return { side: "svg", kind: "title", text, box: measured.box, font: measured.font, letters: measured.letters };
|
|
292
|
+
}
|
|
293
|
+
|
|
276
294
|
function collectHtmlLabels(root, rootRect) {
|
|
277
295
|
const labels = [];
|
|
278
296
|
const selectorPairs = [
|
|
@@ -396,6 +414,21 @@ export async function collectLabelData(page) {
|
|
|
396
414
|
}
|
|
397
415
|
|
|
398
416
|
for (const fragmentEl of root.querySelectorAll("g.fragment")) {
|
|
417
|
+
// Detect direct text.fragment-section-label children (e.g. [else] rendered without a <g> wrapper)
|
|
418
|
+
for (const directLabel of fragmentEl.querySelectorAll(":scope > text.fragment-section-label")) {
|
|
419
|
+
const text = (directLabel.textContent ?? "").trim();
|
|
420
|
+
if (!text) continue;
|
|
421
|
+
const measured = measureTextEntry(directLabel, rootRect);
|
|
422
|
+
labels.push({
|
|
423
|
+
side: "svg",
|
|
424
|
+
kind: text.startsWith("[") ? "fragment-condition" : "fragment-section",
|
|
425
|
+
text,
|
|
426
|
+
ownerText: textOrEmpty(fragmentEl, ":scope > text.fragment-label") || null,
|
|
427
|
+
box: measured.box,
|
|
428
|
+
font: measured.font,
|
|
429
|
+
letters: measured.letters,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
399
432
|
for (const groupEl of fragmentEl.querySelectorAll(":scope > g")) {
|
|
400
433
|
const conditionTextEls = Array.from(groupEl.querySelectorAll(":scope > text.fragment-condition"));
|
|
401
434
|
if (conditionTextEls.length > 0) {
|
|
@@ -443,18 +476,23 @@ export async function collectLabelData(page) {
|
|
|
443
476
|
|
|
444
477
|
const rowEl = participantEl.querySelector(":scope > .flex.items-center.justify-center, :scope > div:last-child");
|
|
445
478
|
const firstChild = rowEl?.firstElementChild ?? null;
|
|
446
|
-
|
|
479
|
+
// Find emoji span (span.mr-1.flex-shrink-0 containing emoji text)
|
|
480
|
+
const emojiSpan = participantEl.querySelector("span.mr-1.flex-shrink-0, span[data-testid='participant-emoji']");
|
|
481
|
+
const emojiText = emojiSpan ? emojiSpan.textContent.trim() : null;
|
|
482
|
+
// Find type icon div (div with aria-description or h-6.w-6 containing an SVG icon)
|
|
483
|
+
const typeIconDiv = firstChild && (
|
|
447
484
|
firstChild.matches("[aria-description]") ||
|
|
448
485
|
firstChild.querySelector("svg") ||
|
|
449
486
|
/\bh-6\b/.test(firstChild.className || "")
|
|
450
|
-
)
|
|
451
|
-
|
|
452
|
-
: null;
|
|
487
|
+
) ? firstChild : null;
|
|
488
|
+
const iconHost = typeIconDiv || emojiSpan || null;
|
|
453
489
|
const labelEl = Array.from(participantEl.querySelectorAll(".name")).at(-1) ?? null;
|
|
454
490
|
const measuredLabel = labelEl ? measureTextEntry(labelEl, rootRect) : null;
|
|
455
491
|
const stereotypeEl = participantEl.querySelector("label.interface");
|
|
456
492
|
const measuredStereotype = stereotypeEl ? measureTextEntry(stereotypeEl, rootRect) : null;
|
|
457
|
-
|
|
493
|
+
// For icon measurement: prefer type icon (so we compare type icon vs type icon across renderers).
|
|
494
|
+
// Fall back to emoji span for emoji-only participants.
|
|
495
|
+
const iconPaintRoot = typeIconDiv ? (typeIconDiv.querySelector("svg") ?? typeIconDiv) : (emojiSpan || null);
|
|
458
496
|
const participantStyle = getComputedStyle(participantEl);
|
|
459
497
|
|
|
460
498
|
participants.push({
|
|
@@ -470,6 +508,7 @@ export async function collectLabelData(page) {
|
|
|
470
508
|
stereotypeFont: measuredStereotype?.font ?? null,
|
|
471
509
|
stereotypeLetters: measuredStereotype?.letters ?? [],
|
|
472
510
|
iconBox: paintedBox(iconPaintRoot, rootRect),
|
|
511
|
+
emojiText: emojiText || null,
|
|
473
512
|
anchorKind: measuredLabel?.box ? "label" : "participant-box",
|
|
474
513
|
anchorBox: measuredLabel?.box ?? participantBox,
|
|
475
514
|
backgroundColor: normalizeColorValue(participantStyle.backgroundColor),
|
|
@@ -500,7 +539,20 @@ export async function collectLabelData(page) {
|
|
|
500
539
|
.sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top)[0]
|
|
501
540
|
|| null;
|
|
502
541
|
const measuredStereotype = stereotypeEl ? measureTextEntry(stereotypeEl, rootRect) : null;
|
|
503
|
-
const iconEl = participantEl.querySelector(":scope > g[transform]");
|
|
542
|
+
const iconEl = participantEl.querySelector(":scope > g.participant-icon[transform]");
|
|
543
|
+
// Detect emoji: separate text.participant-emoji element (present for both emoji-only AND icon+emoji participants)
|
|
544
|
+
// or first tspan in participant-label containing emoji codepoints (legacy inline-tspan format)
|
|
545
|
+
const emojiPattern = /[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\u{FE00}-\u{FE0F}\u{200D}]/u;
|
|
546
|
+
const emojiTextEl = participantEl.querySelector(":scope > text.participant-emoji") ?? null;
|
|
547
|
+
const emojiTspan = !emojiTextEl && labelEl
|
|
548
|
+
? Array.from(labelEl.querySelectorAll("tspan")).find((ts) => emojiPattern.test(ts.textContent))
|
|
549
|
+
: null;
|
|
550
|
+
const svgEmojiText = (emojiTextEl || emojiTspan)
|
|
551
|
+
? (emojiTextEl || emojiTspan).textContent.trim()
|
|
552
|
+
: null;
|
|
553
|
+
// For icon measurement: prefer type icon so we compare type icon vs type icon across renderers.
|
|
554
|
+
// Fall back to emoji element for emoji-only participants.
|
|
555
|
+
const iconTarget = iconEl || emojiTextEl || emojiTspan;
|
|
504
556
|
const participantBoxStyle = participantBoxEl ? getComputedStyle(participantBoxEl) : null;
|
|
505
557
|
|
|
506
558
|
participants.push({
|
|
@@ -515,7 +567,8 @@ export async function collectLabelData(page) {
|
|
|
515
567
|
stereotypeBox: measuredStereotype?.box ?? null,
|
|
516
568
|
stereotypeFont: measuredStereotype?.font ?? null,
|
|
517
569
|
stereotypeLetters: measuredStereotype?.letters ?? [],
|
|
518
|
-
iconBox: paintedBox(
|
|
570
|
+
iconBox: paintedBox(iconTarget, rootRect),
|
|
571
|
+
emojiText: svgEmojiText || null,
|
|
519
572
|
anchorKind: measuredLabel?.box ? "label" : "participant-box",
|
|
520
573
|
anchorBox: measuredLabel?.box ?? participantBox,
|
|
521
574
|
backgroundColor: normalizeColorValue(participantBoxStyle?.fill || participantBoxEl?.getAttribute("fill")),
|
|
@@ -565,20 +618,36 @@ export async function collectLabelData(page) {
|
|
|
565
618
|
}
|
|
566
619
|
|
|
567
620
|
function collectHtmlGroups(root, rootRect) {
|
|
568
|
-
|
|
621
|
+
// Group containers appear twice: once in participant layer (has name label, no overlay)
|
|
622
|
+
// and once in lifeline layer (has overlay rect, no name label).
|
|
623
|
+
// Collect name data from participant-layer containers and outline boxes from
|
|
624
|
+
// lifeline-layer containers, then merge by index order.
|
|
625
|
+
const nameEntries = [];
|
|
626
|
+
const boxEntries = [];
|
|
569
627
|
for (const groupEl of root.querySelectorAll(".lifeline-group-container")) {
|
|
570
628
|
const nameEl = groupEl.querySelector(".text-skin-lifeline-group-name");
|
|
571
|
-
const
|
|
572
|
-
|
|
629
|
+
const outlineRect = groupEl.querySelector("[data-group-overlay] rect");
|
|
630
|
+
if (nameEl) {
|
|
631
|
+
nameEntries.push({
|
|
632
|
+
name: textContentNormalized(nameEl),
|
|
633
|
+
measuredName: measureTextEntry(nameEl, rootRect),
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
if (outlineRect) {
|
|
637
|
+
boxEntries.push(boxOrNull(relRect(outlineRect.getBoundingClientRect(), rootRect)));
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
const groups = [];
|
|
641
|
+
for (let i = 0; i < nameEntries.length; i++) {
|
|
642
|
+
const box = boxEntries[i] || null;
|
|
573
643
|
if (!box) continue;
|
|
574
|
-
const measuredName = nameEl ? measureTextEntry(nameEl, rootRect) : null;
|
|
575
644
|
groups.push({
|
|
576
645
|
side: "html",
|
|
577
|
-
name,
|
|
646
|
+
name: nameEntries[i].name,
|
|
578
647
|
box,
|
|
579
|
-
nameBox: measuredName?.box ?? null,
|
|
580
|
-
nameFont: measuredName?.font ?? null,
|
|
581
|
-
nameLetters: measuredName?.letters ?? [],
|
|
648
|
+
nameBox: nameEntries[i].measuredName?.box ?? null,
|
|
649
|
+
nameFont: nameEntries[i].measuredName?.font ?? null,
|
|
650
|
+
nameLetters: nameEntries[i].measuredName?.letters ?? [],
|
|
582
651
|
});
|
|
583
652
|
}
|
|
584
653
|
return groups;
|
|
@@ -589,7 +658,10 @@ export async function collectLabelData(page) {
|
|
|
589
658
|
for (const groupEl of root.querySelectorAll("g.participant-group")) {
|
|
590
659
|
const nameEl = groupEl.querySelector(":scope > text");
|
|
591
660
|
const name = textContentNormalized(nameEl);
|
|
592
|
-
|
|
661
|
+
// Measure the outline <rect> directly — consistent with HTML side measurement.
|
|
662
|
+
const outlineRect = groupEl.querySelector("rect.group-outline");
|
|
663
|
+
const measureEl = outlineRect || groupEl;
|
|
664
|
+
const box = boxOrNull(relRect(measureEl.getBoundingClientRect(), rootRect));
|
|
593
665
|
if (!box) continue;
|
|
594
666
|
const measuredName = nameEl ? measureTextEntry(nameEl, rootRect) : null;
|
|
595
667
|
groups.push({
|
|
@@ -948,6 +1020,73 @@ export async function collectLabelData(page) {
|
|
|
948
1020
|
return dividers;
|
|
949
1021
|
}
|
|
950
1022
|
|
|
1023
|
+
/**
|
|
1024
|
+
* Critical fragment header color calibration.
|
|
1025
|
+
* HTML renders .fragment-critical .header::before with border-bottom: 2px solid
|
|
1026
|
+
* (the unique thick header separator not present on other fragment types).
|
|
1027
|
+
* SVG renders no extra border for critical kind.
|
|
1028
|
+
* This collects the header bounding box + color style from both sides for comparison.
|
|
1029
|
+
*/
|
|
1030
|
+
function collectHtmlCriticalFragmentHeaders(root, rootRect) {
|
|
1031
|
+
const results = [];
|
|
1032
|
+
for (const frag of root.querySelectorAll(".fragment.fragment-critical")) {
|
|
1033
|
+
const header = frag.querySelector(":scope > .header");
|
|
1034
|
+
if (!header) continue;
|
|
1035
|
+
const r = header.getBoundingClientRect();
|
|
1036
|
+
const box = {
|
|
1037
|
+
x: Math.round(r.left - rootRect.left),
|
|
1038
|
+
y: Math.round(r.top - rootRect.top),
|
|
1039
|
+
w: Math.round(r.width),
|
|
1040
|
+
h: Math.round(r.height),
|
|
1041
|
+
};
|
|
1042
|
+
// ::before pseudo-element carries the border-bottom: 2px solid style
|
|
1043
|
+
const pseudoStyle = window.getComputedStyle(header, "::before");
|
|
1044
|
+
const borderBottomWidth = parseFloat(pseudoStyle.borderBottomWidth || "0") || 0;
|
|
1045
|
+
const borderBottomColor = pseudoStyle.borderBottomColor || "";
|
|
1046
|
+
results.push({
|
|
1047
|
+
side: "html",
|
|
1048
|
+
idx: results.length,
|
|
1049
|
+
box,
|
|
1050
|
+
headerBottomY: box.y + box.h,
|
|
1051
|
+
borderBottomWidth,
|
|
1052
|
+
borderBottomColor,
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
return results;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function collectSvgCriticalFragmentHeaders(root, rootRect) {
|
|
1059
|
+
const results = [];
|
|
1060
|
+
for (const frag of root.querySelectorAll("g.fragment.fragment-critical")) {
|
|
1061
|
+
const headerRect_el = frag.querySelector("rect.fragment-header");
|
|
1062
|
+
if (!headerRect_el) continue;
|
|
1063
|
+
const r = headerRect_el.getBoundingClientRect();
|
|
1064
|
+
const box = {
|
|
1065
|
+
x: Math.round(r.left - rootRect.left),
|
|
1066
|
+
y: Math.round(r.top - rootRect.top),
|
|
1067
|
+
w: Math.round(r.width),
|
|
1068
|
+
h: Math.round(r.height),
|
|
1069
|
+
};
|
|
1070
|
+
const headerBottomY = box.y + box.h;
|
|
1071
|
+
// Look for a line element at the header bottom edge (within 2px)
|
|
1072
|
+
const lines = Array.from(frag.querySelectorAll("line"));
|
|
1073
|
+
const borderLine = lines.find(l => {
|
|
1074
|
+
const lr = l.getBoundingClientRect();
|
|
1075
|
+
return Math.abs((lr.top - rootRect.top) - headerBottomY) < 3;
|
|
1076
|
+
});
|
|
1077
|
+
results.push({
|
|
1078
|
+
side: "svg",
|
|
1079
|
+
idx: results.length,
|
|
1080
|
+
box,
|
|
1081
|
+
headerBottomY,
|
|
1082
|
+
hasHeaderBottomLine: !!borderLine,
|
|
1083
|
+
headerBottomLineColor: borderLine ? (borderLine.getAttribute("stroke") || getComputedStyle(borderLine).stroke || "") : null,
|
|
1084
|
+
headerBottomLineWidth: borderLine ? (parseFloat(borderLine.getAttribute("stroke-width") || getComputedStyle(borderLine).strokeWidth || "0") || 0) : null,
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
return results;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
951
1090
|
const prepared = typeof window.prepareHtmlForCapture === "function"
|
|
952
1091
|
? window.prepareHtmlForCapture()
|
|
953
1092
|
: null;
|
|
@@ -968,6 +1107,8 @@ export async function collectLabelData(page) {
|
|
|
968
1107
|
htmlRootBox: { x: 0, y: 0, w: htmlRootRect.width, h: htmlRootRect.height },
|
|
969
1108
|
svgRootBox: { x: 0, y: 0, w: svgRootRect.width, h: svgRootRect.height },
|
|
970
1109
|
svgFrameBorderBox: boxOrNull(strokedElementOuterRect(svgFrameBorderEl, svgRootRect)),
|
|
1110
|
+
htmlTitle: collectHtmlTitle(htmlRoot, htmlRootRect),
|
|
1111
|
+
svgTitle: collectSvgTitle(svgRoot, svgRootRect),
|
|
971
1112
|
htmlLabels: collectHtmlLabels(htmlRoot, htmlRootRect),
|
|
972
1113
|
svgLabels: collectSvgLabels(svgRoot, svgRootRect),
|
|
973
1114
|
htmlNumbers: collectHtmlNumbers(htmlRoot, htmlRootRect),
|
|
@@ -986,6 +1127,8 @@ export async function collectLabelData(page) {
|
|
|
986
1127
|
svgFragmentDividers: collectSvgFragmentDividers(svgRoot, svgRootRect),
|
|
987
1128
|
htmlDividers: collectHtmlDividers(htmlRoot, htmlRootRect),
|
|
988
1129
|
svgDividers: collectSvgDividers(svgRoot, svgRootRect),
|
|
1130
|
+
htmlCriticalFragmentHeaders: collectHtmlCriticalFragmentHeaders(htmlRoot, htmlRootRect),
|
|
1131
|
+
svgCriticalFragmentHeaders: collectSvgCriticalFragmentHeaders(svgRoot, svgRootRect),
|
|
989
1132
|
};
|
|
990
1133
|
});
|
|
991
1134
|
}
|
|
@@ -63,6 +63,9 @@ function formatArrowSummary(arrow) {
|
|
|
63
63
|
|
|
64
64
|
function formatParticipantIconSummary(icon) {
|
|
65
65
|
const notes = [];
|
|
66
|
+
if (icon.emoji) {
|
|
67
|
+
notes.push(`emoji=${icon.emoji}`);
|
|
68
|
+
}
|
|
66
69
|
if (icon.label_text) {
|
|
67
70
|
notes.push(`label=${icon.label_text}`);
|
|
68
71
|
}
|
|
@@ -123,6 +126,7 @@ export function buildReport(caseName, extracted, diffImage) {
|
|
|
123
126
|
|
|
124
127
|
return {
|
|
125
128
|
case: caseName,
|
|
129
|
+
title: sections.title,
|
|
126
130
|
labels: sections.labels,
|
|
127
131
|
numbers: sections.numbers,
|
|
128
132
|
arrows: sections.arrows,
|
|
@@ -137,6 +141,7 @@ export function buildReport(caseName, extracted, diffImage) {
|
|
|
137
141
|
fragment_dividers: sections.fragmentDividers,
|
|
138
142
|
dividers: sections.dividers,
|
|
139
143
|
residual_scopes: residualScopes.scopes,
|
|
144
|
+
title_summary: sections.title ? formatSectionSummary("title", sections.title) : null,
|
|
140
145
|
summary: sections.labels.map((label) => formatSectionSummary("label", label)),
|
|
141
146
|
number_summary: sections.numbers.map((number) => formatSectionSummary("number", number)),
|
|
142
147
|
arrow_summary: sections.arrows.map((arrow) => `arrow:${arrow.key.text} -> ${formatArrowSummary(arrow)}`),
|
|
@@ -235,6 +235,22 @@ function buildDiffClusters(diffImage, targetClass) {
|
|
|
235
235
|
return clusters.sort((a, b) => b.size - a.size);
|
|
236
236
|
}
|
|
237
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
|
+
|
|
238
254
|
function pickScopeTarget(cluster, items) {
|
|
239
255
|
const centroid = cluster.centroid;
|
|
240
256
|
let best = null;
|
|
@@ -287,10 +303,16 @@ function formatResidualScopeSummary(scope) {
|
|
|
287
303
|
export function buildResidualScopes(extracted, diffImage) {
|
|
288
304
|
const htmlItems = buildScopeItems("html", extracted);
|
|
289
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;
|
|
290
310
|
const clusters = [
|
|
291
311
|
...buildDiffClusters(diffImage, 2),
|
|
292
312
|
...buildDiffClusters(diffImage, 3),
|
|
293
|
-
]
|
|
313
|
+
]
|
|
314
|
+
.map((cluster) => normalizeClusterToFrameSpace(cluster, scaleX, scaleY))
|
|
315
|
+
.sort((a, b) => b.size - a.size);
|
|
294
316
|
|
|
295
317
|
const residualScopes = clusters.map((cluster, index) => ({
|
|
296
318
|
rank: index + 1,
|
|
@@ -376,6 +376,7 @@ function scoreParticipantIcon(htmlParticipant, svgParticipant, diffImage) {
|
|
|
376
376
|
const participant = {
|
|
377
377
|
name: base?.name ?? "",
|
|
378
378
|
label_text: htmlParticipant?.labelText || svgParticipant?.labelText || null,
|
|
379
|
+
emoji: htmlParticipant?.emojiText || svgParticipant?.emojiText || null,
|
|
379
380
|
presence: {
|
|
380
381
|
html: iconPresentHtml,
|
|
381
382
|
svg: iconPresentSvg,
|
|
@@ -770,8 +771,19 @@ function buildFragmentDividerSection(htmlDividers, svgDividers) {
|
|
|
770
771
|
return results;
|
|
771
772
|
}
|
|
772
773
|
|
|
774
|
+
function buildTitleSection(htmlTitle, svgTitle, diffImage) {
|
|
775
|
+
if (!htmlTitle && !svgTitle) return null;
|
|
776
|
+
// Wrap as single-element arrays and reuse buildSection
|
|
777
|
+
const htmlItems = htmlTitle ? [htmlTitle] : [];
|
|
778
|
+
const svgItems = svgTitle ? [svgTitle] : [];
|
|
779
|
+
const results = buildSection(htmlItems, svgItems, diffImage);
|
|
780
|
+
return results.length > 0 ? results[0] : null;
|
|
781
|
+
}
|
|
782
|
+
|
|
773
783
|
export function buildScoredSections(extracted, diffImage) {
|
|
774
784
|
const {
|
|
785
|
+
htmlTitle,
|
|
786
|
+
svgTitle,
|
|
775
787
|
htmlLabels,
|
|
776
788
|
svgLabels,
|
|
777
789
|
htmlNumbers,
|
|
@@ -799,6 +811,7 @@ export function buildScoredSections(extracted, diffImage) {
|
|
|
799
811
|
const svgParticipantStereotypes = buildParticipantStereotypeItems(svgParticipants);
|
|
800
812
|
|
|
801
813
|
return {
|
|
814
|
+
title: buildTitleSection(htmlTitle || null, svgTitle || null, diffImage),
|
|
802
815
|
labels: buildSection(htmlLabels, svgLabels, diffImage),
|
|
803
816
|
numbers: buildSection(htmlNumbers, svgNumbers, diffImage),
|
|
804
817
|
arrows: buildArrowSection(htmlArrows, svgArrows, diffImage),
|