diff --git a/electron/ipc/register/export.ts b/electron/ipc/register/export.ts index a977ee74..f8aa29ee 100644 --- a/electron/ipc/register/export.ts +++ b/electron/ipc/register/export.ts @@ -99,7 +99,12 @@ export async function moveExportedTempFile(tempPath: string, destinationPath: st } if (movedExistingDestination) { - await fs.rm(backupDestinationPath, { force: true }); + await fs.rm(backupDestinationPath, { force: true }).catch((cleanupError) => { + console.warn( + `[export] Failed to remove backup file after replace (${backupDestinationPath}):`, + cleanupError, + ); + }); } } try { diff --git a/src/lib/exporter/nativeStaticLayoutTelemetry.ts b/src/lib/exporter/nativeStaticLayoutTelemetry.ts index 182bb06e..7a8cda11 100644 --- a/src/lib/exporter/nativeStaticLayoutTelemetry.ts +++ b/src/lib/exporter/nativeStaticLayoutTelemetry.ts @@ -41,6 +41,15 @@ function clampUnit(value: number) { return Math.min(1, Math.max(0, value)); } +function isCursorInteractionSample(sample: NativeStaticLayoutCursorTelemetrySample) { + return ( + sample.interactionType === "click" || + sample.interactionType === "double-click" || + sample.interactionType === "right-click" || + sample.interactionType === "middle-click" + ); +} + function sanitizeCursorTelemetry(telemetry: NativeStaticLayoutCursorTelemetrySample[]) { return telemetry .filter((sample) => { @@ -60,110 +69,15 @@ function sanitizeCursorTelemetry(telemetry: NativeStaticLayoutCursorTelemetrySam .sort((left, right) => left.timeMs - right.timeMs); } -function interpolateCursorSample( - samples: NativeStaticLayoutCursorTelemetrySample[], - timeMs: number, -) { - if (timeMs <= samples[0].timeMs) { - return { timeMs, cx: samples[0].cx, cy: samples[0].cy }; - } - - const last = samples[samples.length - 1]; - if (timeMs >= last.timeMs) { - return { timeMs, cx: last.cx, cy: last.cy }; - } - - let lo = 0; - let hi = samples.length - 1; - while (lo < hi - 1) { - const mid = (lo + hi) >> 1; - if (samples[mid].timeMs <= timeMs) { - lo = mid; - } else { - hi = mid; - } - } - - const a = samples[lo]; - const b = samples[hi]; - const span = Math.max(1, b.timeMs - a.timeMs); - const t = (timeMs - a.timeMs) / span; - return { - timeMs, - cx: a.cx + (b.cx - a.cx) * t, - cy: a.cy + (b.cy - a.cy) * t, - }; -} - -function findLatestInteractionSample( - samples: NativeStaticLayoutCursorTelemetrySample[], - timeMs: number, -) { - for (let index = samples.length - 1; index >= 0; index -= 1) { - const sample = samples[index]; - if (sample.timeMs > timeMs) { - continue; - } - - if ( - sample.interactionType === "click" || - sample.interactionType === "double-click" || - sample.interactionType === "right-click" || - sample.interactionType === "middle-click" - ) { - return sample; - } - } - - return null; -} - -function findLatestStableCursorType( - samples: NativeStaticLayoutCursorTelemetrySample[], - timeMs: number, -) { - let lo = 0; - let hi = samples.length - 1; - while (lo < hi) { - const mid = Math.ceil((lo + hi) / 2); - if (samples[mid].timeMs <= timeMs) { - lo = mid; - } else { - hi = mid - 1; - } - } - - for (let index = lo; index >= 0; index -= 1) { - const sample = samples[index]; - if (sample.timeMs > timeMs || !sample.cursorType) { - continue; - } - - if ( - sample.interactionType === "click" || - sample.interactionType === "double-click" || - sample.interactionType === "right-click" || - sample.interactionType === "middle-click" - ) { - continue; - } - - return sample.cursorType; - } - - return "arrow"; -} - function getCursorTypeIndex(cursorType: string | undefined) { return CURSOR_TYPE_INDEX[cursorType ?? "arrow"] ?? CURSOR_TYPE_INDEX.arrow; } function getCursorBounceScale( - samples: NativeStaticLayoutCursorTelemetrySample[], + latestClick: NativeStaticLayoutCursorTelemetrySample | null, timeMs: number, options: NativeStaticLayoutCursorTelemetryOptions, ) { - const latestClick = findLatestInteractionSample(samples, timeMs); if (!latestClick) { return 1; } @@ -182,23 +96,63 @@ function getCursorBounceScale( return Math.max(0.72, 1 - Math.sin(progress * Math.PI) * (0.08 * clickBounce)); } -function buildCursorRenderSample( +function createMonotonicCursorRenderSampler( samples: NativeStaticLayoutCursorTelemetrySample[], - timeMs: number, options: NativeStaticLayoutCursorTelemetryOptions, -): NativeStaticLayoutCursorTelemetrySample { - const position = interpolateCursorSample(samples, timeMs); - const cursorType = findLatestStableCursorType(samples, timeMs); - const projectedPosition = projectCursorPositionToViewport(position, options.sourceCrop); +) { + let interpolationIndex = 0; + let metadataIndex = 0; + let latestClick: NativeStaticLayoutCursorTelemetrySample | null = null; + let latestStableCursorType: CursorTelemetryPoint["cursorType"] = "arrow"; - return { - ...position, - cx: projectedPosition.cx, - cy: projectedPosition.cy, - cursorType, - cursorTypeIndex: getCursorTypeIndex(cursorType), - bounceScale: getCursorBounceScale(samples, timeMs, options), - ...(options.sourceCrop ? { visible: projectedPosition.visible } : {}), + return (timeMs: number): NativeStaticLayoutCursorTelemetrySample => { + while ( + interpolationIndex < samples.length - 2 && + samples[interpolationIndex + 1].timeMs <= timeMs + ) { + interpolationIndex += 1; + } + + while (metadataIndex < samples.length && samples[metadataIndex].timeMs <= timeMs) { + const sample = samples[metadataIndex]; + if (isCursorInteractionSample(sample)) { + latestClick = sample; + } else if (sample.cursorType) { + latestStableCursorType = sample.cursorType; + } + metadataIndex += 1; + } + + let position: Pick; + if (timeMs <= samples[0].timeMs) { + position = { timeMs, cx: samples[0].cx, cy: samples[0].cy }; + } else { + const last = samples[samples.length - 1]; + if (timeMs >= last.timeMs) { + position = { timeMs, cx: last.cx, cy: last.cy }; + } else { + const a = samples[interpolationIndex]; + const b = samples[Math.min(samples.length - 1, interpolationIndex + 1)]; + const span = b.timeMs - a.timeMs; + const t = span > 0 ? (timeMs - a.timeMs) / span : 0; + position = { + timeMs, + cx: a.cx + (b.cx - a.cx) * t, + cy: a.cy + (b.cy - a.cy) * t, + }; + } + } + + const projectedPosition = projectCursorPositionToViewport(position, options.sourceCrop); + return { + ...position, + cx: projectedPosition.cx, + cy: projectedPosition.cy, + cursorType: latestStableCursorType, + cursorTypeIndex: getCursorTypeIndex(latestStableCursorType), + bounceScale: getCursorBounceScale(latestClick, timeMs, options), + ...(options.sourceCrop ? { visible: projectedPosition.visible } : {}), + }; }; } @@ -239,14 +193,15 @@ export function buildNativeStaticLayoutCursorTelemetry( const frameDurationMs = 1000 / frameRate; const targetFrames = Math.max(1, Math.floor(durationMs / frameDurationMs) + 1); const resampled: NativeStaticLayoutCursorTelemetrySample[] = []; + const sampleCursorAt = createMonotonicCursorRenderSampler(sanitized, options); for (let frameIndex = 0; frameIndex < targetFrames; frameIndex += 1) { const timeMs = Math.min(durationMs, frameIndex * frameDurationMs); - pushCursorSample(resampled, buildCursorRenderSample(sanitized, timeMs, options)); + pushCursorSample(resampled, sampleCursorAt(timeMs)); } const last = resampled[resampled.length - 1]; if (!last || Math.abs(last.timeMs - durationMs) > 0.5) { - pushCursorSample(resampled, buildCursorRenderSample(sanitized, durationMs, options)); + pushCursorSample(resampled, sampleCursorAt(durationMs)); } return resampled;