use serde::{Deserialize, Serialize};
use crate::map::Placed;
pub const RELATIONS_VERSION: u32 = 1;
pub const FILE_SUFFIX: &str = ".relations.ron";
pub const FACING_TOLERANCE_DEG: f32 = 30.0;
pub fn path_for_map(map_path: &std::path::Path) -> std::path::PathBuf {
let name = map_path.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
let stem = name.strip_suffix(".map.ron").or_else(|| name.strip_suffix(".ron")).unwrap_or(&name);
map_path.with_file_name(format!("{stem}{FILE_SUFFIX}"))
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Relations {
pub version: u32,
#[serde(default)]
pub relations: Vec<Relation>,
}
impl Default for Relations {
fn default() -> Self {
Relations {
version: RELATIONS_VERSION,
relations: Vec::new(),
}
}
}
impl Relations {
pub fn parse(text: &str) -> Result<Relations, String> {
let r: Relations = ron::from_str(text).map_err(|e| format!("relations: parse: {e}"))?;
if r.version != RELATIONS_VERSION {
return Err(format!(
"relations: version {} is not the {RELATIONS_VERSION} this build reads",
r.version
));
}
for (i, rel) in r.relations.iter().enumerate() {
let metres: [f32; 2] = match &rel.kind {
Kind::Faces(_) | Kind::AlignedWith(_) => [0.0, 0.0],
Kind::Near { within, .. }
| Kind::InFrontOf { within, .. }
| Kind::Behind { within, .. }
| Kind::LeftOf { within, .. }
| Kind::RightOf { within, .. }
| Kind::MirrorOf { within, .. }
| Kind::BacksOnto { within, .. }
| Kind::Between { within, .. } => [*within, 0.0],
Kind::Apart { at_least, .. } => [*at_least, 0.0],
Kind::ClearInFront { depth, width } | Kind::Inside { width, depth, .. } => [*depth, *width],
Kind::VisibleFrom { clearance, .. } | Kind::ReachableFrom { clearance, .. } => [*clearance, 0.0],
Kind::Balances { with, within, .. } => {
if with.is_empty() {
return Err(format!(
"relations: relation {i} (of {:?}) balances no one; an empty group is a Near",
rel.of
));
}
if let Some(k) = with.iter().enumerate().find_map(|(k, id)| {
(*id == rel.of || with[..k].contains(id)).then_some(k)
}) {
return Err(format!(
"relations: relation {i} (of {:?}) names {:?} twice in its group; each piece counts once",
rel.of, with[k]
));
}
[*within, 0.0]
}
Kind::Along { path, within } => {
if path.len() < 2 {
return Err(format!(
"relations: relation {i} (of {:?}) has a path of {} waypoint(s); a path needs two or more",
rel.of,
path.len()
));
}
[*within, 0.0]
}
};
if let Some(bad) = metres.iter().find(|m| !(m.is_finite() && **m >= 0.0)) {
return Err(format!(
"relations: relation {i} (of {:?}) has distance {bad}; distances are finite metres, 0 or more",
rel.of
));
}
}
Ok(r)
}
pub fn to_ron(&self) -> Result<String, String> {
ron::ser::to_string_pretty(self, ron::ser::PrettyConfig::default())
.map_err(|e| format!("relations: write: {e}"))
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Relation {
pub of: String,
pub kind: Kind,
#[serde(default, skip_serializing_if = "is_false")]
pub hard: bool,
}
fn is_false(b: &bool) -> bool {
!b
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum Kind {
Faces(String),
Near { to: String, within: f32 },
Apart { from: String, at_least: f32 },
InFrontOf { of: String, within: f32 },
Behind { of: String, within: f32 },
LeftOf { of: String, within: f32 },
RightOf { of: String, within: f32 },
ClearInFront { depth: f32, width: f32 },
VisibleFrom { from: String, clearance: f32 },
Along { path: Vec<String>, within: f32 },
ReachableFrom { from: String, clearance: f32 },
AlignedWith(String),
Between { a: String, b: String, within: f32 },
MirrorOf { of: String, about: String, within: f32 },
BacksOnto { of: String, within: f32 },
Inside { of: String, width: f32, depth: f32 },
Balances { with: Vec<String>, about: String, within: f32 },
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum Outcome {
Met {
measured: f32,
},
Unmet {
measured: f32,
by: Option<String>,
},
Missing {
id: String,
},
}
pub fn check(placements: &[Placed], relations: &Relations) -> Vec<Outcome> {
relations
.relations
.iter()
.map(|r| check_one(placements, r))
.collect()
}
fn find<'a>(placements: &'a [Placed], id: &str) -> Result<&'a Placed, Outcome> {
placements
.iter()
.find(|p| p.id == id)
.ok_or_else(|| Outcome::Missing { id: id.to_owned() })
}
fn distance(a: (f32, f32), b: (f32, f32)) -> f32 {
((b.0 - a.0).powi(2) + (b.1 - a.1).powi(2)).sqrt()
}
fn local(from: &Placed, p: (f32, f32)) -> (f32, f32) {
let (fx, fz) = (from.yaw.sin(), from.yaw.cos());
let (dx, dz) = (p.0 - from.at.0, p.1 - from.at.1);
(fx * dx + fz * dz, fz * dx - fx * dz)
}
fn mirror(about: &Placed, p: (f32, f32)) -> (f32, f32) {
let (ahead, left) = local(about, p);
let (fx, fz) = (about.yaw.sin(), about.yaw.cos());
(
about.at.0 + fx * ahead - fz * left,
about.at.1 + fz * ahead + fx * left,
)
}
fn verdict(ok: bool, measured: f32) -> Outcome {
if ok {
Outcome::Met { measured }
} else {
Outcome::Unmet { measured, by: None }
}
}
fn check_one(placements: &[Placed], r: &Relation) -> Outcome {
let a = match find(placements, &r.of) {
Ok(a) => a,
Err(missing) => return missing,
};
let other = |id: &str| find(placements, id);
match &r.kind {
Kind::Faces(to) => match other(to) {
Ok(b) => {
let cos = crate::placement::ir::facing_cosine(a.at, a.yaw, b.at).clamp(-1.0, 1.0);
let off = cos.acos().to_degrees();
verdict(off <= FACING_TOLERANCE_DEG, off)
}
Err(m) => m,
},
Kind::Near { to, within } => match other(to) {
Ok(b) => {
let d = distance(a.at, b.at);
verdict(d <= *within, d)
}
Err(m) => m,
},
Kind::Apart { from, at_least } => match other(from) {
Ok(b) => {
let d = distance(a.at, b.at);
verdict(d >= *at_least, d)
}
Err(m) => m,
},
Kind::InFrontOf { of, within } => match other(of) {
Ok(b) => {
let (ahead, side) = local(b, a.at);
verdict(
ahead > 0.0 && ahead <= *within && side.abs() <= ahead,
ahead,
)
}
Err(m) => m,
},
Kind::Behind { of, within } => match other(of) {
Ok(b) => {
let (ahead, side) = local(b, a.at);
let back = -ahead;
verdict(back > 0.0 && back <= *within && side.abs() <= back, back)
}
Err(m) => m,
},
Kind::LeftOf { of, within } => match other(of) {
Ok(b) => {
let (ahead, left) = local(b, a.at);
verdict(left > 0.0 && left <= *within && ahead.abs() <= left, left)
}
Err(m) => m,
},
Kind::RightOf { of, within } => match other(of) {
Ok(b) => {
let (ahead, left) = local(b, a.at);
let right = -left;
verdict(right > 0.0 && right <= *within && ahead.abs() <= right, right)
}
Err(m) => m,
},
Kind::ClearInFront { depth, width } => {
let own = &bottom(placements, a).id;
let intruder = placements
.iter()
.filter(|p| &bottom(placements, p).id != own)
.map(|p| (p, local(a, p.at)))
.filter(|(_, (ahead, side))| {
*ahead > 0.0 && ahead <= depth && side.abs() <= width / 2.0
})
.min_by(|x, y| x.1 .0.total_cmp(&y.1 .0));
match intruder {
Some((p, (ahead, _))) => Outcome::Unmet {
measured: ahead,
by: Some(p.id.clone()),
},
None => Outcome::Met { measured: *depth },
}
}
Kind::VisibleFrom { from, clearance } => match other(from) {
Ok(b) => {
let nearest = blockers(placements, b, a, *clearance)
.into_iter()
.min_by(|x, y| x.1.abs().total_cmp(&y.1.abs()));
match nearest {
Some((p, side)) => Outcome::Unmet {
measured: side.abs(),
by: Some(p.id.clone()),
},
None => Outcome::Met { measured: *clearance },
}
}
Err(m) => m,
},
Kind::ReachableFrom { from, clearance } => match other(from) {
Ok(b) => match walk(placements, b, a, *clearance, None) {
Some(length) => Outcome::Met { measured: length },
None => match opener(placements, b, a, *clearance) {
Some((p, length)) => Outcome::Unmet {
measured: length,
by: Some(p.id.clone()),
},
None => Outcome::Unmet { measured: 0.0, by: None },
},
},
Err(m) => m,
},
Kind::Along { path, within } => match nearest_on_path(placements, path, a.at) {
Ok(q) => {
let d = distance(a.at, q);
verdict(d <= *within, d)
}
Err(m) => m,
},
Kind::AlignedWith(to) => match other(to) {
Ok(b) => {
let off = (a.yaw - b.yaw).rem_euclid(std::f32::consts::TAU);
let off = off.min(std::f32::consts::TAU - off).to_degrees();
verdict(off <= FACING_TOLERANCE_DEG, off)
}
Err(m) => m,
},
Kind::Between { a: from, b: to, within } => match (other(from), other(to)) {
(Ok(p), Ok(q)) => {
let (t, d) = onto_segment(p.at, q.at, a.at);
verdict(t > 0.0 && t < 1.0 && d <= *within, d)
}
(Err(m), _) | (_, Err(m)) => m,
},
Kind::BacksOnto { of, within } => match other(of) {
Ok(b) => {
let (ahead, _) = local(b, a.at);
let off = ahead.abs();
let (fx, fz) = (b.yaw.sin(), b.yaw.cos());
let away = ahead.signum() * (a.yaw.sin() * fx + a.yaw.cos() * fz);
let turned = away.clamp(-1.0, 1.0).acos().to_degrees();
verdict(off > 0.0 && off <= *within && turned <= FACING_TOLERANCE_DEG, off)
}
Err(m) => m,
},
Kind::Inside { of, width, depth } => match other(of) {
Ok(b) => {
let (ahead, left) = local(b, a.at);
let out_side = (left.abs() - width / 2.0).max(0.0);
let out_ahead = (ahead.abs() - depth / 2.0).max(0.0);
verdict(
left.abs() <= width / 2.0 && ahead.abs() <= depth / 2.0,
out_side.hypot(out_ahead),
)
}
Err(m) => m,
},
Kind::MirrorOf { of, about, within } => match (other(of), other(about)) {
(Ok(p), Ok(axis)) => {
let d = distance(a.at, mirror(axis, p.at));
verdict(d <= *within, d)
}
(Err(m), _) | (_, Err(m)) => m,
},
Kind::Balances { with, about, within } => match other(about) {
Ok(c) => {
let mut sum = a.at;
for id in with {
match other(id) {
Ok(p) => sum = (sum.0 + p.at.0, sum.1 + p.at.1),
Err(m) => return m,
}
}
let n = with.len() as f32 + 1.0;
let d = distance((sum.0 / n, sum.1 / n), c.at);
verdict(d <= *within, d)
}
Err(m) => m,
},
}
}
fn onto_segment(from: (f32, f32), to: (f32, f32), p: (f32, f32)) -> (f32, f32) {
let (dx, dz) = (to.0 - from.0, to.1 - from.1);
let len2 = dx * dx + dz * dz;
let t = if len2 > 1e-8 {
((p.0 - from.0) * dx + (p.1 - from.1) * dz) / len2
} else {
0.0
};
let c = t.clamp(0.0, 1.0);
(t, distance(p, (from.0 + dx * c, from.1 + dz * c)))
}
fn nearest_on_path(placements: &[Placed], path: &[String], p: (f32, f32)) -> Result<(f32, f32), Outcome> {
let points = path
.iter()
.map(|id| find(placements, id).map(|w| w.at))
.collect::<Result<Vec<_>, _>>()?;
let closest = |(s, e): ((f32, f32), (f32, f32))| {
let (dx, dz) = (e.0 - s.0, e.1 - s.1);
let len2 = dx * dx + dz * dz;
let t = if len2 > 1e-8 {
(((p.0 - s.0) * dx + (p.1 - s.1) * dz) / len2).clamp(0.0, 1.0)
} else {
0.0
};
(s.0 + dx * t, s.1 + dz * t)
};
Ok(points
.windows(2)
.map(|w| closest((w[0], w[1])))
.min_by(|x, y| distance(p, *x).total_cmp(&distance(p, *y)))
.unwrap_or(points[0]))
}
fn rests_on(placements: &[Placed], p: &Placed, end: &str) -> bool {
let mut host = p.on.as_deref();
for _ in 0..placements.len() {
match host {
Some(h) if h == end => return true,
Some(h) => host = find(placements, h).ok().and_then(|q| q.on.as_deref()),
None => return false,
}
}
false
}
fn bottom<'a>(placements: &'a [Placed], mut p: &'a Placed) -> &'a Placed {
for _ in 0..placements.len() {
let Some(host) = p.on.as_deref().and_then(|h| find(placements, h).ok()) else {
break;
};
p = host;
}
p
}
type Line = ((f32, f32), (f32, f32), f32);
fn line(from: &Placed, to: &Placed) -> Option<Line> {
let length = distance(from.at, to.at);
(length > 1e-4).then(|| {
let u = ((to.at.0 - from.at.0) / length, (to.at.1 - from.at.1) / length);
(from.at, u, length)
})
}
fn on_line(start: (f32, f32), (ux, uz): (f32, f32), p: (f32, f32)) -> (f32, f32) {
let (dx, dz) = (p.0 - start.0, p.1 - start.1);
(ux * dx + uz * dz, uz * dx - ux * dz)
}
fn blockers<'a>(placements: &'a [Placed], from: &Placed, to: &Placed, clearance: f32) -> Vec<(&'a Placed, f32)> {
let Some((start, u, length)) = line(from, to) else {
return Vec::new();
};
let ends = [&bottom(placements, from).id, &bottom(placements, to).id];
placements
.iter()
.filter(|p| !ends.contains(&&bottom(placements, p).id))
.filter_map(|p| {
let (along, side) = on_line(start, u, p.at);
(along > 0.0 && along < length && side.abs() <= clearance).then_some((p, side))
})
.collect()
}
fn clear_sight(placements: &[Placed], a: &Placed, b: &Placed, clearance: f32) -> Vec<Move> {
let Some((start, u, _)) = line(b, a) else {
return Vec::new();
};
let past = clearance * 1.1 + 0.05;
let mut hosts: Vec<&Placed> = Vec::new();
for (p, _) in blockers(placements, b, a, clearance) {
let host = bottom(placements, p);
if !hosts.iter().any(|h| h.id == host.id) {
hosts.push(host);
}
}
hosts
.into_iter()
.map(|host| {
let sides = placements
.iter()
.filter(|q| q.id == host.id || rests_on(placements, q, &host.id))
.map(|q| on_line(start, u, q.at).1);
let (low, high) = sides.fold((f32::INFINITY, f32::NEG_INFINITY), |(l, h), s| (l.min(s), h.max(s)));
let (up, down) = (past - low, -past - high);
let shift = if up.abs() <= down.abs() { up } else { down };
let to = (host.at.0 + u.1 * shift, host.at.1 - u.0 * shift);
(host.id.clone(), to, host.yaw)
})
.collect()
}
const WALK_CELLS: f32 = 160_000.0;
fn walk(placements: &[Placed], from: &Placed, to: &Placed, clearance: f32, skip: Option<&str>) -> Option<f32> {
if clearance <= 0.0 {
return Some(distance(from.at, to.at));
}
let ends = [&bottom(placements, from).id, &bottom(placements, to).id];
let obstacles: Vec<(f32, f32)> = placements
.iter()
.filter(|p| !ends.contains(&&bottom(placements, p).id))
.filter(|p| skip.is_none_or(|s| p.id != s && !rests_on(placements, p, s)))
.map(|p| p.at)
.collect();
let pad = 2.0 * clearance;
let (mut lo, mut hi) = ((f32::INFINITY, f32::INFINITY), (f32::NEG_INFINITY, f32::NEG_INFINITY));
for p in placements {
lo = (lo.0.min(p.at.0), lo.1.min(p.at.1));
hi = (hi.0.max(p.at.0), hi.1.max(p.at.1));
}
let reach_out = distance(from.at, to.at).max(2.0 * pad);
let lo = (
(lo.0 - pad).max(from.at.0.min(to.at.0) - reach_out),
(lo.1 - pad).max(from.at.1.min(to.at.1) - reach_out),
);
let hi = (
(hi.0 + pad).min(from.at.0.max(to.at.0) + reach_out),
(hi.1 + pad).min(from.at.1.max(to.at.1) + reach_out),
);
let area = (hi.0 - lo.0) * (hi.1 - lo.1);
let cell = (clearance / 2.0).max((area / WALK_CELLS).sqrt());
let coarse = cell > clearance / 2.0;
let radius = if coarse {
clearance + cell * std::f32::consts::FRAC_1_SQRT_2
} else {
clearance
};
let (nx, nz) = (((hi.0 - lo.0) / cell).ceil() as usize + 1, ((hi.1 - lo.1) / cell).ceil() as usize + 1);
let index = |p: (f32, f32)| {
let i = (((p.0 - lo.0) / cell).round() as usize).min(nx - 1);
let k = (((p.1 - lo.1) / cell).round() as usize).min(nz - 1);
k * nx + i
};
let mut blocked = vec![false; nx * nz];
let reach = (radius / cell).ceil() as isize;
for o in &obstacles {
let centre = index(*o);
let (ci, ck) = ((centre % nx) as isize, (centre / nx) as isize);
for k in (ck - reach).max(0)..=(ck + reach).min(nz as isize - 1) {
for i in (ci - reach).max(0)..=(ci + reach).min(nx as isize - 1) {
let c = (lo.0 + i as f32 * cell, lo.1 + k as f32 * cell);
if distance(c, *o) < radius {
blocked[k as usize * nx + i as usize] = true;
}
}
}
}
let (start, goal) = (index(from.at), index(to.at));
if !coarse {
blocked[start] = false;
blocked[goal] = false;
}
let mut best = vec![u32::MAX; nx * nz];
let mut queue = std::collections::BinaryHeap::new();
best[start] = 0;
queue.push(std::cmp::Reverse((0u32, start)));
while let Some(std::cmp::Reverse((cost, c))) = queue.pop() {
if c == goal {
return Some(cost as f32 / 1000.0 * cell);
}
if cost > best[c] {
continue;
}
let (i, k) = ((c % nx) as isize, (c / nx) as isize);
let free = |i: isize, k: isize| {
i >= 0 && k >= 0 && i < nx as isize && k < nz as isize && !blocked[k as usize * nx + i as usize]
};
for (di, dk) in [(1, 0), (-1, 0), (0, 1), (0, -1), (1, 1), (1, -1), (-1, 1), (-1, -1)] {
let (ni, nk) = (i + di, k + dk);
if !free(ni, nk) || (di != 0 && dk != 0 && !(free(i + di, k) && free(i, k + dk))) {
continue;
}
let step = if di != 0 && dk != 0 { 1414 } else { 1000 };
let n = nk as usize * nx + ni as usize;
if cost + step < best[n] {
best[n] = cost + step;
queue.push(std::cmp::Reverse((cost + step, n)));
}
}
}
None
}
pub const OPENER_TRIES: usize = 16;
fn opener<'a>(placements: &'a [Placed], from: &Placed, to: &Placed, clearance: f32) -> Option<(&'a Placed, f32)> {
let gap = |p: (f32, f32)| match line(from, to) {
Some((start, u, length)) => {
let (along, side) = on_line(start, u, p);
if along < 0.0 {
distance(p, from.at)
} else if along > length {
distance(p, to.at)
} else {
side.abs()
}
}
None => distance(p, from.at),
};
let ends = [&bottom(placements, from).id, &bottom(placements, to).id];
let mut hosts: Vec<&Placed> = Vec::new();
for p in placements {
let host = bottom(placements, p);
if !ends.contains(&&host.id) && !hosts.iter().any(|h| h.id == host.id) {
hosts.push(host);
}
}
hosts.sort_by(|x, y| gap(x.at).total_cmp(&gap(y.at)));
hosts
.into_iter()
.take(OPENER_TRIES)
.find_map(|h| walk(placements, from, to, clearance, Some(&h.id)).map(|length| (h, length)))
}
fn open_walk(placements: &[Placed], a: &Placed, b: &Placed, clearance: f32, by: &str) -> Option<Move> {
let host = bottom(placements, find(placements, by).ok()?);
let step = 2.0 * clearance * 1.1 + 0.05;
let diagonal = std::f32::consts::FRAC_1_SQRT_2;
let directions = [
(1.0, 0.0),
(-1.0, 0.0),
(0.0, 1.0),
(0.0, -1.0),
(diagonal, diagonal),
(diagonal, -diagonal),
(-diagonal, diagonal),
(-diagonal, -diagonal),
];
(1..=3).find_map(|k| {
directions.iter().find_map(|(dx, dz)| {
let to = (host.at.0 + dx * step * k as f32, host.at.1 + dz * step * k as f32);
let mut trial = placements.to_vec();
let depth = trial.len();
move_piece(&mut trial, &host.id, to, host.yaw, depth);
let (ta, tb) = (find(&trial, &a.id).ok()?, find(&trial, &b.id).ok()?);
walk(&trial, tb, ta, clearance, None).map(|_| (host.id.clone(), to, host.yaw))
})
})
}
type Move = (String, (f32, f32), f32);
#[derive(Clone, Debug, PartialEq)]
pub struct Solved {
pub placements: Vec<Placed>,
pub moved: Vec<String>,
pub unmet_before: usize,
pub unmet_after: usize,
pub unsatisfied: Vec<usize>,
}
const SOLVE_ROUNDS: usize = 8;
pub fn solve(placements: &[Placed], relations: &Relations) -> Solved {
let unmet = |ps: &[Placed]| {
check(ps, relations)
.iter()
.zip(&relations.relations)
.filter(|(o, _)| matches!(o, Outcome::Unmet { .. }))
.fold((0, 0), |(hard, soft), (_, r)| if r.hard { (hard + 1, soft) } else { (hard, soft + 1) })
};
let total = |(hard, soft): (usize, usize)| hard + soft;
let mut current = placements.to_vec();
let unmet_before = total(unmet(¤t));
let mut count = unmet(¤t);
let mut moved: Vec<String> = Vec::new();
for _ in 0..SOLVE_ROUNDS {
let mut progressed = false;
for r in &relations.relations {
let Outcome::Unmet { by, .. } = check_one(¤t, r) else {
continue;
};
let moves = propose(¤t, r, by.as_deref());
if moves.is_empty() {
continue;
}
let mut trial = current.clone();
let depth = trial.len();
for (id, at, yaw) in &moves {
move_piece(&mut trial, id, *at, *yaw, depth);
}
let trial_count = unmet(&trial);
if trial_count < count {
current = trial;
count = trial_count;
progressed = true;
for (id, ..) in moves {
if !moved.contains(&id) {
moved.push(id);
}
}
}
}
if !progressed || count == (0, 0) {
break;
}
}
let unsatisfied = check(¤t, relations)
.iter()
.enumerate()
.filter(|(_, o)| matches!(o, Outcome::Unmet { .. }))
.map(|(i, _)| i)
.collect();
Solved {
placements: current,
moved,
unmet_before,
unmet_after: total(count),
unsatisfied,
}
}
pub fn check_map(placements: &[Placed], relations: &Relations) -> Vec<Outcome> {
check(&in_radians(placements), relations)
}
pub fn solve_map(placements: &[Placed], relations: &Relations) -> Solved {
let radians = in_radians(placements);
let mut solved = solve(&radians, relations);
for ((row, given), stored) in solved.placements.iter_mut().zip(&radians).zip(placements) {
row.yaw = if row.yaw == given.yaw { stored.yaw } else { row.yaw.to_degrees() };
}
solved
}
pub(crate) fn in_radians(placements: &[Placed]) -> Vec<Placed> {
placements
.iter()
.map(|p| Placed {
yaw: p.yaw.to_radians(),
..p.clone()
})
.collect()
}
fn propose(placements: &[Placed], r: &Relation, by: Option<&str>) -> Vec<Move> {
let Ok(a) = find(placements, &r.of) else {
return Vec::new();
};
let other = |id: &str| find(placements, id).ok();
let free = |p: &Placed| p.on.is_none();
let unit = |from: (f32, f32), to: (f32, f32), fallback: (f32, f32)| {
let d = distance(from, to);
if d > 1e-4 {
((to.0 - from.0) / d, (to.1 - from.1) / d)
} else {
fallback
}
};
let forward = |p: &Placed| (p.yaw.sin(), p.yaw.cos());
if let Kind::VisibleFrom { from, clearance } = &r.kind {
return other(from).map_or_else(Vec::new, |b| clear_sight(placements, a, b, *clearance));
}
let one = (|| match &r.kind {
Kind::Faces(to) => {
let b = other(to)?;
let (dx, dz) = (b.at.0 - a.at.0, b.at.1 - a.at.1);
free(a).then(|| (a.id.clone(), a.at, dx.atan2(dz)))
}
Kind::AlignedWith(to) => {
let b = other(to)?;
free(a).then(|| (a.id.clone(), a.at, b.yaw))
}
Kind::Near { to, within } => {
let b = other(to)?;
let (ux, uz) = unit(b.at, a.at, forward(b));
let d = within * 0.9;
free(a).then(|| (a.id.clone(), (b.at.0 + ux * d, b.at.1 + uz * d), a.yaw))
}
Kind::Apart { from, at_least } => {
let b = other(from)?;
let back = forward(a);
let (ux, uz) = unit(b.at, a.at, (-back.0, -back.1));
let d = at_least * 1.1 + 0.05;
free(a).then(|| (a.id.clone(), (b.at.0 + ux * d, b.at.1 + uz * d), a.yaw))
}
Kind::InFrontOf { of, within } => {
let b = other(of)?;
let (fx, fz) = forward(b);
let d = distance(a.at, b.at).clamp(within * 0.1, within * 0.9);
free(a).then(|| (a.id.clone(), (b.at.0 + fx * d, b.at.1 + fz * d), a.yaw))
}
Kind::Behind { of, within } => {
let b = other(of)?;
let (fx, fz) = forward(b);
let d = distance(a.at, b.at).clamp(within * 0.1, within * 0.9);
free(a).then(|| (a.id.clone(), (b.at.0 - fx * d, b.at.1 - fz * d), a.yaw))
}
Kind::LeftOf { of, within } | Kind::RightOf { of, within } => {
let b = other(of)?;
let (fx, fz) = forward(b);
let sign = if matches!(r.kind, Kind::LeftOf { .. }) { 1.0 } else { -1.0 };
let d = sign * distance(a.at, b.at).clamp(within * 0.1, within * 0.9);
free(a).then(|| (a.id.clone(), (b.at.0 + fz * d, b.at.1 - fx * d), a.yaw))
}
Kind::ClearInFront { width, .. } => {
let intruder = bottom(placements, other(by?)?);
let (ahead, side) = local(a, intruder.at);
let sign = if side < 0.0 { -1.0 } else { 1.0 };
let side = sign * (width / 2.0 * 1.1 + 0.05);
let (fx, fz) = forward(a);
let to = (
a.at.0 + fx * ahead + fz * side,
a.at.1 + fz * ahead - fx * side,
);
Some((intruder.id.clone(), to, intruder.yaw))
}
Kind::Along { path, within } => {
let q = nearest_on_path(placements, path, a.at).ok()?;
let d = distance(a.at, q);
let keep = if d > 1e-4 { (within * 0.9).min(d) / d } else { 0.0 };
let to = (q.0 + (a.at.0 - q.0) * keep, q.1 + (a.at.1 - q.1) * keep);
free(a).then(|| (a.id.clone(), to, a.yaw))
}
Kind::Between { a: from, b: to, within } => {
let (p, q) = (other(from)?.at, other(to)?.at);
let len = distance(p, q);
if len <= 1e-4 {
return None;
}
let (t, _) = onto_segment(p, q, a.at);
let t = t.clamp(0.1, 0.9);
let (ux, uz) = ((q.0 - p.0) / len, (q.1 - p.1) / len);
let side = (a.at.0 - p.0) * uz - (a.at.1 - p.1) * ux;
let side = side.signum() * side.abs().min(within * 0.9);
let to = (
p.0 + ux * len * t + uz * side,
p.1 + uz * len * t - ux * side,
);
free(a).then(|| (a.id.clone(), to, a.yaw))
}
Kind::MirrorOf { of, about, .. } => {
let p = other(of)?;
let axis = other(about)?;
free(a).then(|| (a.id.clone(), mirror(axis, p.at), a.yaw))
}
Kind::BacksOnto { of, within } => {
let b = other(of)?;
let (ahead, left) = local(b, a.at);
let sign = if ahead < 0.0 { -1.0 } else { 1.0 };
let ahead = sign * ahead.abs().clamp(within * 0.1, within * 0.9);
let (fx, fz) = forward(b);
let to = (b.at.0 + fx * ahead + fz * left, b.at.1 + fz * ahead - fx * left);
let yaw = (sign * fx).atan2(sign * fz);
free(a).then(|| (a.id.clone(), to, yaw))
}
Kind::Inside { of, width, depth } => {
let b = other(of)?;
let (ahead, left) = local(b, a.at);
let into = |v: f32, half: f32| if v.abs() > half { v.signum() * half * 0.9 } else { v };
let (ahead, left) = (into(ahead, depth / 2.0), into(left, width / 2.0));
let (fx, fz) = forward(b);
let to = (b.at.0 + fx * ahead + fz * left, b.at.1 + fz * ahead - fx * left);
free(a).then(|| (a.id.clone(), to, a.yaw))
}
Kind::ReachableFrom { from, clearance } => {
let b = other(from)?;
open_walk(placements, a, b, *clearance, by?)
}
Kind::Balances { with, about, .. } => {
let members: Vec<(f32, f32)> = with.iter().map(|id| other(id).map(|p| p.at)).collect::<Option<_>>()?;
let (sx, sz) = members.iter().fold((0.0, 0.0), |s, p| (s.0 + p.0, s.1 + p.1));
let n = members.len() as f32 + 1.0;
let to = if *about == a.id {
(sx / (n - 1.0), sz / (n - 1.0))
} else {
let c = other(about)?.at;
(n * c.0 - sx, n * c.1 - sz)
};
free(a).then(|| (a.id.clone(), to, a.yaw))
}
Kind::VisibleFrom { .. } => None,
})();
one.into_iter().collect()
}
fn move_piece(placements: &mut [Placed], id: &str, at: (f32, f32), yaw: f32, depth: usize) {
if depth == 0 {
return;
}
let Some(host) = placements.iter().position(|p| p.id == id) else {
return;
};
let (from, turn) = (placements[host].at, yaw - placements[host].yaw);
let (s, c) = turn.sin_cos();
placements[host].at = at;
placements[host].yaw = yaw;
let riders: Vec<(String, (f32, f32), f32)> = placements
.iter()
.filter(|p| p.on.as_deref() == Some(id))
.map(|p| {
let (dx, dz) = (p.at.0 - from.0, p.at.1 - from.1);
let (rx, rz) = (dx * c + dz * s, -dx * s + dz * c);
(p.id.clone(), (at.0 + rx, at.1 + rz), p.yaw + turn)
})
.collect();
for (rider, to, rider_yaw) in riders {
move_piece(placements, &rider, to, rider_yaw, depth - 1);
}
}