fix(vom): add fixtures, remove fixed canvas numbers under budget, improve efficiency

This commit is contained in:
Ljy-0827
2026-09-08 13:13:25 +08:00
parent aeece8d7e4
commit 309d270045
24 changed files with 960 additions and 247 deletions
@@ -1,8 +1,17 @@
import type { Rect } from "@browser-skill/vom";
import type { RenderedSurface, RenderedSurfaceGroup } from "./types";
import type { ClusteredRenderedSurfaces, RenderedSurface, RenderedSurfaceGroup } from "./types";
const STACK_IOU_RATIO = 0.9;
const STACK_SIZE_RATIO = 0.9;
const SIZE_BAND_BASE = 1 / STACK_SIZE_RATIO;
const POSITION_CELL_RATIO = 0.1;
const MAX_CLUSTER_COMPARISONS = 1_000_000;
const MAX_CLUSTER_SURFACES = 50_000;
interface IndexedGroup extends RenderedSurfaceGroup {
creationIndex: number;
indexKey: string;
}
function area(rect: Rect): number {
return Math.max(0, rect.w) * Math.max(0, rect.h);
@@ -14,9 +23,7 @@ function intersectionArea(a: Rect, b: Rect): number {
return width * height;
}
function sameVisualStack(a: RenderedSurface, b: RenderedSurface): boolean {
if (a.frameId !== b.frameId) return false;
if (a.parentBackendNodeId !== b.parentBackendNodeId) return false;
function stackScore(a: RenderedSurface, b: RenderedSurface): number | null {
const aArea = area(a.visibleRect);
const bArea = area(b.visibleRect);
const intersection = intersectionArea(a.visibleRect, b.visibleRect);
@@ -25,41 +32,158 @@ function sameVisualStack(a: RenderedSurface, b: RenderedSurface): boolean {
Math.min(a.visibleRect.w, b.visibleRect.w) / Math.max(a.visibleRect.w, b.visibleRect.w);
const heightRatio =
Math.min(a.visibleRect.h, b.visibleRect.h) / Math.max(a.visibleRect.h, b.visibleRect.h);
return (
union > 0 &&
intersection / union >= STACK_IOU_RATIO &&
widthRatio >= STACK_SIZE_RATIO &&
heightRatio >= STACK_SIZE_RATIO
);
const iou = union > 0 ? intersection / union : 0;
return iou >= STACK_IOU_RATIO && widthRatio >= STACK_SIZE_RATIO && heightRatio >= STACK_SIZE_RATIO
? iou
: null;
}
function toGroup(members: RenderedSurface[]): RenderedSurfaceGroup {
const representative = [...members].sort(
(a, b) => area(b.visibleRect) - area(a.visibleRect) || b.paintOrder - a.paintOrder,
)[0];
function sizeBand(value: number): number {
return Math.floor(Math.log(Math.max(Number.EPSILON, value)) / Math.log(SIZE_BAND_BASE));
}
function bandSize(band: number): number {
return SIZE_BAND_BASE ** band;
}
function spatialKey(rect: Rect): string {
const widthBand = sizeBand(rect.w);
const heightBand = sizeBand(rect.h);
const cellWidth = bandSize(widthBand) * POSITION_CELL_RATIO;
const cellHeight = bandSize(heightBand) * POSITION_CELL_RATIO;
const centerX = rect.x + rect.w / 2;
const centerY = rect.y + rect.h / 2;
return `${widthBand}:${heightBand}:${Math.floor(centerX / cellWidth)}:${Math.floor(centerY / cellHeight)}`;
}
function candidateKeys(rect: Rect): string[] {
const keys: string[] = [];
const centerX = rect.x + rect.w / 2;
const centerY = rect.y + rect.h / 2;
const widthBand = sizeBand(rect.w);
const heightBand = sizeBand(rect.h);
// A >=0.9 size ratio crosses at most one logarithmic band. IoU >=0.9
// likewise keeps centers within one 10%-of-size position cell.
for (let dw = -1; dw <= 1; dw += 1) {
for (let dh = -1; dh <= 1; dh += 1) {
const candidateWidthBand = widthBand + dw;
const candidateHeightBand = heightBand + dh;
const cellWidth = bandSize(candidateWidthBand) * POSITION_CELL_RATIO;
const cellHeight = bandSize(candidateHeightBand) * POSITION_CELL_RATIO;
const cellX = Math.floor(centerX / cellWidth);
const cellY = Math.floor(centerY / cellHeight);
for (let dx = -1; dx <= 1; dx += 1) {
for (let dy = -1; dy <= 1; dy += 1) {
keys.push(`${candidateWidthBand}:${candidateHeightBand}:${cellX + dx}:${cellY + dy}`);
}
}
}
}
return keys;
}
function preferredRepresentative(a: RenderedSurface, b: RenderedSurface): RenderedSurface {
const aArea = area(a.visibleRect);
const bArea = area(b.visibleRect);
return bArea > aArea || (bArea === aArea && b.paintOrder > a.paintOrder) ? b : a;
}
function partitionKey(surface: RenderedSurface): string {
return `${surface.frameId}\u0000${surface.parentBackendNodeId ?? "root"}`;
}
export function clusterRenderedSurfaces(surfaces: RenderedSurface[]): ClusteredRenderedSurfaces {
const partitions = new Map<string, Map<string, Set<IndexedGroup>>>();
const groups: IndexedGroup[] = [];
let comparisons = 0;
let processed = 0;
let truncated = false;
for (const surface of surfaces) {
if (processed >= MAX_CLUSTER_SURFACES || comparisons >= MAX_CLUSTER_COMPARISONS) {
truncated = true;
break;
}
const key = partitionKey(surface);
let index = partitions.get(key);
if (!index) {
index = new Map();
partitions.set(key, index);
}
const candidates = new Set<IndexedGroup>();
for (const candidateKey of candidateKeys(surface.visibleRect)) {
for (const candidate of index.get(candidateKey) ?? []) candidates.add(candidate);
}
let best: { group: IndexedGroup; score: number; areaDelta: number } | null = null;
for (const group of candidates) {
comparisons += 1;
if (comparisons > MAX_CLUSTER_COMPARISONS) {
truncated = true;
break;
}
const score = stackScore(group.representative, surface);
if (score === null) continue;
const areaDelta = Math.abs(
area(group.representative.visibleRect) - area(surface.visibleRect),
);
if (
!best ||
score > best.score ||
(score === best.score && areaDelta < best.areaDelta) ||
(score === best.score &&
areaDelta === best.areaDelta &&
group.creationIndex < best.group.creationIndex)
) {
best = { group, score, areaDelta };
}
}
if (truncated) break;
if (best) {
const group = best.group;
const representative = preferredRepresentative(group.representative, surface);
group.memberCount += 1;
group.label ??= surface.label;
if (representative !== group.representative) {
index.get(group.indexKey)?.delete(group);
group.representative = representative;
group.indexKey = spatialKey(representative.visibleRect);
const indexed = index.get(group.indexKey) ?? new Set<IndexedGroup>();
indexed.add(group);
index.set(group.indexKey, indexed);
}
processed += 1;
continue;
}
const indexKey = spatialKey(surface.visibleRect);
const group: IndexedGroup = {
frameId: surface.frameId,
parentBackendNodeId: surface.parentBackendNodeId,
representative: surface,
...(surface.label ? { label: surface.label } : {}),
memberCount: 1,
creationIndex: groups.length,
indexKey,
};
groups.push(group);
const indexed = index.get(indexKey) ?? new Set<IndexedGroup>();
indexed.add(group);
index.set(indexKey, indexed);
processed += 1;
}
return {
frameId: representative.frameId,
members,
representative,
label: members.map((surface) => surface.label).find((label) => label !== undefined),
groups: groups
.map(({ creationIndex: _creationIndex, indexKey: _indexKey, ...group }) => group)
.sort(
(a, b) =>
a.frameId.localeCompare(b.frameId) ||
a.representative.visibleRect.y - b.representative.visibleRect.y ||
a.representative.visibleRect.x - b.representative.visibleRect.x,
),
truncated,
...(truncated ? { omittedCount: surfaces.length - processed } : {}),
};
}
export function clusterRenderedSurfaces(surfaces: RenderedSurface[]): RenderedSurfaceGroup[] {
const groups: RenderedSurface[][] = [];
for (const surface of surfaces) {
const matching = groups.find((group) =>
group.some((member) => sameVisualStack(member, surface)),
);
if (matching) matching.push(surface);
else groups.push([surface]);
}
return groups
.map(toGroup)
.sort(
(a, b) =>
a.frameId.localeCompare(b.frameId) ||
a.representative.visibleRect.y - b.representative.visibleRect.y ||
a.representative.visibleRect.x - b.representative.visibleRect.x,
);
}