Read docs/plan.md before making architectural changes. It is the source of truth for the intended design. Keep Bevy-facing application, rendering, input, and UI code outside the Bevy-independent brush and document modules.
For Bevy work, read the relevant project skill before editing:
- bevy-019 for version-specific APIs, features, and migration checks
- bevy-bsn-ui for
bsn!, scene composition, and UI authoring - bevy-feathers-ui for Feathers widgets and the path toward custom headless widgets
- bevy-tablet-input for octotablet event framing, Bevy window ownership, input units, and mouse arbitration
- mypaint-brush-assets before adding, discovering, cataloging, or displaying MyPaint presets and preview icons
- libmypaint-port before changing the MyPaint engine, reference oracle, parity fixtures, or related WESL kernels
Rasterra targets Bevy 0.19. Check versioned 0.19 documentation and examples when an API is uncertain; main-branch examples may already target a later release.
Author new UI with bsn! scene functions. Use Feathers as the initial styled widget set and compose it with Rasterra-specific marker components, observers, layout patches, and accessibility metadata. Do not use deprecated Feathers bundle constructors.
Use Feathers' semantic theme tokens and theme-bound color components for ordinary editor surfaces, controls, text, borders, and interaction states. Add a rasterra.* token only when Feathers has no matching semantic role; do not create a parallel project palette for colors Feathers already names.
UI icons are SVG sources loaded as Bevy Image assets by Rasterra's SVG image
loader. Prefer the existing Lucide vocabulary, vendor only used glyphs, keep
icon-only controls accessible, and update assets/README.md plus the bundled
upstream license when changing icon or font assets.
Keep application state authoritative outside widgets. Handle Activate and ValueChange<T> events by updating application state and reflecting that state back into widget components. This preserves a clean migration path from Feathers styling to custom widgets built on bevy_ui_widgets headless behavior.
Treat Feathers and bevy_ui_widgets as evolving APIs. Wrap repeated Rasterra UI patterns in small scene functions or project-owned SceneComponents rather than spreading framework-specific construction throughout unrelated systems.
Organize application UI by the feature that owns the workflow. Keep a panel's
scene, markers, observers, and state synchronization together; keep the editor
shell focused on composition and major placement. Reserve app/ui for shared
theme infrastructure and controls reused by multiple features. Read the
UI structure reference
before splitting the application or proposing UI crates.
Run the interactive painting proof of concept from the project shell:
cargo run
Paint with the left mouse button, pan the infinite canvas with a middle-button
drag, and zoom around the cursor with the mouse wheel. Select a preset from the
Feathers brush list and press F12 to save target/rasterra-poc-<n>.png. For a
deterministic visual smoke test, use the command below.
The canvas fills the window and the bottom-right icon-and-text quick toolbar is
the only persistent editor chrome. Hold Q to enter its spring-loaded menu:
horizontal mouse or hovering tablet motion selects Brushes, Tools, Palette,
Layers, or Document; moving upward enters the revealed flyout and selects
progressively higher entries. Release Q to apply the highlighted command.
Escape or window focus loss cancels. The OS cursor is hidden and locked during
mouse use, while tablet positions are reduced to relative deltas without moving
the real pointer.
The earlier brush browser and dense all-controls inspector remain instantiated
but hidden while this clickless interaction is explored. Their actions still
cross the same EditorAction seam used by shortcuts,
automation, and the quick menu.
Bevy 0.19 has blurred box shadows but no backdrop-filter primitive. Rasterra's glass panels use a project-owned, region-masked camera post-process adapted from Bevy PR #24609. Content that should appear through the glass must render in the 2D scene before Bevy's UI pass; the canvas therefore uses a scene sprite while a transparent UI node owns layout and input.
The current file buttons use predictable development paths rather than a file dialog:
- Open/save:
target/rasterra.rasterra - Save As:
target/rasterra-copy.rasterra - PNG import:
target/import.png - PNG export:
target/rasterra-export.png - Recovery:
target/rasterra-autosave.rasterra
Common shortcuts are B brush, E eraser, I picker, H hand, F fill,
R rectangle, L line, brackets for brush size, Ctrl+Z/Ctrl+Shift+Z for
history, and the conventional Ctrl+N/O/S document commands. 0 fits the
artboard and 1 selects 100% zoom.
With a tablet pen in proximity, hold B1 and move the pen to pan. Hold B2 and move right or up from the press origin to zoom in; move left or down to zoom out. The tablet inspector shows Rasterra's current B1/B2 assignment.
The automated capture exits after saving:
cargo run -- --screenshot target/rasterra-poc.png
Run the same editor against a fixed-size offscreen render target when a real window would be throttled by the compositor or no display server is available:
cargo run -- --headless --screenshot target/rasterra-headless.png
cargo run -- --headless --remote-debug
Headless mode uses a schedule runner instead of Winit and therefore has no mouse, keyboard, or tablet host. Automation must enter through Rasterra's shared actions and pointer-sample boundary. Interactive tablet validation still requires the ordinary windowed mode. See remote debugging and headless automation for the reflected request types, completion protocol, and stroke format.
Enable Bevy's JSON-RPC remote debugging server for one run with:
cargo run -- --remote-debug
cargo run -- --remote-debug 127.0.0.1:16000
The default address is 127.0.0.1:15702. Prefer the built-in Bevy Remote
Protocol methods for inspection, ECS changes, input injection, and screenshots
before adding project-specific commands. Remote debugging can inspect and
modify application state, so it remains opt-in and should not be bound to a
public interface casually.
Use debug-profile commands unless a performance investigation explicitly requires release mode.
Before handing off Rust changes, run the narrowest applicable checks, then widen only as needed:
cargo fmt --check
cargo check
cargo test --test <integration-test>
cargo clippy --all-targets --all-features -- -D warnings
Run a specific test target or test name before the full test suite. Do not treat warnings as pre-existing without confirming their cause.
For dependency changes, edit only the intended dependency. Never run a blanket dependency update. Use cargo update -p <package> --precise <version> when an existing lockfile entry must change.
The project Nix shell supplies Rasterra's Bevy/Linux libraries, CMake-based oracle, and clangd dependencies; it deliberately does not replace the developer's Rust toolchain. The checked-in .envrc enters it while preserving the caller's existing cargo and rustc:
nix develop .#mypaint-port
Nix shells do not compose their build inputs automatically. The project shell therefore declares the native dependencies needed by Rasterra itself instead of relying on packages or PKG_CONFIG_PATH inherited from a personal outer Rust shell.
Entering the Nix shell does not configure or compile libmypaint. Cargo debug builds invoke the test-oracle CMake project and publish its compile_commands.json at the repository root. Release builds never invoke that CMake project. Run a normal debug cargo build after cargo clean before expecting clangd to index the vendored C sources.
Use the profiling Cargo profile for optimized oracle tests and future benchmarks. It inherits the release profile's optimizations, retains debug information, and includes the native oracle build without changing production release behavior:
cargo test --profile profiling --test mypaint_reference
The shell places a project-owned clangd wrapper first on PATH. It allows clangd to query GCC and G++ wrappers under /nix/store for implicit standard-library include paths. Restart an existing editor language-server process after .envrc reloads so it uses this wrapper. Editor tool managers that select their own clangd binary, such as Mason, must pass the same --query-driver=/nix/store/**/bin/gcc,/nix/store/**/bin/g++ argument explicitly.
Treat the vendored libmypaint revision as the behavioral specification, not as production code. The production application must never link it. Test-only code may call a narrow C wrapper or its oracle executable.
Plain cargo test includes the libmypaint parity target. This is intentional:
reference comparison is the primary validation path while the Rust engine is
being ported. Use cargo test --test mypaint_reference for a focused run.
Port one behavioral unit at a time and add focused parity coverage before continuing. Preserve operation order, constants, numeric types, and unusual-looking formulas until parity is demonstrated. Record intentional divergences in the port map.
Implement pixel behavior in Rust first and compare it with the C oracle. Port a passing Rust CPU kernel to WESL afterward, then compare actual GPU output with the Rust implementation. Never use a final image as the only parity signal.
Attempt to cover every behavior change. Prefer integration tests under tests/it/ and copy the structure of nearby tests. Prefer insta snapshots for structured or rendered output when a nearby test establishes that pattern; avoid brittle substring assertions.
Keep exploratory test coverage proportional. Add a test when it is needed to validate a change, protect a stable contract, or reproduce a regression. Avoid building broad test suites around provisional UI, command-line, or debugging wiring before its behavior has settled.
UI tests should separate behavior from appearance where practical:
- Test emitted events and state synchronization at the ECS level.
- Snapshot stable scene or application state where useful.
- Use image comparison only when visual rendering itself is the behavior under test.
- Handle fallibility without
panic!,unreachable!, or.unwrap()in production paths. - Prefer
if letand let chains over nested conditional matching. - Avoid unsafe code. When it is unavoidable, document every unsafe block with a
SAFETYcomment stating the upheld invariant. - Prefer
#[expect(...)]over#[allow(...)]for a necessary lint exception and explain why it is expected. - Put imports at module scope and follow the ordering used by nearby code.
- Use descriptive names rather than abbreviations.
- Link Rust types as [
TypeName] in doc comments when rustdoc can resolve them.
Write code so a reader can recover both the mechanism and the reason for it without consulting commit history. For non-obvious algorithms, state the invariant, data flow, ownership transition, or upstream behavior being preserved close to the implementation. Include a compact formula, example, or ASCII diagram when it makes the transformation materially easier to follow.
Document private functions when their name and signature do not explain their role. Prefer comments that answer “why this shape or order?” over comments that repeat syntax. Compatibility ports should name the upstream file and function for unusual formulas, evaluation order, constants, and deliberate quirks.
Begin every Rust module with module-level documentation containing explicit
Purpose and Usage sections. State whether the module is production code,
format adaptation, application integration, generated support, or test-only
infrastructure, and name the neighboring module or type that normally consumes
it. Keep these sections current when ownership moves.
Keep documentation proportional: ordinary accessors and direct control flow do not need narration, while unsafe boundaries, generated-code contracts, numerical kernels, state machines, and ownership transfers do.
Make an atomic commit after each coherent batch. Use a short subject line and omit the body unless it adds essential context. Never add generated-by or co-author attribution trailers.