May your aim be truer than your excuses.
Cue D’état is here to ostensibly help billiards players aim, determine shot angles, make the right cuts and banks, understand the tangent line, and improve their geometric understanding of the game. Stop wondering which magical potion made of mostly alcohol best improves your game, and instead see pool as a series of physics problems you are consistently failing. Maybe you get called a cheater, even though using this app is entirely legal. At the very least, get yourself a high-tech understanding of how bad you are at pool.
This is an Android application that uses your device's camera and a frankly excessive amount of mathematics to overlay aiming guides onto a pool table. It exists because the universe is governed by knowable laws, and you don't really live in that universe. A problem this app might, reluctantly, help rectify.
(Warning: May induce an inflated sense of skill, followed by the crushing reality of physics. Use with a healthy dose of self-deprecating humor.)
- Live Camera Augmented Reality Overlay:
- See the guides directly on your pool game.
- Designed for easy one or two-handed use.
- Guided AR Table Setup Wizard: Point at the felt and tap once. That is the whole setup; the pocket-tapping ritual has been retired, having never once worked. If ARCore's tracking blips for a moment, the app just floats on the last known table pose instead of throwing your whole scan away — it turns out constantly nuking your progress every time a hand crosses the lens was more annoying than the tracking hiccup itself, so a full rescan is now only required if you back all the way out of AR setup yourself.
- Protractor Mode
- To remind you of the basic, soul-crushing simplicity of a cut shot.
- See where the balls will go before you hit them.
- Rotates and zooms with on-screen gestures, tilts using the gyroscope.
- Banking made easy, displaying a "diamond count," so you can gently rail your balls in the way they like best.
- See what a tangent line is, why it's important for knowing what will happen to the cue ball, even if English (sidespin) is applied.
- Make Bank
- Calculate your multi-rail bank shots.
- Proof that even chaos subscribes to the laws of reflection, and it might be time to look at your own.
- All the Balls You Could Want
- (You definitely want them.)
- Simulated motherf---ing balls on a projected motherf---ing plane.
- Determine whether another ball is in the way.
- Spin Control
- A tool for applying English. Maybe even British.
- Explore the subtle arts of post-impact trajectory, and other new and exciting ways to scratch.
- Massé Master
- Pick your impact point and the angle of attack to make the prettiest loop-de-loos on the table.
- Leave your opponent's jaw on the floor as you tear the felt up with such precision.
- Dynamic 3D Perspective
- Use your phone's sensor data to create a 3D illusion. This feature's primary purpose is to induce a subtle vertigo that mirrors the existential dread of a poorly-played safety.
- VERY Helpful Help:
- Labels for key lines and what to do with them.
- Instructions better than Ikea's.
- Toggleable Help visibility for a cleaner view.
- Uplifting messages of slightly disdainful encouragement.
- Pretend this is a screenshot.
- This, too.
- Imagine looking at a photo of the app in use.
- Note the craft.
- The flippant attitude towards detail.
- I'm a genieaouxess.
- And this is a photo from a vacation two years ago that I accidentally pretend included.
Every feature is free and unconditional. There is no Expert tier, no paywall, no entitlement, no trial and nothing to restore — the whole billing stack was removed, along with the tester-licence console that used to ship to every user in every build. If the app is useful to you, there are donation links under Support in the nav rail. Nothing behind them unlocks anything.
The game model lives in core/ as pure Kotlin Multiplatform modules —
units, table geometry, ball physics, the aim solver and the projection — with no
Android, OpenCV or ARCore types anywhere in them:
./gradlew -Pcuedetat.coreOnly=true allTests # no Android SDK requiredThree things this buys that the previous architecture could not:
- Real units. The world is metres. It used to be a dimensionless plane scaled
from
LOGICAL_BALL_RADIUS = 25f, which is how spin decay constants ended up specified "per logical unit" and how the on-screen distance readout ended up as1200 / screenRadiusInPixels— a number that changed when you moved the zoom slider. - Aim that accounts for throw. The ghost ball is the starting estimate, not the answer. Cut-induced throw pushes the object ball off the line of centres by up to about 5°, so the solver pre-compensates; squirt is applied at the cue tip, where it actually happens, so putting english on the ball now changes the line you are told to shoot along.
- Pockets that are apertures. A mouth of width
mentered atθoff its axis presentsm·cos(θ), so a ball rolling along the rail is rejected by the side pocket and accepted by the corner. Six bare points could not express that.
The Details.
Cue D’état is built upon a single, immutable truth: Unidirectional Data Flow is the master of the master of the universe. State flows down, events flow up. To question this is to question physics, which is how you got yourself to this point in the first place.
This app is not a custom View with a Canvas and a prayer — it's a strict Model-View-Intent (MVI)
app, unidirectional data flow all the way down. State flows down, events flow up, and the moment
someone tries to sneak business logic into a Composable, physics itself should intervene.
- Camera Preview: Uses CameraX (and, for the AR flow, ARCore) to display a live feed from the device camera.
- Sensor Input: Leverages the
TYPE_ROTATION_VECTORsensor to determine the phone's pitch, roll, and yaw. The pitch is primarily used to tilt the 2D protractor plane. An offset is applied to account for natural phone holding angles. - MVI Pipeline: Every user interaction becomes a
MainScreenEvent, sent up toMainViewModel. TheStateReducer(and its sub-reducers, one per concern — gestures, controls, CV, etc.) produces a new immutable state as a pure function of the old one. It is mostly, not solely, responsible forCueDetatState:MainViewModelstill applies a few.copy()calls of its own, including one real state-transition rule. Calling that "the only thing allowed to touch state" would be a claim this README used to make and the code did not honour.UpdateStateUseCasethen runs on that new state to derive everything downstream of it — perspective matrices, aiming lines, bank shots, spin paths — before the final state is emitted from aStateFlowand the UI redraws itself as a pure function of it. - Rendering (Compose, not a custom
View):ProtractorOverlayis a ComposeCanvasthat drivesOverlayRenderer.drawon every recomposition, delegating to sub-renderers (TableRenderer,RailRenderer,BallRenderer,LineRenderer, …) in strict z-order.- Protractor Plane: A logical 2D plane is defined. Circles representing the cue and target ball positions, protractor angle lines, and deflection lines are drawn on this plane.
- 3D Projection (Simplified):
UpdateStateUseCasebuilds the projection withandroid.graphics.Camera— aworldMatrixhandles 2D zoom, then aperspectiveMatrixapplies table rotation (Y-axis) followed by device pitch/tilt (X-axis), assembled into the finalpitchMatrixthe renderer uses every frame. - Ghost Balls: Screen-space circles are drawn to represent the "3D" position of the cue and target balls. Their Y-offset from the projected plane centers is scaled by the sine of the pitch angle (raised to a power for a more pronounced effect) to simulate them floating above the plane.
- Helper Text: Text labels are drawn either on the (lifted) protractor plane or directly in screen space, with basic collision avoidance and dynamic sizing.
- Gesture Handling: A single Compose
pointerInputmodifier,view/gestures/GestureHandler.kt, handles everything — single-finger drag (move objects / pan), two-finger pinch-to-zoom, and two-finger rotation — and turns raw touches into the sameMainScreenEvents as everything else. NoScaleGestureDetector, no rawMotionEventplumbing. - Theming: Uses Jetpack Compose Material 3 theming throughout; derived color values feed a
PaintCacheof pre-configuredPaintobjects for theCanvasdrawing calls.
- A Virtual Table for Virtually Useful Bank Shot Projection: Using more sophisticated dynamic layout involving a line drawing of a billiards table will come someday.
- True 3D Rendering: This app fakes 3D with 2D canvas tricks. Moving to OpenGL ES or a 3D engine like Filament would allow for actual 3D models and lighting, but would also drastically increase complexity. And probably anxiety. But probably not usefulness.
- Ball, Table and Pocket Detection: Partially real, believe it or not. Pocket/table detection uses a merged TFLite (YOLOv8n) model,
MergedTFLiteDetector, with a felt-boundary-extraction fallback if the model asset isn't available. Table scanning accumulates observations across frames, fits a 2:1 geometry model, and builds a TPS warp map for lens-distortion correction. Ball detection is colour-based:CvBallDetectorfinds non-felt islands inside the table on the full-resolution frame and names them by their white and dark pixel shares. (It used to run on a frame shrunk until a ball was two pixels across, behind an ML Kit "scout" that never saw a ball. It found nothing, with great confidence.) The pocket/table fallback is felt-boundary extraction, not Hough circles — this README previously claimed the opposite on both counts. What remains fantasy: doing all of this reliably on a $12 phone held by someone who has had three beers. - Insulting Warnings: The pool of sarcastic remarks is finite. Contributions welcome if they tickle me the required level of pink.
- Performance: Drawing many complex paths and text elements on every frame can be demanding. Optimizations are an ongoing battle. And yet, somehow, it feels more like a you-problem.
One build serves both channels: Google Play gets the signed App Bundle, GitHub Releases the signed APK. The TFLite model and the Expert-AR code are compiled into the app. The in-app updater checks GitHub only on copies Play didn't install; Play installs update through Play.
Quick local builds (versionCode = git commit count, kept monotonic for Play):
./gradlew bundleRelease -PversionBuild=$(git rev-list --count HEAD) # signed AAB (Play)
./gradlew assembleRelease -PversionBuild=$(git rev-list --count HEAD) # signed APK (GitHub)The build needs GitHub Packages credentials (GH_ACTOR/GH_TOKEN, or gh_user/gh_token
in local.properties) for the Meta Wearables SDK.
Publishing to Play runs centrally (HereLiesAz/workflows' Android Play Release, bound to
.github/workflows/play_publish.yml): every push to main uploads one AAB, live on the
internal and closed-testing tracks and as a draft on open testing and production.
Required repo secrets: KEYSTORE_PRIVATE, KEYSTORE_CHAIN, KEYSTORE_PASSWORD,
KEY_ALIAS, KEY_PASSWORD (signing) and PLAY_SERVICE_ACCOUNT_JSON (Play
publishing). Full details, one-time Play Console setup, and the Data-safety
checklist are in docs/RELEASE.md.
Distributed under the MIT License. Basically, completely free to use however you'd like, just gimme a shoutout. I make money making art. So, like this: Cue D’état by HereLiesAz (https://instagram.com/hereliesaz)
- The ghosts of billiards past whose missed shots inspired all this.
- The people I've tried to teach all these things.
- Physics. And geometry. Where my hoes at?! Pythagoras! Decartes! Newton, you bish!