Skip to content

feat: Add annotated and size limited snapshots to flame_cli - #4138

Merged
spydon merged 2 commits into
mainfrom
feat/cli-annotated-snapshot
Oct 25, 2026
Merged

spydon merged 2 commits into
mainfrom
feat/cli-annotated-snapshot

Conversation

@spydon

@spydon spydon commented Oct 24, 2026

Copy link
Copy Markdown
Member

Description

Adds three options to flame snapshot, so that AI models can match what they see in a snapshot with the component tree and get images that stay within their size limits:

  • --annotate (-a) draws the outline of every visible PositionComponent on the image, labeled with its id (the same id that flame tree prints) and its type, and prints the annotated components as Player (id: 220731871) box 368,268,64,64, with the box in the pixel coordinates of the image, before the path of the image. Text only consumers get the same mapping that way.
  • --filter (-f) is a case insensitive regular expression on the type, like flame tree --filter, so that only the matching components are annotated. It needs --annotate.
  • --max-size (-m) limits the longest side of the image to that many pixels. The game lowers the pixel ratio before rendering instead of resizing the image afterwards, so the aspect ratio is kept, nothing is cropped and the image stays sharp. When the ratio was lowered the command prints the size and the pixel ratio that was used, so that pixel coordinates can be converted back to canvas coordinates for the input commands.

All three work for the whole game, --world, --rect and --component, at any --pixel-ratio.

Drawing on the game side

The annotations are computed and drawn on the game side, while the snapshot is rendered, in the new packages/flame/lib/src/devtools/snapshot_annotations.dart. The getGameSnapshot and getComponentSnapshot extensions take the new annotate, annotationFilter and maxSize parameters, and return pixelRatio and, when annotating, annotations with the id, name and box of each component.

The game is the only place that knows all the transforms involved: the transforms of every position component up the tree, the viewfinder (position, zoom, angle), the viewport (position, anchor and the scale of a FixedResolutionViewport), and the snapshot mode itself (canvas, world rect or component bounds, and the pixel ratio). Doing it on the CLI side would mean sending all of that over and duplicating the camera math in a second place that has to be kept in sync. On the game side the outline of a component is found by mapping its four corners up the tree with positionOf, Viewport.localToGlobal and CameraComponent.localToGlobal, which are the same functions that the camera uses, so the outlines line up with the rendering by construction. A world is mapped through the camera that renders it, the backdrop of a camera through its viewport, and the box of a world, viewport or backdrop component is checked against that viewport, so components that the viewport clips away are left out. Components that are hidden with HasVisibility, that have no area or that are outside of the image are left out too, and the outlines are drawn as the rotated quad of the component, while the printed box is its axis aligned bounding box.

The annotations are drawn in image pixels after the game is rendered, so the labels have the same legible size at every pixel ratio. Every outline has a dark border under a colored line, every label has a solid colored background with dark text, and a label is placed at the first of a few positions around its box that doesn't cover the labels placed before it, or at the one that covers them the least.

Older Flame versions

An older game ignores the unknown parameters, so the CLI checks that the response has annotations when --annotate was passed and pixelRatio when --max-size was passed. Otherwise it fails with The running game does not support --annotate, update Flame to the latest version. and exit code 69, like a missing extension, before anything is written.

Testing

The geometry is tested directly instead of through goldens, so the tests don't depend on font rendering: through a zoomed and moved camera, a rotated camera (compared against the pixels of the rendered snapshot), a FixedResolutionViewport, viewport components, a camera backdrop (compared against the rendered pixels), nested and scaled components, viewport clipping, --world with a rect and a pixel ratio, single components, and the filtering rules. The maximum size is tested to never be exceeded through rounding. On the CLI side the options that are sent to the game, the output, the usage errors and the error for an older game are tested with a fake connection.

I also verified it end to end with a small game on flutter run -d flutter-tester with a rotated and zoomed camera, a fixed resolution viewport, a nested child and a viewport component, and checked that the outlines in the produced PNGs line up for the whole game, --max-size, --world and --rect --pixel-ratio 2.

The options are documented in the CLI docs, the service extensions in the debug docs, and there is an example in the readme.

