Repository navigation
The rest of the work: shapes, layers, an eraser, and two bugs the emulator found - #13
Merged
Merged
Conversation
Seven shapes and any text, from the command line, with nothing to prepare:
mthread shape heart --scale 0.6
mthread shape star --points 7 --rotate 20
mthread text "hello" --font arial.ttf --y 0.35
The shapes are parametric curves evaluated straight into points, so they are
smooth at any size and cost nothing to produce. The wave needed one correction
worth recording: its x span was fixed at one while its height was two, and _fit
normalises by whichever side is longer, so three cycles came out as a narrow
zigzag rather than a wave.
Text takes a different route on purpose. Rather than carry a stroke font
around, it renders the words with Pillow and hands the result to the ordinary
tracer: every font on the machine becomes available and the letters look like
that font. The cost, and it is unavoidable, is that letters arrive as outlines -
a filled glyph is a shape with an inside and an outside, and this draws with one
finger.
**Placement moves into the library**, because placing a drawing is part of
drawing it rather than part of any one interface. mthread_draw.geometry
re-exports it so existing callers are unaffected, and the command line now
places shapes with exactly the code the window places a photograph with.
**Flipping is added** while the file was open: --flip-x and --flip-y, and
Placement.mirrored, which flips rather than sets, so flipping twice is not
flipping.
Sixteen tests, and two of them are the interesting ones: that every shape fits
the unit square without being stretched, and that a drawing turned to any angle
still lands inside the margin - the rotated bounding box being what has to fit,
not the upright one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
**Layers.** Several pictures, arranged against each other before anything is drawn, each with its own placement and its own tracer settings. Hide one, reorder them, remove one; what gets drawn is every visible layer in order. Each keeps the paths it traced to, so changing one does not re-trace the others. **An eraser.** Drag across the live view with it on and the strokes under the cursor come out. Which strokes count as near enough is decided after placement, because the cursor is over a picture of the phone and not over the tracer's coordinate space; the reach is a fraction of the screen so it means the same thing whatever size the window has been dragged to. Erasures are recorded as indices and forgotten when a layer is re-traced, because stroke five of one tracing is not stroke five of the next - keeping them would silently take out whatever happened to land in that position. Erasing is one request at a time. A drag reports faster than an overlay can be re-rendered, and queueing every report would leave the eraser running behind the mouse. **Flipping**, and buttons for the transforms that are not gestures: Flip, Fit and Undo erase under the phone. Placement.mirrored flips rather than sets, so flipping twice is not flipping. **A screenshot taken by hand can stand in for ADB capture**, which fails on some devices and in some emulators. A still picture does not update, but its proportions are the ones placement needs. Placement now works with no phone attached at all, which falls out of the same change and is useful on its own: a drawing can be arranged before the cable is anywhere near. The window answers every layer operation by redrawing from the whole state the engine returns rather than from a delta. It is a few hundred bytes and it removes the class of bug where the list and the engine disagree about which layer is selected. Fourteen engine tests, driven the way a front end drives it and with no device attached: layers keep their own placements, a hidden layer is not drawn, erasing takes out what is under the point and nothing else, undo brings it back, and re-tracing forgets erasures. They use small generated fixtures rather than the photographs in examples/ - tracing a real photograph a dozen times over cost the suite most of a minute and told it nothing extra. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The three steps still described loading one picture and dragging it, which was true when there was nothing else to do to a drawing. There is now a table of what can be done before anything is drawn - move, resize, turn, flip, layers, the eraser, re-tracing without reopening the file, and a screenshot taken by hand when capture will not work - and the command line section covers shapes and text. The claim that recordings do not travel between phones is removed, that having been the point of the format change: they hold fractions of a screen now, and a file from an older version says so rather than misfiring. Both halves, English and Russian. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Neither of these had a chance of being noticed without a second device to try. **getevent's output never arrived.** `adb shell` allocates no pty when its own stdin is not a terminal, and without one getevent's stdout on the device is a pipe, so its libc buffering holds output back in four-kilobyte blocks. A short recording ends up entirely inside that buffer and appears only when the process exits - by which time recording has been stopped and the file looks empty. A minute of touching worked; five seconds of it did not. One `-t` is not enough to fix it: adb refuses the pty and says so on stderr rather than failing. Two forces it, the way `ssh -tt` does. The mirror needs the exact opposite, `-T`, because a pty would corrupt the JPEG it carries; both reasons are now written down where the flags are. **The recorder listened to the wrong device.** It defaulted to the node Android reports as the touchscreen, which on this emulator is `event12` while touches actually arrive on `event2` - there are a dozen identical virtio multi-touch devices and Android nominates one that stays silent. The result was a recording with nothing in it and no error to explain why. It now listens to all of them and lets the gesture decoder ignore what is not a touch, which costs a few bytes and removes the whole failure. Verified end to end against the emulator, with events injected through the emulator console so they arrive at kernel level the way a finger would: a drag from (300, 700) to (700, 1420) records, decodes to one stroke of nine points, and normalises to fractions that map back to (300, 700) on the screen it was made on and to (373, 918) on a 1344x2992 one. That is the portability claim, tested rather than asserted. Also verified there: touches land where they are aimed on a device whose digitizer range is 0..32767 while its display is 1080x2280 - the rescaling path that the Pixel, whose ranges match its display exactly, never exercised. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two corrections to tools/test_canvas.py, both found by running it against an emulator rather than reasoning about it. The margin was a twelfth of the height on every side. Above the canvas sits a browser address bar and the page's own header, which on a 1080x2280 emulator came to about 280 pixels - more than that twelfth - so the rectangle's top edge was drawn on the toolbar and did not appear. Below it there is only a gesture bar. The margins are asymmetric now, twenty per cent above and ten below, and --top-margin adjusts the one that varies between browsers. The centre cross was drawn at the centre of the screen while the rectangle's diagonals meet at the centre of the rectangle. With symmetric margins those are the same point and nobody notices; with asymmetric ones the cross sits above the crossing, which looks exactly like a calibration error and is not. It is drawn at the rectangle's centre now. Both matter because this tool exists to answer "are the coordinates arriving where they were aimed", and it was answering "no" for its own reasons. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`mthread record` still wrote the old raw-event format and `mthread play` still replayed it through /dev/input - the format that holds digitizer coordinates and that every recent phone refuses. Meanwhile the README had started saying recordings travel between phones. Both statements were true of different code. Recording now writes the portable format and says so; replaying scales it to whatever screen it is given and reports which phone it came from. A version 1 file is refused with the reason and an escape hatch: --raw replays it the old way if this is the phone that made it and that phone allows raw input. Verified on the emulator: a recorded drag writes a version 2 file naming the device, replays at 2x scaled to 1080x2280, and an older file produces the explanation rather than a traceback. **The description, checked line by line.** Three claims had been overtaken by the code: Rotation was listed as not handled. Drawing follows the orientation now - read from the captured frame, since wm size does not change when a phone is turned - so the limit is narrower than it was and belongs to recordings, which hold fractions of the screen they were made on. Said that way instead. "Some devices expose no touchscreen usable by sendevent, drawing then fails" has not been true since the injector arrived, and "an automatic fallback is an open task" was that same fallback. Both gone, replaced by the limit that is real and was missing: replay carries a fixed second or two of injector startup, so the strokes are faithful and the total duration is not. Two open ends were closed and still listed - the input fallback and skeletonising edges instead of halving contours. Replaced with two that are real: rotation in recordings, and shedding OpenCV, five functions of which account for half the download. Numbers spot-checked against the code rather than trusted: 57 strokes and 478 points for the guitar at the demo settings, 294 for the page's, both as printed. **Deleted.** examples/castle.png and tools/make_sample.py, a synthetic drawing and the script that made it. They existed because the demo once needed art the repository owned outright; the demo has used four CC0 photographs for some time and nothing referenced the castle but its own credit line. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Six commits that never reached
main. PR #11 was merged while its branch headwas
9d6486a, and everything pushed after that stayed behind — about 1,600lines. The missing thirty tests are what gave it away: the suite runs 207 on the
branch and 177 on
main.What is in here
Shapes and text from the command line, with nothing to prepare —
mthread shape heart,mthread shape star --points 7 --rotate 20,mthread text "hello" --font arial.ttf. Placement moved into the library at thesame time, so the command line places a shape with the code the window places a
photograph with, and flipping arrived with it.
Layers, an eraser, flip buttons, and a screenshot when capture will not.
Several pictures arranged against each other, each with its own placement and
tracer settings; drag across strokes to take them out; a still picture standing
in for the live view when ADB capture fails — which also made placement work
with no phone attached at all.
Two bugs the emulator found, neither of which could be noticed on one device.
geteventoutput never arrived becauseadb shellallocates no pty when its ownstdin is not a terminal, so a short recording sat entirely inside a four-kilobyte
libc buffer and appeared empty. One
-tis not enough — adb declines and says soon stderr; two forces it, as
ssh -ttdoes. And the recorder listened to thenode Android calls the touchscreen, which on this emulator is
event12whiletouches arrive on
event2; it listens to all of them now and lets the decoderignore what is not a touch.
The command line now keeps the promise the README makes.
recordwrote theold raw format and
playreplayed it through/dev/input; the README hadalready started saying recordings travel. Recording writes the portable format;
replay scales it to the screen it is given; a version 1 file is refused with the
reason and a
--rawescape hatch.The description, checked line by line. Three statements had been overtaken by
the code — rotation "not handled", drawing failing where
sendeventis refused,and two open ends that were already closed — and one real limit was missing:
replay carries a fixed second or two of injector startup, so the strokes are
faithful and the total duration is not.
Deleted:
examples/castle.pngandtools/make_sample.py. A syntheticdrawing and the script that made it, from when the demo needed art the repository
owned; the demo has used four CC0 photographs for a while and nothing referenced
the castle but its own credit line.
Also
tools/make_avd.py, which creates an emulator without the SDK's packagemanager — the current Android CLI crashes with a stack buffer overrun on this
machine and the deprecated
sdkmanagerforwards to it. An AVD is two ini files.Verified on the emulator, not asserted
went, 124 of 165 drawn.
wm sizestill says 1080x2280, the frame says 520x246, the drawingcomes out upright rather than sideways.
where it was made, on a device whose digitizer range is 0..32767 against a
1080x2280 display.
earlier commit message claimed, because the look-ahead budget is counted in
nominal pacing milliseconds and a slow device stretches them.
207 tests pass.
🤖 Generated with Claude Code