openpencil/tests/helpers/tauri/windows-input.ts
Danila Poyarkov e532f616ba
refactor!: move shared primitives below dom-css and core (#771)
* refactor!: move shared primitives below dom-css and core

dom-css depended on core for color conversion, base64 helpers, text
direction, and web-font assets, so core could not use dom-css and every
caller special-cased HTML and Tailwind output.

Color conversion and management, base64 helpers, and text/layout
direction now live in scene-graph under `color`, `bytes`, and
`text-direction`. dom-css takes web-font resolution as an injected
`fonts` option and owns the font face types, so it depends only on
scene-graph and core can depend on it.

BREAKING CHANGE: `@open-pencil/core/color` and `@open-pencil/core/bytes`
are removed, and the direction helpers are no longer exported from
`@open-pencil/core/text`; import them from `@open-pencil/scene-graph`
subpaths. `exportHTMLBundle` takes a font resolver in `fonts` instead of
`'assets'`.

* fix(tools): import color parsing from scene-graph in visual bisect

* fix(mcp): declare the scene-graph dependency

MCP imports `@open-pencil/scene-graph/bytes` since base64 helpers moved
there, but only reached scene-graph through core, so isolated installs
and package checks depended on transitive resolution.

* refactor!: use js-base64 directly instead of a base64 wrapper

Base64 helpers had moved into scene-graph only to sit below dom-css,
but they are a thin wrapper over js-base64 and unrelated to the graph;
fig already called js-base64 directly.

Callers use js-base64 and check `isValid` where input comes from outside
(clipboard, imported HTML, tool arguments, the plugin API). A new
`open-pencil/no-hand-rolled-base64` lint rule rejects atob, btoa, and
Buffer Base64 conversions, and AGENTS.md records the convention.

BREAKING CHANGE: `@open-pencil/core/bytes` is removed; use `js-base64`.

* fix(dom-css): keep images with invalid Base64 inline in HTML export

`exportHTMLBundle` accepts documents parsed from outside HTML, and
js-base64 drops characters it cannot decode, so extracting an invalid
image data URL wrote different bytes. Such images now stay inline.
2026-09-26 11:39:11 +04:00

107 lines
3.9 KiB
TypeScript

import { execFile } from 'node:child_process'
import { promisify } from 'node:util'
import { fromUint8Array } from 'js-base64'
const execFileAsync = promisify(execFile)
interface ClientPoint {
x: number
y: number
}
type ElementClientGeometry = Pick<DOMRect, 'height' | 'width' | 'x' | 'y'>
const USER32_DECLARATION = String.raw`
using System;
using System.Runtime.InteropServices;
public static class NativeInput {
[StructLayout(LayoutKind.Sequential)]
public struct Point {
public int X;
public int Y;
}
[DllImport("user32.dll")]
public static extern bool ClientToScreen(IntPtr window, ref Point point);
[DllImport("user32.dll")]
public static extern uint GetDpiForWindow(IntPtr window);
[DllImport("user32.dll")]
public static extern bool SetProcessDpiAwarenessContext(IntPtr value);
[DllImport("user32.dll")]
public static extern bool SetCursorPos(int x, int y);
[DllImport("user32.dll")]
public static extern bool SetForegroundWindow(IntPtr window);
[DllImport("user32.dll")]
public static extern bool ShowWindow(IntPtr window, int command);
[DllImport("user32.dll")]
public static extern void mouse_event(uint flags, uint dx, uint dy, uint data, UIntPtr extraInfo);
}`
const FOREGROUND_SCRIPT = String.raw`
$process = Get-Process OpenPencil -ErrorAction Stop |
Where-Object { $_.MainWindowTitle -eq 'OpenPencil Native Test' } |
Select-Object -First 1
if (-not $process) { throw 'OpenPencil Native Test window not found' }
[NativeInput]::SetProcessDpiAwarenessContext([IntPtr](-4)) | Out-Null
[NativeInput]::ShowWindow($process.MainWindowHandle, 9) | Out-Null
[NativeInput]::SetForegroundWindow($process.MainWindowHandle) | Out-Null
Start-Sleep -Milliseconds 150
$clientOrigin = New-Object NativeInput+Point
if (-not [NativeInput]::ClientToScreen($process.MainWindowHandle, [ref]$clientOrigin)) {
throw 'ClientToScreen failed'
}
$dpi = [NativeInput]::GetDpiForWindow($process.MainWindowHandle)
if ($dpi -eq 0) { throw 'GetDpiForWindow failed' }
$scale = $dpi / 96.0
`
async function runPowerShell(script: string): Promise<void> {
if (process.platform !== 'win32') throw new Error('Win32 input is only available on Windows')
// PowerShell -EncodedCommand takes UTF-16LE text as Base64.
const encoded = fromUint8Array(Buffer.from(script, 'utf16le'))
await execFileAsync('powershell.exe', ['-NoProfile', '-EncodedCommand', encoded])
}
export async function readElementClientGeometry(selector: string): Promise<ElementClientGeometry> {
return browser.execute((targetSelector) => {
const element = document.querySelector(targetSelector)
if (!(element instanceof HTMLElement)) throw new Error(`Element not found: ${targetSelector}`)
const rect = element.getBoundingClientRect()
return {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height
}
}, selector)
}
export async function nativeDrag(from: ClientPoint, to: ClientPoint): Promise<void> {
const steps = Array.from({ length: 20 }, (_, index) => {
const progress = (index + 1) / 20
return {
x: Math.round(from.x + (to.x - from.x) * progress),
y: Math.round(from.y + (to.y - from.y) * progress)
}
})
const moves = steps
.map(
(point) =>
`$x = [int][Math]::Round($clientOrigin.X + (${point.x} * $scale))\n$y = [int][Math]::Round($clientOrigin.Y + (${point.y} * $scale))\n[NativeInput]::SetCursorPos($x, $y) | Out-Null\nStart-Sleep -Milliseconds 35`
)
.join('\n')
await runPowerShell(`
Add-Type -TypeDefinition '${USER32_DECLARATION}'
${FOREGROUND_SCRIPT}
$x = [int][Math]::Round($clientOrigin.X + (${from.x} * $scale))
$y = [int][Math]::Round($clientOrigin.Y + (${from.y} * $scale))
[NativeInput]::SetCursorPos($x, $y) | Out-Null
Start-Sleep -Milliseconds 100
[NativeInput]::mouse_event(0x0002, 0, 0, 0, [UIntPtr]::Zero)
Start-Sleep -Milliseconds 250
${moves}
Start-Sleep -Milliseconds 250
[NativeInput]::mouse_event(0x0004, 0, 0, 0, [UIntPtr]::Zero)
`)
}