Skip to content

Installers that need nothing else, and a demo worth looking at - #3

Merged
MAXAWER merged 7 commits into
mainfrom
feature/installers-and-demo
Aug 26, 2026
Merged

MAXAWER merged 7 commits into
mainfrom
feature/installers-and-demo

Conversation

@MAXAWER

@MAXAWER MAXAWER commented Aug 26, 2026

Copy link
Copy Markdown
Owner

Two things a first-time visitor decides on: does this do something impressive,
and can I run it without a toolchain. Neither had a good answer.

A demo that proves something

The old sample was a black-on-white cat — already line art, so tracing it showed
nothing about what happens to an ordinary picture. examples/castle.png is a
colour landscape with towers, crenellations, bridge arches, a treeline and
mountains, drawn from scratch by tools/make_sample.py so the repository owns
its demo art with no licence to track.

docs/demo.gif now shows the colour source beside the phone drawing it — 215
strokes, 1,542 touch points, the real path list in the real order. The phone in
the mock-up lies on its side because the scene is landscape; on a portrait screen
a wide picture sits in a thin band and reads as underwhelming rather than
accurate.

Installers that carry everything

Downloading a repository, installing Python, learning what a virtualenv is and
separately fetching Android platform-tools is a lot to ask of someone who wants
to draw a picture on their phone.

  • Windows — AutoDraw-1.1.0-x64.msi, WiX 5. Program Files, Start Menu entry,
    uninstaller, ARP icon.
  • macOS — .dmg with the drag-to-Applications layout, built for Apple
    Silicon and Intel separately.
  • Linux — source, as before.

All of them carry Python, OpenCV, Tk and adb inside. find_adb learned to
look inside the bundle, ahead of PATH, so an installed AutoDraw behaves the
same on every machine; in a source checkout bundled_candidates() returns an
empty list and nothing changes.

One build command, tools/build_app.py, used identically by hand and by CI.

The self-test, and why it exists

The first Windows build looked perfect and died on launch:

No module named 'adbtouch.vectorize'

The vectoriser is imported lazily through __getattr__ so the core library stays
dependency-free, and PyInstaller's static analysis cannot see through that. I had
"verified" the build by checking the process was still alive after eight seconds
— it was, because an error dialog is a process.

So every packaged build now ends by running AutoDraw --selftest: import the
whole application, resolve adb, run it, report. An incomplete bundle fails the
build instead of failing on a desktop.

Verified

On this Windows machine:

  • python tools/build_app.py --msi → self-test passes, adb resolves to the
    bundled copy and reports 1.0.41.
  • The MSI was expanded with msiexec /a (no system change) and the extracted
    payload passes the same self-test from its install location.
  • 109 unit tests pass.

The macOS half has no local equivalent — this PR's checks are the first real run
of it. The build jobs deliberately also fire on pull requests touching
packaging/, because packaging breaks quietly and only on the platform you are
not using.

Two things to know before merging

  • Bundling adb is a deliberate choice. platform-tools is published under the
    Android SDK licence, which restricts redistribution. scrcpy and similar tools
    bundle it anyway; NOTICE.txt ships beside the binary, and
    tools/build_app.py --no-adb builds without it if you would rather not.
  • Nothing is code-signed. SmartScreen will warn once, and macOS needs a
    right-click → Open on first launch. The release notes say so, with the
    xattr command. Signing needs an Authenticode certificate and an Apple
    Developer account.

WiX is pinned to 5.0.2: version 6 and later refuse to build until someone accepts
a separate maintenance-fee licence, which a build script cannot do unattended.

MAXAWER and others added 6 commits August 26, 2026 16:24
The old sample begged the question: a black-on-white cat is already line
art, so tracing it proves nothing about what happens to an ordinary
picture. examples/castle.png is a colour landscape - towers, crenellations,
bridge arches, a treeline, mountains - and the demo now shows it beside the
phone drawing it, so the colour-in / strokes-out step is the first thing a
visitor sees.

The phone in the mock-up lies on its side because the scene is landscape;
on a portrait screen a wide picture sits in a thin band and reads as
underwhelming rather than accurate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Downloading a repository, installing Python, learning what a virtualenv is
and separately fetching Android platform-tools is a lot to ask of someone
who wants to draw a picture on their phone. The packaged builds now carry
Python, OpenCV, Tk and adb inside them: a Windows .msi that installs like
any other program, and a macOS .dmg for both architectures.

find_adb learned to look inside the bundle, ahead of PATH, so an installed
AutoDraw behaves the same on every machine rather than inheriting whichever
adb happens to be lying around. In a source checkout the bundled list is
empty and nothing changes.

The build is one command - tools/build_app.py, used identically by hand and
by CI - and it ends by running the packaged app's own self-test: import the
whole application, resolve adb, run it. That check exists because the first
build looked perfect and died on launch with 'No module named
adbtouch.vectorize': the vectoriser is imported lazily through __getattr__
to keep the core library dependency-free, and PyInstaller cannot see through
that. A build that ships incomplete now fails here instead.