Checklist

  • I have followed the Contributor Guide when preparing my PR.
  • I have updated/added tests for ALL new/updated/fixed functionality.
  • I have updated/added relevant documentation in docs and added dartdoc comments with ///.
  • [-] I have updated/added relevant examples in examples or docs.

Breaking Change?

  • Yes, this PR is a breaking change.
  • No, this PR is not a breaking change.

Related Issues

Part of #4131

@spydon
spydon added this pull request to stack #4135 October 24, 2026 09:50
@spydon
spydon removed this pull request from stack #4135 October 24, 2026 10:30
@spydon
spydon added this pull request to stack #4140 October 24, 2026 10:30
@spydon
spydon force-pushed the feat/cli-annotated-snapshot branch from f1479a7 to ef8c3f9 Compare October 25, 2026 09:47

@ufrshubham ufrshubham left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@spydon
spydon force-pushed the feat/cli-annotated-snapshot branch 4 times, most recently from 6a49254 to 8ab8195 Compare October 25, 2026 19:37
Base automatically changed from feat/cli-wait to main October 25, 2026 19:37
spydon and others added 2 commits October 25, 2026 20:38
Adds a `flame wait` command to `flame_cli` that blocks until a condition
holds in the running game, or until a timeout is reached, so that
scripts and AI agents don't need sleep loops and guessed timings. For
example, tap a button and then wait until the game over overlay is
shown:

```shell
flame input tap 400,300
flame wait overlay GameOver
```

The conditions are subcommands, like `overlay show` and `input tap`:

- `wait component <type pattern>`: a component whose type matches the
regular expression exists, matched case insensitively like `tree
--filter`. `--id` matches a single component, `--count` waits for an
exact number of matches, `--at-least` for a minimum number, and `--gone`
until nothing matches, which covers waiting until a component is removed
(`flame wait component --id 123 --gone`). `--where "position.x > 100"`
compares a numeric attribute from the same data that `tree` and
`inspect` return (`position.x`, `position.y`, `size.x`, `size.y`,
`scale.x`, `scale.y`, `angle` and `priority`), and can be given several
times.
- `wait overlay <name>`: the overlay is shown, or hidden with
`--hidden`. An unregistered overlay fails right away with `65`.
- `wait paused` and `wait resumed`: the game loop is paused or running.

All of them take `--timeout` (10 seconds by default, `0` checks once and
works as an assertion), `--interval` (0.2 seconds by default) and
`--json`. When the condition holds, the command prints what matched, for
example the matching components with their ids and attributes, and exits
with `0`. When the timeout is reached first it says what it found
instead, for example `Timed out after 10 seconds waiting for a component
with a type matching enemy, found 0.`, and exits with `75`
(`ExitCode.tempFail`), which is added to the documented exit codes. A
game that doesn't answer a check in time, for example because it is
stopped at a breakpoint, also fails with `75` instead of blocking the
command past its timeout.

The command polls the existing `getComponentTree`, `getOverlays` and
`getPaused` service extensions over one connection, so nothing changes
on the game side. The clock is injectable through a `WaitClock` passed
to `FlameCommandRunner`, so the tests are fast and deterministic.

To support this:
- `FlameCommand` takes an optional `connect`, and the runner passes its
connector to the wait command, so that commands talking to a game can be
tested with a fake connection.
- `FlameConnection.call` now reports a closed connection, which is what
happens when the game stops while a command is waiting, as `The game is
no longer running` with exit code `69`, instead of the generic `70`
error. `FlameConnection.withService` creates a connection from an
existing `VmService`, which the tests use.
- `findComponentNodes` in `component_tree.dart` returns the matching
nodes of the tree.

The command is documented in the CLI docs and the readme.
@spydon
spydon force-pushed the feat/cli-annotated-snapshot branch from 8ab8195 to c47b4da Compare October 25, 2026 19:38
@spydon
spydon enabled stacked auto-merge (squash) October 25, 2026 19:38
@spydon
spydon merged commit b43f4ab into main Oct 25, 2026
8 checks passed
@spydon
spydon deleted the feat/cli-annotated-snapshot branch October 25, 2026 19:49
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