# bevy_symbios_shape
Bevy integration for [symbios-shape](https://crates.io/crates/symbios-shape) — a CGA Shape Grammar engine for procedural architecture.
Converts grammar outputs (`ShapeModel` / `Terminal` nodes) into Bevy entity hierarchies with procedural or asset-based meshes.
## Features
- **Plugin**: `BevySymbiosShapePlugin` registers `ShapeRegistry`, `ShapeMeshCache`, `LastDerivedModel`, and `SnapPlanes` resources, plus an observer for `SpawnShapeRequest` events.
- **Asset routing**: Register GLTF scenes and materials by string ID; unregistered terminals fall back to procedural meshes.
- **Procedural meshes**: `build_profiled_mesh` generates geometry for tapered prisms, triangles, trapezoids, and arbitrary polygons based on the terminal's `FaceProfile`.
- **Round cross-sections**: register an asset ID with `register_round_mesh` and its terminals bake as elliptical prisms — cylinders, frusta, and cones — so columns, silos, and spires read as round while the grammar keeps working in axis-aligned boxes.
- **Coordinate translation**: `scope_to_transform` handles the f64→f32 downcast and corner-to-centroid offset between `symbios-shape` scopes and Bevy transforms.
- **Spawn extension**: `commands.spawn_shape(...)` derives a grammar and spawns the full terminal hierarchy in one call.
- **Event-driven spawning**: trigger `SpawnShapeRequest` and observe `SpawnShapeSpawned` / `SpawnShapeFailed` for decoupled call sites.
- **Cross-spawn mesh cache**: `ShapeMeshCache` deduplicates procedural meshes across `spawn_shape` calls (with hit/miss/eviction counters) and is **bounded** — least-recently-used entries are evicted past its capacity, so long-lived re-rolling sessions cannot grow it without limit.
- **Mass properties**: terminals whose grammar carries `Mat(id, density)` get a `TerminalMass` component (mass / centroid / inertia) for downstream physics, IK, and LOD.
- **Occlusion labels**: terminals derived under `Label("class")` get a `TerminalLabel` component, so ECS systems can select the same groups the grammar's `IfClear` / `IfInside` conditionals reasoned about.
- **Snap planes**: `RegSnap("label")` recordings are surfaced via the `SnapPlanes` resource.
- **Spatial queries**: `LastDerivedModel` plus the re-exported `TerminalQuery` / `obb_overlap` give downstream systems OBB collision tests against spawned terminals.
## Quick start
```rust
use bevy::prelude::*;
use bevy_symbios_shape::prelude::*;
use symbios_shape::{Interpreter, Scope, grammar::parse_ops};
fn main() {
App::new()
.add_plugins((DefaultPlugins, BevySymbiosShapePlugin))
.add_systems(Startup, setup)
.run();
}
fn setup(
mut commands: Commands,
registry: Res<ShapeRegistry>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut cache: ResMut<ShapeMeshCache>,
) {
let mut interp = Interpreter::new();
interp.add_rule("Lot", parse_ops("Extrude(12) Split(Y) { 3: Ground | ~1: Upper | 2: Roof }").unwrap());
interp.add_rule("Ground", parse_ops(r#"I("GroundFloor")"#).unwrap());
interp.add_rule("Upper", parse_ops(r#"I("Floor")"#).unwrap());
interp.add_rule("Roof", parse_ops(r#"Taper(0.8) I("Roof")"#).unwrap());
let footprint = Scope::new(
symbios_shape::Vec3::ZERO,
symbios_shape::Quat::IDENTITY,
symbios_shape::Vec3::new(10.0, 0.0, 10.0),
);
commands
.spawn_shape(&interp, footprint, "Lot", ®istry, &mut meshes, &mut materials, &mut cache)
.unwrap();
}
```
## Modules
| `cache` | `ShapeMeshCache` — persistent procedural mesh cache shared across spawns |
| `events` | `SpawnShapeRequest` / `SpawnShapeSpawned` / `SpawnShapeFailed` events + observer |
| `label` | `TerminalLabel` Component — the occlusion class stamped by the grammar's `Label("…")` |
| `mass` | `TerminalMass` Component — mass / centroid / inertia from upstream `MassProperties` |
| `mesh` | `build_profiled_mesh` / `build_profiled_mesh_with` / `build_tapered_cuboid` — meshing |
| `query` | `LastDerivedModel` resource holding the most recently derived `ShapeModel` |
| `registry` | `ShapeRegistry` — asset routing table (mesh IDs → scenes, material IDs → materials) |
| `snap` | `SnapPlane` / `SnapPlanes` resource exposing `RegSnap` recordings |
| `spawner` | `SpawnShapeExt` — `Commands` extension trait for one-call grammar derivation + spawning |
| `transform` | `scope_to_transform` — f64 scope to f32 Bevy `Transform` with corner-to-centroid offset |
## Asset registration
Register GLTF scenes and materials before spawning so the grammar can resolve them:
```rust
fn setup(
mut registry: ResMut<ShapeRegistry>,
mut materials: ResMut<Assets<StandardMaterial>>,
asset_server: Res<AssetServer>,
) {
registry.register_mesh("Window", asset_server.load("window.glb#Scene0"));
registry.register_material("Brick", asset_server.load("brick_material.glb#Material0"));
registry.default_material = Some(materials.add(StandardMaterial::default()));
// Stretch UVs across the face for materials whose texture should fit exactly
// once per face (signage, stained glass) instead of tiling in world space.
registry.register_stretch_material("Window");
}
```
Terminals whose `mesh_id` is not registered get a procedural mesh colored by a stable hash of the ID. Material fallback order is: registered handle → `default_material` → generated grey.
## Round cross-sections
The grammar stays OBB-pure — every scope is an axis-aligned box — but a
terminal's *rendering* can be round. Opt an asset ID in and the mesher
inscribes an elliptical prism in the scope's footprint:
```rust
// Grammar: Column --> Extrude(0.6) Taper(0.14) Mat("Marble") I("Column")
registry.register_round_mesh("Column"); // or register_round_material("Marble")
registry.set_round_segments(32); // default 24, clamped to [3, 256]
```
| `Rectangle` | cylinder (circular when the footprint is square, elliptical otherwise) |
| `Taper(t)` | frustum — the top ring shrinks by `t` |
| `Taper(1.0)` | cone (the top cap is dropped) |
| `Triangle` / `Trapezoid` / `Polygon` | unchanged — flat panels have no cross-section to round |
Side UVs wrap the circumference in world units (so texel density matches
flat walls) unless the ID is also registered as a stretch target. Because
roundness is a rendering choice, splits, occlusion queries, and mass
properties are all computed on the original box.
## Event-driven spawning
For decoupled call sites — e.g. a UI button that requests a building without holding the mesh/material asset registries — trigger [`SpawnShapeRequest`]:
```rust
use std::sync::Arc;
fn on_button_click(mut commands: Commands, interp: Res<MyInterpreter>) {
commands.trigger(SpawnShapeRequest {
interpreter: Arc::new(interp.0.clone()),
root_scope: footprint(),
root_rule: "Lot".into(),
});
}
// Observe the result anywhere:
fn track_spawns(mut spawned: On<SpawnShapeSpawned>) { /* spawned.event().0 = root Entity */ }
fn track_errors(mut failed: On<SpawnShapeFailed>) { /* failed.event().0 = error string */ }
```
The plugin's built-in observer performs the derivation and spawn, then triggers `SpawnShapeSpawned(root_entity)` on success or `SpawnShapeFailed(message)` on derivation failure.
## Cargo features
| `egui` | off | Pulls in [`bevy_egui`](https://crates.io/crates/bevy_egui) for downstream inspector/editor UIs. |
| `mutation` | off | Enables the `mutation` module: `MaterialMutationPlugin`, `TextureConfigStore`, and the `MutateMaterialsRequest` / `MaterialTextureMutated` event pair. Drives procedural-texture evolution via [`bevy_symbios_texture`](https://crates.io/crates/bevy_symbios_texture) and [`symbios-genetics`](https://crates.io/crates/symbios-genetics). |
## Mesh cache capacity
The cache defaults to `DEFAULT_MESH_CACHE_CAPACITY` (8192) entries and
evicts least-recently-used meshes beyond it:
```rust
let mut cache = ShapeMeshCache::with_capacity(2048);
cache.set_capacity(None); // opt out of eviction entirely
println!("{} evictions", cache.evictions());
```
Eviction drops the cached *handle*, not the GPU mesh — anything still
spawned keeps its geometry alive. A steadily climbing `evictions()` means
the working set exceeds the ceiling; raise it, or accept the re-upload
churn.
## Examples
```bash
# The 0.3 language: attributes, styles, guards, rhythm groups, Pick,
# ShapeL, Scatter, Label — plus round columns. Keys 1/2/3 switch the
# prosperity register, R re-rolls the seed, C toggles roundness.
cargo run --example parametric_hall
# Click-to-evolve buildings with procedural textures.
cargo run --example detailed_villa
cargo run --example medieval_castle
```
## License
MIT