#![allow(dead_code)]
use std::sync::Arc;
use serde::{Deserialize, Serialize};
pub type RegionId = u32;
pub type ConstraintId = u32;
pub type CandidateIx = usize;
pub type Dir = usize;
pub type AssetKey = String;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Rect2 {
pub min: [i32; 2],
pub max: [i32; 2],
}
impl Rect2 {
#[inline]
pub fn width(&self) -> i32 {
self.max[0] - self.min[0]
}
#[inline]
pub fn height(&self) -> i32 {
self.max[1] - self.min[1]
}
#[inline]
pub fn center_cell(&self) -> [i32; 2] {
[
(self.min[0] + self.max[0]) / 2,
(self.min[1] + self.max[1]) / 2,
]
}
#[inline]
pub fn contains(&self, c: [i32; 2]) -> bool {
c[0] >= self.min[0] && c[0] < self.max[0] && c[1] >= self.min[1] && c[1] < self.max[1]
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct Footprint {
pub x: f32,
pub z: f32,
pub yaw: f32,
pub hw: f32,
pub hd: f32,
}
impl Footprint {
#[inline]
pub fn half_extents(&self) -> (f32, f32) {
if self.yaw.sin().abs() > 0.5 {
(self.hd, self.hw)
} else {
(self.hw, self.hd)
}
}
}
#[inline]
pub fn overlap_area(a: &Footprint, b: &Footprint) -> f32 {
let (ahw, ahd) = a.half_extents();
let (bhw, bhd) = b.half_extents();
let ox = (ahw + bhw) - (a.x - b.x).abs();
let oz = (ahd + bhd) - (a.z - b.z).abs();
if ox > 0.0 && oz > 0.0 { ox * oz } else { 0.0 }
}
#[inline]
pub fn escapes_bounds(f: &Footprint, bounds: (f32, f32, f32, f32)) -> f64 {
let (ahw, ahd) = f.half_extents();
let (x0, x1, z0, z1) = bounds;
let (bx0, bx1, bz0, bz1) = (x0 + ahw, x1 - ahw, z0 + ahd, z1 - ahd);
let over = |v: f32| -> f64 { v.max(0.0) as f64 };
over(bx0 - f.x) + over(f.x - bx1) + over(bz0 - f.z) + over(f.z - bz1)
}
#[inline]
pub fn facing_cosine(from: (f32, f32), yaw: f32, to: (f32, f32)) -> f32 {
let (fx, fz) = (yaw.sin(), yaw.cos());
let (dx, dz) = (to.0 - from.0, to.1 - from.1);
let len = (dx * dx + dz * dz).sqrt().max(1e-4);
(fx * dx + fz * dz) / len
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub struct Opening {
pub dir: Dir,
pub cell: [i32; 2],
#[serde(default = "one")]
pub width: usize,
}
fn one() -> usize {
1
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct PropertyBag {
pub tags: Vec<String>,
}
impl PropertyBag {
pub fn has(&self, tag: &str) -> bool {
self.tags.iter().any(|t| t == tag)
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Region {
pub id: RegionId,
pub rect: Rect2,
pub openings: Vec<Opening>,
pub adjacency: Vec<RegionId>,
pub props: PropertyBag,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Host {
Ceiling,
Floor,
Wall,
Opening,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Role {
Anchor { host: Host },
Tiled,
Freestanding,
Scatter { surface: String },
Custom(String),
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
pub struct Dof {
pub translate: bool,
pub rotate_quarter: bool,
pub rotate_free: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Candidate {
pub asset: AssetKey,
pub role: Role,
pub footprint: [f32; 2],
pub dof: Dof,
pub affordances: Vec<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum Scope {
Object(CandidateIx),
Pair(CandidateIx, CandidateIx),
Group(Vec<CandidateIx>),
Region,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum Predicate {
Clearance(f32),
AgainstWall,
Facing(CandidateIx),
MinDistance(f32),
Near(f32),
Count { tag: String, count: usize },
Aligned(String),
Custom(String),
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum Modality {
Hard,
Soft(f64),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Guard(pub String);
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Constraint {
pub id: ConstraintId,
pub scope: Scope,
pub predicate: Predicate,
pub modality: Modality,
pub guard: Option<Guard>,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub struct Placement {
pub candidate: CandidateIx,
pub pos: [f32; 3],
pub yaw: f32,
}
pub struct PlacementProblem<'a> {
pub region: &'a Region,
pub candidates: Arc<[Candidate]>,
pub constraints: Vec<Constraint>,
}
pub enum Outcome {
Assignment(Vec<Placement>),
Ranked(Vec<(f64, Vec<Placement>)>),
Partial {
placed: Vec<Placement>,
unsatisfied: Vec<ConstraintId>,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Hardness {
Hard,
Soft,
Both,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Locality {
Local,
Relational,
Global,
}
#[derive(Clone, Copy, Debug)]
pub struct Capabilities {
pub hardness: Hardness,
pub locality: Locality,
pub cardinality: bool,
pub deterministic: bool,
pub needs_training_data: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SolveError {
Contradiction,
Timeout,
Unsupported,
}
impl std::fmt::Display for SolveError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SolveError::Contradiction => write!(f, "no consistent assignment for the hard constraints"),
SolveError::Timeout => write!(f, "solve budget exhausted before convergence"),
SolveError::Unsupported => write!(f, "no registered solver covers the required capabilities"),
}
}
}
impl std::error::Error for SolveError {}
#[cfg(test)]
mod tests {
use super::*;
#[allow(clippy::excessive_precision)]
#[test]
fn the_hinge_sum_is_accumulated_in_f64_not_f32() {
let f = Footprint {
x: 0.685_108_4,
z: 0.121_921_23,
yaw: 0.0,
hw: 1.133_890_7,
hd: 1.855_970_6,
};
let bounds = (0.0, 1.094_911_6, 0.0, 1.860_573_1);
let got = escapes_bounds(&f, bounds);
let (ahw, ahd) = f.half_extents();
let (bx0, bx1) = (bounds.0 + ahw, bounds.1 - ahw);
let (bz0, bz1) = (bounds.2 + ahd, bounds.3 - ahd);
let w = |v: f32| -> f64 { v.max(0.0) as f64 };
let want = w(bx0 - f.x) + w(f.x - bx1) + w(bz0 - f.z) + w(f.z - bz1);
assert_eq!(
got.to_bits(),
want.to_bits(),
"escapes_bounds must accumulate in f64; got {got:?}, want {want:?}"
);
let n = |v: f32| -> f32 { v.max(0.0) };
let f32_sum =
(n(bx0 - f.x) + n(f.x - bx1) + n(bz0 - f.z) + n(f.z - bz1)) as f64;
assert_ne!(
got.to_bits(),
f32_sum.to_bits(),
"this input no longer distinguishes the two summation orders — find another, or the \
test has stopped guarding anything"
);
}
}