#![allow(dead_code)]
use bevy::{color::palettes::css::WHITE, prelude::*};
use bevy_symbios_shape::{ShapeMeshCache, ShapeRegistry, SpawnShapeExt};
use bevy_symbios_texture::async_gen::TextureReady;
use rand::{SeedableRng, rngs::StdRng};
use symbios_genetics::Genotype;
use symbios_shape::{
Axis, Interpreter, Quat as DQuat, Scope, ShapeOp, SplitSize, Vec3 as DVec3,
expr::Expr,
genetics::ShapeGenotype,
ops::{CompTarget, FaceSelector, OffsetSelector, SplitEntry},
};
#[derive(Component)]
pub struct PendingMaterialTexture(pub Handle<StandardMaterial>);
#[derive(Resource, Default)]
pub struct BuildingState {
pub root: Option<Entity>,
pub generation: u64,
pub genotype: Option<ShapeGenotype>,
}
#[derive(Resource)]
pub struct MutationConfig {
pub grammar_strength: f32,
pub root_rule: String,
pub trigger_button: MouseButton,
}
impl Default for MutationConfig {
fn default() -> Self {
Self {
grammar_strength: 0.4,
root_rule: "Lot".into(),
trigger_button: MouseButton::Left,
}
}
}
#[derive(Resource)]
pub struct InterpreterFactory(pub Box<dyn Fn() -> Interpreter + Send + Sync>);
#[derive(Resource)]
pub struct ScopeComputer(pub Box<dyn Fn(&ShapeGenotype) -> DVec3 + Send + Sync>);
pub fn setup_lighting(mut commands: Commands) {
commands.insert_resource(GlobalAmbientLight {
color: WHITE.into(),
brightness: 800.0,
..default()
});
commands.spawn((
DirectionalLight {
illuminance: 12_000.0,
shadow_maps_enabled: true,
..default()
},
Transform::from_xyz(20.0, 35.0, -15.0).looking_at(Vec3::ZERO, Vec3::Y),
));
}
pub fn apply_textures(
mut commands: Commands,
ready_q: Query<(Entity, &TextureReady, &PendingMaterialTexture)>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
for (entity, ready, PendingMaterialTexture(mat_handle)) in &ready_q {
if let Some(mut mat) = materials.get_mut(mat_handle) {
mat.base_color_texture = Some(ready.0.albedo.clone());
mat.normal_map_texture = Some(ready.0.normal.clone());
mat.metallic_roughness_texture = Some(ready.0.roughness.clone());
mat.occlusion_texture = Some(ready.0.roughness.clone());
}
commands.entity(entity).despawn();
}
}
#[allow(clippy::too_many_arguments)] pub fn spawn_building(
mut commands: Commands,
factory: Res<InterpreterFactory>,
scope_computer: Res<ScopeComputer>,
config: Res<MutationConfig>,
registry: Res<ShapeRegistry>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut cache: ResMut<ShapeMeshCache>,
mut state: ResMut<BuildingState>,
) {
let interp = (factory.0)();
let mut genotype = ShapeGenotype::from_interpreter(&interp);
let scope = (scope_computer.0)(&genotype);
propagate_normalize(&mut genotype, scope, &config.root_rule);
state.genotype = Some(genotype);
let footprint = Scope::new(DVec3::ZERO, DQuat::IDENTITY, scope);
match commands.spawn_shape(
&interp,
footprint,
&config.root_rule,
®istry,
&mut meshes,
&mut materials,
&mut cache,
) {
Ok(entity) => {
info!("Spawned building (Entity {:?})", entity);
state.root = Some(entity);
}
Err(e) => error!("Derivation failed: {e}"),
}
}
#[allow(clippy::too_many_arguments)] pub fn mutate_on_click(
mouse_input: Res<ButtonInput<MouseButton>>,
mut commands: Commands,
mut state: ResMut<BuildingState>,
scope_computer: Res<ScopeComputer>,
config: Res<MutationConfig>,
registry: Res<ShapeRegistry>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut cache: ResMut<ShapeMeshCache>,
) {
if !mouse_input.just_pressed(config.trigger_button) {
return;
}
if let Some(root) = state.root.take() {
commands.entity(root).despawn();
}
state.generation += 1;
let generation = state.generation;
let mut rng = StdRng::seed_from_u64(generation);
let Some(ref mut genotype) = state.genotype else {
error!("BuildingState genotype not initialised — skipping mutation");
return;
};
genotype.mutate(&mut rng, config.grammar_strength);
let scope = (scope_computer.0)(genotype);
propagate_normalize(genotype, scope, &config.root_rule);
let interp = genotype.to_interpreter();
let footprint = Scope::new(DVec3::ZERO, DQuat::IDENTITY, scope);
match commands.spawn_shape(
&interp,
footprint,
&config.root_rule,
®istry,
&mut meshes,
&mut materials,
&mut cache,
) {
Ok(entity) => {
info!(
"Mutated building to generation {} (Entity {:?})",
generation, entity
);
state.root = Some(entity);
}
Err(e) => error!("Derivation failed on mutation: {e}"),
}
}
pub fn sum_absolute_splits(genotype: &ShapeGenotype, rule: &str) -> f64 {
genotype
.rules
.get(rule)
.and_then(|def| def.variants.first())
.and_then(|v| {
v.ops.iter().find_map(|op| {
if let ShapeOp::Split { entries, .. } = op {
let s: f64 = entries
.iter()
.filter_map(SplitEntry::as_slot)
.filter_map(|sl| match &sl.size {
SplitSize::Absolute(e) => e.as_lit(),
_ => None,
})
.sum();
(s > 0.0).then_some(s)
} else {
None
}
})
})
.unwrap_or(0.0)
}
pub fn propagate_normalize(genotype: &mut ShapeGenotype, lot: DVec3, root_rule: &str) {
use std::collections::{HashMap, VecDeque};
let mut queue: VecDeque<(String, f64, f64, f64)> = VecDeque::new();
let mut min_scope: HashMap<String, (f64, f64, f64)> = HashMap::new();
queue.push_back((root_rule.to_string(), lot.x, 0.0, lot.z));
while let Some((rule_name, avail_x, avail_y, avail_z)) = queue.pop_front() {
let new_or_tighter = match min_scope.get(&rule_name) {
None => true,
Some(&(mx, my, mz)) => avail_x < mx || avail_y < my || avail_z < mz,
};
if !new_or_tighter {
continue;
}
let entry = min_scope
.entry(rule_name.clone())
.or_insert((avail_x, avail_y, avail_z));
entry.0 = entry.0.min(avail_x);
entry.1 = entry.1.min(avail_y);
entry.2 = entry.2.min(avail_z);
let variants_count = genotype
.rules
.get(&rule_name)
.map(|def| def.variants.len())
.unwrap_or(0);
for v_idx in 0..variants_count {
let ops = genotype.rules.get(&rule_name).unwrap().variants[v_idx]
.ops
.clone();
let mut sx = avail_x;
let mut sy = avail_y;
let mut sz = avail_z;
for (i, op) in ops.iter().enumerate() {
match op {
ShapeOp::Extrude(h) => {
if let Some(v) = h.as_lit() {
sy = v;
}
}
ShapeOp::Scale(v) => {
sx *= v[0].as_lit().unwrap_or(1.0);
sy *= v[1].as_lit().unwrap_or(1.0);
sz *= v[2].as_lit().unwrap_or(1.0);
}
ShapeOp::Split { axis, .. } => {
let dim = match axis {
Axis::X => sx,
Axis::Y => sy,
Axis::Z => sz,
};
let mut entries = if let ShapeOp::Split { entries, .. } = &ops[i] {
entries.clone()
} else {
continue;
};
let lit_abs_sum = |entries: &[SplitEntry]| -> f64 {
entries
.iter()
.filter_map(SplitEntry::as_slot)
.filter_map(|s| match &s.size {
SplitSize::Absolute(e) => e.as_lit(),
_ => None,
})
.sum()
};
let mut abs_sum = lit_abs_sum(&entries);
if abs_sum > dim {
let scale = dim / abs_sum * 0.95;
for entry in entries.iter_mut() {
if let SplitEntry::Slot(slot) = entry
&& let SplitSize::Absolute(e) = &mut slot.size
&& let Some(v) = e.as_lit()
{
*e = Expr::lit((v * scale).max(0.01));
}
}
if let Some(def) = genotype.rules.get_mut(&rule_name)
&& let ShapeOp::Split {
entries: ref mut v_entries,
..
} = def.variants[v_idx].ops[i]
{
*v_entries = entries.clone();
}
abs_sum = lit_abs_sum(&entries);
}
let float_weight: f64 = entries
.iter()
.filter_map(SplitEntry::as_slot)
.filter_map(|s| match &s.size {
SplitSize::Floating(e) => e.as_lit(),
_ => None,
})
.sum();
let float_space = (dim - abs_sum).max(0.0);
for slot in entries.iter().filter_map(SplitEntry::as_slot) {
let child_dim = match &slot.size {
SplitSize::Absolute(e) => e.as_lit().unwrap_or(0.0),
SplitSize::Floating(e) => {
let v = e.as_lit().unwrap_or(0.0);
if float_weight > 0.0 {
v / float_weight * float_space
} else {
0.0
}
}
SplitSize::Relative(e) => e.as_lit().unwrap_or(0.0) * dim,
};
if child_dim <= 0.0 {
continue;
}
let (cx, cy, cz) = match axis {
Axis::X => (child_dim, sy, sz),
Axis::Y => (sx, child_dim, sz),
Axis::Z => (sx, sy, child_dim),
};
queue.push_back((slot.rule.name.clone(), cx, cy, cz));
}
}
ShapeOp::Comp(CompTarget::Faces(cases)) => {
for case in cases {
let (fx, fy) = match case.selector {
FaceSelector::Front | FaceSelector::Back => (sx, sy),
FaceSelector::Left | FaceSelector::Right => (sz, sy),
FaceSelector::Top | FaceSelector::Bottom => (sx, sz),
FaceSelector::Side | FaceSelector::All => (sx.min(sz), sy),
};
queue.push_back((case.rule.name.clone(), fx, fy, 0.0));
}
}
ShapeOp::Repeat {
axis,
tile_sizes,
rule,
} => {
for tile_size in tile_sizes {
let tile = tile_size.as_lit().unwrap_or(0.0);
if tile <= 0.0 {
continue;
}
let (rx, ry, rz) = match axis {
Axis::X => (tile, sy, sz),
Axis::Y => (sx, tile, sz),
Axis::Z => (sx, sy, tile),
};
queue.push_back((rule.name.clone(), rx, ry, rz));
}
}
ShapeOp::Rule(call) => {
queue.push_back((call.name.clone(), sx, sy, sz));
}
ShapeOp::Roof { cases, .. } => {
for case in cases {
queue.push_back((case.rule.name.clone(), sx, sy, sz));
}
}
ShapeOp::Offset { distance, cases } => {
let inset = -distance.as_lit().unwrap_or(0.0);
let mut inner_x = sx;
let mut inner_y = sy;
if inset > 0.0 {
inner_x = (sx - 2.0 * inset).max(0.1);
inner_y = (sy - 2.0 * inset).max(0.1);
}
for case in cases {
if case.selector == OffsetSelector::Inside {
queue.push_back((case.rule.name.clone(), inner_x, inner_y, sz));
} else {
queue.push_back((case.rule.name.clone(), sx, sy, sz));
}
}
}
_ => {}
}
}
}
}
}