use bevy::color::Color;
use bevy::platform::collections::HashMap;
use bevy::prelude::*;
use symbios_shape::{FaceProfile, Interpreter, Scope, ShapeError};
use crate::mesh::build_profiled_mesh;
use crate::registry::ShapeRegistry;
use crate::transform::scope_to_transform;
pub trait SpawnShapeExt {
fn spawn_shape(
&mut self,
interpreter: &Interpreter,
root_scope: Scope,
root_rule: &str,
registry: &ShapeRegistry,
meshes: &mut Assets<Mesh>,
materials: &mut Assets<StandardMaterial>,
) -> Result<Entity, ShapeError>;
}
impl SpawnShapeExt for Commands<'_, '_> {
fn spawn_shape(
&mut self,
interpreter: &Interpreter,
root_scope: Scope,
root_rule: &str,
registry: &ShapeRegistry,
meshes: &mut Assets<Mesh>,
materials: &mut Assets<StandardMaterial>,
) -> Result<Entity, ShapeError> {
let model = interpreter.derive(root_scope, root_rule)?;
let root = self
.spawn((Transform::default(), Visibility::default()))
.id();
let mut mesh_cache: HashMap<(ProfileKey, u32, u32, u32, bool), Handle<Mesh>> =
HashMap::new();
for terminal in &model.terminals {
let transform = scope_to_transform(&terminal.scope);
if let Some(scene_handle) = registry.get_mesh(&terminal.mesh_id) {
let child = self
.spawn((SceneRoot(scene_handle.clone()), transform))
.id();
self.entity(root).add_child(child);
} else {
let size = Vec3::new(
terminal.scope.size.x as f32,
terminal.scope.size.y as f32,
terminal.scope.size.z as f32,
);
let stretch_uvs =
registry.should_stretch_uvs(&terminal.mesh_id, terminal.material.as_deref());
let mesh_key = (
ProfileKey::from_profile(&terminal.face_profile),
size.x.to_bits(),
size.y.to_bits(),
size.z.to_bits(),
stretch_uvs,
);
let mesh_handle = mesh_cache
.entry(mesh_key)
.or_insert_with(|| {
meshes.add(build_profiled_mesh(
&terminal.face_profile,
size,
stretch_uvs,
))
})
.clone();
let material_handle = registry
.resolve_material(terminal.material.as_deref())
.unwrap_or_else(|| {
let hue = string_to_hue(&terminal.mesh_id);
materials.add(StandardMaterial {
base_color: Color::hsl(hue, 0.4, 0.6),
..Default::default()
})
});
let child = self
.spawn((
Mesh3d(mesh_handle),
MeshMaterial3d(material_handle),
transform,
))
.id();
self.entity(root).add_child(child);
}
}
Ok(root)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum ProfileKey {
Rectangle,
Taper(u32),
Triangle(u32),
Trapezoid(u32, u32),
Polygon(Vec<(u32, u32)>),
}
impl ProfileKey {
fn from_profile(profile: &FaceProfile) -> Self {
match profile {
FaceProfile::Rectangle => Self::Rectangle,
FaceProfile::Taper(t) => Self::Taper((*t as f32).to_bits()),
FaceProfile::Triangle { peak_offset } => {
Self::Triangle((*peak_offset as f32).to_bits())
}
FaceProfile::Trapezoid {
top_width,
offset_x,
} => Self::Trapezoid((*top_width as f32).to_bits(), (*offset_x as f32).to_bits()),
FaceProfile::Polygon(pts) => Self::Polygon(
pts.iter()
.map(|p| ((p.x as f32).to_bits(), (p.y as f32).to_bits()))
.collect(),
),
}
}
}
fn string_to_hue(s: &str) -> f32 {
let hash = s.bytes().fold(5381u32, |acc, b| {
acc.wrapping_mul(31).wrapping_add(b as u32)
});
(hash % 360) as f32
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn string_to_hue_is_in_range() {
for id in ["Window", "Door", "Roof", "Ground", "Wall", ""] {
let h = string_to_hue(id);
assert!((0.0..360.0).contains(&h), "hue {h} out of range for '{id}'");
}
}
#[test]
fn string_to_hue_is_stable() {
assert_eq!(string_to_hue("Building"), string_to_hue("Building"));
}
}