fix: 框选轮询脚本免 System.Drawing 引用, 采样队列化并取消阻塞等待
This commit is contained in:
parent
82765a7f19
commit
0960be88e5
@ -722,40 +722,41 @@ interface MouseSample {
|
||||
}
|
||||
|
||||
interface MouseButtonPoller {
|
||||
/** 至少读到一次采样后为 true;超时/启动失败为 false,调用方回退到渲染层事件 */
|
||||
ready: Promise<boolean>;
|
||||
/** 最近一次采样;轮询进程退出后返回 null,调用方回退渲染层事件 */
|
||||
sample: () => MouseSample | null;
|
||||
/** live=已产出采样; dead=进程退出(脚本编译失败/被杀); starting=启动中 */
|
||||
status: () => "live" | "dead" | "starting";
|
||||
/** 取走自上次调用以来的全部采样流(40ms 一条), 按下→松开的转移不会被主循环轮询间隔漏掉 */
|
||||
drain: () => MouseSample[];
|
||||
stop: () => void;
|
||||
}
|
||||
|
||||
// 覆盖层窗口可能被其他窗口盖住/失去焦点, 渲染层 mouseup/keydown 都可能收不到,
|
||||
// 导致"画得出选区却永远完不成"。改用主进程轮询物理左键状态+光标位置
|
||||
// (GetAsyncKeyState/GetCursorPos), 与任何窗口的焦点、层级、事件路由完全解耦。
|
||||
// 按键状态与坐标来自同一次采样, 按下点不会因轮询间隔/PowerShell 冷启动延迟而漂移。
|
||||
// 采样进队列: 即使主循环在忙别的(或 PowerShell 冷启动后才就绪),
|
||||
// 拖拽全程的按下点/松开点也都精确保留在采样流里。
|
||||
// 注意: GetCursorPos 参数用 int[] 而非 System.Drawing.Point——
|
||||
// Add-Type -MemberDefinition 编译期默认不引用 System.Drawing.dll, 用 Point 会导致编译失败、脚本直接退出
|
||||
function spawnMouseButtonPoller(): MouseButtonPoller {
|
||||
const script = [
|
||||
"Add-Type -MemberDefinition '[DllImport(\"user32.dll\")] public static extern bool SetProcessDpiAwarenessContext(IntPtr value); [DllImport(\"user32.dll\")] public static extern short GetAsyncKeyState(int vKey); [DllImport(\"user32.dll\")] public static extern bool GetCursorPos(int[] pt);' -Name K -Namespace WMouseButton | Out-Null",
|
||||
// 必须 DPI 感知, 否则 GetCursorPos 返回的是被系统虚拟化(96dpi)后的坐标
|
||||
"Add-Type -MemberDefinition '[DllImport(\"user32.dll\")] public static extern bool SetProcessDpiAwarenessContext(IntPtr value); [DllImport(\"user32.dll\")] public static extern short GetAsyncKeyState(int vKey); [DllImport(\"user32.dll\")] public static extern bool GetCursorPos(out System.Drawing.Point p);' -Name K -Namespace WMouseButton | Out-Null",
|
||||
"Add-Type -AssemblyName System.Drawing | Out-Null",
|
||||
"try { [void][WMouseButton.K]::SetProcessDpiAwarenessContext([IntPtr](-4)) } catch {}",
|
||||
"while ($true) {",
|
||||
" $down = ([WMouseButton.K]::GetAsyncKeyState(1) -band 0x8000) -ne 0",
|
||||
" $p = New-Object System.Drawing.Point",
|
||||
" [void][WMouseButton.K]::GetCursorPos([ref]$p)",
|
||||
" [Console]::Out.Write([string]([int]$down) + ' ' + $p.X + ' ' + $p.Y + [char]10)",
|
||||
" [int[]]$p = @(0,0)",
|
||||
" [void][WMouseButton.K]::GetCursorPos($p)",
|
||||
" [Console]::Out.Write([string]([int]$down) + ' ' + $p[0] + ' ' + $p[1] + [char]10)",
|
||||
" Start-Sleep -Milliseconds 40",
|
||||
"}"
|
||||
].join("\n");
|
||||
const child = spawn(
|
||||
"powershell.exe",
|
||||
["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script],
|
||||
{ stdio: ["ignore", "pipe", "ignore"], windowsHide: true }
|
||||
{ stdio: ["ignore", "pipe", "pipe"], windowsHide: true }
|
||||
);
|
||||
let sample: MouseSample | null = null;
|
||||
const queue: MouseSample[] = [];
|
||||
let live = false;
|
||||
let dead = false;
|
||||
let readyResolve: (value: boolean) => void = () => {};
|
||||
const ready = new Promise<boolean>(resolve => { readyResolve = resolve; });
|
||||
let buffer = "";
|
||||
child.stdout?.on("data", (chunk: Buffer) => {
|
||||
buffer += chunk.toString();
|
||||
@ -768,17 +769,30 @@ function spawnMouseButtonPoller(): MouseButtonPoller {
|
||||
const x = Number(parts[1]);
|
||||
const y = Number(parts[2]);
|
||||
if (Number.isFinite(x) && Number.isFinite(y)) {
|
||||
sample = { down: parts[0] === "1", x, y };
|
||||
readyResolve(true);
|
||||
queue.push({ down: parts[0] === "1", x, y });
|
||||
if (!live) {
|
||||
live = true;
|
||||
console.log("[region-pick] poller live");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
child.on("error", () => { dead = true; readyResolve(false); });
|
||||
child.on("exit", () => { dead = true; });
|
||||
child.on("error", error => {
|
||||
dead = true;
|
||||
console.warn("[region-pick] poller spawn failed", error);
|
||||
});
|
||||
child.on("exit", code => {
|
||||
dead = true;
|
||||
console.warn("[region-pick] poller exited, code=" + code);
|
||||
});
|
||||
child.stderr?.on("data", (chunk: Buffer) => {
|
||||
const text = chunk.toString().trim();
|
||||
if (text) console.warn("[region-pick] poller stderr:", text.slice(0, 500));
|
||||
});
|
||||
return {
|
||||
ready,
|
||||
sample: () => (dead ? null : sample),
|
||||
status: () => (dead ? "dead" : live ? "live" : "starting"),
|
||||
drain: () => queue.splice(0, queue.length),
|
||||
stop: () => {
|
||||
try { child.kill(); } catch {}
|
||||
}
|
||||
@ -817,12 +831,8 @@ export async function pickScreenRegion(): Promise<{ x: number; y: number; width:
|
||||
if (!win.isDestroyed() && display.id === cursorDisplay.id) win.focus();
|
||||
}
|
||||
|
||||
const pollerReady = await Promise.race([
|
||||
poller.ready,
|
||||
new Promise<boolean>(resolve => setTimeout(() => resolve(false), 2500))
|
||||
]);
|
||||
if (!pollerReady) console.warn("[region-pick] 按键轮询未就绪, 回退渲染层事件");
|
||||
|
||||
// 不再阻塞等待轮询就绪: 采样进队列, 冷启动期间发生的拖拽也不会丢;
|
||||
// 轮询死亡(starting/dead)时按 tick 回退渲染层事件 + getCursorScreenPoint
|
||||
const deadline = Date.now() + 120000;
|
||||
let cancelled = false;
|
||||
// Esc 全局兜底: 覆盖层失去键盘焦点时也能取消
|
||||
@ -836,14 +846,15 @@ export async function pickScreenRegion(): Promise<{ x: number; y: number; width:
|
||||
}
|
||||
|
||||
// 选区坐标不取自窗口 clientX/clientY(混合 DPI 下窗口坐标映射不可靠);
|
||||
// 优先用轮询采样(物理坐标经 screenToDipPoint 换算), 轮询不可用时回退渲染层事件 + getCursorScreenPoint
|
||||
// 轮询 live: 按采样流识别按下→松开(40ms 粒度, 精确到帧);
|
||||
// 轮询 starting/dead: 回退渲染层事件 + getCursorScreenPoint
|
||||
let downPoint: { x: number; y: number } | null = null;
|
||||
let sawDown = false;
|
||||
let picked: { rect: { x: number; y: number; width: number; height: number }; display: (typeof displays)[number] } | null = null;
|
||||
try {
|
||||
try {
|
||||
while (Date.now() < deadline) {
|
||||
await new Promise(r => setTimeout(r, 80));
|
||||
await new Promise(r => setTimeout(r, 60));
|
||||
if (cancelled) break;
|
||||
// 聚合所有覆盖层状态: 任一屏按 Esc 取消; 按下/松开仅作轮询失败时的回退
|
||||
let rendererDown = false;
|
||||
@ -859,17 +870,39 @@ export async function pickScreenRegion(): Promise<{ x: number; y: number; width:
|
||||
} catch {}
|
||||
}
|
||||
if (cancelled) break;
|
||||
|
||||
if (poller.status() === "live") {
|
||||
// 主链路: 处理采样队列里的按下→松开转移
|
||||
const samples = poller.drain();
|
||||
for (const s of samples) {
|
||||
const dip = screen.screenToDipPoint({ x: s.x, y: s.y });
|
||||
if (!downPoint && s.down) {
|
||||
downPoint = { x: dip.x, y: dip.y };
|
||||
sawDown = true;
|
||||
console.log("[region-pick] down", downPoint, "(polled)");
|
||||
} else if (downPoint && !s.down) {
|
||||
const display = displayForPoint(downPoint.x, downPoint.y);
|
||||
const rx0 = Math.min(downPoint.x, dip.x);
|
||||
const ry0 = Math.min(downPoint.y, dip.y);
|
||||
const rx1 = Math.max(downPoint.x, dip.x);
|
||||
const ry1 = Math.max(downPoint.y, dip.y);
|
||||
picked = { rect: { x: rx0, y: ry0, width: rx1 - rx0, height: ry1 - ry0 }, display };
|
||||
console.log("[region-pick] up", picked.rect, "(polled)");
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (picked) break;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 回退链路: 轮询不可用, 用渲染层事件 + getCursorScreenPoint
|
||||
if (!anyAlive) continue;
|
||||
const mouse = pollerReady ? poller.sample() : null;
|
||||
// 按键与坐标同源采样; 轮询不可用时回退: 渲染层事件 + getCursorScreenPoint
|
||||
const cursor = mouse
|
||||
? screen.screenToDipPoint({ x: mouse.x, y: mouse.y })
|
||||
: screen.getCursorScreenPoint();
|
||||
const down = mouse ? mouse.down : rendererDown;
|
||||
const cursor = screen.getCursorScreenPoint();
|
||||
const down = rendererDown;
|
||||
if (down && !downPoint) {
|
||||
downPoint = { x: cursor.x, y: cursor.y };
|
||||
sawDown = true;
|
||||
console.log("[region-pick] down", downPoint, mouse ? "(polled)" : "(renderer)");
|
||||
console.log("[region-pick] down", downPoint, "(renderer)");
|
||||
}
|
||||
if (!down && sawDown && downPoint) {
|
||||
const display = displayForPoint(downPoint.x, downPoint.y);
|
||||
@ -878,7 +911,7 @@ export async function pickScreenRegion(): Promise<{ x: number; y: number; width:
|
||||
const rx1 = Math.max(downPoint.x, cursor.x);
|
||||
const ry1 = Math.max(downPoint.y, cursor.y);
|
||||
picked = { rect: { x: rx0, y: ry0, width: rx1 - rx0, height: ry1 - ry0 }, display };
|
||||
console.log("[region-pick] up", picked.rect);
|
||||
console.log("[region-pick] up", picked.rect, "(renderer)");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user