fix(export): tighten native cursor and save cleanup

This commit is contained in:
wiiiii123 committed 2026-05-08 12:41:00 +07:00
1 parent ae40a13e85
commit 490a5eb305
2 files changed
+73 -113

No files matched your search

+6 -1
View File
@@ -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 {
+67 -112
View File
@@ -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<NativeStaticLayoutCursorTelemetrySample, "cx" | "cy" | "timeMs">;
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;