use alloc::collections::BTreeMap;
use alloc::vec::Vec;
use core::fmt;
use crate::math::Vec2;
use crate::point::round_half_away_from_zero;
use crate::polygon::EdgeId;
use crate::skeleton::{NodeId, NodeKind, Skeleton};
use crate::Point;
const BREAK_EPS: f32 = 1e-2;
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Profile {
Hip {
pitch: f32,
},
Mansard {
lower_pitch: f32,
break_offset: f32,
upper_pitch: f32,
},
}
impl Profile {
#[inline]
pub fn height_at(self, offset: f32) -> f32 {
match self {
Profile::Hip { pitch } => offset * pitch,
Profile::Mansard {
lower_pitch,
break_offset,
upper_pitch,
} => {
if offset <= break_offset {
offset * lower_pitch
} else {
break_offset * lower_pitch + (offset - break_offset) * upper_pitch
}
}
}
}
#[inline]
fn break_offset(self) -> Option<f32> {
match self {
Profile::Hip { .. } => None,
Profile::Mansard { break_offset, .. } => Some(break_offset),
}
}
fn validate(self) -> Result<(), RoofError> {
let bad = |p: f32| !p.is_finite() || p < 0.0;
match self {
Profile::Hip { pitch } => {
if bad(pitch) {
return Err(RoofError::InvalidPitch { pitch });
}
}
Profile::Mansard {
lower_pitch,
break_offset,
upper_pitch,
} => {
if bad(lower_pitch) {
return Err(RoofError::InvalidPitch { pitch: lower_pitch });
}
if bad(upper_pitch) {
return Err(RoofError::InvalidPitch { pitch: upper_pitch });
}
if !break_offset.is_finite() || break_offset < 0.0 {
return Err(RoofError::InvalidBreak { break_offset });
}
}
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Point3 {
pub x: i16,
pub y: i16,
pub z: i16,
}
impl Point3 {
pub const ORIGIN: Point3 = Point3 { x: 0, y: 0, z: 0 };
#[inline]
pub const fn new(x: i16, y: i16, z: i16) -> Self {
Point3 { x, y, z }
}
}
impl From<Point3> for [i16; 3] {
#[inline]
fn from(p: Point3) -> Self {
[p.x, p.y, p.z]
}
}
impl From<[i16; 3]> for Point3 {
#[inline]
fn from([x, y, z]: [i16; 3]) -> Self {
Point3::new(x, y, z)
}
}
impl From<Point3> for (i16, i16, i16) {
#[inline]
fn from(p: Point3) -> Self {
(p.x, p.y, p.z)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub enum RoofError {
InvalidPitch {
pitch: f32,
},
UnwalkableFace {
wall: EdgeId,
},
InvalidBreak {
break_offset: f32,
},
UnevenLimits {
node: NodeId,
stopped_at: f32,
reaches: f32,
},
BreakSplitsPanel {
wall: EdgeId,
crossings: usize,
},
HeightOverflow {
node: NodeId,
height: f32,
},
}
impl fmt::Display for RoofError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RoofError::InvalidPitch { pitch } => {
write!(f, "pitch {pitch} is not a finite, non-negative number")
}
RoofError::UnwalkableFace { wall } => {
write!(f, "could not walk the face of wall {}", wall.0)
}
RoofError::InvalidBreak { break_offset } => write!(
f,
"mansard break {break_offset} is not a finite, non-negative distance"
),
RoofError::UnevenLimits {
node,
stopped_at,
reaches,
} => write!(
f,
"an edge stopped at {stopped_at} (node {}) while the rest of the roof \
reaches {reaches}; a roof needs every limit to be the same, or none",
node.0
),
RoofError::BreakSplitsPanel { wall, crossings } => write!(
f,
"the mansard break left wall {}'s face with {crossings} loose ends, \
so it could not be cut into coherent panels",
wall.0
),
RoofError::HeightOverflow { node, height } => write!(
f,
"node {} would stand {height} high, which overflows i16; \
lower the pitch or scale the plan down",
node.0
),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for RoofError {}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RoofVertexId(pub u32);
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RoofVertex {
pub position: Point3,
pub exact: [f32; 3],
pub node: Option<NodeId>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum PanelKind {
Slope {
wall: EdgeId,
band: u8,
},
Flat,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Panel {
pub kind: PanelKind,
pub corners: Vec<RoofVertexId>,
}
impl Panel {
#[inline]
pub fn wall(&self) -> Option<EdgeId> {
match self.kind {
PanelKind::Slope { wall, .. } => Some(wall),
PanelKind::Flat => None,
}
}
#[inline]
pub fn is_flat(&self) -> bool {
matches!(self.kind, PanelKind::Flat)
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Roof {
verts: Vec<RoofVertex>,
panels: Vec<Panel>,
profile: Profile,
}
impl Roof {
pub fn new(skeleton: &Skeleton, pitch: f32) -> Result<Roof, RoofError> {
Roof::with_profile(skeleton, Profile::Hip { pitch })
}
pub fn mansard(
skeleton: &Skeleton,
lower_pitch: f32,
break_offset: f32,
upper_pitch: f32,
) -> Result<Roof, RoofError> {
Roof::with_profile(
skeleton,
Profile::Mansard {
lower_pitch,
break_offset,
upper_pitch,
},
)
}
pub fn with_profile(skeleton: &Skeleton, profile: Profile) -> Result<Roof, RoofError> {
profile.validate()?;
check_limits_are_even(skeleton)?;
let mut build = Build {
skel: skeleton,
profile,
verts: Vec::with_capacity(skeleton.node_count()),
cuts: BTreeMap::new(),
};
for (i, node) in skeleton.nodes().iter().enumerate() {
let id = NodeId(i as u32);
let height = profile.height_at(node.offset);
let z = lattice_height(height).ok_or(RoofError::HeightOverflow { node: id, height })?;
build.verts.push(RoofVertex {
position: Point3::new(node.position.x, node.position.y, z),
exact: [node.exact[0], node.exact[1], height],
node: Some(id),
});
}
let mut panels = Vec::with_capacity(skeleton.input_edge_count());
for i in 0..skeleton.input_edge_count() as u16 {
let wall = EdgeId(i);
let face = skeleton
.face(wall)
.ok_or(RoofError::UnwalkableFace { wall })?;
build.push_slopes(wall, &face, &mut panels)?;
}
for loop_ in skeleton.residual() {
panels.push(Panel {
kind: PanelKind::Flat,
corners: loop_.nodes.iter().map(|&n| RoofVertexId(n.0)).collect(),
});
}
Ok(Roof {
verts: build.verts,
panels,
profile,
})
}
#[inline]
pub fn verts(&self) -> &[RoofVertex] {
&self.verts
}
#[inline]
pub fn panels(&self) -> &[Panel] {
&self.panels
}
#[inline]
pub fn profile(&self) -> Profile {
self.profile
}
#[inline]
pub fn vertex(&self, v: RoofVertexId) -> &RoofVertex {
&self.verts[v.0 as usize]
}
#[inline]
pub fn vertex_at(&self, n: NodeId) -> &RoofVertex {
&self.verts[n.0 as usize]
}
pub fn panels_of(&self, wall: EdgeId) -> impl Iterator<Item = &Panel> + '_ {
self.panels.iter().filter(move |p| p.wall() == Some(wall))
}
pub fn flat(&self) -> impl Iterator<Item = &Panel> + '_ {
self.panels.iter().filter(|p| p.is_flat())
}
pub fn ridge_height(&self) -> i16 {
self.verts.iter().map(|v| v.position.z).max().unwrap_or(0)
}
pub fn outline(&self, panel: &Panel) -> Vec<Point3> {
panel
.corners
.iter()
.map(|&v| self.vertex(v).position)
.collect()
}
}
fn check_limits_are_even(skeleton: &Skeleton) -> Result<(), RoofError> {
let reaches = skeleton.max_offset();
for (i, node) in skeleton.nodes().iter().enumerate() {
if node.kind == NodeKind::LimitReached && node.offset < reaches - BREAK_EPS {
return Err(RoofError::UnevenLimits {
node: NodeId(i as u32),
stopped_at: node.offset,
reaches,
});
}
}
Ok(())
}
struct Build<'a> {
skel: &'a Skeleton,
profile: Profile,
verts: Vec<RoofVertex>,
cuts: BTreeMap<(u32, u32), RoofVertexId>,
}
impl Build<'_> {
fn side(&self, n: NodeId, at: f32) -> i8 {
let o = self.skel.node(n).offset;
if o < at - BREAK_EPS {
-1
} else if o > at + BREAK_EPS {
1
} else {
0
}
}
fn cut_between(&mut self, a: NodeId, b: NodeId, at: f32) -> Result<RoofVertexId, RoofError> {
let (lo, hi) = if a.0 < b.0 { (a, b) } else { (b, a) };
if let Some(&v) = self.cuts.get(&(lo.0, hi.0)) {
return Ok(v);
}
let (nlo, nhi) = (self.skel.node(lo), self.skel.node(hi));
let span = nhi.offset - nlo.offset;
let t = if span.abs() < f32::EPSILON {
0.0
} else {
(at - nlo.offset) / span
};
let x = nlo.exact[0] + (nhi.exact[0] - nlo.exact[0]) * t;
let y = nlo.exact[1] + (nhi.exact[1] - nlo.exact[1]) * t;
let height = self.profile.height_at(at);
let z = lattice_height(height).ok_or(RoofError::HeightOverflow { node: lo, height })?;
let p = Point::from_vec2_rounded(Vec2::new(x, y));
let id = RoofVertexId(self.verts.len() as u32);
self.verts.push(RoofVertex {
position: Point3::new(p.x, p.y, z),
exact: [x, y, height],
node: None,
});
self.cuts.insert((lo.0, hi.0), id);
Ok(id)
}
fn push_slopes(
&mut self,
wall: EdgeId,
face: &[NodeId],
out: &mut Vec<Panel>,
) -> Result<(), RoofError> {
let Some(at) = self.profile.break_offset() else {
out.push(Panel {
kind: PanelKind::Slope { wall, band: 0 },
corners: face.iter().map(|&n| RoofVertexId(n.0)).collect(),
});
return Ok(());
};
let sides: Vec<i8> = face.iter().map(|&n| self.side(n, at)).collect();
if sides.iter().all(|&s| s <= 0) || sides.iter().all(|&s| s >= 0) {
let band = if sides.iter().any(|&s| s > 0) { 1 } else { 0 };
out.push(Panel {
kind: PanelKind::Slope { wall, band },
corners: face.iter().map(|&n| RoofVertexId(n.0)).collect(),
});
return Ok(());
}
self.split_at_break(wall, face, &sides, at, out)
}
fn split_at_break(
&mut self,
wall: EdgeId,
face: &[NodeId],
sides: &[i8],
at: f32,
out: &mut Vec<Panel>,
) -> Result<(), RoofError> {
let a0 = self.skel.node(face[0]).exact;
let a1 = self.skel.node(face[1]).exact;
let (dx, dy) = (a1[0] - a0[0], a1[1] - a0[1]);
let along = |v: RoofVertexId, verts: &[RoofVertex]| {
let e = verts[v.0 as usize].exact;
(e[0] - a0[0]) * dx + (e[1] - a0[1]) * dy
};
let mut bound: Vec<Bv> = Vec::with_capacity(face.len() + 4);
let n = face.len();
for i in 0..n {
let v = RoofVertexId(face[i].0);
let key = along(v, &self.verts);
bound.push(Bv {
id: v,
side: sides[i],
key,
});
let j = (i + 1) % n;
if sides[i] * sides[j] < 0 {
let c = self.cut_between(face[i], face[j], at)?;
let key = along(c, &self.verts);
bound.push(Bv {
id: c,
side: 0,
key,
});
}
}
let lower = self.trace_band(&bound, true, wall)?;
let upper = self.trace_band(&bound, false, wall)?;
for (band, pieces) in [(0u8, lower), (1u8, upper)] {
for corners in pieces {
if corners.len() >= 3 {
out.push(Panel {
kind: PanelKind::Slope { wall, band },
corners,
});
}
}
}
Ok(())
}
fn trace_band(
&self,
bound: &[Bv],
lower: bool,
wall: EdgeId,
) -> Result<Vec<Vec<RoofVertexId>>, RoofError> {
let m = bound.len();
let in_band = |s: i8| if lower { s <= 0 } else { s >= 0 };
let mut next = alloc::vec![usize::MAX; m];
let mut has_prev = alloc::vec![false; m];
for i in 0..m {
let j = (i + 1) % m;
let (si, sj) = (bound[i].side, bound[j].side);
if si == 0 && sj == 0 {
continue;
}
if in_band(si) && in_band(sj) {
next[i] = j;
has_prev[j] = true;
}
}
let mut needs_out: Vec<usize> = Vec::new();
let mut needs_in: Vec<usize> = Vec::new();
for i in 0..m {
if bound[i].side != 0 {
continue;
}
match (has_prev[i], next[i] != usize::MAX) {
(true, false) => needs_out.push(i),
(false, true) => needs_in.push(i),
_ => {}
}
}
let mut ends: Vec<usize> = needs_out.iter().chain(&needs_in).copied().collect();
ends.sort_by(|&a, &b| bound[a].key.total_cmp(&bound[b].key));
if ends.len() % 2 != 0 {
return Err(RoofError::BreakSplitsPanel {
wall,
crossings: ends.len(),
});
}
for pair in ends.chunks_exact(2) {
let (a, b) = (pair[0], pair[1]);
let out_in = match (next[a] == usize::MAX, next[b] == usize::MAX) {
(true, false) => Some((a, b)),
(false, true) => Some((b, a)),
_ => None,
};
let Some((from, to)) = out_in else {
return Err(RoofError::BreakSplitsPanel {
wall,
crossings: ends.len(),
});
};
next[from] = to;
}
let mut pieces = Vec::new();
let mut seen = alloc::vec![false; m];
for start in 0..m {
if seen[start] || next[start] == usize::MAX {
continue;
}
let mut corners = Vec::new();
let mut cur = start;
while !seen[cur] {
seen[cur] = true;
corners.push(bound[cur].id);
cur = next[cur];
if cur == usize::MAX {
return Err(RoofError::BreakSplitsPanel {
wall,
crossings: ends.len(),
});
}
}
pieces.push(corners);
}
Ok(pieces)
}
}
struct Bv {
id: RoofVertexId,
side: i8,
key: f32,
}
fn lattice_height(h: f32) -> Option<i16> {
if !h.is_finite() {
return None;
}
let r = round_half_away_from_zero(h);
if r < i16::MIN as f32 || r > i16::MAX as f32 {
return None;
}
Some(r as i16)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lattice_height_rounds_to_nearest() {
assert_eq!(lattice_height(0.0), Some(0));
assert_eq!(lattice_height(2.4), Some(2));
assert_eq!(lattice_height(2.5), Some(3));
assert_eq!(lattice_height(2.6), Some(3));
}
#[test]
fn lattice_height_refuses_rather_than_saturating() {
assert_eq!(lattice_height(32767.0), Some(i16::MAX));
assert_eq!(lattice_height(32768.0), None);
assert_eq!(lattice_height(1e9), None);
assert_eq!(lattice_height(f32::INFINITY), None);
assert_eq!(lattice_height(f32::NAN), None);
}
#[test]
fn point3_conversions() {
let p = Point3::new(1, -2, 3);
assert_eq!(<[i16; 3]>::from(p), [1, -2, 3]);
assert_eq!(Point3::from([1, -2, 3]), p);
assert_eq!(<(i16, i16, i16)>::from(p), (1, -2, 3));
assert_eq!(Point3::ORIGIN, Point3::new(0, 0, 0));
}
}