bevy_map_scatter 0.5.0

Bevy plugin that integrates the `map_scatter` core crate for object scattering with field-graph evaluation and sampling
Documentation
# bevy_map_scatter

[![License: MIT or Apache 2.0](https://img.shields.io/badge/License-MIT%20or%20Apache2-blue.svg)](https://github.com/morgenthum/map_scatter#license)
[![Docs](https://docs.rs/bevy_map_scatter/badge.svg)](https://docs.rs/bevy_map_scatter)
[![Crate](https://img.shields.io/crates/v/bevy_map_scatter.svg)](https://crates.io/crates/bevy_map_scatter)
[![Build Status](https://github.com/morgenthum/map_scatter/actions/workflows/ci.yml/badge.svg)](https://github.com/morgenthum/map_scatter/actions/workflows/ci.yml)

Bevy plugin for rule-based scattering with asset loading, async execution, and ECS-friendly results.

![logo](./logo.png)

## Features

Asset-driven scattering for Bevy with async execution and ECS-friendly results:
- Asset-based authoring of scatter plans (RON): load `*.scatter` files via `AssetServer`.
- Texture integration: snapshot Bevy `Image`s to CPU textures with configurable domain mapping and typed errors for unsupported or unavailable CPU data.
- Asynchronous execution: runs scatter jobs on `AsyncComputeTaskPool` and cancels unfinished jobs when their request entity is despawned.
- ECS-friendly: placements become entities; components can be attached for rendering, gameplay, or tooling.
- Streaming helper: manage chunked scatter around moving anchors (optional plugin).
- Diagnostics: forward core events as Bevy messages (`ScatterMessage`) with configurable filtering; completed runs emit `ScatterFinished` as an `EntityEvent`.
- Reflection support: scatter assets, Bevy-facing definitions, streaming components, and `Handle<ScatterPlanAsset>` are registered for Bevy reflection and handle serialization workflows.

## Use cases

Use cases: decorative dressing, resource distribution, or any placement driven by textures and rules.

Workflow notes:
- Plans are assets and can hot-reload
- Tweak textures, thresholds, or layer order and rerun
- Deterministic seeds by default; vary them per run when needed

Examples:
1. Stylized forest: trees sparse, mushrooms where tree probability is low, grass fills gaps.
2. Town dressing: benches in plaza masks, lamp posts along roads with spacing, clutter where nothing else landed.
3. Dungeon encounters: camps in large rooms, enemies avoid camp influence, rare loot in dead ends with minimum spacing.

Integration details:
- Assets: `*.scatter` plans loaded by `AssetServer`
- ECS: placements become entities you can tag and decorate
- Async: jobs run on `AsyncComputeTaskPool`
- Determinism: same seed + plan + textures = identical placements
- Events: listen to `ScatterFinished` or `ScatterMessage` to drive gameplay or tooling

## Examples

See the [example crate](https://github.com/morgenthum/map_scatter/blob/main/crates/bevy_map_scatter_examples/README.md) for curated demos you can run locally.

![Forest Trail streaming example](./images/forest-trail.png)

## Quick start

Add the crates to a Bevy application:

```toml
# Cargo.toml
[dependencies]
bevy = "0.19"
bevy_map_scatter = "0.5"
map_scatter = "0.4"
```

Create a scatter plan in `assets/simple.scatter`:

```ron
(
  layers: [
    (
      id: "dots",
      kinds: [
        (
          id: "dots",
          spec: (
            nodes: {
              "probability": Constant(
                params: ConstantParams(value: 1.0),
              ),
            },
            semantics: {
              "probability": Probability,
            },
          ),
        ),
      ],
      sampling: JitterGrid(
        jitter: 1.0,
        cell_size: 1.0,
      ),
      selection_strategy: WeightedRandom,
    ),
  ],
)
```

Trigger a single scatter run once the asset is ready:

```rust
use bevy::prelude::*;
use bevy_map_scatter::prelude::*;

#[derive(Resource, Default)]
struct PlanHandle(Handle<ScatterPlanAsset>);

fn main() {
    App::new()
        .init_resource::<PlanHandle>()
        .add_plugins(DefaultPlugins)
        .add_plugins(MapScatterPlugin)
        .add_systems(Startup, load_plan)
        .add_systems(Update, trigger_request)
        .add_observer(log_finished)
        .run();
}

/// Loads the scatter plan asset on startup.
fn load_plan(mut handle: ResMut<PlanHandle>, assets: Res<AssetServer>) {
    handle.0 = assets.load("simple.scatter");
}

/// Triggers a scatter request once the plan asset is loaded.
fn trigger_request(
    mut commands: Commands,
    mut once: Local<bool>,
    handle: Res<PlanHandle>,
    assets: Res<Assets<ScatterPlanAsset>>,
) {
    // Only run once.
    if *once {
        return;
    }
    // Wait until the asset is loaded.
    if assets.get(&handle.0).is_none() {
        return;
    }

    // The domain size for scattering.
    let domain = Vec2::new(10.0, 10.0);

    // Create run configuration and seed for (deterministic) randomness.
    let config = RunConfig::new(domain)
        .with_chunk_extent(domain.x)
        .with_raster_cell_size(1.0);

    // Spawn an entity to track the request (attach your own components if needed).
    let entity = commands.spawn_empty().id();

    // Trigger the scatter run.
    commands.trigger(ScatterRequest::new(entity, handle.0.clone(), config, 42));

    // Mark as done.
    *once = true;
}

/// Observes the `EntityEvent` when a scatter run has finished.
fn log_finished(finished: On<ScatterFinished>, mut commands: Commands) {
    info!(
        "Scatter run {} finished: placements={} evaluated={} rejected={}",
        finished.entity,
        finished.result.placements.len(),
        finished.result.positions_evaluated,
        finished.result.positions_rejected
    );

    // Clean up the entity used for the request (keep it if you need it later).
    commands.entity(finished.entity).despawn();
}
```

Run the application with `cargo run`. After the scatter job completes, a summary appears in the log; continue with placement logic as needed.

## Saving edited scatter plans

Runtime saving is available for editor and tooling workflows that modify
`ScatterPlanAsset` values and want to write `.scatter` RON back to disk. It is
not required for normal scattering. `ScatterPlanAssetSaver` is available when
the default `ron` feature is enabled.

```rust
use bevy::asset::saver::{save_using_saver, SavedAsset};
use bevy::asset::AssetPath;
use bevy::prelude::*;
use bevy::tasks::IoTaskPool;
use bevy_map_scatter::prelude::*;

fn save_edited_plan(
    asset_server: Res<AssetServer>,
    plans: Res<Assets<ScatterPlanAsset>>,
    handle: Res<PlanHandle>,
) {
    let Some(plan) = plans.get(&handle.0).cloned() else {
        return;
    };

    let asset_server = asset_server.clone();
    IoTaskPool::get()
        .spawn(async move {
            let asset_path: AssetPath<'static> = "simple.scatter".into();
            let saved_asset = SavedAsset::from_asset(&plan);
            let saver = ScatterPlanAssetSaver;

            save_using_saver(
                asset_server,
                &saver,
                &asset_path,
                saved_asset,
                &(),
            )
            .await
            .expect("scatter plan should save");
        })
        .detach();
}
```

## Async job lifetime

Each `ScatterRequest` is tied to a request entity. `MapScatterPlugin` stores the in-flight `Task<RunResult>` on that entity, and the entity owns the job lifetime.

- If the job finishes, the plugin removes the internal job component and triggers `ScatterFinished`.
- If the request entity is despawned before the job finishes, the task is dropped and the scatter work is cancelled.
- Cancelled jobs do not emit `ScatterFinished`.
- The plugin does not call `Task::detach()`, so jobs do not outlive their request entity.

This is intentional for streaming: chunk entities own their scatter work, and despawning a chunk because it left the view or because its settings/assets changed cancels any in-flight scatter run for that chunk.

## Migration from 0.4 / Bevy 0.18

`bevy_map_scatter` 0.5 targets Bevy 0.19. Update application dependencies and Bevy APIs together:

```toml
[dependencies]
bevy = "0.19"
bevy_map_scatter = "0.5"
map_scatter = "0.4"
```

Breaking migration notes:

- Scatter completion is an `EntityEvent`; observe `ScatterFinished` with `On<ScatterFinished>` and trigger requests with `Commands::trigger`.
- Forwarded diagnostics use Bevy messages; read `ScatterMessage` with `MessageReader<ScatterMessage>`.
- In-flight scatter jobs are no longer detached. Despawning the request entity cancels unfinished work and no `ScatterFinished` is emitted for that job.
- `ImageTexture::try_from_image` returns `Result<ImageTexture, ImageTextureError>` with explicit errors such as `UnsupportedFormat`, `MissingImageData`, invalid dimensions, invalid domain extents, or unexpected buffer lengths. The older `from_image` helpers remain as `Option` convenience wrappers.
- `ScatterPlanAsset`, Bevy-facing plan definition types, streaming components, and `Handle<ScatterPlanAsset>` are reflected for Bevy 0.19 serialization and editor/tooling workflows.
- Runtime saving of edited `.scatter` assets is available through `ScatterPlanAssetSaver` and Bevy's `save_using_saver` API when the `ron` feature is enabled.
- Bevy 0.19's BSN support is code-driven in upstream Bevy. This crate loads `*.scatter` RON assets; it does not provide `.bsn` asset loading.

## Streaming (optional)

For moving worlds or endless maps, use `MapScatterStreamingPlugin` and attach
`ScatterStreamSettings` to an anchor entity. The plugin will spawn/despawn chunks
around the anchor and emit placement entities tagged with `ScatterStreamPlacement`.

## Alternatives

- [`bevy_feronia`]https://github.com/NicoZweifel/bevy_feronia: more opinionated, art-focused scattering with built-in wind/material/LOD workflows; likely a better fit if you want an end-to-end visual pipeline rather than a low-level, data-driven scatter core.

## Compatibility

| `bevy_map_scatter` | `map_scatter` | `bevy` |
| ------------------ | ------------- | ------ |
| `0.5`              | `0.4`         | `0.19` |
| `0.4`              | `0.4`         | `0.18` |
| `0.3`              | `0.3`         | `0.17` |
| `0.2`              | `0.2`         | `0.17` |

## Local verification

Before opening a release PR, run the same meaningful Bevy modes covered by CI:

```bash
cargo fmt --all -- --check
cargo check --workspace --all-targets
cargo test --workspace
cargo check -p bevy_map_scatter --no-default-features
cargo check -p bevy_map_scatter --all-features
cargo check -p bevy_map_scatter_examples --bins
```