Skip to content

Windows port and shape snapping - #21

Open
CatalystMonish wants to merge 5 commits into
nilbuild:mainfrom
CatalystMonish:windows-port-and-shape-snapping
Open

CatalystMonish wants to merge 5 commits into
nilbuild:mainfrom
CatalystMonish:windows-port-and-shape-snapping

Conversation

@CatalystMonish

Copy link
Copy Markdown

Summary

This brings TinyHand to Windows 10 and 11 (x64), and adds shape snapping to both the macOS app and the Windows app.

  • Windows port (windows/): a native Win32 + GDI+ app, built with mingw-w64 into one static TinyHand.exe. It needs no runtime and no installer.
    • The hand on a stick: all 36 hands and the laser dot, the same arm math and shadow, tap and press animation.
    • Taps (nothing underneath gets clicked), drawing with fade, tap sounds, Alt to click for real.
    • The bottom toolbar with the ink tray and the first-run hint.
    • A tray icon and menu, global shortcuts (Ctrl+Alt+H, Ctrl+Alt+X, Esc), and open at login.
  • Shape snapping (macOS and Windows): draw a rough shape and it is redrawn clean when you let go. A small V drawn at the end of a fresh line turns it into an arrow. It is on by default and can be turned off with the Snap shapes switch in Settings, Drawing, or from the menu.
    • Shapes it recognizes: line, arrow, circle, ellipse, triangle, square, rectangle, diamond, pentagon, hexagon, star and heart.
  • Settings on Windows have the same pages as the macOS Settings: General, Pointers, Appearance, Drawing, Sound and About.
    • The same building blocks: cards and rows, switches, keycap shortcut recorders, menus, ink swatches and sound tiles.
    • The toolbar editor works the same way: drag to reorder, drag out or press x to remove, + to add.
    • It follows the Windows light or dark app theme. The first-run welcome card matches the Mac one.

How it fits together

Sources/ShapeKit: one C99 recognizer that both apps compile, so they snap in exactly the same way.

  • On macOS it is a SwiftPM C target that TinyHand depends on; on Windows the Makefile builds it.
  • It has no state and allocates nothing. Each call takes well under 1 ms.
  • It works in either y direction, so shapes come out upright on both platforms.
  • Curves come back as fine polylines, so each app only ever draws straight segments.

Tests/shapes/shape_test.c generates about 5,500 synthetic hand-drawn strokes. They include jitter, wobble, rounded corners, random rotation and aspect, overshoot or gaps at the close, either drawing direction, and uneven spacing. The test checks:

  • accuracy for each shape;
  • false snaps on strokes that must stay freehand: scribbles, zigzags, cursive, spirals, S curves, figure eights, open arcs, check marks, L and U shapes, a circle traced twice, and tiny marks;
  • two-stroke arrows;
  • orientation.

scripts/test-shapes.sh runs it.

macOS (Ink.swift): when a stroke ends, its points go through th_recognize. If they match a shape, the layer's path is swapped for the clean shape with a 0.12 s crossfade. The last snapped line is kept for 3 seconds so that a following V can turn it into an arrow. Fades now carry a token, so a line that became an arrow restarts its fade instead of being removed early.

Windows (windows/src), compared with the macOS app:

macOS Windows
Menu bar item Tray icon (white or black to match the taskbar)
CGEvent tap on its own thread WH_MOUSE_LL hook on its own thread; the drag points are queued for the UI thread
Transparent NSWindow + Core Animation Per-pixel-alpha layered window, GDI+, dirty-rect UpdateLayeredWindowIndirect
CGDisplayHideCursor System cursors swapped for a blank one; restored on exit, on a crash, and on the next start if needed
Carbon hot keys RegisterHotKey
Option to click for real Alt (or Ctrl, Shift, off); a masking key event stops Alt from opening the focused app's menu bar
Menu bar and Dock pass-through Taskbar, tray overflow, Start and open menus pass through, and the real cursor comes back over them
UserDefaults HKCU\Software\TinyHand
Open at login (SMAppService) HKCU\...\CurrentVersion\Run

The hand landmarks (tip, wrist, rod end, grip) are measured from each picture's alpha channel with the same algorithm as HandAsset.measure. The arm pose math is ported line for line, with easing scaled to the frame rate.

Build: make -C windows cross-compiles on macOS or Linux. make -C windows CROSS= builds natively in MSYS2. make -C windows dist makes a zip containing the exe and the assets folder.

CI

build.yml has three changes:

  • The macOS job also runs the shape tests.
  • A new windows job cross-compiles on Ubuntu with mingw-w64 and uploads the exe and the zip.
  • A new windows-check job runs the shape tests, --render-test and --settings-ui-test on windows-latest.

Testing