WiX is pinned to 5: version 6 refuses to build until someone accepts a
separate maintenance-fee licence, which a build script cannot do.

Nothing is code-signed. The release notes say which button to press.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
*.spec was there to ignore the spec PyInstaller generates in the repository
root, but it matched packaging/AutoDraw.spec too - so 'git add -A' skipped
it without a word and every packaging job failed on a checkout that did not
contain the file it was told to build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
macos-13 has been retired. A job asking for a retired label does not fail:
it queues indefinitely, and everything downstream waits behind it - the
release job included. macos-15-intel is the current Intel label, and every
packaging job now has a timeout so a runner that never arrives fails loudly
within half an hour instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nvas

Two gaps the packaging left. Installed builds carry their own adb, but a
source checkout still expected you to find platform-tools yourself - so
run.bat and run.sh now fetch it when the machine has none, find_adb looks in
a platform-tools directory beside the working directory, and the whole
lookup order is written down in the README.

The second gap was having nothing to test against. tools/test_canvas.py
serves a drawing canvas over adb reverse and opens it on the device: no app
to install, and it works the same on a phone over USB, a phone over wireless
ADB, and an emulator. The page counts strokes and points as they arrive and
draws a labelled grid, and --pattern draws a rectangle inset by a tenth of
the screen with its diagonals and a centre cross. Held against the grid that
answers the question this project keeps asking of unfamiliar hardware: are
the coordinates landing where they were aimed, or is the digitizer scaled,
swapped or mirrored?

Device.adb() came out of it - the public way to run reverse, forward, push
and install without callers assembling -s <serial> themselves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two things this could not do on the phone it was tested against, a Pixel 8
Pro on Android 17.

It could not draw at all. sendevent fails there with Permission denied on
every line - not a file mode problem, since the shell user is in the input
group and the node is rw for it, but SELinux denying the shell domain the
write. The script still exits cleanly, so the app reported "Drawing 414
strokes..." and did nothing, which is the worst way for this to fail. There
is now a probe, supports_raw_touch, and an automatic fallback through
input motionevent: separate invocations join into one gesture, so a polyline
still arrives as a single stroke. adbtouch info says which path a device will
take, and the app says so too, with an estimate - that path costs about 110ms
per point, measured, and a progress bar creeping along with no explanation is
indistinguishable from one that has hung.

And it drew like a machine. adbtouch/hand.py rounds corners over a capped
radius, varies the pen speed by varying point spacing (every point costs the
same to send, so spacing is speed), adds a slow tremor rather than per-point
static, overshoots stroke ends and starts them a fraction late, and reorders
strokes nearest-first the way someone would rather than in contour order. One
amount dial scales all of it; the app exposes it, and a speed slider, next to
the existing controls.

Verified against the phone: the calibration pattern lands exactly where it was
aimed, 8 strokes and 970 points, with visible wobble.

tools/test_canvas.py is what made that checkable - a drawing canvas served over
adb reverse and opened on the device, no app to install. Its page paints its own
white background, because a browser with dark mode for web pages turns a
transparent canvas black, and black ink on black looks exactly like touches that
never arrived.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@MAXAWER
MAXAWER force-pushed the feature/installers-and-demo branch from 7c43dc4 to ff55202 Compare August 26, 2026 16:08
The input command costs a whole process per point - 110ms measured on a
Pixel 8 Pro - and that is not pacing that can be tuned away. A sixty-point
stroke takes seven seconds, and nothing that takes seven seconds looks
hand-drawn. The ceiling is the transport, so the transport had to go.

injector/ is a 2.5 KB jar, built by tools/build_injector.py from one Java
file and shipped inside the package. It is started once through app_process
under the shell uid, where hidden-API restrictions do not apply, and reads
points from stdin: D/M/U to draw, S to wait. Injection then costs
microseconds and the wait between points is ours to set, which is the whole
point - timing is what a hand is. Device.draw_paths grew a method argument;
auto takes raw events where SELinux allows them, the injector where it does
not, and input if even that fails.

Two things had to be learned the hard way. Events sharing a millisecond get
coalesced, so the injector forces event time forward. And instant is not
zero: whatever receives the touches samples them per frame, so a stroke
delivered in two milliseconds arrives as a press and a release with nothing
between - the first attempt drew 475 of 1542 points and looked like
confetti. Six milliseconds is a frame at 120Hz and is as fast as anything
can actually see.

adbtouch/paths.py joins what findContours fragments back into strokes and
drops specks. That is the one change that helps speed and quality together:
215 strokes became 136 on the sample image, each saved fragment being a pen
lift that costs time and shows.

Measured end to end against the phone, drawing the castle on a canvas served
by tools/test_canvas.py: 136 strokes, all 1,679 points registered, about 19
seconds. The same picture through input would have been 3 minutes 40.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@MAXAWER
MAXAWER merged commit 80fbab6 into main Aug 26, 2026
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.

1 participant