@sliiu/present-transition-fx 0.0.0-stage → 0.1.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/COPYRIGHT +5 -0
- package/LICENSE +661 -0
- package/README.md +8 -2
- package/THIRD-PARTY-NOTICES.md +221 -0
- package/dist/cjs/2d.js +487 -0
- package/dist/cjs/3d.js +566 -0
- package/dist/cjs/geometry.js +141 -0
- package/dist/cjs/index.js +98 -0
- package/dist/cjs/morph-audit.js +603 -0
- package/dist/cjs/morph-match.js +103 -0
- package/dist/cjs/morph-path.js +649 -0
- package/dist/cjs/morph.js +223 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/registry.js +130 -0
- package/dist/cjs/snapshot.js +420 -0
- package/dist/cjs/twin.js +88 -0
- package/dist/cjs/types.js +2 -0
- package/dist/cjs/util.js +50 -0
- package/dist/cjs/webgl.js +271 -0
- package/dist/esm/2d.js +454 -0
- package/dist/esm/3d.js +538 -0
- package/dist/esm/geometry.js +127 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/morph-audit.js +558 -0
- package/dist/esm/morph-match.js +97 -0
- package/dist/esm/morph-path.js +636 -0
- package/dist/esm/morph.js +219 -0
- package/dist/esm/registry.js +91 -0
- package/dist/esm/snapshot.js +414 -0
- package/dist/esm/twin.js +84 -0
- package/dist/esm/types.js +1 -0
- package/dist/esm/util.js +43 -0
- package/dist/esm/webgl.js +231 -0
- package/dist/types/2d.d.ts +40 -0
- package/dist/types/3d.d.ts +38 -0
- package/dist/types/geometry.d.ts +38 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/morph-audit.d.ts +127 -0
- package/dist/types/morph-match.d.ts +28 -0
- package/dist/types/morph-path.d.ts +37 -0
- package/dist/types/morph.d.ts +2 -0
- package/dist/types/registry.d.ts +6 -0
- package/dist/types/snapshot.d.ts +19 -0
- package/dist/types/twin.d.ts +2 -0
- package/dist/types/types.d.ts +13 -0
- package/dist/types/util.d.ts +12 -0
- package/dist/types/webgl.d.ts +5 -0
- package/package.json +36 -4
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.morphCandidateFromShape = morphCandidateFromShape;
|
|
4
|
+
exports.collectMorphCandidatesFromSlide = collectMorphCandidatesFromSlide;
|
|
5
|
+
exports.auditMorphSlides = auditMorphSlides;
|
|
6
|
+
exports.formatMorphReadinessSummary = formatMorphReadinessSummary;
|
|
7
|
+
exports.morphReadinessPercent = morphReadinessPercent;
|
|
8
|
+
exports.summarizeMorphMatchReasons = summarizeMorphMatchReasons;
|
|
9
|
+
exports.formatMorphMatchReasonBreakdown = formatMorphMatchReasonBreakdown;
|
|
10
|
+
exports.morphIssueCount = morphIssueCount;
|
|
11
|
+
exports.averageMorphReadinessPercent = averageMorphReadinessPercent;
|
|
12
|
+
exports.findWeakestMorphPair = findWeakestMorphPair;
|
|
13
|
+
exports.findStrongestMorphPair = findStrongestMorphPair;
|
|
14
|
+
exports.formatMorphReadinessScoreLine = formatMorphReadinessScoreLine;
|
|
15
|
+
exports.pathMorphCoveragePercent = pathMorphCoveragePercent;
|
|
16
|
+
exports.countMorphDeckIssues = countMorphDeckIssues;
|
|
17
|
+
exports.morphPairsWithIssues = morphPairsWithIssues;
|
|
18
|
+
exports.averagePathMorphCoveragePercent = averagePathMorphCoveragePercent;
|
|
19
|
+
exports.findMostIssueMorphPair = findMostIssueMorphPair;
|
|
20
|
+
exports.findHighestPathCoverageMorphPair = findHighestPathCoverageMorphPair;
|
|
21
|
+
exports.formatMorphReadinessLine = formatMorphReadinessLine;
|
|
22
|
+
exports.exportMorphReadinessText = exportMorphReadinessText;
|
|
23
|
+
exports.formatMorphDeckReadinessLine = formatMorphDeckReadinessLine;
|
|
24
|
+
exports.formatMorphDeckChromeLines = formatMorphDeckChromeLines;
|
|
25
|
+
exports.exportMorphDeckReadinessText = exportMorphDeckReadinessText;
|
|
26
|
+
exports.countMorphPairsClean = countMorphPairsClean;
|
|
27
|
+
exports.countMorphPairsWithIssues = countMorphPairsWithIssues;
|
|
28
|
+
exports.averageMorphIssuesPerPair = averageMorphIssuesPerPair;
|
|
29
|
+
exports.findFirstIssueMorphPair = findFirstIssueMorphPair;
|
|
30
|
+
exports.findLowestPathCoverageMorphPair = findLowestPathCoverageMorphPair;
|
|
31
|
+
exports.formatMorphPairCoverageBreakdownLine = formatMorphPairCoverageBreakdownLine;
|
|
32
|
+
exports.formatMorphLowestPathLine = formatMorphLowestPathLine;
|
|
33
|
+
exports.collectUnmatchedMorphShapesInDeck = collectUnmatchedMorphShapesInDeck;
|
|
34
|
+
exports.exportMorphUnmatchedShapesText = exportMorphUnmatchedShapesText;
|
|
35
|
+
exports.exportMorphPairCoverageText = exportMorphPairCoverageText;
|
|
36
|
+
exports.filterMorphDeckEntries = filterMorphDeckEntries;
|
|
37
|
+
exports.exportMorphDeckFilteredCompactText = exportMorphDeckFilteredCompactText;
|
|
38
|
+
exports.formatMorphDeckStatsLine = formatMorphDeckStatsLine;
|
|
39
|
+
exports.exportMorphPairCompactText = exportMorphPairCompactText;
|
|
40
|
+
exports.exportMorphDeckIssuesCompactText = exportMorphDeckIssuesCompactText;
|
|
41
|
+
exports.auditMorphDeck = auditMorphDeck;
|
|
42
|
+
exports.exportMorphDeckAuditText = exportMorphDeckAuditText;
|
|
43
|
+
exports.exportMorphDeckCompactText = exportMorphDeckCompactText;
|
|
44
|
+
exports.exportMorphIssuesCompactText = exportMorphIssuesCompactText;
|
|
45
|
+
exports.exportMorphAuditText = exportMorphAuditText;
|
|
46
|
+
const morph_path_1 = require("./morph-path");
|
|
47
|
+
const morph_match_1 = require("./morph-match");
|
|
48
|
+
function morphTagForShape(shape) {
|
|
49
|
+
if (shape.kind === "group")
|
|
50
|
+
return "g";
|
|
51
|
+
if (shape.kind === "image" || shape.kind === "video")
|
|
52
|
+
return "image";
|
|
53
|
+
if (shape.kind === "text")
|
|
54
|
+
return "text";
|
|
55
|
+
if (shape.kind === "line")
|
|
56
|
+
return "line";
|
|
57
|
+
return shape.kind;
|
|
58
|
+
}
|
|
59
|
+
function pathDForShape(shape) {
|
|
60
|
+
if (shape.kind === "path")
|
|
61
|
+
return shape.d;
|
|
62
|
+
if (shape.kind === "polygon") {
|
|
63
|
+
const flat = shape.points.flatMap(([x, y]) => [x, y]);
|
|
64
|
+
return (0, morph_path_1.polygonPointsToPath)(flat.join(" "), true);
|
|
65
|
+
}
|
|
66
|
+
if (shape.kind === "line") {
|
|
67
|
+
return (0, morph_path_1.linePathFromPoints)(shape.x1, shape.y1, shape.x2, shape.y2);
|
|
68
|
+
}
|
|
69
|
+
const { w, h } = shape.bbox;
|
|
70
|
+
if (shape.kind === "rect" || shape.kind === "group")
|
|
71
|
+
return (0, morph_path_1.rectPathFromSize)(w, h);
|
|
72
|
+
if (shape.kind === "ellipse")
|
|
73
|
+
return (0, morph_path_1.ellipsePathFromSize)(w, h);
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
/** Build a morph candidate from an in-memory shape (slide coordinates). */
|
|
77
|
+
function morphCandidateFromShape(shape) {
|
|
78
|
+
if (shape.visible === false)
|
|
79
|
+
return null;
|
|
80
|
+
if (shape.id.startsWith("dec_"))
|
|
81
|
+
return null;
|
|
82
|
+
const { x, y, w, h } = shape.bbox;
|
|
83
|
+
if (w < 1 && h < 1)
|
|
84
|
+
return null;
|
|
85
|
+
return {
|
|
86
|
+
id: shape.id,
|
|
87
|
+
name: shape.name ?? shape.id,
|
|
88
|
+
tag: morphTagForShape(shape),
|
|
89
|
+
rect: { left: x, top: y, width: w, height: h },
|
|
90
|
+
pathD: pathDForShape(shape),
|
|
91
|
+
placeholderType: shape.placeholder?.type,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** Collect morph candidates from all visible layers on a slide (top-level shapes, as the player matches them). */
|
|
95
|
+
function collectMorphCandidatesFromSlide(slide) {
|
|
96
|
+
const out = [];
|
|
97
|
+
for (const layer of slide.layers) {
|
|
98
|
+
if (layer.visible === false)
|
|
99
|
+
continue;
|
|
100
|
+
for (const shape of layer.shapes) {
|
|
101
|
+
const c = morphCandidateFromShape(shape);
|
|
102
|
+
if (c)
|
|
103
|
+
out.push(c);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
/** Analyze morph readiness between two slides (current → next). */
|
|
109
|
+
function auditMorphSlides(fromSlide, toSlide) {
|
|
110
|
+
const ghost = collectMorphCandidatesFromSlide(fromSlide);
|
|
111
|
+
const stage = collectMorphCandidatesFromSlide(toSlide);
|
|
112
|
+
const pairs = (0, morph_match_1.matchMorphShapePairs)(ghost, stage);
|
|
113
|
+
const matchedGhost = new Set(pairs.map((p) => p.ghost.id));
|
|
114
|
+
const matchedStage = new Set(pairs.map((p) => p.stage.id));
|
|
115
|
+
const detailed = pairs.map((p) => {
|
|
116
|
+
const pathMorph = !!p.ghost.pathD && !!p.stage.pathD && (0, morph_path_1.morphPathCompatible)(p.ghost.pathD, p.stage.pathD);
|
|
117
|
+
return { ...p, pathMorph };
|
|
118
|
+
});
|
|
119
|
+
return {
|
|
120
|
+
ghostCount: ghost.length,
|
|
121
|
+
stageCount: stage.length,
|
|
122
|
+
matched: pairs.length,
|
|
123
|
+
pathMorphCount: detailed.filter((p) => p.pathMorph).length,
|
|
124
|
+
flipOnlyCount: detailed.filter((p) => !p.pathMorph).length,
|
|
125
|
+
unmatchedStage: stage.filter((s) => !matchedStage.has(s.id)),
|
|
126
|
+
unmatchedGhost: ghost.filter((g) => !matchedGhost.has(g.id)),
|
|
127
|
+
pairs: detailed,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function formatMorphReadinessSummary(report) {
|
|
131
|
+
if (report.stageCount === 0 && report.ghostCount === 0) {
|
|
132
|
+
return "No shapes on either slide.";
|
|
133
|
+
}
|
|
134
|
+
const parts = [`${report.matched}/${report.stageCount} matched`];
|
|
135
|
+
if (report.pathMorphCount > 0)
|
|
136
|
+
parts.push(`${report.pathMorphCount} path`);
|
|
137
|
+
if (report.flipOnlyCount > 0)
|
|
138
|
+
parts.push(`${report.flipOnlyCount} FLIP`);
|
|
139
|
+
const unmatched = report.unmatchedStage.length + report.unmatchedGhost.length;
|
|
140
|
+
if (unmatched > 0)
|
|
141
|
+
parts.push(`${unmatched} unmatched`);
|
|
142
|
+
return parts.join(" · ");
|
|
143
|
+
}
|
|
144
|
+
/** 0–100 morph readiness score for the slide pair. */
|
|
145
|
+
function morphReadinessPercent(report) {
|
|
146
|
+
if (report.stageCount === 0 && report.ghostCount === 0)
|
|
147
|
+
return 100;
|
|
148
|
+
if (report.stageCount === 0)
|
|
149
|
+
return 0;
|
|
150
|
+
const matchRate = report.matched / report.stageCount;
|
|
151
|
+
const pathRate = report.matched > 0 ? report.pathMorphCount / report.matched : 0;
|
|
152
|
+
const totalShapes = report.stageCount + report.ghostCount;
|
|
153
|
+
const unmatched = report.unmatchedStage.length + report.unmatchedGhost.length;
|
|
154
|
+
const unmatchedPenalty = totalShapes > 0 ? unmatched / totalShapes : 0;
|
|
155
|
+
const raw = matchRate * 0.55 + pathRate * 0.35 + (1 - unmatchedPenalty) * 0.1;
|
|
156
|
+
return Math.round(Math.max(0, Math.min(100, raw * 100)));
|
|
157
|
+
}
|
|
158
|
+
/** Count matched pairs by match reason (id / name / placeholder / geometry). */
|
|
159
|
+
function summarizeMorphMatchReasons(report) {
|
|
160
|
+
const out = {};
|
|
161
|
+
for (const pair of report.pairs) {
|
|
162
|
+
const reason = (0, morph_match_1.morphMatchReason)(pair.ghost, pair.stage);
|
|
163
|
+
out[reason] = (out[reason] ?? 0) + 1;
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
function formatMorphMatchReasonBreakdown(report) {
|
|
168
|
+
const kinds = summarizeMorphMatchReasons(report);
|
|
169
|
+
const parts = Object.entries(kinds).map(([kind, count]) => `${kind}×${count}`);
|
|
170
|
+
return parts.length ? parts.join(", ") : "no matches";
|
|
171
|
+
}
|
|
172
|
+
/** Unmatched shapes + FLIP-only pairs (actionable morph issues). */
|
|
173
|
+
function morphIssueCount(report) {
|
|
174
|
+
return report.unmatchedStage.length + report.unmatchedGhost.length + report.flipOnlyCount;
|
|
175
|
+
}
|
|
176
|
+
function averageMorphReadinessPercent(entries) {
|
|
177
|
+
if (!entries.length)
|
|
178
|
+
return 100;
|
|
179
|
+
const total = entries.reduce((sum, entry) => sum + morphReadinessPercent(entry.report), 0);
|
|
180
|
+
return Math.round(total / entries.length);
|
|
181
|
+
}
|
|
182
|
+
function findWeakestMorphPair(entries) {
|
|
183
|
+
if (!entries.length)
|
|
184
|
+
return null;
|
|
185
|
+
return entries.reduce((best, entry) => morphReadinessPercent(entry.report) < morphReadinessPercent(best.report) ? entry : best);
|
|
186
|
+
}
|
|
187
|
+
function findStrongestMorphPair(entries) {
|
|
188
|
+
if (!entries.length)
|
|
189
|
+
return null;
|
|
190
|
+
return entries.reduce((best, entry) => morphReadinessPercent(entry.report) > morphReadinessPercent(best.report) ? entry : best);
|
|
191
|
+
}
|
|
192
|
+
function formatMorphReadinessScoreLine(report) {
|
|
193
|
+
const score = morphReadinessPercent(report);
|
|
194
|
+
return `Morph readiness: ${score}% · ${formatMorphReadinessSummary(report)}`;
|
|
195
|
+
}
|
|
196
|
+
/** Percent of matched pairs that use path morph (0–100). */
|
|
197
|
+
function pathMorphCoveragePercent(report) {
|
|
198
|
+
if (report.matched <= 0)
|
|
199
|
+
return 0;
|
|
200
|
+
return Math.round((report.pathMorphCount / report.matched) * 100);
|
|
201
|
+
}
|
|
202
|
+
function countMorphDeckIssues(entries) {
|
|
203
|
+
return entries.reduce((sum, entry) => sum + morphIssueCount(entry.report), 0);
|
|
204
|
+
}
|
|
205
|
+
function morphPairsWithIssues(entries) {
|
|
206
|
+
return entries.filter((entry) => morphIssueCount(entry.report) > 0);
|
|
207
|
+
}
|
|
208
|
+
/** Average path-morph coverage across adjacent slide pairs (0–100). */
|
|
209
|
+
function averagePathMorphCoveragePercent(entries) {
|
|
210
|
+
if (!entries.length)
|
|
211
|
+
return 0;
|
|
212
|
+
const total = entries.reduce((sum, entry) => sum + pathMorphCoveragePercent(entry.report), 0);
|
|
213
|
+
return Math.round(total / entries.length);
|
|
214
|
+
}
|
|
215
|
+
/** Slide pair with the most morph issues (ties → lowest from index). */
|
|
216
|
+
function findMostIssueMorphPair(entries) {
|
|
217
|
+
if (!entries.length)
|
|
218
|
+
return null;
|
|
219
|
+
return entries.reduce((best, entry) => {
|
|
220
|
+
const issues = morphIssueCount(entry.report);
|
|
221
|
+
const bestIssues = morphIssueCount(best.report);
|
|
222
|
+
if (issues > bestIssues)
|
|
223
|
+
return entry;
|
|
224
|
+
if (issues === bestIssues && issues > 0 && entry.fromIndex < best.fromIndex)
|
|
225
|
+
return entry;
|
|
226
|
+
return best;
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
/** Slide pair with the highest path-morph coverage (ties → lowest from index). */
|
|
230
|
+
function findHighestPathCoverageMorphPair(entries) {
|
|
231
|
+
if (!entries.length)
|
|
232
|
+
return null;
|
|
233
|
+
return entries.reduce((best, entry) => {
|
|
234
|
+
const cov = pathMorphCoveragePercent(entry.report);
|
|
235
|
+
const bestCov = pathMorphCoveragePercent(best.report);
|
|
236
|
+
if (cov > bestCov)
|
|
237
|
+
return entry;
|
|
238
|
+
if (cov === bestCov && cov > 0 && entry.fromIndex < best.fromIndex)
|
|
239
|
+
return entry;
|
|
240
|
+
return best;
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
/** One-line morph readiness for chips and compact copy. */
|
|
244
|
+
function formatMorphReadinessLine(report) {
|
|
245
|
+
const parts = [`Morph ${morphReadinessPercent(report)}%`, formatMorphReadinessSummary(report)];
|
|
246
|
+
if (report.matched > 0)
|
|
247
|
+
parts.push(`path ${pathMorphCoveragePercent(report)}%`);
|
|
248
|
+
const issues = morphIssueCount(report);
|
|
249
|
+
if (issues)
|
|
250
|
+
parts.push(`${issues} issue(s)`);
|
|
251
|
+
return parts.join(" · ");
|
|
252
|
+
}
|
|
253
|
+
/** Labeled readiness export for a single slide pair. */
|
|
254
|
+
function exportMorphReadinessText(report, opts = {}) {
|
|
255
|
+
const head = opts.fromSlide != null && opts.toSlide != null
|
|
256
|
+
? `Morph readiness — slide ${opts.fromSlide + 1} → ${opts.toSlide + 1}`
|
|
257
|
+
: "Morph readiness";
|
|
258
|
+
return [head, formatMorphReadinessLine(report), formatMorphReadinessScoreLine(report)].join("\n");
|
|
259
|
+
}
|
|
260
|
+
function deckStatsLineFromEntries(entries) {
|
|
261
|
+
if (!entries.length)
|
|
262
|
+
return "No morph pairs (need 2+ slides)";
|
|
263
|
+
const parts = [
|
|
264
|
+
`${entries.length} pair(s)`,
|
|
265
|
+
`avg ${averageMorphReadinessPercent(entries)}%`,
|
|
266
|
+
];
|
|
267
|
+
const issuePairs = morphPairsWithIssues(entries).length;
|
|
268
|
+
if (issuePairs)
|
|
269
|
+
parts.push(`${issuePairs} with issues`);
|
|
270
|
+
const totalIssues = countMorphDeckIssues(entries);
|
|
271
|
+
if (totalIssues)
|
|
272
|
+
parts.push(`${totalIssues} issue(s)`);
|
|
273
|
+
const weakest = findWeakestMorphPair(entries);
|
|
274
|
+
if (weakest)
|
|
275
|
+
parts.push(`weakest ${weakest.fromIndex + 1}→${weakest.toIndex + 1}`);
|
|
276
|
+
return parts.join(" · ");
|
|
277
|
+
}
|
|
278
|
+
function deckReadinessLineFromEntries(entries) {
|
|
279
|
+
if (!entries.length)
|
|
280
|
+
return "Morph: need 2+ slides";
|
|
281
|
+
const parts = [deckStatsLineFromEntries(entries), `path avg ${averagePathMorphCoveragePercent(entries)}%`];
|
|
282
|
+
const clean = entries.filter((e) => morphIssueCount(e.report) === 0).length;
|
|
283
|
+
if (clean)
|
|
284
|
+
parts.push(`${clean} clean pair(s)`);
|
|
285
|
+
return parts.join(" · ");
|
|
286
|
+
}
|
|
287
|
+
/** One-line deck morph readiness (stats + average path coverage). */
|
|
288
|
+
function formatMorphDeckReadinessLine(pres) {
|
|
289
|
+
return deckReadinessLineFromEntries(auditMorphDeck(pres));
|
|
290
|
+
}
|
|
291
|
+
/** Deck morph lines for the developer panel. Audits the deck once. */
|
|
292
|
+
function formatMorphDeckChromeLines(pres) {
|
|
293
|
+
const entries = auditMorphDeck(pres);
|
|
294
|
+
return {
|
|
295
|
+
stats: deckStatsLineFromEntries(entries),
|
|
296
|
+
readiness: deckReadinessLineFromEntries(entries),
|
|
297
|
+
pairCoverage: pairCoverageBreakdownFromEntries(entries),
|
|
298
|
+
lowestPath: lowestPathLineFromEntries(entries),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
/** Labeled deck readiness export for copy. */
|
|
302
|
+
function exportMorphDeckReadinessText(pres) {
|
|
303
|
+
return [
|
|
304
|
+
"Morph deck readiness",
|
|
305
|
+
formatMorphDeckReadinessLine(pres),
|
|
306
|
+
formatMorphPairCoverageBreakdownLine(pres),
|
|
307
|
+
"",
|
|
308
|
+
exportMorphDeckCompactText(pres),
|
|
309
|
+
].join("\n");
|
|
310
|
+
}
|
|
311
|
+
/** Count of adjacent slide pairs with zero morph issues. */
|
|
312
|
+
function countMorphPairsClean(entries) {
|
|
313
|
+
return entries.filter((entry) => morphIssueCount(entry.report) === 0).length;
|
|
314
|
+
}
|
|
315
|
+
/** Count of adjacent slide pairs with at least one morph issue. */
|
|
316
|
+
function countMorphPairsWithIssues(entries) {
|
|
317
|
+
return morphPairsWithIssues(entries).length;
|
|
318
|
+
}
|
|
319
|
+
/** Average morph issues per adjacent slide pair (0 if no pairs). */
|
|
320
|
+
function averageMorphIssuesPerPair(entries) {
|
|
321
|
+
if (!entries.length)
|
|
322
|
+
return 0;
|
|
323
|
+
const total = countMorphDeckIssues(entries);
|
|
324
|
+
return Math.round((total / entries.length) * 10) / 10;
|
|
325
|
+
}
|
|
326
|
+
/** First slide pair (lowest from index) with morph issues. */
|
|
327
|
+
function findFirstIssueMorphPair(entries) {
|
|
328
|
+
for (const entry of entries) {
|
|
329
|
+
if (morphIssueCount(entry.report) > 0)
|
|
330
|
+
return entry;
|
|
331
|
+
}
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
/** Slide pair with the lowest path-morph coverage among pairs with matches (ties → lowest from index). */
|
|
335
|
+
function findLowestPathCoverageMorphPair(entries) {
|
|
336
|
+
let best = null;
|
|
337
|
+
for (const entry of entries) {
|
|
338
|
+
if (entry.report.matched <= 0)
|
|
339
|
+
continue;
|
|
340
|
+
const cov = pathMorphCoveragePercent(entry.report);
|
|
341
|
+
if (!best ||
|
|
342
|
+
cov < pathMorphCoveragePercent(best.report) ||
|
|
343
|
+
(cov === pathMorphCoveragePercent(best.report) && entry.fromIndex < best.fromIndex)) {
|
|
344
|
+
best = entry;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return best;
|
|
348
|
+
}
|
|
349
|
+
function pairCoverageBreakdownFromEntries(entries) {
|
|
350
|
+
if (!entries.length)
|
|
351
|
+
return "No morph pairs";
|
|
352
|
+
const clean = countMorphPairsClean(entries);
|
|
353
|
+
const withIssues = countMorphPairsWithIssues(entries);
|
|
354
|
+
const parts = [`${clean} clean`, `${withIssues} with issues`];
|
|
355
|
+
const avgIssues = averageMorphIssuesPerPair(entries);
|
|
356
|
+
if (avgIssues > 0)
|
|
357
|
+
parts.push(`avg ${avgIssues} issue(s)/pair`);
|
|
358
|
+
return parts.join(" · ");
|
|
359
|
+
}
|
|
360
|
+
/** One-line pair coverage breakdown (clean / issues / avg issues). */
|
|
361
|
+
function formatMorphPairCoverageBreakdownLine(pres) {
|
|
362
|
+
return pairCoverageBreakdownFromEntries(auditMorphDeck(pres));
|
|
363
|
+
}
|
|
364
|
+
function lowestPathLineFromEntries(entries) {
|
|
365
|
+
const entry = findLowestPathCoverageMorphPair(entries);
|
|
366
|
+
if (!entry)
|
|
367
|
+
return "Lowest path: none";
|
|
368
|
+
return `Lowest path: slide ${entry.fromIndex + 1}→${entry.toIndex + 1} · ${pathMorphCoveragePercent(entry.report)}%`;
|
|
369
|
+
}
|
|
370
|
+
/** One-line lowest path-coverage pair summary. */
|
|
371
|
+
function formatMorphLowestPathLine(pres) {
|
|
372
|
+
return lowestPathLineFromEntries(auditMorphDeck(pres));
|
|
373
|
+
}
|
|
374
|
+
/** Unmatched morph shapes across all adjacent slide pairs. */
|
|
375
|
+
function collectUnmatchedMorphShapesInDeck(pres) {
|
|
376
|
+
const out = [];
|
|
377
|
+
for (const entry of auditMorphDeck(pres)) {
|
|
378
|
+
for (const shape of entry.report.unmatchedStage) {
|
|
379
|
+
out.push({
|
|
380
|
+
slideIndex: entry.toIndex,
|
|
381
|
+
direction: "incoming",
|
|
382
|
+
pairFromIndex: entry.fromIndex,
|
|
383
|
+
id: shape.id,
|
|
384
|
+
name: shape.name || shape.id,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
for (const shape of entry.report.unmatchedGhost) {
|
|
388
|
+
out.push({
|
|
389
|
+
slideIndex: entry.fromIndex,
|
|
390
|
+
direction: "outgoing",
|
|
391
|
+
pairFromIndex: entry.fromIndex,
|
|
392
|
+
id: shape.id,
|
|
393
|
+
name: shape.name || shape.id,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return out;
|
|
398
|
+
}
|
|
399
|
+
/** Plain-text report of unmatched morph shapes in the deck. */
|
|
400
|
+
function exportMorphUnmatchedShapesText(pres) {
|
|
401
|
+
const refs = collectUnmatchedMorphShapesInDeck(pres);
|
|
402
|
+
const lines = ["Morph unmatched shapes", `Count: ${refs.length}`];
|
|
403
|
+
if (!refs.length) {
|
|
404
|
+
lines.push("All morph candidates are matched across slide pairs.");
|
|
405
|
+
return lines.join("\n");
|
|
406
|
+
}
|
|
407
|
+
lines.push("");
|
|
408
|
+
for (const ref of refs) {
|
|
409
|
+
lines.push(`- Slide ${ref.slideIndex + 1} (${ref.direction}, pair ${ref.pairFromIndex + 1}→${ref.pairFromIndex + 2}): ${ref.name} (${ref.id})`);
|
|
410
|
+
}
|
|
411
|
+
return lines.join("\n");
|
|
412
|
+
}
|
|
413
|
+
/** Labeled pair-coverage export for copy. */
|
|
414
|
+
function exportMorphPairCoverageText(pres) {
|
|
415
|
+
return [
|
|
416
|
+
"Morph pair coverage",
|
|
417
|
+
formatMorphPairCoverageBreakdownLine(pres),
|
|
418
|
+
`path avg ${averagePathMorphCoveragePercent(auditMorphDeck(pres))}%`,
|
|
419
|
+
formatMorphLowestPathLine(pres),
|
|
420
|
+
].join("\n");
|
|
421
|
+
}
|
|
422
|
+
function filterMorphDeckEntries(entries, filter = "all") {
|
|
423
|
+
if (filter === "all")
|
|
424
|
+
return [...entries];
|
|
425
|
+
if (filter === "issues")
|
|
426
|
+
return morphPairsWithIssues(entries);
|
|
427
|
+
if (filter === "path")
|
|
428
|
+
return entries.filter((e) => e.report.pathMorphCount > 0);
|
|
429
|
+
return entries.filter((e) => morphIssueCount(e.report) === 0);
|
|
430
|
+
}
|
|
431
|
+
/** Compact deck pair list filtered by issues / path / clean. */
|
|
432
|
+
function exportMorphDeckFilteredCompactText(pres, filter = "all") {
|
|
433
|
+
const entries = filterMorphDeckEntries(auditMorphDeck(pres), filter);
|
|
434
|
+
if (!entries.length)
|
|
435
|
+
return `Morph deck (${filter}): none`;
|
|
436
|
+
const labels = entries.map((e) => `${e.fromIndex + 1}→${e.toIndex + 1}@${morphReadinessPercent(e.report)}%`);
|
|
437
|
+
return `Morph deck (${filter}, ${entries.length}): ${labels.join(", ")}`;
|
|
438
|
+
}
|
|
439
|
+
/** One-line deck morph stats for chips and compact copy. */
|
|
440
|
+
function formatMorphDeckStatsLine(pres) {
|
|
441
|
+
return deckStatsLineFromEntries(auditMorphDeck(pres));
|
|
442
|
+
}
|
|
443
|
+
/** Compact single-pair morph summary (one-liners). */
|
|
444
|
+
function exportMorphPairCompactText(report, opts = {}) {
|
|
445
|
+
const parts = ["Morph pair (compact)"];
|
|
446
|
+
if (opts.fromSlide != null && opts.toSlide != null) {
|
|
447
|
+
parts.push(`slide ${opts.fromSlide + 1}→${opts.toSlide + 1}`);
|
|
448
|
+
}
|
|
449
|
+
parts.push(`${morphReadinessPercent(report)}%`);
|
|
450
|
+
parts.push(formatMorphReadinessSummary(report));
|
|
451
|
+
if (report.matched > 0)
|
|
452
|
+
parts.push(`path coverage ${pathMorphCoveragePercent(report)}%`);
|
|
453
|
+
if (report.pairs.length)
|
|
454
|
+
parts.push(formatMorphMatchReasonBreakdown(report));
|
|
455
|
+
parts.push(`${morphIssueCount(report)} issue(s)`);
|
|
456
|
+
return parts.join(" · ");
|
|
457
|
+
}
|
|
458
|
+
/** Comma-separated deck pairs that have morph issues. */
|
|
459
|
+
function exportMorphDeckIssuesCompactText(pres) {
|
|
460
|
+
const entries = morphPairsWithIssues(auditMorphDeck(pres));
|
|
461
|
+
if (!entries.length)
|
|
462
|
+
return "Morph deck issues: none";
|
|
463
|
+
const labels = entries.map((entry) => `${entry.fromIndex + 1}→${entry.toIndex + 1}@${morphReadinessPercent(entry.report)}%(${morphIssueCount(entry.report)})`);
|
|
464
|
+
return `Morph deck issues (${entries.length}): ${labels.join(", ")}`;
|
|
465
|
+
}
|
|
466
|
+
/** Audit morph readiness for every adjacent slide pair in a deck. */
|
|
467
|
+
function auditMorphDeck(pres) {
|
|
468
|
+
const out = [];
|
|
469
|
+
for (let i = 0; i < pres.slides.length - 1; i++) {
|
|
470
|
+
out.push({
|
|
471
|
+
fromIndex: i,
|
|
472
|
+
toIndex: i + 1,
|
|
473
|
+
report: auditMorphSlides(pres.slides[i], pres.slides[i + 1]),
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
return out;
|
|
477
|
+
}
|
|
478
|
+
/** Plain-text morph audit across the whole deck. */
|
|
479
|
+
function exportMorphDeckAuditText(pres) {
|
|
480
|
+
const entries = auditMorphDeck(pres);
|
|
481
|
+
const lines = ["Morph deck audit", `Slide pairs: ${entries.length}`, ""];
|
|
482
|
+
if (!entries.length) {
|
|
483
|
+
lines.push("Need at least two slides.");
|
|
484
|
+
return lines.join("\n");
|
|
485
|
+
}
|
|
486
|
+
let totalScore = 0;
|
|
487
|
+
for (const entry of entries) {
|
|
488
|
+
totalScore += morphReadinessPercent(entry.report);
|
|
489
|
+
lines.push(`Slide ${entry.fromIndex + 1} → ${entry.toIndex + 1}`);
|
|
490
|
+
lines.push(formatMorphReadinessScoreLine(entry.report));
|
|
491
|
+
lines.push("");
|
|
492
|
+
}
|
|
493
|
+
const avg = Math.round(totalScore / entries.length);
|
|
494
|
+
lines.push(`Average readiness: ${avg}%`);
|
|
495
|
+
return lines.join("\n").trimEnd();
|
|
496
|
+
}
|
|
497
|
+
/** Compact deck-wide morph summary for copy / support. */
|
|
498
|
+
function exportMorphDeckCompactText(pres) {
|
|
499
|
+
const entries = auditMorphDeck(pres);
|
|
500
|
+
const lines = ["Morph deck summary (compact)", `Pairs: ${entries.length}`];
|
|
501
|
+
if (!entries.length) {
|
|
502
|
+
lines.push("Need at least two slides.");
|
|
503
|
+
return lines.join("\n");
|
|
504
|
+
}
|
|
505
|
+
lines.push(`Average readiness: ${averageMorphReadinessPercent(entries)}%`);
|
|
506
|
+
const weakest = findWeakestMorphPair(entries);
|
|
507
|
+
const strongest = findStrongestMorphPair(entries);
|
|
508
|
+
if (weakest) {
|
|
509
|
+
lines.push(`Weakest: slide ${weakest.fromIndex + 1}→${weakest.toIndex + 1} (${morphReadinessPercent(weakest.report)}%)`);
|
|
510
|
+
}
|
|
511
|
+
if (strongest && strongest !== weakest) {
|
|
512
|
+
lines.push(`Strongest: slide ${strongest.fromIndex + 1}→${strongest.toIndex + 1} (${morphReadinessPercent(strongest.report)}%)`);
|
|
513
|
+
}
|
|
514
|
+
lines.push("");
|
|
515
|
+
for (const entry of entries) {
|
|
516
|
+
lines.push(`- ${entry.fromIndex + 1}→${entry.toIndex + 1}: ${formatMorphReadinessScoreLine(entry.report)}`);
|
|
517
|
+
}
|
|
518
|
+
return lines.join("\n");
|
|
519
|
+
}
|
|
520
|
+
/** Short issues-only export (unmatched + FLIP-only pairs). */
|
|
521
|
+
function exportMorphIssuesCompactText(report, opts = {}) {
|
|
522
|
+
const head = opts.fromSlide != null && opts.toSlide != null
|
|
523
|
+
? `Morph issues — slide ${opts.fromSlide + 1} → ${opts.toSlide + 1}`
|
|
524
|
+
: "Morph issues";
|
|
525
|
+
const lines = [head, `Issues: ${morphIssueCount(report)}`, ""];
|
|
526
|
+
if (report.flipOnlyCount > 0) {
|
|
527
|
+
lines.push(`FLIP-only pairs (${report.flipOnlyCount}):`);
|
|
528
|
+
for (const pair of report.pairs.filter((p) => !p.pathMorph)) {
|
|
529
|
+
lines.push(`- ${pair.stage.name || pair.stage.id}`);
|
|
530
|
+
}
|
|
531
|
+
lines.push("");
|
|
532
|
+
}
|
|
533
|
+
if (report.unmatchedStage.length) {
|
|
534
|
+
lines.push("Incoming only:");
|
|
535
|
+
for (const shape of report.unmatchedStage)
|
|
536
|
+
lines.push(`- ${shape.name || shape.id}`);
|
|
537
|
+
lines.push("");
|
|
538
|
+
}
|
|
539
|
+
if (report.unmatchedGhost.length) {
|
|
540
|
+
lines.push("Outgoing only:");
|
|
541
|
+
for (const shape of report.unmatchedGhost)
|
|
542
|
+
lines.push(`- ${shape.name || shape.id}`);
|
|
543
|
+
lines.push("");
|
|
544
|
+
}
|
|
545
|
+
if (morphIssueCount(report) === 0)
|
|
546
|
+
lines.push("No morph issues.");
|
|
547
|
+
return lines.join("\n").trimEnd();
|
|
548
|
+
}
|
|
549
|
+
function shouldIncludePair(pair, filter) {
|
|
550
|
+
if (filter === "all")
|
|
551
|
+
return true;
|
|
552
|
+
if (filter === "path")
|
|
553
|
+
return pair.pathMorph;
|
|
554
|
+
if (filter === "flip" || filter === "issues")
|
|
555
|
+
return !pair.pathMorph;
|
|
556
|
+
return true;
|
|
557
|
+
}
|
|
558
|
+
/** Plain-text morph audit for copy / export. */
|
|
559
|
+
function exportMorphAuditText(report, opts = {}) {
|
|
560
|
+
const filter = opts.filter ?? "all";
|
|
561
|
+
const head = opts.fromSlide != null && opts.toSlide != null
|
|
562
|
+
? `Morph audit — slide ${opts.fromSlide + 1} → ${opts.toSlide + 1}`
|
|
563
|
+
: "Morph audit";
|
|
564
|
+
const lines = [
|
|
565
|
+
head,
|
|
566
|
+
formatMorphReadinessScoreLine(report),
|
|
567
|
+
`Shapes: ${report.ghostCount} outgoing, ${report.stageCount} incoming`,
|
|
568
|
+
];
|
|
569
|
+
if (report.pairs.length) {
|
|
570
|
+
lines.push(`Match reasons: ${formatMorphMatchReasonBreakdown(report)}`);
|
|
571
|
+
}
|
|
572
|
+
if (filter !== "all")
|
|
573
|
+
lines.push(`Filter: ${filter}`);
|
|
574
|
+
lines.push("");
|
|
575
|
+
const pairs = report.pairs.filter((p) => shouldIncludePair(p, filter));
|
|
576
|
+
if (pairs.length) {
|
|
577
|
+
lines.push("Matched pairs:");
|
|
578
|
+
for (const p of pairs) {
|
|
579
|
+
const via = (0, morph_match_1.morphMatchReason)(p.ghost, p.stage);
|
|
580
|
+
const morph = p.pathMorph ? "path morph" : "FLIP";
|
|
581
|
+
lines.push(`- ${p.stage.name || p.stage.id} ← ${p.ghost.name || p.ghost.id} (${via}, ${morph})`);
|
|
582
|
+
}
|
|
583
|
+
lines.push("");
|
|
584
|
+
}
|
|
585
|
+
if (filter === "all" || filter === "issues") {
|
|
586
|
+
if (report.unmatchedStage.length) {
|
|
587
|
+
lines.push("Incoming only (fade in):");
|
|
588
|
+
for (const s of report.unmatchedStage)
|
|
589
|
+
lines.push(`- ${s.name || s.id}`);
|
|
590
|
+
lines.push("");
|
|
591
|
+
}
|
|
592
|
+
if (report.unmatchedGhost.length) {
|
|
593
|
+
lines.push("Outgoing only (fade out):");
|
|
594
|
+
for (const g of report.unmatchedGhost)
|
|
595
|
+
lines.push(`- ${g.name || g.id}`);
|
|
596
|
+
lines.push("");
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
if (filter === "flip" && !pairs.length && !report.flipOnlyCount) {
|
|
600
|
+
lines.push("No FLIP-only pairs.");
|
|
601
|
+
}
|
|
602
|
+
return lines.join("\n").trimEnd();
|
|
603
|
+
}
|