use serde::{Deserialize, Serialize};
use crate::descriptor::tipped_footprint;
use crate::library::Library;
use crate::map::Placed;
use crate::stack::descriptor_of;
pub const YAW_TOLERANCE_DEG: f32 = 1.0;
pub const FILE: &str = "repetition.ron";
pub const REPETITION_VERSION: u32 = 1;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct RepetitionRules {
#[serde(default = "crate::format_version::first", deserialize_with = "crate::format_version::read::<_, REPETITION_VERSION>")]
pub version: u32,
pub radius_m: f32,
pub structure: Vec<String>,
pub structure_weight: f32,
pub round: Vec<String>,
}
impl Default for RepetitionRules {
fn default() -> Self {
Self {
version: REPETITION_VERSION,
radius_m: 6.0,
structure: ["building-module", "fence", "tile", "landmark", "grave", "door"]
.map(str::to_owned)
.to_vec(),
structure_weight: 0.1,
round: Vec::new(),
}
}
}
impl RepetitionRules {
pub fn parse(text: &str) -> Result<RepetitionRules, String> {
let r: RepetitionRules =
ron::from_str(text).map_err(|e| format!("repetition: parse: {e}"))?;
if !(r.radius_m.is_finite() && r.radius_m > 0.0) {
return Err(format!(
"repetition: radius_m {} is not a positive number of metres",
r.radius_m
));
}
if !(r.structure_weight.is_finite() && r.structure_weight >= 0.0) {
return Err(format!(
"repetition: structure_weight {} is not a weight of zero or more",
r.structure_weight
));
}
Ok(r)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Role {
Structure,
Clutter,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Repeat {
pub descriptor: String,
pub patch: Option<crate::descriptor::Descriptor>,
pub role: Role,
pub pairs: usize,
pub pieces: usize,
pub score: f32,
}
pub fn repeats(placements: &[Placed], library: &Library, rules: &RepetitionRules) -> Vec<Repeat> {
let mut groups: Vec<(String, Role, bool, Vec<&Placed>)> = Vec::new();
for p in placements {
let Some(d) = descriptor_of(library, p) else {
continue;
};
let key = look_key(p);
match groups.iter_mut().find(|(k, ..)| *k == key) {
Some((.., rows)) => rows.push(p),
None => {
let role = role_of(&d, rules);
groups.push((key, role, is_round(&d, rules), vec![p]));
}
}
}
let radius2 = rules.radius_m * rules.radius_m;
let mut report: Vec<Repeat> = groups
.into_iter()
.filter_map(|(_, role, round, rows)| {
let mut paired = vec![false; rows.len()];
let mut pairs = 0;
for i in 0..rows.len() {
for j in i + 1..rows.len() {
if look_alike(rows[i], rows[j], radius2, round) {
pairs += 1;
paired[i] = true;
paired[j] = true;
}
}
}
if pairs == 0 {
return None;
}
let pieces = paired.iter().filter(|p| **p).count();
let weight = match role {
Role::Structure => rules.structure_weight,
Role::Clutter => 1.0,
};
Some(Repeat {
descriptor: rows[0].descriptor.clone(),
patch: rows[0].patch.clone(),
role,
pairs,
pieces,
score: pieces as f32 * weight,
})
})
.collect();
report.sort_by(|a, b| {
b.score
.total_cmp(&a.score)
.then_with(|| a.descriptor.cmp(&b.descriptor))
.then_with(|| a.patch.is_some().cmp(&b.patch.is_some()))
});
report
}
fn role_of(d: &crate::descriptor::Descriptor, rules: &RepetitionRules) -> Role {
if d.kind.iter().any(|k| rules.structure.contains(k)) {
Role::Structure
} else {
Role::Clutter
}
}
pub const TURNS_DEG: [f32; 3] = [180.0, 90.0, 270.0];
pub const SQUARE_TOLERANCE_M: f32 = 0.01;
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct Variation {
pub turned: Vec<(usize, f32)>,
pub left: usize,
}
pub fn vary(placements: &[Placed], library: &Library, rules: &RepetitionRules) -> Variation {
let radius2 = rules.radius_m * rules.radius_m;
let traits: Vec<(bool, bool, bool)> = placements
.iter()
.map(|p| {
descriptor_of(library, p).map_or((false, false, false), |d| {
let long = tipped_footprint(&d, p.tip)
.is_some_and(|(w, depth)| (w - depth).abs() > SQUARE_TOLERANCE_M);
(role_of(&d, rules) == Role::Clutter, is_round(&d, rules), long)
})
})
.collect();
let keys: Vec<String> = placements.iter().map(look_key).collect();
let mut yaws: Vec<f32> = placements.iter().map(|p| p.yaw).collect();
let alike = |yaws: &[f32], i: usize, yaw: f32, upto: usize| {
let p = &placements[i];
(0..upto).any(|j| {
let q = &placements[j];
let (dx, dz) = (p.at.0 - q.at.0, p.at.1 - q.at.1);
let turn = (yaw - yaws[j]).rem_euclid(360.0);
j != i
&& dx * dx + dz * dz < radius2
&& p.tip == q.tip
&& (traits[i].1 || turn.min(360.0 - turn) < YAW_TOLERANCE_DEG)
&& keys[j] == keys[i]
})
};
let n = placements.len();
let mut turned = Vec::new();
for i in 0..n {
let (clutter, round, long) = traits[i];
if !clutter || round || !alike(&yaws, i, yaws[i], i) {
continue;
}
let found = TURNS_DEG[..if long { 1 } else { TURNS_DEG.len() }]
.iter()
.map(|turn| (yaws[i] + turn).rem_euclid(360.0))
.find(|&yaw| !alike(&yaws, i, yaw, n));
if let Some(yaw) = found {
yaws[i] = yaw;
turned.push((i, yaw));
}
}
let left = (0..n).filter(|&i| traits[i].0 && alike(&yaws, i, yaws[i], n)).count();
Variation { turned, left }
}
fn look_key(p: &Placed) -> String {
match &p.patch {
None => p.descriptor.clone(),
Some(patch) => format!("{}{patch:?}", p.descriptor),
}
}
fn is_round(d: &crate::descriptor::Descriptor, rules: &RepetitionRules) -> bool {
d.kind.iter().any(|k| rules.round.contains(k))
}
fn look_alike(a: &Placed, b: &Placed, radius2: f32, round: bool) -> bool {
let (dx, dz) = (a.at.0 - b.at.0, a.at.1 - b.at.1);
let turn = (a.yaw - b.yaw).rem_euclid(360.0);
a.tip == b.tip
&& dx * dx + dz * dz < radius2
&& (round || turn.min(360.0 - turn) < YAW_TOLERANCE_DEG)
}