use serde::{Deserialize, Serialize};
use crate::descriptor::Descriptor;
use crate::library::Library;
pub const MAX_POSTS: usize = 1024;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Run {
pub descriptor: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pitch: Option<f32>,
pub from: (f32, f32),
pub to: (f32, f32),
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub corners: Vec<Corner>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub gaps: Vec<Gap>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RunPlaced {
pub id: String,
pub run: Run,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub owned: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub owned_because: Option<String>,
}
pub fn post_id(run: &str, n: usize) -> String {
format!("{run}/{n}")
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Corner {
pub at: (f32, f32),
pub yaw: f32,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Gap {
pub count: usize,
pub at_post: usize,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Post {
pub at: (f32, f32),
pub yaw: f32,
}
pub fn lay(run: &Run, library: &Library) -> Result<Vec<Post>, String> {
let Some(descriptor) = library.get(&run.descriptor) else {
return Err(format!(
"run: descriptor `{}` is not in this library, so there is no piece to repeat",
run.descriptor
));
};
let descriptor: &Descriptor = descriptor;
let mut vertices: Vec<(f32, f32)> = Vec::with_capacity(run.corners.len() + 2);
vertices.push(run.from);
for c in &run.corners {
if !c.at.0.is_finite() || !c.at.1.is_finite() {
return Err(format!(
"run: a corner is at ({}, {}), which is not a position",
c.at.0, c.at.1
));
}
if !c.yaw.is_finite() {
return Err(format!(
"run: the corner at ({}, {}) turns to {yaw} degrees, which is not an angle",
c.at.0,
c.at.1,
yaw = c.yaw
));
}
vertices.push(c.at);
}
vertices.push(run.to);
if !run.from.0.is_finite() || !run.from.1.is_finite() {
return Err(format!(
"run: starts at ({}, {}), which is not a position",
run.from.0, run.from.1
));
}
if !run.to.0.is_finite() || !run.to.1.is_finite() {
return Err(format!(
"run: ends at ({}, {}), which is not a position",
run.to.0, run.to.1
));
}
let (w, depth) = crate::descriptor::placed_footprint(descriptor).ok_or_else(|| {
format!(
"run: `{}` has no measured footprint, so there is no honest pitch to repeat it at. \
Measure it on the tiles tab first.",
run.descriptor
)
})?;
if !(w.is_finite() && depth.is_finite() && w > 0.0 && depth > 0.0) {
return Err(format!(
"run: `{}` has a footprint of {w} by {depth} metres, which is not a size a run can \
repeat it at.",
run.descriptor
));
}
let mut posts: Vec<Post> = Vec::new();
let mut yaw = 0.0f32;
for (i, seg) in vertices.windows(2).enumerate() {
let (a, b) = (seg[0], seg[1]);
let dx = b.0 - a.0;
let dz = b.1 - a.1;
let length = (dx * dx + dz * dz).sqrt();
if length <= f32::EPSILON {
return Err(format!(
"run: the segment from ({}, {}) to ({}, {}) has no length, so there is nowhere for \
a post to stand",
a.0, a.1, b.0, b.1
));
}
let along_x = dx.abs() >= dz.abs();
let extent = if along_x { w } else { depth };
if i == 0 && extent < w.max(depth) * 0.999 {
return Err(format!(
"run: `{}` is {w} by {depth} metres, and a run along {} would repeat it across \
its short side — the posts would not span the gaps. Run it along its long side \
instead.",
run.descriptor,
if along_x { "X" } else { "Z" },
));
}
let pitch = match run.pitch {
Some(p) => {
if !(p.is_finite() && p > 0.0) {
return Err(format!(
"run: the pitch is {p} metres, which is not a distance a run can step by"
));
}
p
}
None => w.max(depth),
};
if length < pitch {
return Err(format!(
"run: the segment from ({}, {}) to ({}, {}) is {length:.3} metres long, which is \
shorter than one pitch of {pitch:.3} metres. There is nowhere for a post to stand.",
a.0, a.1, b.0, b.1
));
}
let segment_yaw = if along_x {
if dx >= 0.0 { 0.0 } else { 180.0 }
} else if dz >= 0.0 {
90.0
} else {
270.0
};
let post_yaw = if i == 0 {
segment_yaw
} else {
let corner = run.corners[i - 1];
corner.yaw
};
let _ = yaw;
yaw = post_yaw;
let count = (length / pitch).floor() as usize;
let ux = dx / length;
let uz = dz / length;
for n in 0..=count {
let at = (a.0 + ux * pitch * n as f32, a.1 + uz * pitch * n as f32);
posts.push(Post { at, yaw: post_yaw });
}
if posts.len() > MAX_POSTS {
return Err(format!(
"run: this run lays more than {MAX_POSTS} posts, which is a mistake rather than a \
fence. Shorten it or raise the pitch."
));
}
}
if posts.is_empty() {
return Err("run: the path laid no posts at all".to_owned());
}
for gap in &run.gaps {
if gap.count == 0 {
continue;
}
let end = gap.at_post + gap.count;
if end > posts.len() {
return Err(format!(
"run: a gap of {} post(s) at post {index} would need {end} posts, and this run \
lays {len}. A gap cannot eat more than the run has.",
gap.count,
index = gap.at_post,
len = posts.len()
));
}
for corner in &run.corners {
if posts
.iter()
.position(|p| {
(p.at.0 - corner.at.0).abs() < 1e-3 && (p.at.1 - corner.at.1).abs() < 1e-3
})
.is_some_and(|i| i >= gap.at_post && i < end)
{
return Err(format!(
"run: a gap of {} post(s) at post {index} would eat the post at the corner \
({}, {}), and the next stretch is measured from it.",
gap.count,
corner.at.0,
corner.at.1,
index = gap.at_post
));
}
}
posts.drain(gap.at_post..end);
}
if posts.len() > MAX_POSTS {
return Err(format!(
"run: this run lays {} posts, which is more than {MAX_POSTS}.",
posts.len()
));
}
Ok(posts)
}
pub const CONVERT_TOLERANCE: f32 = 1e-3;
pub fn rows_to_run(
rows: &[crate::map::Placed],
library: &Library,
) -> Result<(Run, bool, Option<String>), String> {
let first = rows.first().ok_or("run: no rows to convert")?;
let bad = |row: &crate::map::Placed, why: &str| {
Err(format!("run: row `{}` does not fit a run: {why}", row.id))
};
for row in rows {
let plain = crate::map::Placed {
id: row.id.clone(),
descriptor: row.descriptor.clone(),
at: row.at,
yaw: row.yaw,
owned: row.owned,
owned_because: row.owned_because.clone(),
..Default::default()
};
if row != &plain {
return bad(row, "it carries more than a position and a yaw (lift, tip, mount, patch, note...), which a run does not lay");
}
if row.descriptor != first.descriptor {
return bad(row, &format!("it is `{}` and the first row is `{}`", row.descriptor, first.descriptor));
}
if row.owned != first.owned || row.owned_because != first.owned_because {
return bad(row, "its ownership differs from the first row's");
}
if !row.at.0.is_finite() || !row.at.1.is_finite() || !row.yaw.is_finite() {
return bad(row, "its position or yaw is not a number");
}
}
let descriptor = library
.get(&first.descriptor)
.ok_or_else(|| format!("run: descriptor `{}` is not in this library", first.descriptor))?;
let (w, depth) = crate::descriptor::placed_footprint(descriptor).ok_or_else(|| {
format!("run: `{}` has no measured footprint, so there is no pitch", first.descriptor)
})?;
let default_pitch = w.max(depth);
let dir = |a: (f32, f32), b: (f32, f32)| {
let (dx, dz) = (b.0 - a.0, b.1 - a.1);
let l = (dx * dx + dz * dz).sqrt();
(l > CONVERT_TOLERANCE).then(|| (dx / l, dz / l))
};
let mut starts: Vec<usize> = vec![0];
let mut seg_dir: Option<(f32, f32)> = None;
for i in 1..rows.len() {
let same_yaw = (rows[i].yaw - rows[i - 1].yaw).abs() < 1e-3;
let step = dir(rows[i - 1].at, rows[i].at);
let continues = same_yaw
&& match (step, seg_dir) {
(None, _) => false,
(Some(_), None) => true,
(Some(s), Some(d)) => (s.0 * d.0 + s.1 * d.1) > 1.0 - 1e-3,
};
if continues {
seg_dir = seg_dir.or(step);
} else {
starts.push(i);
seg_dir = None;
}
}
let mut steps: Vec<f32> = Vec::new();
for (k, &s) in starts.iter().enumerate() {
let end = starts.get(k + 1).copied().unwrap_or(rows.len());
for i in s + 1..end {
let (a, b) = (rows[i - 1].at, rows[i].at);
steps.push(((b.0 - a.0).powi(2) + (b.1 - a.1).powi(2)).sqrt());
}
}
let pitch = if steps.is_empty() || steps.iter().any(|d| (d - default_pitch).abs() < CONVERT_TOLERANCE) {
None
} else {
steps.iter().copied().reduce(f32::min)
};
let last_start = *starts.last().unwrap_or(&0);
let tail = &rows[last_start..];
let last = tail[tail.len() - 1].at;
let to = match (tail.len() > 1).then(|| dir(tail[0].at, last)).flatten() {
Some(d) => {
let half = pitch.unwrap_or(default_pitch) * 0.5;
(last.0 + d.0 * half, last.1 + d.1 * half)
}
None if starts.len() == 1 && rows.len() == 1 => {
return bad(&rows[0], "one row is not a run");
}
None => return bad(&tail[0], "the last stretch is a single post, so its direction is unknown"),
};
let corners: Vec<Corner> = starts
.iter()
.skip(1)
.map(|&i| Corner { at: rows[i].at, yaw: rows[i].yaw })
.collect();
let mut run = Run {
descriptor: first.descriptor.clone(),
pitch,
from: first.at,
to,
corners,
gaps: Vec::new(),
};
let posts = lay(&run, library)?;
let mut cursor = 0usize;
let mut skipped: Vec<usize> = Vec::new();
for row in rows {
let found = (cursor..posts.len()).find(|&j| {
(posts[j].at.0 - row.at.0).abs() <= CONVERT_TOLERANCE
&& (posts[j].at.1 - row.at.1).abs() <= CONVERT_TOLERANCE
&& (posts[j].yaw - row.yaw).abs() < 1e-3
});
let Some(j) = found else {
return bad(row, "it is not on the pitch the other rows keep, or faces another way");
};
skipped.extend(cursor..j);
cursor = j + 1;
}
skipped.extend(cursor..posts.len());
let mut i = 0;
while i < skipped.len() {
let at_post = skipped[i];
let mut count = 1;
while i + count < skipped.len() && skipped[i + count] == at_post + count {
count += 1;
}
run.gaps.push(Gap { count, at_post });
i += count;
}
Ok((run, first.owned, first.owned_because.clone()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::descriptor::Extent;
use crate::library::{LIBRARY_VERSION, Library};
fn library() -> Library {
Library {
version: LIBRARY_VERSION,
note: None,
descriptors: vec![Descriptor {
id: "kit/fence".to_owned(),
extent: Extent {
footprint: Some((2.064, 0.12)),
height: Some(0.838),
},
..Default::default()
}],
}
}
fn run() -> Run {
Run {
descriptor: "kit/fence".to_owned(),
pitch: None,
from: (0.0, 0.0),
to: (4.0, 0.0),
corners: Vec::new(),
gaps: Vec::new(),
}
}
#[test]
fn the_pitch_is_the_pieces_own_footprint() {
let posts = lay(&run(), &library()).unwrap_or_else(|e| panic!("{e}"));
assert_eq!(posts.len(), 2, "0 and 2.064: {posts:?}");
assert!((posts[1].at.0 - 2.064).abs() < 1e-4, "{posts:?}");
for rung in [1.0f32, 1.0 / 3.0, 1.0 / 9.0] {
assert!(
(posts[1].at.0 / rung).fract().abs() > 1e-3,
"the pitch landed on the {rung} rung, which is the grid the fill uses"
);
}
}
#[test]
fn a_run_across_the_pieces_short_side_is_refused() {
let mut r = run();
r.to = (0.0, 4.0);
let err = lay(&r, &library()).expect_err("across the short side");
assert!(err.contains("kit/fence"), "{err}");
assert!(err.contains("short side"), "{err}");
}
#[test]
fn a_corner_turns_the_piece() {
let mut r = run();
r.to = (4.0, 4.0);
r.corners = vec![Corner {
at: (4.0, 0.0),
yaw: 90.0,
}];
let posts = lay(&r, &library()).unwrap_or_else(|e| panic!("{e}"));
assert_eq!(posts[0].yaw, 0.0, "{posts:?}");
assert_eq!(posts[2].yaw, 90.0, "the corner post turns: {posts:?}");
assert!((posts[2].at.0 - 4.0).abs() < 1e-4, "{posts:?}");
assert!((posts[2].at.1 - 0.0).abs() < 1e-4, "{posts:?}");
assert!((posts[3].at.0 - 4.0).abs() < 1e-4, "{posts:?}");
assert!((posts[3].at.1 - 2.064).abs() < 1e-4, "{posts:?}");
}
#[test]
fn a_gap_removes_posts_and_leaves_the_rest_at_the_pitch() {
let mut r = run();
r.to = (8.0, 0.0);
r.gaps = vec![Gap {
count: 2,
at_post: 1,
}];
let posts = lay(&r, &library()).unwrap_or_else(|e| panic!("{e}"));
assert_eq!(posts.len(), 2, "{posts:?}");
assert!((posts[1].at.0 - 6.192).abs() < 1e-4, "{posts:?}");
}
#[test]
fn a_gap_wider_than_the_run_is_refused() {
let mut r = run();
r.gaps = vec![Gap {
count: 99,
at_post: 0,
}];
let err = lay(&r, &library()).expect_err("the gap is wider than the run");
assert!(err.contains("cannot eat more than the run has"), "{err}");
}
#[test]
fn an_unknown_piece_is_refused_by_name() {
let mut r = run();
r.descriptor = "kit/no_such_fence".to_owned();
let err = lay(&r, &library()).expect_err("no such piece");
assert!(err.contains("kit/no_such_fence"), "{err}");
}
#[test]
fn an_unmeasured_piece_is_refused() {
let mut lib = library();
lib.descriptors[0].extent.footprint = None;
let err = lay(&run(), &lib).expect_err("unmeasured");
assert!(err.contains("no measured footprint"), "{err}");
}
#[test]
fn a_bad_pitch_is_refused() {
for pitch in [0.0f32, -1.0, f32::NAN, f32::INFINITY] {
let mut r = run();
r.pitch = Some(pitch);
let err = lay(&r, &library()).expect_err("a bad pitch");
assert!(
!err.is_empty(),
"the refusal must say something about {pitch}"
);
}
}
#[test]
fn a_span_shorter_than_a_pitch_is_refused() {
let mut r = run();
r.to = (0.5, 0.0);
let err = lay(&r, &library()).expect_err("too short");
assert!(err.contains("shorter than one pitch"), "{err}");
}
}