Files
SubMiner/src/renderer/overlay-content-measurement.ts

118 lines
3.2 KiB
TypeScript

import type { OverlayContentMeasurement, OverlayContentRect } from '../types';
import type { RendererContext } from './context';
const MEASUREMENT_DEBOUNCE_MS = 80;
function isMeasurableOverlayLayer(layer: string): layer is 'visible' | 'invisible' {
return layer === 'visible' || layer === 'invisible';
}
function round2(value: number): number {
return Math.round(value * 100) / 100;
}
function toMeasuredRect(rect: DOMRect): OverlayContentRect | null {
if (!Number.isFinite(rect.left) || !Number.isFinite(rect.top)) {
return null;
}
if (!Number.isFinite(rect.width) || !Number.isFinite(rect.height)) {
return null;
}
const width = Math.max(0, rect.width);
const height = Math.max(0, rect.height);
return {
x: round2(rect.left),
y: round2(rect.top),
width: round2(width),
height: round2(height),
};
}
function unionRects(a: OverlayContentRect, b: OverlayContentRect): OverlayContentRect {
const left = Math.min(a.x, b.x);
const top = Math.min(a.y, b.y);
const right = Math.max(a.x + a.width, b.x + b.width);
const bottom = Math.max(a.y + a.height, b.y + b.height);
return {
x: round2(left),
y: round2(top),
width: round2(Math.max(0, right - left)),
height: round2(Math.max(0, bottom - top)),
};
}
function hasVisibleTextContent(element: HTMLElement): boolean {
return Boolean(element.textContent && element.textContent.trim().length > 0);
}
function collectContentRect(ctx: RendererContext): OverlayContentRect | null {
let combinedRect: OverlayContentRect | null = null;
const subtitleHasContent = hasVisibleTextContent(ctx.dom.subtitleRoot);
if (subtitleHasContent) {
const subtitleRect = toMeasuredRect(ctx.dom.subtitleRoot.getBoundingClientRect());
if (subtitleRect) {
combinedRect = subtitleRect;
}
}
const secondaryHasContent = hasVisibleTextContent(ctx.dom.secondarySubRoot);
if (secondaryHasContent) {
const secondaryRect = toMeasuredRect(ctx.dom.secondarySubContainer.getBoundingClientRect());
if (secondaryRect) {
combinedRect = combinedRect ? unionRects(combinedRect, secondaryRect) : secondaryRect;
}
}
if (!combinedRect) {
return null;
}
return {
x: combinedRect.x,
y: combinedRect.y,
width: round2(Math.max(0, combinedRect.width)),
height: round2(Math.max(0, combinedRect.height)),
};
}
export function createOverlayContentMeasurementReporter(ctx: RendererContext) {
let debounceTimer: number | null = null;
function emitNow(): void {
if (!isMeasurableOverlayLayer(ctx.platform.overlayLayer)) {
return;
}
const measurement: OverlayContentMeasurement = {
layer: ctx.platform.overlayLayer,
measuredAtMs: Date.now(),
viewport: {
width: window.innerWidth,
height: window.innerHeight,
},
// Explicit null rect signals "no content yet", and main should use fallback bounds.
contentRect: collectContentRect(ctx),
};
window.electronAPI.reportOverlayContentBounds(measurement);
}
function schedule(): void {
if (debounceTimer !== null) {
window.clearTimeout(debounceTimer);
}
debounceTimer = window.setTimeout(() => {
debounceTimer = null;
emitNow();
}, MEASUREMENT_DEBOUNCE_MS);
}
return {
emitNow,
schedule,
};
}