Bevy Forge 0.1.0: visual scene editor for Bevy 0.20 BSN

This commit is contained in:
Emil Shanaty
2026-10-27 18:52:10 +03:00
commit 13bf16d510
79 changed files with 44353 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
# Local Bevy build cache (several GB on the first check).
/target/
+4232
View File
File diff suppressed because it is too large Load Diff
+29
View File
@@ -0,0 +1,29 @@
[package]
name = "bsn-smoke"
version = "0.1.0"
edition = "2021"
publish = false
description = "Compiles Bevy Forge's generated BSN against real Bevy 0.20.0 (excluded verification crate)"
# Standalone crate: it is excluded from the Bevy Forge workspace so that the
# editor/core/CLI crates never depend on Bevy. Do not add this to the root
# workspace.
[workspace]
[dependencies]
# Keep this feature set in sync with `.swarm/recon/R02.md`: it is the minimal
# set that provides scenes, sprites, text, 3D primitives/materials, cameras and
# lights for the code-generation smoke test.
bevy = { version = "0.20", default-features = false, features = [
"std",
"bevy_scene",
"bevy_sprite",
"bevy_asset",
"bevy_color",
"bevy_text",
"bevy_pbr",
"bevy_mesh",
"bevy_camera",
"bevy_light",
"bevy_image",
] }
+126
View File
@@ -0,0 +1,126 @@
# bsn-smoke — Bevy 0.20 generated-code compile check
`bsn-smoke` is Bevy Forge's proof that exported scenes are **real Bevy 0.20 code**, not
just plausible-looking text. It is the only crate in this repository that depends on
Bevy; it is excluded from the workspace (`Cargo.toml` → `exclude = ["compat/bsn-smoke"]`)
so the editor, core and CLI crates stay Bevy-free and build in seconds.
```text
compat/bsn-smoke/
├── Cargo.toml standalone package (empty [workspace]), pinned Bevy 0.20.0
├── Cargo.lock lockfile for the verification dependency graph
├── src/lib.rs hand-written scenes mirroring every codegen shape + support types
├── src/generated.rs BSN export of the 3D garden demo (written by the gate script)
├── src/generated_2d.rs BSN export of the 2D platformer demo (written by the gate script)
└── src/rust_export.rs imperative `Commands::spawn` export shape, also compiled
```
## What is verified
Compile-checked against `bevy 0.20.0` (`default-features = false`):
- **Roots:** single root `bsn!` / `-> impl Scene`; multiple roots `bsn_list!` /
`-> impl SceneList` with `--` separators.
- **Hierarchy:** nested `Children [ ... -- ... ]`, including a `Visibility` patch on a
grandchild and components before/after `Children`.
- **Names and transforms:** `Name("...")`, full three-field `Transform` with
`Quat::from_euler(EulerRot::XYZ, x.to_radians(), ...)`.
- **Sprites:** image path handles, `Color::srgba`, `flip_x`/`flip_y`, and braced
`custom_size: { Some(Vec2::new(w, h)) }` (the brace form is mandatory in BSN patches).
- **Text:** `Text2d`, `TextFont { font_size: FontSize::Px(..) }`, `TextColor`.
- **Cameras:** bare `Camera2d`, `Camera3d::default()`.
- **Lights:** `PointLight` (color/intensity/range/shadows) and `DirectionalLight`
(color/illuminance/shadows).
- **Custom components:** struct-with-fields patches (`Health { current: 10.0, max: 10.0 }`,
`forge_demo::Orbit { radius: 2.4, speed: 0.45 }`) and bare unit-component patches
(`Marker`).
- **3D meshes:** all five primitive shapes (`Cuboid`, `Sphere`, `Plane3d`, `Cylinder`,
`Capsule3d`) with braced method-call handles (`Mesh3d({ meshes.add(..) })`) and inline
materials (`MeshMaterial3d::<StandardMaterial>({ materials.add(StandardMaterial { ..default() }) })`),
plus an asset mesh path (`Mesh3d("models/...#Mesh0/Primitive0")`) with an inline material.
- **Generated signatures:** mesh scenes take `&mut Assets<Mesh>` and
`&mut Assets<StandardMaterial>` only when needed.
- **Imperative Rust export shape** (`src/rust_export.rs`): `commands.spawn`,
`with_children`, `Name::new`, `Transform::from_xyz(..).with_rotation(..).with_scale(..)`,
`Sprite { ..default() }`, `Mesh3d`/`MeshMaterial3d`, `Text2d::new`, `TextFont`,
`TextColor`, `Camera3d::default()`, lights and `Visibility`.
The committed `src/generated*.rs` files are the CLI exports produced by the last
successful gate run (plus the shim described below), so the crate compiles on a fresh
checkout even before the script runs. `scripts/check-generated-code.sh` regenerates them
from the demo project and recompiles, verifying the generator + demo scenes end to end.
The exports are deterministic, so re-running the gate leaves the files unchanged.
### Compatibility shim for custom type paths
Generated BSN references custom components by their project type path, e.g.
`forge_demo::Orbit`. In a real Bevy project the generated function is pasted where that
path is already in scope. Here the generated file is a module, so the gate script prepends
four lines (`#![allow(unused_imports)]`, two comments, and `use crate::*;`) to expose the
crate-root support types; **the generated code itself is appended unchanged**. `lib.rs`
provides `forge_demo::Orbit` with the same fields as `examples/demo-project/src/main.rs`.
If a demo scene introduces another custom type, add its support type to `lib.rs`.
## Bevy version and feature set
Pinned to **Bevy 0.20.0** through `Cargo.lock`. Feature set (chosen by
`.swarm/recon/R02.md` to keep compile time and APIs aligned with the verified BSN forms):
```toml
bevy = { version = "0.20", default-features = false, features = [
"std", "bevy_scene", "bevy_sprite", "bevy_asset", "bevy_color", "bevy_text",
"bevy_pbr", "bevy_mesh", "bevy_camera", "bevy_light", "bevy_image",
] }
```
No windowing or audio backends are compiled in and no GPU device is initialized: this is a
**compile** check that needs no display, asset files or `App`.
## Running
Directly (the load-bearing check):
```bash
cd compat/bsn-smoke
CARGO_NET_OFFLINE=true cargo check
```
End to end (generator → CLI → Bevy compile):
```bash
bash scripts/check-generated-code.sh
```
The script exports `examples/demo-project/scenes/garden.forge.ron` and
`.../platformer.forge.ron` with `cargo run -q -p bevy_forge_cli -- export ... --format bsn`,
writes the results into this crate, and runs the check. It fails loudly on any error; if
the CLI or demo project does not exist yet it fails with a clear message instead of
silently passing. `FORGE_SKIP_GENERATED=1` skips with an explicit message (bootstrap only);
`FORGE_SKIP_SECOND_SCENE=1` skips the 2D export; `FORGE_GARDEN_SCENE` /
`FORGE_PLATFORMER_SCENE` override the scene paths; `CARGO_TARGET_DIR` is passed through.
## Expected cost
Measured on this Linux machine (Rust 1.99.1, 2026-10):
| Build | Wall time | `target/` size |
|---|---|---|
| First `cargo check`, cold target dir | **2 min 15 s** | ~803 MB |
| Subsequent `cargo check`, warm target dir | < 1 s | — |
| Reusing an existing Bevy 0.20 `target/` (e.g. recon) | ~1 s | — |
`compat/bsn-smoke/target/` is ignored by `.gitignore`; never commit it. If you already
have a warm Bevy 0.20 target directory, point `CARGO_TARGET_DIR` at it to avoid the cold
build (the dependency feature set must match exactly).
## Runtime caveats (compile ≠ loaded assets)
- Asset **paths** (`Sprite { image: "textures/x.png" }`, `Mesh3d("models/...gltf#...")`)
compile, but Bevy turns them into `HandleTemplate::Path` values that are resolved by the
`AssetServer` at spawn time. If the project has no loader/asset for the path, the handle
stays loading/failed and nothing renders — it is not a compile error (see R02 §9).
- Bevy Forge never emits material **asset paths** (`MeshMaterial3d::<StandardMaterial>("...")`),
because those need a project-registered material loader; materials are always inline
`materials.add(StandardMaterial { ..default() })` handles.
- The check is compile-only: it does not spawn the scenes or validate asset existence.
Asset references are validated separately by `forge validate` / the editor.
+130
View File
@@ -0,0 +1,130 @@
// compat/bsn-smoke shim: expose crate-root support types (demo custom components).
// The generated code below this line is unchanged.
#![allow(unused_imports)] // the crate::* glob also provides the Bevy prelude names
use crate::*;
use bevy::prelude::*;
use bevy::scene::bsn_list;
pub fn scene(
meshes: &mut Assets<Mesh>,
materials: &mut Assets<StandardMaterial>,
) -> impl SceneList {
bsn_list! {
Name("Camera")
Transform {
translation: Vec3::new(7.5, 5.0, 12.0),
rotation: Quat::from_euler(EulerRot::XYZ, -16.0f32.to_radians(), 30.0f32.to_radians(), 0.0f32.to_radians()),
scale: Vec3::new(1.0, 1.0, 1.0),
}
Camera3d::default()
--
Name("Sun")
Transform {
translation: Vec3::new(0.0, 0.0, 0.0),
rotation: Quat::from_euler(EulerRot::XYZ, -55.0f32.to_radians(), -25.0f32.to_radians(), 0.0f32.to_radians()),
scale: Vec3::new(1.0, 1.0, 1.0),
}
DirectionalLight { color: Color::srgba(1.0, 0.95, 0.85, 1.0), illuminance: 11000.0, shadow_maps_enabled: true }
--
Name("Ground")
Transform {
translation: Vec3::new(0.0, 0.0, 0.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians()),
scale: Vec3::new(1.0, 1.0, 1.0),
}
Mesh3d({ meshes.add(Plane3d::new(Vec3::Y, Vec2::new(13.0, 13.0))) })
MeshMaterial3d::<StandardMaterial>({ materials.add(StandardMaterial {
base_color: Color::srgba(0.31, 0.48, 0.23, 1.0),
metallic: 0.0,
perceptual_roughness: 0.92,
unlit: false,
..default()
}) })
--
Name("Shrub")
Transform {
translation: Vec3::new(-3.4, 1.0, 1.8),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians()),
scale: Vec3::new(1.25, 0.9, 1.25),
}
Mesh3d({ meshes.add(Sphere::new(1.0)) })
MeshMaterial3d::<StandardMaterial>({ materials.add(StandardMaterial {
base_color: Color::srgba(0.24, 0.45, 0.22, 1.0),
metallic: 0.0,
perceptual_roughness: 0.85,
unlit: false,
..default()
}) })
--
Name("Flower Stem")
Transform {
translation: Vec3::new(2.6, 0.75, 1.4),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians()),
scale: Vec3::new(1.0, 1.0, 1.0),
}
Mesh3d({ meshes.add(Cylinder::new(0.09, 1.5)) })
MeshMaterial3d::<StandardMaterial>({ materials.add(StandardMaterial {
base_color: Color::srgba(0.35, 0.55, 0.27, 1.0),
metallic: 0.0,
perceptual_roughness: 0.8,
unlit: false,
..default()
}) })
Children [
Name("Flower Bloom")
Transform {
translation: Vec3::new(0.0, 1.0, 0.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians()),
scale: Vec3::new(1.0, 0.75, 1.0),
}
Mesh3d({ meshes.add(Sphere::new(0.42)) })
MeshMaterial3d::<StandardMaterial>({ materials.add(StandardMaterial {
base_color: Color::srgba(0.91, 0.42, 0.65, 1.0),
metallic: 0.0,
perceptual_roughness: 0.4,
unlit: false,
..default()
}) })
]
--
Name("Orbit Crystal")
Transform {
translation: Vec3::new(0.0, 2.0, 0.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians()),
scale: Vec3::new(1.0, 1.0, 1.0),
}
Mesh3d({ meshes.add(Sphere::new(0.8)) })
MeshMaterial3d::<StandardMaterial>({ materials.add(StandardMaterial {
base_color: Color::srgba(0.27, 0.76, 0.69, 1.0),
metallic: 0.15,
perceptual_roughness: 0.2,
unlit: false,
..default()
}) })
forge_demo::Orbit { radius: 2.4, speed: 0.45 }
--
Name("Lantern Post")
Transform {
translation: Vec3::new(3.8, 1.2, -1.2),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians()),
scale: Vec3::new(1.0, 1.0, 1.0),
}
Mesh3d({ meshes.add(Cylinder::new(0.12, 2.4)) })
MeshMaterial3d::<StandardMaterial>({ materials.add(StandardMaterial {
base_color: Color::srgba(0.18, 0.2, 0.25, 1.0),
metallic: 0.6,
perceptual_roughness: 0.35,
unlit: false,
..default()
}) })
Children [
Name("Lantern Glow")
Transform {
translation: Vec3::new(0.0, 1.25, 0.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians()),
scale: Vec3::new(1.0, 1.0, 1.0),
}
PointLight { color: Color::srgba(1.0, 0.82, 0.55, 1.0), intensity: 600.0, range: 12.0, shadow_maps_enabled: true }
]
}
}
+191
View File
@@ -0,0 +1,191 @@
// compat/bsn-smoke shim: expose crate-root support types (demo custom components).
// The generated code below this line is unchanged.
#![allow(unused_imports)] // the crate::* glob also provides the Bevy prelude names
use crate::*;
use bevy::prelude::*;
use bevy::scene::bsn_list;
pub fn scene() -> impl SceneList {
bsn_list! {
Name("Backdrop")
Transform {
translation: Vec3::new(0.0, 0.0, -30.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians()),
scale: Vec3::new(1.0, 1.0, 1.0),
}
Sprite { image: "textures/background.png", color: Color::srgba(1.0, 1.0, 1.0, 1.0), custom_size: { Some(Vec2::new(960.0, 540.0)) }, flip_x: false, flip_y: false }
--
Name("Camera")
Transform {
translation: Vec3::new(0.0, 0.0, 10.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians()),
scale: Vec3::new(1.0, 1.0, 1.0),
}
Camera2d
--
Name("Title")
Transform {
translation: Vec3::new(0.0, 214.0, 5.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians()),
scale: Vec3::new(1.0, 1.0, 1.0),
}
Text2d("Bevy Forge Platformer")
TextFont { font_size: FontSize::Px(44.0) }
TextColor(Color::srgba(0.96, 0.94, 0.88, 1.0))
--
Name("Hint")
Transform {
translation: Vec3::new(0.0, 176.0, 5.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians()),
scale: Vec3::new(1.0, 1.0, 1.0),
}
Text2d("Collect the coins!")
TextFont { font_size: FontSize::Px(20.0) }
TextColor(Color::srgba(0.95, 0.67, 0.25, 1.0))
--
Name("Ground 1")
Transform {
translation: Vec3::new(-336.0, -224.0, -2.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians()),
scale: Vec3::new(1.0, 1.0, 1.0),
}
Sprite { image: "textures/tile.png", color: Color::srgba(1.0, 1.0, 1.0, 1.0), custom_size: { Some(Vec2::new(96.0, 96.0)) }, flip_x: false, flip_y: false }
--
Name("Ground 2")
Transform {
translation: Vec3::new(-240.0, -224.0, -2.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians()),
scale: Vec3::new(1.0, 1.0, 1.0),
}
Sprite { image: "textures/tile.png", color: Color::srgba(1.0, 1.0, 1.0, 1.0), custom_size: { Some(Vec2::new(96.0, 96.0)) }, flip_x: false, flip_y: false }
--
Name("Ground 3")
Transform {
translation: Vec3::new(-144.0, -224.0, -2.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians()),
scale: Vec3::new(1.0, 1.0, 1.0),
}
Sprite { image: "textures/tile.png", color: Color::srgba(1.0, 1.0, 1.0, 1.0), custom_size: { Some(Vec2::new(96.0, 96.0)) }, flip_x: false, flip_y: false }
--
Name("Ground 4")
Transform {
translation: Vec3::new(-48.0, -224.0, -2.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians()),
scale: Vec3::new(1.0, 1.0, 1.0),
}
Sprite { image: "textures/tile.png", color: Color::srgba(1.0, 1.0, 1.0, 1.0), custom_size: { Some(Vec2::new(96.0, 96.0)) }, flip_x: false, flip_y: false }
--
Name("Ground 5")
Transform {
translation: Vec3::new(48.0, -224.0, -2.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians()),
scale: Vec3::new(1.0, 1.0, 1.0),
}
Sprite { image: "textures/tile.png", color: Color::srgba(1.0, 1.0, 1.0, 1.0), custom_size: { Some(Vec2::new(96.0, 96.0)) }, flip_x: false, flip_y: false }
--
Name("Ground 6")
Transform {
translation: Vec3::new(144.0, -224.0, -2.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians()),
scale: Vec3::new(1.0, 1.0, 1.0),
}
Sprite { image: "textures/tile.png", color: Color::srgba(1.0, 1.0, 1.0, 1.0), custom_size: { Some(Vec2::new(96.0, 96.0)) }, flip_x: false, flip_y: false }
--
Name("Ground 7")
Transform {
translation: Vec3::new(240.0, -224.0, -2.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians()),
scale: Vec3::new(1.0, 1.0, 1.0),
}
Sprite { image: "textures/tile.png", color: Color::srgba(1.0, 1.0, 1.0, 1.0), custom_size: { Some(Vec2::new(96.0, 96.0)) }, flip_x: false, flip_y: false }
--
Name("Ground 8")
Transform {
translation: Vec3::new(336.0, -224.0, -2.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians()),
scale: Vec3::new(1.0, 1.0, 1.0),
}
Sprite { image: "textures/tile.png", color: Color::srgba(1.0, 1.0, 1.0, 1.0), custom_size: { Some(Vec2::new(96.0, 96.0)) }, flip_x: false, flip_y: false }
--
Name("Ledge 1")
Transform {
translation: Vec3::new(-48.0, -64.0, -2.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians()),
scale: Vec3::new(1.0, 1.0, 1.0),
}
Sprite { image: "textures/tile.png", color: Color::srgba(1.0, 1.0, 1.0, 1.0), custom_size: { Some(Vec2::new(96.0, 96.0)) }, flip_x: false, flip_y: false }
--
Name("Ledge 2")
Transform {
translation: Vec3::new(48.0, -64.0, -2.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians()),
scale: Vec3::new(1.0, 1.0, 1.0),
}
Sprite { image: "textures/tile.png", color: Color::srgba(1.0, 1.0, 1.0, 1.0), custom_size: { Some(Vec2::new(96.0, 96.0)) }, flip_x: false, flip_y: false }
--
Name("Ledge 3")
Transform {
translation: Vec3::new(144.0, -64.0, -2.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians()),
scale: Vec3::new(1.0, 1.0, 1.0),
}
Sprite { image: "textures/tile.png", color: Color::srgba(1.0, 1.0, 1.0, 1.0), custom_size: { Some(Vec2::new(96.0, 96.0)) }, flip_x: false, flip_y: false }
--
Name("Player")
Transform {
translation: Vec3::new(-192.0, -128.0, 0.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians()),
scale: Vec3::new(1.0, 1.0, 1.0),
}
Sprite { image: "textures/player.png", color: Color::srgba(1.0, 1.0, 1.0, 1.0), custom_size: { Some(Vec2::new(64.0, 96.0)) }, flip_x: false, flip_y: false }
Children [
Name("Hat")
Transform {
translation: Vec3::new(0.0, 42.0, 1.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians()),
scale: Vec3::new(1.0, 1.0, 1.0),
}
Sprite { image: "textures/hat.png", color: Color::srgba(1.0, 1.0, 1.0, 1.0), custom_size: { Some(Vec2::new(56.0, 28.0)) }, flip_x: false, flip_y: false }
]
--
Name("Coin 1")
Transform {
translation: Vec3::new(-288.0, -104.0, 1.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians()),
scale: Vec3::new(1.0, 1.0, 1.0),
}
Sprite { image: "textures/coin.png", color: Color::srgba(1.0, 1.0, 1.0, 1.0), custom_size: { Some(Vec2::new(48.0, 48.0)) }, flip_x: false, flip_y: false }
--
Name("Coin 2")
Transform {
translation: Vec3::new(48.0, 28.0, 1.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians()),
scale: Vec3::new(1.0, 1.0, 1.0),
}
Sprite { image: "textures/coin.png", color: Color::srgba(1.0, 1.0, 1.0, 1.0), custom_size: { Some(Vec2::new(48.0, 48.0)) }, flip_x: false, flip_y: false }
--
Name("Coin 3")
Transform {
translation: Vec3::new(240.0, -96.0, 1.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians()),
scale: Vec3::new(1.0, 1.0, 1.0),
}
Sprite { image: "textures/coin.png", color: Color::srgba(1.0, 1.0, 1.0, 1.0), custom_size: { Some(Vec2::new(48.0, 48.0)) }, flip_x: false, flip_y: false }
--
Name("Crate")
Transform {
translation: Vec3::new(352.0, -144.0, -1.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians()),
scale: Vec3::new(1.0, 1.0, 1.0),
}
Sprite { image: "textures/crate.png", color: Color::srgba(1.0, 1.0, 1.0, 1.0), custom_size: { Some(Vec2::new(64.0, 64.0)) }, flip_x: false, flip_y: false }
--
Name("Crate 2")
Transform {
translation: Vec3::new(352.0, -80.0, -1.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians()),
scale: Vec3::new(1.0, 1.0, 1.0),
}
Sprite { image: "textures/crate.png", color: Color::srgba(1.0, 1.0, 1.0, 1.0), custom_size: { Some(Vec2::new(64.0, 64.0)) }, flip_x: false, flip_y: false }
}
}
+334
View File
@@ -0,0 +1,334 @@
//! Compile-smoke crate for Bevy Forge's generated code.
//!
//! This crate is intentionally **not** a workspace member: it is the only place
//! in the repository that depends on Bevy (0.20.0). It compiles:
//!
//! * hand-written scenes that mirror `ExportFormat::Bsn` output for every
//! supported component, hierarchy shape and asset pattern (this file), and
//! * the real files written by `scripts/check-generated-code.sh`
//! (`src/generated.rs`, `src/generated_2d.rs`).
//!
//! See `README.md` for the feature set, commands and expected build cost.
//! The generated code targets the compile-verified forms in `.swarm/recon/R02.md`.
use bevy::prelude::*;
use bevy::scene::bsn;
use bevy::scene::bsn_list;
/// Bevy Forge's BSN export for `examples/demo-project/scenes/garden.forge.ron`.
///
/// Committed with a harmless starter scene so this crate compiles before the
/// end-to-end gate has run; `scripts/check-generated-code.sh` replaces it with
/// the real CLI export.
#[path = "generated.rs"]
pub mod generated;
/// Bevy Forge's BSN export for `examples/demo-project/scenes/platformer.forge.ron`.
#[path = "generated_2d.rs"]
pub mod generated_2d;
/// Imperative `Commands::spawn` export shape (`ExportFormat::Rust`), mirroring
/// what the CLI emits. It is compiled here so the secondary export format is
/// verified against real Bevy types as well.
pub mod rust_export;
/// A custom component with fields, as emitted for `ComponentData::Custom`.
///
/// `Component + Default + Clone` is enough for Bevy 0.20's blanket
/// `FromTemplate` implementation (see R02 §4), so generated
/// `Health { current: 10.0, max: 10.0 }` patches compile.
#[derive(Component, Default, Clone)]
pub struct Health {
pub current: f32,
pub max: f32,
}
/// A unit custom component: Bevy Forge emits the bare type path when a custom
/// component has no fields.
#[derive(Component, Default, Clone)]
pub struct Marker;
/// Support types from `examples/demo-project/src/main.rs`.
///
/// The demo garden scene carries a custom `forge_demo::Orbit` component, so the
/// generated garden BSN references that path. Providing the module here lets the
/// export compile in the smoke crate without editing the generated file.
pub mod forge_demo {
use bevy::prelude::*;
/// Slowly orbits an entity around the world origin (demo project component).
#[derive(Component, Default, Clone)]
pub struct Orbit {
pub radius: f32,
pub speed: f32,
}
}
/// 2D scene: single root, `Name`/`Transform`/`Sprite` with image and braced
/// `custom_size`, a custom component with fields, a unit custom component, and
/// nested `Children` (including a visibility patch on a grandchild).
///
/// Mirrors the shape generated for a typical platformer root entity.
pub fn scene_2d() -> impl Scene {
bsn! {
Name("Player")
Transform {
translation: Vec3::new(0.0, 1.0, 0.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0, 0.0, 0.0),
scale: Vec3::new(1.0, 1.0, 1.0),
}
Sprite {
image: "textures/player.png",
color: Color::srgba(1.0, 1.0, 1.0, 1.0),
custom_size: { Some(Vec2::new(64.0, 64.0)) },
flip_x: false,
flip_y: false,
}
Health { current: 10.0, max: 10.0 }
forge_demo::Orbit { radius: 2.5, speed: 0.8 }
Children [
Name("Hat")
Transform {
translation: Vec3::new(0.0, 40.0, 0.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0, 0.0, 0.0),
scale: Vec3::new(1.0, 1.0, 1.0),
}
Sprite { color: Color::srgba(1.0, 0.5, 0.2, 1.0) }
Marker
Children [
Name("Glow")
Transform {
translation: Vec3::new(0.0, 0.5, 0.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0, 0.0, 0.0),
scale: Vec3::new(0.5, 0.5, 1.0),
}
Sprite { color: Color::srgba(1.0, 1.0, 0.4, 0.6) }
Visibility::Visible
]
]
}
}
/// Multiple roots: the generator emits `bsn_list!` plus `--` separators and a
/// `-> impl SceneList` return type.
pub fn scene_2d_list() -> impl SceneList {
bsn_list! {
Name("Left")
Transform {
translation: Vec3::new(-2.0, 0.0, 0.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0, 0.0, 0.0),
scale: Vec3::new(1.0, 1.0, 1.0),
}
Sprite { image: "textures/tile.png", color: Color::srgba(1.0, 1.0, 1.0, 1.0) }
--
Name("Right")
Transform {
translation: Vec3::new(2.0, 0.0, 0.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0, 0.0, 0.0),
scale: Vec3::new(1.0, 1.0, 1.0),
}
Sprite { color: Color::srgba(0.9, 0.6, 0.3, 1.0) }
Children [
Name("Right Child")
Transform {
translation: Vec3::new(0.0, 1.0, 0.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0, 0.0, 0.0),
scale: Vec3::new(1.0, 1.0, 1.0),
}
Sprite { color: Color::srgba(0.2, 0.8, 0.9, 1.0) }
]
}
}
/// Text: `Text2d` payload plus `TextFont`/`TextColor` patches.
pub fn scene_text() -> impl Scene {
bsn! {
Name("Title")
Text2d("Bevy Forge\nHello \"BSN\"")
TextFont { font_size: FontSize::Px(32.0) }
TextColor(Color::srgba(1.0, 0.9, 0.8, 1.0))
Transform {
translation: Vec3::new(0.0, 200.0, 0.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0, 0.0, 0.0),
scale: Vec3::new(1.0, 1.0, 1.0),
}
}
}
/// Bare `Camera2d` patch, as emitted for 2D scenes.
pub fn scene_camera_2d() -> impl Scene {
bsn! {
Name("Main Camera")
Camera2d
Transform {
translation: Vec3::new(0.0, 0.0, 999.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0, 0.0, 0.0),
scale: Vec3::new(1.0, 1.0, 1.0),
}
}
}
/// 3D stage: `Camera3d::default()`, both light types and the `Visibility`
/// variants on children (a single-root scene with three child entities).
pub fn scene_3d_stage() -> impl Scene {
bsn! {
Name("Stage")
Transform {
translation: Vec3::new(0.0, 0.0, 0.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0, 0.0, 0.0),
scale: Vec3::new(1.0, 1.0, 1.0),
}
Children [
Name("Main Camera")
Camera3d::default()
Transform {
translation: Vec3::new(0.0, 4.0, 10.0),
rotation: Quat::from_euler(EulerRot::XYZ, -0.35, 0.0, 0.0),
scale: Vec3::new(1.0, 1.0, 1.0),
}
--
Name("Sun")
DirectionalLight {
color: Color::srgb(1.0, 0.95, 0.9),
illuminance: 10000.0,
shadow_maps_enabled: true,
}
Transform {
translation: Vec3::new(0.0, 8.0, 0.0),
rotation: Quat::from_euler(EulerRot::XYZ, -0.8, 0.4, 0.0),
scale: Vec3::new(1.0, 1.0, 1.0),
}
Visibility::Visible
--
Name("Lantern")
PointLight {
color: Color::srgb(1.0, 0.6, 0.25),
intensity: 800.0,
range: 20.0,
shadow_maps_enabled: true,
}
Transform {
translation: Vec3::new(1.5, 1.0, 1.5),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0, 0.0, 0.0),
scale: Vec3::new(1.0, 1.0, 1.0),
}
Visibility::Inherited
]
}
}
/// 3D primitives and inline materials: the generated function takes the
/// `meshes`/`materials` asset parameters when a scene contains meshes.
///
/// Covers all five `PrimitiveShape` variants from the codegen contract.
pub fn scene_3d_primitives(
meshes: &mut Assets<Mesh>,
materials: &mut Assets<StandardMaterial>,
) -> impl Scene {
bsn! {
Name("Primitives")
Transform {
translation: Vec3::new(0.0, 0.0, 0.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0, 0.0, 0.0),
scale: Vec3::new(1.0, 1.0, 1.0),
}
Children [
Name("Cube")
Mesh3d({ meshes.add(Cuboid::new(1.0, 1.0, 1.0)) })
MeshMaterial3d::<StandardMaterial>({ materials.add(StandardMaterial {
base_color: Color::srgb(0.9, 0.35, 0.3),
metallic: 0.0,
perceptual_roughness: 0.5,
unlit: false,
..default()
}) })
Transform {
translation: Vec3::new(-2.0, 0.5, 0.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0, 0.0, 0.0),
scale: Vec3::new(1.0, 1.0, 1.0),
}
--
Name("Sphere")
Mesh3d({ meshes.add(Sphere::new(0.5)) })
MeshMaterial3d::<StandardMaterial>({ materials.add(StandardMaterial {
base_color: Color::srgb(0.3, 0.6, 0.9),
metallic: 0.0,
perceptual_roughness: 0.5,
unlit: false,
..default()
}) })
Transform {
translation: Vec3::new(-1.0, 0.5, 0.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0, 0.0, 0.0),
scale: Vec3::new(1.0, 1.0, 1.0),
}
--
Name("Plane")
Mesh3d({ meshes.add(Plane3d::new(Vec3::Y, Vec2::new(6.0, 6.0))) })
MeshMaterial3d::<StandardMaterial>({ materials.add(StandardMaterial {
base_color: Color::srgb(0.35, 0.5, 0.3),
metallic: 0.0,
perceptual_roughness: 0.9,
unlit: false,
..default()
}) })
Transform {
translation: Vec3::new(0.0, 0.0, 0.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0, 0.0, 0.0),
scale: Vec3::new(1.0, 1.0, 1.0),
}
--
Name("Cylinder")
Mesh3d({ meshes.add(Cylinder::new(0.4, 1.6)) })
MeshMaterial3d::<StandardMaterial>({ materials.add(StandardMaterial {
base_color: Color::srgb(0.8, 0.7, 0.25),
metallic: 0.1,
perceptual_roughness: 0.4,
unlit: false,
..default()
}) })
Transform {
translation: Vec3::new(1.0, 0.8, 0.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0, 0.0, 0.0),
scale: Vec3::new(1.0, 1.0, 1.0),
}
--
Name("Capsule")
Mesh3d({ meshes.add(Capsule3d::new(0.3, 1.0)) })
MeshMaterial3d::<StandardMaterial>({ materials.add(StandardMaterial {
base_color: Color::srgb(0.55, 0.4, 0.8),
metallic: 0.0,
perceptual_roughness: 0.6,
unlit: false,
..default()
}) })
Transform {
translation: Vec3::new(2.0, 0.8, 0.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0, 0.0, 0.0),
scale: Vec3::new(1.0, 1.0, 1.0),
}
]
}
}
/// Asset mesh: `Mesh3d("<path>")` uses a path handle while the material is an
/// inline `materials.add(..)` handle (the generator never emits asset material
/// paths, which would need a project-registered loader at runtime).
pub fn scene_asset_mesh(materials: &mut Assets<StandardMaterial>) -> impl Scene {
bsn! {
Name("Crate")
Mesh3d("models/crate.gltf#Mesh0/Primitive0")
MeshMaterial3d::<StandardMaterial>({ materials.add(StandardMaterial {
base_color: Color::srgb(0.6, 0.45, 0.3),
metallic: 0.0,
perceptual_roughness: 0.8,
unlit: false,
..default()
}) })
Transform {
translation: Vec3::new(0.0, 0.5, 0.0),
rotation: Quat::from_euler(EulerRot::XYZ, 0.0, 0.0, 0.0),
scale: Vec3::new(1.0, 1.0, 1.0),
}
}
}
+98
View File
@@ -0,0 +1,98 @@
//! Compile check for Bevy Forge's secondary export format: imperative
//! `Commands::spawn` code (`ExportFormat::Rust`, contract §3.2).
//!
//! This mirrors what a user gets when pasting the Rust export into a system.
//! It is hand-written from the contract shape (A04's generator output is
//! verified separately by its tests); the value here is that every Bevy API call
//! the exporter emits is checked against real Bevy 0.20.0.
use bevy::prelude::*;
use crate::Health;
/// Representative Rust export: a 2D sprite with a child and a custom component,
/// a primitive mesh with an inline material, text and a 3D camera + light.
///
/// The parameter set matches the contract: `commands` always, `asset_server`
/// only because a sprite image is loaded, `meshes`/`materials` only because a
/// primitive mesh is spawned.
pub fn scene(
commands: &mut Commands,
asset_server: &AssetServer,
meshes: &mut Assets<Mesh>,
materials: &mut Assets<StandardMaterial>,
) {
commands
.spawn((
Name::new("Player"),
Transform::from_xyz(0.0, 1.0, 0.0)
.with_rotation(Quat::from_euler(EulerRot::XYZ, 0.0, 0.0, 0.0))
.with_scale(Vec3::new(1.0, 1.0, 1.0)),
Sprite {
image: asset_server.load("textures/player.png"),
color: Color::srgba(1.0, 1.0, 1.0, 1.0),
custom_size: Some(Vec2::new(64.0, 64.0)),
..default()
},
Health {
current: 10.0,
max: 10.0,
},
))
.with_children(|parent| {
parent.spawn((
Name::new("Hat"),
Transform::from_xyz(0.0, 40.0, 0.0),
Sprite {
color: Color::srgba(1.0, 0.5, 0.2, 1.0),
..default()
},
));
});
commands.spawn((
Name::new("Cube"),
Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))),
MeshMaterial3d(materials.add(StandardMaterial {
base_color: Color::srgb(0.9, 0.35, 0.3),
metallic: 0.0,
perceptual_roughness: 0.5,
unlit: false,
..default()
})),
Transform::from_xyz(0.0, 0.5, 0.0),
));
commands.spawn((
Name::new("Title"),
Text2d::new("Bevy Forge"),
TextFont {
font_size: FontSize::Px(32.0),
..default()
},
TextColor(Color::WHITE),
Transform::from_xyz(0.0, 2.0, 0.0),
));
commands.spawn((
Name::new("Main Camera"),
Camera3d::default(),
Transform::from_xyz(0.0, 4.0, 10.0),
));
commands.spawn((
Name::new("Sun"),
DirectionalLight {
illuminance: 10000.0,
shadow_maps_enabled: true,
..default()
},
Transform::default(),
));
commands.spawn((
Name::new("Hidden Helper"),
Transform::default(),
Visibility::Hidden,
));
}