Everything below was run on macOS 27 (Apple silicon). The Windows build was run under Wine 11.21, x86_64 through Rosetta.

  • macOS
    • swift build succeeds, and scripts/build-app.sh produces a signed release app.
    • I installed that app and drew by hand: a triangle, hexagon, ellipse, rectangle and arrow all snapped, and a long freehand stroke stayed freehand.
    • scripts/test-shapes.sh passes on seeds 1 to 5 with -Wall -Wextra -Werror.
    • Seed 1 results: 98.7% to 100% per shape; 0.0% false snaps overall (the worst single case is a scribble snapping 0.3% of the time); two-stroke arrows 100% accepted at a line end and 100% rejected elsewhere.
  • Windows build
    • mingw-w64 GCC 16 with -Wall -Wextra: no warnings.
    • The 778 KB exe imports only stock system DLLs.
  • Under Wine (the screenshots are attached):
    • shape_test.exe: all checks pass, the same numbers as on macOS.
    • --render-test: 13 of 13 sample shapes snap to the right kind (the scribble stays freehand), and all 37 pointers load, measure and draw.
    • --toolbar-test, --settings-test (every page, the picker, dark mode) and --welcome-test: rendered and checked by eye against the macOS Settings.
    • --settings-ui-test: 16 of 16 pass. It covers sidebar navigation, switches, swatches, sound tiles, the disabled laser switch, drag to reorder, x to remove, the + picker, drag out to remove, Reset, and the slider reaching both ends.
    • --e2e-test was stopped partway through. It drives real input through the hook. The 4 checks that ran all passed: a click reaches the app while the pointer is off, Ctrl+Alt+H turns it on, the hook installs, and a click becomes a tap the app never sees. The drawing, snapping, Alt-click, Ctrl+Alt+X and Esc checks did not run.

Not tested yet:

  • Real Windows hardware.
  • High-DPI and multi-monitor setups.
  • Hiding the system cursor (Wine stubs SetSystemCursor).
  • The GitHub Actions jobs (they first run on this PR).

Not ported yet

  • Making your own hand from a picture (HandMaker / custom hands).
  • Automatic updates (Sparkle). The Windows build has no updater, and its About page has no update button.

Possible follow-ups:

  • Attach the Windows zip in release.yml.
  • Code-sign the exe.
  • Port custom hands.

Screenshots (from Wine)

Screenshots of the shapes, the pointers, the toolbar, Settings and the welcome card, all rendered under Wine, are attached in a comment below.

🤖 Generated with Claude Code

CatalystMonish and others added 4 commits November 9, 2026 18:14
Plain C99 so both the macOS app (SwiftPM C target) and the Windows port
compile the same code. Recognizes lines, arrows (one stroke, or a line
followed by a V), circles, ellipses, triangles, squares, rectangles,
diamonds, pentagons, hexagons, stars and hearts, and returns clean
polylines. Scribbles and handwriting stay freehand.

Tests/shapes/shape_test.c draws thousands of synthetic hand-drawn strokes
and checks accuracy per shape, false positives, two-stroke arrows and
orientation. scripts/test-shapes.sh runs it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a stroke ends it goes through ShapeKit and, if it reads as a shape,
is redrawn as that shape with a short crossfade. A V drawn at the end of
a freshly snapped line turns it into an arrow. On by default, with a
Snap shapes switch in Settings, Drawing and in the menu bar menu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A native Win32 + GDI+ app in windows/, built with mingw-w64 into one
static TinyHand.exe. It brings the hand on a stick, all 36 hands and the
laser dot, taps, drawing with fades, shape snapping (same ShapeKit
recognizer), tap sounds, the toolbar with ink tray and first-run hint, a
tray icon and menu, global shortcuts, Alt to click for real, and a
Settings window with the same pages as the macOS one.

Clicks become taps through a WH_MOUSE_LL hook on its own thread; the
taskbar, Start and open menus pass through. The overlay is a per-pixel
alpha layered window updated by dirty rectangles.

Self-test switches (--render-test, --toolbar-test, --settings-test,
--settings-ui-test, --welcome-test, --e2e-test) run on Windows and
under Wine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Windows job cross-compiles on Ubuntu with mingw-w64, then a
windows-latest job runs the shape tests, the render test and the
Settings UI test. The macOS job also runs the shape tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@CatalystMonish

Copy link
Copy Markdown
Author

Screenshots from the Windows build, rendered by its own self-test switches under Wine 11.21 on macOS.

Shape snapping and all 37 pointers (--render-test): grey is the rough input, purple is what it snapped to.

Shapes and pointers

Toolbar with the first-run hint and ink tray (--toolbar-test)

Toolbar

Settings (--settings-test), the same pages as the macOS Settings:

General Pointers with picker
Appearance Drawing
Sound About
Drawing, dark Welcome

Conflicts resolved keeping both sides:
- Ink.swift: a stroke drawn holding Command (the new arrow feature) is not
  snapped unless it is straight, in which case it becomes a clean straight
  arrow with the same head geometry. Command strokes never take part in the
  two-stroke V arrow.
- SettingsView.swift: the ink button row moved to Pointers, Toolbar upstream;
  Drawing keeps the Snap shapes row.
- Preferences.swift: reset clears both the new keys and snapShapes.
- README.md: both the Command arrow row and the snapping rows; both the
  Automation and Windows sections.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants