@crouton-kit/capture 1.4.2 → 1.4.11
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/README.md +187 -0
- package/bin/capture +31278 -7312
- package/package.json +3 -1
- package/src/capture.ts +21 -18
- package/src/cdp/args.ts +269 -49
- package/src/cdp/bridge/server.ts +6 -5
- package/src/cdp/bridge/spawn.ts +41 -97
- package/src/cdp/commands/bridge-serve.ts +11 -21
- package/src/cdp/commands/cdp.ts +44 -22
- package/src/cdp/commands/heap/census.ts +61 -0
- package/src/cdp/commands/heap/common.ts +77 -0
- package/src/cdp/commands/heap/diff.ts +92 -0
- package/src/cdp/commands/heap/index.ts +38 -0
- package/src/cdp/commands/heap/objects.ts +42 -0
- package/src/cdp/commands/heap/retainers.ts +52 -0
- package/src/cdp/commands/heap/snapshot.ts +94 -0
- package/src/cdp/commands/lib.ts +9 -17
- package/src/cdp/commands/lighthouse.ts +414 -0
- package/src/cdp/commands/measure/census.ts +2 -1
- package/src/cdp/commands/measure/check.ts +70 -16
- package/src/cdp/commands/measure/diff.ts +1 -2
- package/src/cdp/commands/measure/explain.ts +44 -7
- package/src/cdp/commands/measure/index.ts +24 -32
- package/src/cdp/commands/measure/map-ax.ts +2 -1
- package/src/cdp/commands/measure/map-focus.ts +2 -1
- package/src/cdp/commands/measure/map-layers.ts +5 -3
- package/src/cdp/commands/measure/map-scroll.ts +2 -1
- package/src/cdp/commands/measure/snap.ts +5 -4
- package/src/cdp/commands/measure/sweep.ts +16 -11
- package/src/cdp/commands/measure/text.ts +147 -0
- package/src/cdp/commands/motion/index.ts +10 -18
- package/src/cdp/commands/motion/jank.ts +13 -22
- package/src/cdp/commands/motion/mask.ts +10 -9
- package/src/cdp/commands/motion/rec.ts +228 -26
- package/src/cdp/commands/motion/timeline.ts +1 -1
- package/src/cdp/commands/page/click.ts +15 -5
- package/src/cdp/commands/page/elements.ts +43 -11
- package/src/cdp/commands/page/exec.ts +53 -18
- package/src/cdp/commands/page/index.ts +20 -20
- package/src/cdp/commands/page/inspect.ts +338 -0
- package/src/cdp/commands/page/navigate.ts +83 -38
- package/src/cdp/commands/page/repeat.ts +176 -0
- package/src/cdp/commands/page/scroll.ts +10 -5
- package/src/cdp/commands/page/shot.ts +66 -31
- package/src/cdp/commands/page/type.ts +6 -2
- package/src/cdp/commands/perf/index.ts +32 -0
- package/src/cdp/commands/perf/insights.ts +214 -0
- package/src/cdp/commands/perf/trace.ts +235 -0
- package/src/cdp/commands/perf/vitals.ts +76 -0
- package/src/cdp/commands/session/collectors.ts +54 -0
- package/src/cdp/commands/stub.ts +10 -0
- package/src/cdp/commands/tab/close.ts +2 -2
- package/src/cdp/commands/tab/index.ts +16 -23
- package/src/cdp/commands/tab/launch.ts +1 -1
- package/src/cdp/commands/tab/mock/index.ts +24 -0
- package/src/cdp/commands/tab/mock/lifecycle.ts +88 -0
- package/src/cdp/commands/tab/mock/start.ts +65 -0
- package/src/cdp/commands/tab/mock/stop.ts +39 -0
- package/src/cdp/commands/tab/network.ts +22 -13
- package/src/cdp/commands/tab/reset.ts +11 -12
- package/src/cdp/connection.ts +19 -39
- package/src/cdp/detect.ts +27 -7
- package/src/cdp/dispatch.ts +25 -4
- package/src/cdp/domains.ts +2 -2
- package/src/cdp/har-recorder.ts +0 -1
- package/src/cdp/heap-snapshot.ts +647 -0
- package/src/cdp/host/client.ts +22 -0
- package/src/cdp/host/collector.ts +74 -0
- package/src/cdp/host/collectors/heap.ts +79 -0
- package/src/cdp/host/collectors/intercept.ts +247 -0
- package/src/cdp/{recorder-bridge.ts → host/collectors/motion.ts} +388 -473
- package/src/cdp/host/collectors/trace.ts +108 -0
- package/src/cdp/host/core.ts +217 -0
- package/src/cdp/host/handle.ts +47 -0
- package/src/cdp/host/held-client.ts +91 -0
- package/src/cdp/host/kinds.ts +97 -0
- package/src/cdp/host/lifecycle.ts +123 -0
- package/src/cdp/host/server.ts +88 -0
- package/src/cdp/measure/check.ts +181 -16
- package/src/cdp/measure/collectors/hittest.ts +16 -10
- package/src/cdp/measure/collectors/scroll.ts +15 -7
- package/src/cdp/measure/collectors/style-provenance.ts +11 -0
- package/src/cdp/measure/collectors/styles.ts +4 -4
- package/src/cdp/measure/explain.ts +14 -2
- package/src/cdp/measure/map-layers.ts +52 -8
- package/src/cdp/measure/snapshot.ts +5 -3
- package/src/cdp/measure/sweep.ts +29 -6
- package/src/cdp/measure/text.ts +227 -0
- package/src/cdp/motion/jank.ts +29 -83
- package/src/cdp/motion/mask.ts +18 -9
- package/src/cdp/motion/recorder.ts +86 -731
- package/src/cdp/scope-lock.ts +9 -9
- package/src/cdp/screenshot.ts +51 -21
- package/src/cdp/timing.ts +5 -5
- package/src/cdp/trace-analysis.ts +320 -0
- package/src/cdp/types.ts +31 -1
- package/src/cdp.ts +4 -6
- package/src/har-manager.ts +2 -4
- package/src/interact.ts +50 -23
- package/src/output/artifact.ts +49 -5
- package/src/output/render.ts +60 -9
- package/src/output/selection.ts +6 -0
- package/src/session/artifacts.ts +122 -12
- package/src/session/commands.ts +348 -113
- package/src/session/coordinator.ts +0 -93
- package/src/session/har-redact.ts +29 -23
- package/src/session-context.ts +13 -9
- package/src/cdp/bridge/protocol.ts +0 -224
- package/src/cdp/recorder-client.ts +0 -326
- package/src/cdp/tab-lock.ts +0 -132
package/src/cdp/motion/jank.ts
CHANGED
|
@@ -13,15 +13,6 @@ export interface JankFrame {
|
|
|
13
13
|
readonly tMs: number;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export interface DroppedFrameFact {
|
|
17
|
-
readonly afterFrame: number;
|
|
18
|
-
readonly beforeFrame: number;
|
|
19
|
-
readonly startMs: number;
|
|
20
|
-
readonly endMs: number;
|
|
21
|
-
readonly intervalMs: number;
|
|
22
|
-
readonly estimatedDroppedFrames: number;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
16
|
export type LongTaskTimingDomain = 'recorder-performance' | 'trace-relative-first-event' | 'unavailable';
|
|
26
17
|
|
|
27
18
|
export interface LongTaskFact {
|
|
@@ -32,8 +23,6 @@ export interface LongTaskFact {
|
|
|
32
23
|
readonly durationMs: number;
|
|
33
24
|
/** Null when no synchronized recorder-relative observer baseline was retained. */
|
|
34
25
|
readonly endMs: number | null;
|
|
35
|
-
/** Null when task/frame timing is incomparable or unavailable. */
|
|
36
|
-
readonly overlapsDroppedFrames: readonly number[] | null;
|
|
37
26
|
}
|
|
38
27
|
|
|
39
28
|
export interface LayoutShiftRectFact {
|
|
@@ -57,7 +46,7 @@ export interface LayoutShiftFact {
|
|
|
57
46
|
readonly rects: readonly LayoutShiftRectFact[];
|
|
58
47
|
}
|
|
59
48
|
|
|
60
|
-
export type JankCount = '
|
|
49
|
+
export type JankCount = 'long-task-records' | 'layout-shift-records';
|
|
61
50
|
|
|
62
51
|
export interface ArtifactLossFact {
|
|
63
52
|
readonly kind: string;
|
|
@@ -69,15 +58,13 @@ export interface ArtifactLossFact {
|
|
|
69
58
|
|
|
70
59
|
export interface MotionJankAnalysis {
|
|
71
60
|
readonly frameCount: number;
|
|
72
|
-
readonly cadenceMs: number | null;
|
|
73
61
|
readonly missingFrameSampleCount: number;
|
|
74
|
-
readonly droppedFrames: readonly DroppedFrameFact[];
|
|
75
|
-
readonly droppedFrameCount: number;
|
|
76
|
-
readonly droppedFramesIncomplete: boolean;
|
|
77
62
|
readonly longTasks: readonly LongTaskFact[];
|
|
78
63
|
readonly longTasksIncomplete: boolean;
|
|
79
64
|
readonly layoutShifts: readonly LayoutShiftFact[];
|
|
80
65
|
readonly layoutShiftsIncomplete: boolean;
|
|
66
|
+
/** Buffered PerformanceObserver entries that predate the recorder arm baseline and are excluded from result records. */
|
|
67
|
+
readonly preRecordingObserverEntriesExcluded: number;
|
|
81
68
|
readonly artifactLoss: readonly ArtifactLossFact[];
|
|
82
69
|
readonly frameTimestampUncertainty: '±frame' | 'unavailable';
|
|
83
70
|
readonly timingNote: string;
|
|
@@ -202,13 +189,6 @@ function inferredRectFacts(shiftAtMs: number, frames: readonly { tMs: number; el
|
|
|
202
189
|
return { rects, beforeFrameMs: before.tMs, afterFrameMs: after.tMs };
|
|
203
190
|
}
|
|
204
191
|
|
|
205
|
-
/** The lower quartile keeps a frequent long interval from redefining the nominal frame cadence. */
|
|
206
|
-
function stableLowCadence(intervals: readonly number[]): number | null {
|
|
207
|
-
if (intervals.length < 2) return null;
|
|
208
|
-
const sorted = [...intervals].sort((a, b) => a - b);
|
|
209
|
-
return sorted[Math.floor((sorted.length - 1) * 0.25)] ?? null;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
192
|
function missingFrameSamples(frames: readonly JankFrame[]): number {
|
|
213
193
|
let missing = 0;
|
|
214
194
|
for (let i = 1; i < frames.length; i++) {
|
|
@@ -257,12 +237,12 @@ function artifactLoss(events: readonly unknown[]): ArtifactLossFact[] {
|
|
|
257
237
|
case 'error': {
|
|
258
238
|
const lower = (message ?? '').toLowerCase();
|
|
259
239
|
const affectedCounts: JankCount[] = lower.includes('rect sample')
|
|
260
|
-
? [
|
|
240
|
+
? []
|
|
261
241
|
: lower.includes('observer')
|
|
262
242
|
? ['long-task-records', 'layout-shift-records']
|
|
263
243
|
: lower.includes('trace')
|
|
264
244
|
? ['long-task-records']
|
|
265
|
-
: ['
|
|
245
|
+
: ['long-task-records', 'layout-shift-records'];
|
|
266
246
|
facts.push({ kind: event.kind, count, reason, message, affectedCounts });
|
|
267
247
|
break;
|
|
268
248
|
}
|
|
@@ -280,42 +260,29 @@ export function analyzeMotionJank(input: { rects: readonly unknown[]; events: re
|
|
|
280
260
|
.filter((item): item is { item: UnknownRecord; tMs: number } => item.tMs !== null)
|
|
281
261
|
.sort((a, b) => a.tMs - b.tMs);
|
|
282
262
|
const frames: JankFrame[] = frameRecords.map(({ item, tMs }, index) => ({ frame: finite(item.frame) ?? index, tMs }));
|
|
283
|
-
const intervals = frames.slice(1).map((frame, index) => frame.tMs - frames[index].tMs).filter((interval) => interval > 0);
|
|
284
|
-
const cadenceMs = stableLowCadence(intervals);
|
|
285
263
|
const missingFrameSampleCount = missingFrameSamples(frames);
|
|
286
|
-
const droppedFrames: DroppedFrameFact[] = [];
|
|
287
|
-
if (cadenceMs !== null && cadenceMs > 0) {
|
|
288
|
-
for (let i = 1; i < frames.length; i++) {
|
|
289
|
-
const intervalMs = frames[i].tMs - frames[i - 1].tMs;
|
|
290
|
-
const estimatedDroppedFrames = Math.max(0, Math.round(intervalMs / cadenceMs) - 1);
|
|
291
|
-
if (estimatedDroppedFrames > 0) droppedFrames.push({ afterFrame: frames[i].frame, beforeFrame: frames[i - 1].frame, startMs: frames[i - 1].tMs, endMs: frames[i].tMs, intervalMs, estimatedDroppedFrames });
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
264
|
|
|
295
265
|
const traceEvents = flattenTraceEvents(input.events);
|
|
296
266
|
const observedTraceBaselineUs = traceEvents.map((event) => finite(event.ts)).filter((ts): ts is number => ts !== undefined).sort((a, b) => a - b)[0];
|
|
297
267
|
const observerEntries = observerRecords(input.events);
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
let observerNavGapUnavailable = false; // null recorder-relative time: no synchronized baseline (post-navigation-gap).
|
|
301
|
-
let observerPreArmUnavailable = false; // negative recorder-relative time: buffered entry whose document timestamp predates the arm baseline.
|
|
268
|
+
let observerNavGapUnavailable = false;
|
|
269
|
+
let preRecordingObserverEntriesExcluded = 0;
|
|
302
270
|
const longTasks: LongTaskFact[] = [];
|
|
303
271
|
for (const { item, timingAvailable } of observerEntries) {
|
|
304
272
|
if (item.entryType !== 'longtask') continue;
|
|
305
273
|
const durationMs = finite(item.duration);
|
|
306
274
|
if (durationMs === undefined) continue;
|
|
307
|
-
// A negative recorder-relative start comes from a buffered entry whose document
|
|
308
|
-
// time origin predates the recorder arm baseline; it has no valid recorder-relative
|
|
309
|
-
// baseline, so route it (like a null or post-navigation-gap start) to 'unavailable'
|
|
310
|
-
// rather than emitting a negative time or silently dropping it.
|
|
311
275
|
const startMs = timingAvailable ? observerToPerformanceMs(item, input.markers) : null;
|
|
312
|
-
if (startMs === null
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
longTasks.push({ source: 'observer', timingDomain: 'unavailable', startMs: null, durationMs, endMs: null, overlapsDroppedFrames: null });
|
|
276
|
+
if (startMs === null) {
|
|
277
|
+
observerNavGapUnavailable = true;
|
|
278
|
+
longTasks.push({ source: 'observer', timingDomain: 'unavailable', startMs: null, durationMs, endMs: null });
|
|
316
279
|
continue;
|
|
317
280
|
}
|
|
318
|
-
|
|
281
|
+
if (startMs < 0) {
|
|
282
|
+
preRecordingObserverEntriesExcluded++;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
longTasks.push(longTask('observer', 'recorder-performance', startMs, durationMs));
|
|
319
286
|
}
|
|
320
287
|
for (const trace of traceEvents) {
|
|
321
288
|
const name = typeof trace.name === 'string' ? trace.name : '';
|
|
@@ -323,7 +290,7 @@ export function analyzeMotionJank(input: { rects: readonly unknown[]; events: re
|
|
|
323
290
|
const timestampUs = finite(trace.ts);
|
|
324
291
|
if (!/(longtask|run(task|microtasks)|task)/i.test(name) || durationMs === undefined || durationMs < 50_000 || timestampUs === undefined) continue;
|
|
325
292
|
const timing = traceTime(timestampUs, input.markers, observedTraceBaselineUs);
|
|
326
|
-
if (timing !== null) longTasks.push(longTask('trace', timing.domain, timing.tMs, durationMs / 1000
|
|
293
|
+
if (timing !== null) longTasks.push(longTask('trace', timing.domain, timing.tMs, durationMs / 1000));
|
|
327
294
|
}
|
|
328
295
|
|
|
329
296
|
const frameElementRecords = frameRecords.map(({ item, tMs }) => ({ tMs, elements: (Array.isArray(item.elements) ? item.elements : []).map(record).filter((v): v is UnknownRecord => v !== null) }));
|
|
@@ -332,14 +299,9 @@ export function analyzeMotionJank(input: { rects: readonly unknown[]; events: re
|
|
|
332
299
|
if (item.entryType !== 'layout-shift') continue;
|
|
333
300
|
const value = finite(item.value);
|
|
334
301
|
if (value === undefined) continue;
|
|
335
|
-
// A negative recorder-relative time comes from a buffered entry whose document
|
|
336
|
-
// time origin predates the recorder arm baseline; it has no valid recorder-relative
|
|
337
|
-
// baseline, so route it (like a null or post-navigation-gap time) to 'unavailable'
|
|
338
|
-
// rather than emitting a negative time or silently dropping it.
|
|
339
302
|
const tMs = timingAvailable ? observerToPerformanceMs(item, input.markers) : null;
|
|
340
|
-
if (tMs === null
|
|
341
|
-
|
|
342
|
-
else observerPreArmUnavailable = true;
|
|
303
|
+
if (tMs === null) {
|
|
304
|
+
observerNavGapUnavailable = true;
|
|
343
305
|
layoutShifts.push({
|
|
344
306
|
tMs: null,
|
|
345
307
|
value,
|
|
@@ -349,6 +311,10 @@ export function analyzeMotionJank(input: { rects: readonly unknown[]; events: re
|
|
|
349
311
|
});
|
|
350
312
|
continue;
|
|
351
313
|
}
|
|
314
|
+
if (tMs < 0) {
|
|
315
|
+
preRecordingObserverEntriesExcluded++;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
352
318
|
const explicit = rectFactsFromObserverSources(item);
|
|
353
319
|
const inferred = explicit.length ? undefined : inferredRectFacts(tMs, frameElementRecords);
|
|
354
320
|
layoutShifts.push({
|
|
@@ -367,22 +333,15 @@ export function analyzeMotionJank(input: { rects: readonly unknown[]; events: re
|
|
|
367
333
|
losses.push({
|
|
368
334
|
kind: 'orphaned-finalized',
|
|
369
335
|
message: 'recorder was finalized best-effort from artifacts already flushed to disk',
|
|
370
|
-
affectedCounts: ['
|
|
336
|
+
affectedCounts: ['long-task-records', 'layout-shift-records'],
|
|
371
337
|
});
|
|
372
338
|
}
|
|
373
339
|
const incomplete = new Set<JankCount>(losses.flatMap((loss) => loss.affectedCounts));
|
|
374
|
-
if (missingFrameSampleCount > 0 || cadenceMs === null) incomplete.add('dropped-frames');
|
|
375
340
|
const traceAligned = longTasks.some((task) => task.source === 'trace' && task.timingDomain === 'recorder-performance');
|
|
376
341
|
const traceRelative = longTasks.some((task) => task.source === 'trace' && task.timingDomain === 'trace-relative-first-event');
|
|
377
342
|
const frameTimestampUncertainty: '±frame' | 'unavailable' = frames.length ? '±frame' : 'unavailable';
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
// asserting navigation for a pre-arm entry would be false provenance.
|
|
381
|
-
const observerUnavailableCauses: string[] = [];
|
|
382
|
-
if (observerNavGapUnavailable) observerUnavailableCauses.push('some occurred after a navigation gap with no synchronized recorder-relative baseline');
|
|
383
|
-
if (observerPreArmUnavailable) observerUnavailableCauses.push('some were buffered from before the recorder arm baseline, with a document timestamp that predates it');
|
|
384
|
-
const observerTiming = observerUnavailableCauses.length
|
|
385
|
-
? `Observer entries with unavailable timing (${observerUnavailableCauses.join('; ')}) have no recorder-relative baseline; their timing, dropped-frame overlap, and frame-diff attribution are unavailable.`
|
|
343
|
+
const observerTiming = observerNavGapUnavailable
|
|
344
|
+
? 'Observer entries after a navigation gap with no synchronized recorder-relative baseline have unavailable timing and frame-diff attribution.'
|
|
386
345
|
: 'Observer and screencast timestamps are recorder-relative performance.now() milliseconds.';
|
|
387
346
|
const timingNote = traceRelative
|
|
388
347
|
? `${observerTiming} Frame-derived intervals have ±frame uncertainty. Trace timestamps are relative to the first trace event because no explicit trace/performance baseline marker was retained.`
|
|
@@ -394,33 +353,20 @@ export function analyzeMotionJank(input: { rects: readonly unknown[]; events: re
|
|
|
394
353
|
|
|
395
354
|
return {
|
|
396
355
|
frameCount: frames.length,
|
|
397
|
-
cadenceMs,
|
|
398
356
|
missingFrameSampleCount,
|
|
399
|
-
droppedFrames,
|
|
400
|
-
droppedFrameCount: droppedFrames.reduce((count, item) => count + item.estimatedDroppedFrames, 0),
|
|
401
|
-
droppedFramesIncomplete: incomplete.has('dropped-frames'),
|
|
402
357
|
longTasks,
|
|
403
358
|
longTasksIncomplete: incomplete.has('long-task-records'),
|
|
404
359
|
layoutShifts,
|
|
405
360
|
layoutShiftsIncomplete: incomplete.has('layout-shift-records'),
|
|
361
|
+
preRecordingObserverEntriesExcluded,
|
|
406
362
|
artifactLoss: losses,
|
|
407
363
|
frameTimestampUncertainty,
|
|
408
364
|
timingNote,
|
|
409
365
|
};
|
|
410
366
|
}
|
|
411
367
|
|
|
412
|
-
function longTask(source: 'observer' | 'trace', timingDomain: LongTaskTimingDomain, startMs: number, durationMs: number
|
|
413
|
-
|
|
414
|
-
return {
|
|
415
|
-
source,
|
|
416
|
-
timingDomain,
|
|
417
|
-
startMs,
|
|
418
|
-
durationMs,
|
|
419
|
-
endMs,
|
|
420
|
-
overlapsDroppedFrames: timingDomain === 'recorder-performance'
|
|
421
|
-
? droppedFrames.filter((drop) => startMs < drop.endMs && endMs > drop.startMs).map((drop) => drop.afterFrame)
|
|
422
|
-
: null,
|
|
423
|
-
};
|
|
368
|
+
function longTask(source: 'observer' | 'trace', timingDomain: LongTaskTimingDomain, startMs: number, durationMs: number): LongTaskFact {
|
|
369
|
+
return { source, timingDomain, startMs, durationMs, endMs: startMs + durationMs };
|
|
424
370
|
}
|
|
425
371
|
|
|
426
372
|
export function readMotionJank(ref: RecRef): { analysis: MotionJankAnalysis; meta: Record<string, unknown> } {
|
|
@@ -430,7 +376,7 @@ export function readMotionJank(ref: RecRef): { analysis: MotionJankAnalysis; met
|
|
|
430
376
|
// completed recording, not one awaiting `--stop` (which only applies to an
|
|
431
377
|
// active composed lifecycle recording) — its retained event/trace records
|
|
432
378
|
// remain readable, so long-task and layout-shift facts stay available;
|
|
433
|
-
//
|
|
379
|
+
// while frame-derived timing remains unavailable without rect timestamps.
|
|
434
380
|
// Mirrors the same exception already made in response.ts's loadResponseTimeline.
|
|
435
381
|
const readablePartial = state === 'partial' && meta.reason === 'no_frames';
|
|
436
382
|
if (state !== 'finalized' && state !== 'orphaned-finalized' && !readablePartial) {
|
package/src/cdp/motion/mask.ts
CHANGED
|
@@ -66,8 +66,8 @@ interface Component {
|
|
|
66
66
|
|
|
67
67
|
/** Per-component facts computed in single shared passes over the label map. */
|
|
68
68
|
interface ComponentStats {
|
|
69
|
-
/** Best element attribution by
|
|
70
|
-
attribution: { element: MotionElement; overlap: number } | null;
|
|
69
|
+
/** Best element attribution by changed-pixel coverage relative to its recorded area. */
|
|
70
|
+
attribution: { element: MotionElement; overlap: number; area: number } | null;
|
|
71
71
|
/** Pair-ordered centroids of this component's changed pixels, one per active sample. */
|
|
72
72
|
activeCentroids: Array<{ pair: number; cx: number; cy: number }>;
|
|
73
73
|
}
|
|
@@ -288,7 +288,7 @@ function computeComponentStats(
|
|
|
288
288
|
}
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
-
const candidates: Array<Map<string, { element: MotionElement; overlap: number }>> = components.map(() => new Map());
|
|
291
|
+
const candidates: Array<Map<string, { element: MotionElement; overlap: number; area: number }>> = components.map(() => new Map());
|
|
292
292
|
for (const record of recordsByFile.values()) {
|
|
293
293
|
for (const element of record.elements ?? []) {
|
|
294
294
|
if (![element.x, element.y, element.width, element.height].every((value) => typeof value === 'number' && Number.isFinite(value))) continue;
|
|
@@ -299,22 +299,31 @@ function computeComponentStats(
|
|
|
299
299
|
const x1 = Math.min(width, Math.ceil(transformed.x + transformed.width));
|
|
300
300
|
const y0 = Math.max(0, Math.floor(transformed.y));
|
|
301
301
|
const y1 = Math.min(height, Math.ceil(transformed.y + transformed.height));
|
|
302
|
+
const area = (x1 - x0) * (y1 - y0);
|
|
303
|
+
if (area <= 0) continue;
|
|
304
|
+
const overlaps = new Map<number, number>();
|
|
302
305
|
for (let py = y0; py < y1; py++) {
|
|
303
306
|
const rowBase = py * width;
|
|
304
307
|
for (let px = x0; px < x1; px++) {
|
|
305
308
|
const label = labels[rowBase + px];
|
|
306
|
-
if (label
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
309
|
+
if (label >= 0) overlaps.set(label, (overlaps.get(label) ?? 0) + 1);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
for (const [label, overlap] of overlaps) {
|
|
313
|
+
const map = candidates[label];
|
|
314
|
+
const existing = map.get(key);
|
|
315
|
+
if (existing) {
|
|
316
|
+
existing.overlap += overlap;
|
|
317
|
+
existing.area += area;
|
|
318
|
+
} else {
|
|
319
|
+
map.set(key, { element, overlap, area });
|
|
311
320
|
}
|
|
312
321
|
}
|
|
313
322
|
}
|
|
314
323
|
}
|
|
315
324
|
|
|
316
325
|
return components.map((_, index) => ({
|
|
317
|
-
attribution: [...candidates[index].values()].sort((a, b) => b.overlap - a.overlap)[0] ?? null,
|
|
326
|
+
attribution: [...candidates[index].values()].sort((a, b) => b.overlap / b.area - a.overlap / a.area || b.overlap - a.overlap)[0] ?? null,
|
|
318
327
|
activeCentroids: activeCentroids[index],
|
|
319
328
|
}));
|
|
320
329
|
}
|