#![allow(dead_code)]
use rand_chacha::ChaCha8Rng;
use super::ir::{
Capabilities, Constraint, Hardness, Locality, Modality, Outcome, Predicate, PlacementProblem,
Role, SolveError,
};
pub trait Solver: Send + Sync {
fn name(&self) -> &str;
fn handles(&self, role: &Role) -> bool;
fn capabilities(&self) -> Capabilities;
fn solve(&self, problem: &PlacementProblem, rng: &mut ChaCha8Rng) -> Result<Outcome, SolveError>;
}
#[derive(Clone, Copy, Debug)]
pub struct Requirement {
pub hardness: Hardness,
pub locality: Locality,
pub cardinality: bool,
}
impl Requirement {
pub fn of(constraints: &[Constraint]) -> Self {
let mut has_hard = false;
let mut has_soft = false;
let mut locality = Locality::Local;
let mut cardinality = false;
for c in constraints {
match c.modality {
Modality::Hard => has_hard = true,
Modality::Soft(_) => has_soft = true,
}
locality = locality.max(predicate_locality(&c.predicate));
if let Predicate::Count { .. } = c.predicate {
cardinality = true;
}
}
let hardness = match (has_hard, has_soft) {
(true, true) => Hardness::Both,
(false, true) => Hardness::Soft,
_ => Hardness::Hard,
};
Requirement {
hardness,
locality,
cardinality,
}
}
}
fn predicate_locality(p: &Predicate) -> Locality {
match p {
Predicate::Clearance(_) | Predicate::AgainstWall => Locality::Local,
Predicate::Facing(_)
| Predicate::MinDistance(_)
| Predicate::Near(_)
| Predicate::Aligned(_) => Locality::Relational,
Predicate::Count { .. } => Locality::Global,
Predicate::Custom(_) => Locality::Global,
}
}
fn covers(cap: &Capabilities, req: &Requirement) -> bool {
let hardness_ok = matches!(
(cap.hardness, req.hardness),
(Hardness::Both, _) | (Hardness::Hard, Hardness::Hard) | (Hardness::Soft, Hardness::Soft)
);
let locality_ok = cap.locality >= req.locality;
let cardinality_ok = !req.cardinality || cap.cardinality;
hardness_ok && locality_ok && cardinality_ok
}
#[derive(Default)]
pub struct Orchestrator {
solvers: Vec<Box<dyn Solver>>,
}
impl Orchestrator {
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, solver: Box<dyn Solver>) -> &mut Self {
self.solvers.push(solver);
self
}
pub fn route(&self, role: &Role) -> Option<&dyn Solver> {
self.solvers
.iter()
.find(|s| s.handles(role))
.map(|b| b.as_ref())
}
pub fn solve_group(
&self,
problem: &PlacementProblem,
rng: &mut ChaCha8Rng,
) -> Result<Outcome, SolveError> {
let Some(role) = problem.candidates.first().map(|c| &c.role) else {
return Ok(Outcome::Assignment(Vec::new()));
};
let Some(solver) = self.route(role) else {
return Err(SolveError::Unsupported);
};
if !covers(&solver.capabilities(), &Requirement::of(&problem.constraints)) {
return Err(SolveError::Unsupported);
}
solver.solve(problem, rng)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::placement::ir::{Candidate, Dof, Host, Opening, PropertyBag, Rect2, Region, Scope};
use crate::rng::seeded;
struct Mock {
role: Role,
caps: Capabilities,
label: &'static str,
}
impl Solver for Mock {
fn name(&self) -> &str {
self.label
}
fn handles(&self, role: &Role) -> bool {
*role == self.role
}
fn capabilities(&self) -> Capabilities {
self.caps
}
fn solve(&self, _: &PlacementProblem, _: &mut ChaCha8Rng) -> Result<Outcome, SolveError> {
Ok(Outcome::Assignment(Vec::new()))
}
}
fn cap(hardness: Hardness, locality: Locality, cardinality: bool) -> Capabilities {
Capabilities {
hardness,
locality,
cardinality,
deterministic: true,
needs_training_data: false,
}
}
fn mock(role: Role, caps: Capabilities, label: &'static str) -> Box<dyn Solver> {
Box::new(Mock { role, caps, label })
}
fn region_4x4() -> Region {
Region {
id: 0,
rect: Rect2 { min: [0, 0], max: [4, 4] },
openings: vec![Opening { dir: 0, cell: [2, 0], width: 1 }],
adjacency: Vec::new(),
props: PropertyBag::default(),
}
}
fn candidate(role: Role) -> Candidate {
Candidate {
asset: "x".into(),
role,
footprint: [1.0, 1.0],
dof: Dof::default(),
affordances: Vec::new(),
}
}
#[test]
fn routes_by_candidate_role() {
let mut o = Orchestrator::new();
o.register(mock(Role::Tiled, cap(Hardness::Hard, Locality::Local, false), "wfc"));
o.register(mock(
Role::Freestanding,
cap(Hardness::Soft, Locality::Relational, false),
"metropolis",
));
assert_eq!(o.route(&Role::Tiled).map(|s| s.name()), Some("wfc"));
assert_eq!(o.route(&Role::Freestanding).map(|s| s.name()), Some("metropolis"));
}
#[test]
fn routing_is_registration_order_independent() {
let tiled = || mock(Role::Tiled, cap(Hardness::Hard, Locality::Local, false), "wfc");
let cons = || {
mock(
Role::Anchor { host: Host::Opening },
cap(Hardness::Hard, Locality::Global, true),
"constraint",
)
};
let mut a = Orchestrator::new();
a.register(tiled());
a.register(cons());
let mut b = Orchestrator::new();
b.register(cons());
b.register(tiled());
assert_eq!(a.route(&Role::Tiled).map(|s| s.name()), Some("wfc"));
assert_eq!(b.route(&Role::Tiled).map(|s| s.name()), Some("wfc"));
}
#[test]
fn unhandled_role_has_no_route() {
let mut o = Orchestrator::new();
o.register(mock(Role::Tiled, cap(Hardness::Hard, Locality::Local, false), "wfc"));
assert!(o.route(&Role::Freestanding).is_none());
}
#[test]
fn empty_candidates_is_empty_success() {
let region = region_4x4();
let o = Orchestrator::new();
let problem = PlacementProblem {
region: ®ion,
candidates: Vec::new().into(),
constraints: Vec::new(),
};
assert!(matches!(
o.solve_group(&problem, &mut seeded(1)),
Ok(Outcome::Assignment(ref p)) if p.is_empty()
));
}
#[test]
fn post_route_guard_rejects_constraint_mismatch() {
let region = region_4x4();
let mut o = Orchestrator::new();
o.register(mock(
Role::Freestanding,
cap(Hardness::Soft, Locality::Relational, false),
"metropolis",
));
let problem = PlacementProblem {
region: ®ion,
candidates: vec![candidate(Role::Freestanding)].into(),
constraints: vec![Constraint {
id: 0,
scope: Scope::Region,
predicate: Predicate::Count { tag: "door".into(), count: 1 },
modality: Modality::Hard,
guard: None,
}],
};
assert!(matches!(
o.solve_group(&problem, &mut seeded(1)),
Err(SolveError::Unsupported)
));
}
#[test]
fn role_routed_group_with_matching_constraints_solves() {
let region = region_4x4();
let mut o = Orchestrator::new();
o.register(mock(
Role::Freestanding,
cap(Hardness::Soft, Locality::Relational, false),
"metropolis",
));
let problem = PlacementProblem {
region: ®ion,
candidates: vec![candidate(Role::Freestanding)].into(),
constraints: vec![Constraint {
id: 0,
scope: Scope::Object(0),
predicate: Predicate::AgainstWall,
modality: Modality::Soft(1.0),
guard: None,
}],
};
assert!(matches!(o.solve_group(&problem, &mut seeded(1)), Ok(Outcome::Assignment(_))));
}
#[test]
fn requirement_of_mixed_hard_and_soft_is_both() {
let req = Requirement::of(&[
Constraint { id: 0, scope: Scope::Region, predicate: Predicate::AgainstWall, modality: Modality::Hard, guard: None },
Constraint { id: 1, scope: Scope::Region, predicate: Predicate::MinDistance(1.0), modality: Modality::Soft(1.0), guard: None },
]);
assert!(matches!(req.hardness, Hardness::Both));
}
}