Skip to main content

straight_skeleton/
roof.rs

1//! Roofs, raised from a straight skeleton.
2//!
3//! This is the classic application of a straight skeleton, and it is almost too
4//! neat. Picture a roof being built by raising the walls' eaves inward at a
5//! constant slope. At any moment the still-unroofed floor area is exactly the
6//! shrinking wavefront, and the height reached is exactly how far it has
7//! travelled. So the roof is not *computed* from the skeleton — it is *read off*
8//! it:
9//!
10//! - every skeleton [`Node`] is a roof vertex, at height [`Profile::height_at`]
11//!   its offset;
12//! - every skeleton [face] is one flat roof panel, rising from the wall that
13//!   face belongs to;
14//! - every skeleton [`Arc`] is a hip, valley, or ridge, where two panels meet.
15//!
16//! ```text
17//!      plan (a 2:1 rectangle)              roof, seen from the side
18//!
19//!    +-------------------+                        ______
20//!    | \               / |                       /|    |\
21//!    |   \___________/   |          =>          / |    | \
22//!    |   /           \   |                     /  |    |  \
23//!    | /               \ |                    /___|____|___\
24//!    +-------------------+
25//!         the skeleton                  the ridge is the skeleton's ridge,
26//!      (four hips, one ridge)           at height ridge_offset * pitch
27//! ```
28//!
29//! # The skeleton is the plan, not the roof
30//!
31//! Worth separating, because it is what makes everything below cheap. A
32//! straight skeleton says where the hips, valleys and ridges *run*. It says
33//! nothing about how high anything is — height is a function of one variable,
34//! [`Node::offset`], and that function is the [`Profile`].
35//!
36//! So the roof styles here are not different algorithms. They are the same
37//! skeleton read with a different profile:
38//!
39//! ```text
40//!     hip                mansard              truncated         truncated
41//!                                                                mansard
42//!        /\               ___                  _____              _____
43//!       /  \             /   \                /     \            /     \
44//!      /    \           |     |              /       \          |       |
45//!     /______\          |_____|             /_________\         |_______|
46//!
47//!     one pitch       steep, then          stopped short,     all three at
48//!                      shallow             leaving a flat        once
49//! ```
50//!
51//! The last two need a [`skeleton_constrained`], whose wavefront stops rather
52//! than collapsing; the flat is that [residual] raised to the limit's height.
53//! See [`Roof::with_profile`].
54//!
55//! [`Node`]: crate::Node
56//! [`Node::offset`]: crate::Node::offset
57//! [`Arc`]: crate::Arc
58//! [face]: crate::Skeleton::face
59//! [`skeleton_constrained`]: crate::skeleton_constrained
60//! [residual]: crate::Skeleton::residual
61//!
62//! # Examples
63//!
64//! ```
65//! use straight_skeleton::{skeleton, skeleton_constrained, Point, Polygon, Roof};
66//!
67//! // A 120 x 80 floor plan.
68//! let plan = Polygon::from_outer(&[
69//!     Point::new(0, 0),
70//!     Point::new(120, 0),
71//!     Point::new(120, 80),
72//!     Point::new(0, 80),
73//! ])?;
74//! let skel = skeleton(&plan)?;
75//!
76//! // A hip roof: one panel per wall. The ridge sits over the middle of the
77//! // short side, 40 in, so at a pitch of 0.5 it is 20 high.
78//! let hip = Roof::new(&skel, 0.5)?;
79//! assert_eq!(hip.panels().len(), 4);
80//! assert_eq!(hip.ridge_height(), 20);
81//!
82//! // A mansard over the *same* skeleton: steep to 10, then shallow. Two
83//! // panels per wall now, because the break cuts each one in half.
84//! let mansard = Roof::mansard(&skel, 2.0, 10.0, 0.25)?;
85//! assert_eq!(mansard.panels().len(), 8);
86//! assert_eq!(mansard.ridge_height(), 28);   // 10 * 2 + 30 * 0.25
87//!
88//! // Stop every wall at 15 and the apex is cut off, leaving a flat on top.
89//! let truncated = Roof::new(&skeleton_constrained(&plan, &[15.0; 4])?, 0.5)?;
90//! assert_eq!(truncated.flat().count(), 1);
91//! assert_eq!(truncated.ridge_height(), 8);  // 15 * 0.5, rounded
92//! # Ok::<(), Box<dyn std::error::Error>>(())
93//! ```
94
95use alloc::collections::BTreeMap;
96use alloc::vec::Vec;
97use core::fmt;
98
99use crate::math::Vec2;
100use crate::point::round_half_away_from_zero;
101use crate::polygon::EdgeId;
102use crate::skeleton::{NodeId, NodeKind, Skeleton};
103use crate::Point;
104
105/// Tolerance for deciding which side of a profile's break a corner sits on, in
106/// coordinate units.
107///
108/// Comfortably above `f32`'s resolution at the top of the coordinate range
109/// (~0.002) and far below one lattice unit, so a corner is only called "on the
110/// break" when it genuinely is.
111const BREAK_EPS: f32 = 1e-2;
112
113/// How a roof's height grows with distance from the eaves.
114///
115/// A straight skeleton gives the *plan* of a roof: where the hips, valleys and
116/// ridges run. It does not say how high anything is — that is this. Height is a
117/// function of one variable, [`Node::offset`], because a skeleton node's offset
118/// is how far the wavefront had travelled to reach it, which is exactly the run
119/// the roof has had to rise over.
120///
121/// So changing the roof *style* does not change the skeleton at all. A mansard
122/// over a plan has the same hips and ridge as a hip roof over it; only `z`
123/// differs.
124///
125/// [`Node::offset`]: crate::Node::offset
126///
127/// # Examples
128///
129/// ```
130/// use straight_skeleton::Profile;
131///
132/// // A hip: one slope all the way up.
133/// let hip = Profile::Hip { pitch: 0.5 };
134/// assert_eq!(hip.height_at(10.0), 5.0);
135///
136/// // A mansard: steep to 10, then shallow.
137/// let mansard = Profile::Mansard {
138///     lower_pitch: 2.0,
139///     break_offset: 10.0,
140///     upper_pitch: 0.25,
141/// };
142/// assert_eq!(mansard.height_at(10.0), 20.0);       // the break
143/// assert_eq!(mansard.height_at(30.0), 25.0);       // 20 + 20 * 0.25
144/// ```
145/// Exhaustive on purpose, unlike the crate's error types: this is a value
146/// callers are meant to match on, and a roof style they cannot handle is better
147/// caught by the compiler than by a `_` arm quietly treating it as something
148/// else.
149#[derive(Clone, Copy, Debug, PartialEq)]
150#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
151pub enum Profile {
152    /// One slope all the way to the ridge: `z = offset * pitch`.
153    ///
154    /// The classic hip roof.
155    Hip {
156        /// Rise over run. 1.0 gives 45°, 0.0 gives a flat roof.
157        pitch: f32,
158    },
159    /// Two slopes with a break between them: a **mansard**.
160    ///
161    /// Steep from the eaves up to `break_offset`, shallow from there on. That
162    /// is what a mansard is for — the steep lower slope buys headroom in the
163    /// storey inside it, and the shallow upper one keeps the whole thing from
164    /// becoming absurdly tall.
165    ///
166    /// The break is at a constant *offset*, which is a constant *height*
167    /// (`break_offset * lower_pitch`), so it comes out as a level line all the
168    /// way round the roof — the kerb a real mansard has.
169    ///
170    /// Nothing stops `upper_pitch` being the steeper of the two; the type does
171    /// not police taste. Equal pitches reduce to a [`Profile::Hip`].
172    Mansard {
173        /// Rise over run below the break.
174        lower_pitch: f32,
175        /// How far from the eaves the pitch changes, in plan units.
176        break_offset: f32,
177        /// Rise over run above the break.
178        upper_pitch: f32,
179    },
180}
181
182impl Profile {
183    /// The height at a given distance from the eaves.
184    #[inline]
185    pub fn height_at(self, offset: f32) -> f32 {
186        match self {
187            Profile::Hip { pitch } => offset * pitch,
188            Profile::Mansard {
189                lower_pitch,
190                break_offset,
191                upper_pitch,
192            } => {
193                if offset <= break_offset {
194                    offset * lower_pitch
195                } else {
196                    break_offset * lower_pitch + (offset - break_offset) * upper_pitch
197                }
198            }
199        }
200    }
201
202    /// The offset the slope changes at, if it changes at all.
203    ///
204    /// This is where panels have to be cut in two: a panel spanning it would be
205    /// bent rather than flat.
206    #[inline]
207    fn break_offset(self) -> Option<f32> {
208        match self {
209            Profile::Hip { .. } => None,
210            Profile::Mansard { break_offset, .. } => Some(break_offset),
211        }
212    }
213
214    fn validate(self) -> Result<(), RoofError> {
215        let bad = |p: f32| !p.is_finite() || p < 0.0;
216        match self {
217            Profile::Hip { pitch } => {
218                if bad(pitch) {
219                    return Err(RoofError::InvalidPitch { pitch });
220                }
221            }
222            Profile::Mansard {
223                lower_pitch,
224                break_offset,
225                upper_pitch,
226            } => {
227                if bad(lower_pitch) {
228                    return Err(RoofError::InvalidPitch { pitch: lower_pitch });
229                }
230                if bad(upper_pitch) {
231                    return Err(RoofError::InvalidPitch { pitch: upper_pitch });
232                }
233                if !break_offset.is_finite() || break_offset < 0.0 {
234                    return Err(RoofError::InvalidBreak { break_offset });
235                }
236            }
237        }
238        Ok(())
239    }
240}
241
242/// A point on the 3D integer lattice.
243///
244/// The `z` axis is up. Like [`Point`], every coordinate is `i16`, so a roof is
245/// `i16` in and `i16` out just as the rest of the crate is.
246///
247/// [`Point`]: crate::Point
248///
249/// # Examples
250///
251/// ```
252/// use straight_skeleton::Point3;
253///
254/// let p = Point3::new(1, 2, 3);
255/// assert_eq!(p.z, 3);
256/// assert_eq!(<[i16; 3]>::from(p), [1, 2, 3]);
257/// ```
258#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
259#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
260pub struct Point3 {
261    /// Horizontal coordinate.
262    pub x: i16,
263    /// Depth coordinate.
264    pub y: i16,
265    /// Height above the eaves.
266    pub z: i16,
267}
268
269impl Point3 {
270    /// The origin.
271    pub const ORIGIN: Point3 = Point3 { x: 0, y: 0, z: 0 };
272
273    /// Constructs a point from its coordinates.
274    #[inline]
275    pub const fn new(x: i16, y: i16, z: i16) -> Self {
276        Point3 { x, y, z }
277    }
278}
279
280impl From<Point3> for [i16; 3] {
281    #[inline]
282    fn from(p: Point3) -> Self {
283        [p.x, p.y, p.z]
284    }
285}
286
287impl From<[i16; 3]> for Point3 {
288    #[inline]
289    fn from([x, y, z]: [i16; 3]) -> Self {
290        Point3::new(x, y, z)
291    }
292}
293
294impl From<Point3> for (i16, i16, i16) {
295    #[inline]
296    fn from(p: Point3) -> Self {
297        (p.x, p.y, p.z)
298    }
299}
300
301/// Why a roof could not be raised.
302#[derive(Clone, Copy, Debug, PartialEq)]
303#[non_exhaustive]
304pub enum RoofError {
305    /// The pitch was negative, NaN, or infinite.
306    InvalidPitch {
307        /// The value supplied.
308        pitch: f32,
309    },
310    /// A wall's face could not be walked, so its panel has no outline.
311    ///
312    /// Should not happen for a skeleton of a valid polygon, constrained or not.
313    UnwalkableFace {
314        /// The wall whose panel could not be outlined.
315        wall: EdgeId,
316    },
317    /// A [`Profile::Mansard`]'s break was negative, NaN, or infinite.
318    InvalidBreak {
319        /// The value supplied.
320        break_offset: f32,
321    },
322    /// The skeleton's edges stopped **partway**, at different distances, so
323    /// there is no roof over it.
324    ///
325    /// A roof's height is a function of [`Node::offset`], and that only works
326    /// while offset means *distance from the wall*. On a plain skeleton it
327    /// always does. On a [`skeleton_constrained`] it is really the wavefront's
328    /// **time**, which is the same thing only until something stops early: an
329    /// edge that halted at 3 is 3 away from its face forever, however long the
330    /// clock runs on.
331    ///
332    /// What is allowed follows from that, and it is more than it first looks:
333    ///
334    /// - **No limits**, or limits that never bind. A plain hip roof.
335    /// - **One uniform limit.** Every edge stops together, at the top, so
336    ///   nothing stops *early* and the roof is a hip truncated to a flat.
337    /// - **A limit of zero**, mixed freely with either of the above. A wall that
338    ///   never moves sweeps nothing, so it has no sloping panel to be
339    ///   inconsistent about: its face is degenerate in plan, and stands up as a
340    ///   vertical **gable**. The neighbouring walls' corners slide along it, so
341    ///   the ridge runs out to it. See [`PanelKind::Slope`].
342    ///
343    /// What is refused is an edge stopping at some distance *in between*, while
344    /// others go further. That edge has a real sloping panel, and its far
345    /// corners would sit at their offset's height while being only `limit` from
346    /// the wall — so the panel would want to end lower than its neighbour's, and
347    /// the surface between them would have to tear. There is no roof to return,
348    /// so this says so rather than inventing one.
349    ///
350    /// [`Node::offset`]: crate::Node::offset
351    /// [`skeleton_constrained`]: crate::skeleton_constrained
352    UnevenLimits {
353        /// A node where an edge stopped before the rest of the roof did.
354        node: NodeId,
355        /// The offset it stopped at.
356        stopped_at: f32,
357        /// The offset the rest of the roof reaches.
358        reaches: f32,
359    },
360    /// A [`Profile::Mansard`]'s break left a wall's face in pieces that could
361    /// not be closed back up into coherent panels.
362    ///
363    /// The break is a straight line across a face, so it normally splits it into
364    /// a lower piece and an upper one — and a face whose wavefront was split
365    /// apart by two reflex vertices into several lower and upper pieces is
366    /// handled too, each piece emitted as its own panel. This is the safety net
367    /// for the geometry that is left: a face the break meets in a way that does
368    /// not resolve into closed pieces, which should not arise for the face of a
369    /// valid polygon. Refused rather than returned bent or self-touching.
370    BreakSplitsPanel {
371        /// The wall whose panel the break could not cleanly cut.
372        wall: EdgeId,
373        /// How many loose ends the break left on the face's outline. An odd
374        /// count is the tell — a closed outline must cross a line an even number
375        /// of times.
376        crossings: usize,
377    },
378    /// A vertex would stand higher than `i16` can hold.
379    ///
380    /// The plan is too wide for this pitch. Lower the pitch, or scale the plan
381    /// down. This is reported rather than saturated: a silently flattened ridge
382    /// is a wrong roof, not an approximate one.
383    HeightOverflow {
384        /// The offending node.
385        node: NodeId,
386        /// The height it wanted.
387        height: f32,
388    },
389}
390
391impl fmt::Display for RoofError {
392    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
393        match self {
394            RoofError::InvalidPitch { pitch } => {
395                write!(f, "pitch {pitch} is not a finite, non-negative number")
396            }
397            RoofError::UnwalkableFace { wall } => {
398                write!(f, "could not walk the face of wall {}", wall.0)
399            }
400            RoofError::InvalidBreak { break_offset } => write!(
401                f,
402                "mansard break {break_offset} is not a finite, non-negative distance"
403            ),
404            RoofError::UnevenLimits {
405                node,
406                stopped_at,
407                reaches,
408            } => write!(
409                f,
410                "an edge stopped at {stopped_at} (node {}) while the rest of the roof \
411                 reaches {reaches}; a roof needs every limit to be the same, or none",
412                node.0
413            ),
414            RoofError::BreakSplitsPanel { wall, crossings } => write!(
415                f,
416                "the mansard break left wall {}'s face with {crossings} loose ends, \
417                 so it could not be cut into coherent panels",
418                wall.0
419            ),
420            RoofError::HeightOverflow { node, height } => write!(
421                f,
422                "node {} would stand {height} high, which overflows i16; \
423                 lower the pitch or scale the plan down",
424                node.0
425            ),
426        }
427    }
428}
429
430#[cfg(feature = "std")]
431impl std::error::Error for RoofError {}
432
433/// Identifies a corner of a [`Roof`].
434///
435/// The first [`Skeleton::node_count`] of these stand directly over the skeleton
436/// nodes of the same number, so `RoofVertexId(i)` is `NodeId(i)` raised — which
437/// is what keeps provenance alive into 3D. A [`Profile::Mansard`] appends more
438/// beyond that, where its break line cuts across a panel; those stand over no
439/// skeleton node, and their [`RoofVertex::node`] is `None`.
440///
441/// [`Skeleton::node_count`]: crate::Skeleton::node_count
442#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
443#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
444pub struct RoofVertexId(pub u32);
445
446/// One corner of a roof.
447#[derive(Clone, Copy, Debug, PartialEq)]
448#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
449pub struct RoofVertex {
450    /// The corner's position, rounded to the integer lattice.
451    ///
452    /// `z` is [`Profile::height_at`] the corner's offset.
453    pub position: Point3,
454    /// The corner's unrounded position.
455    ///
456    /// Rounding `z` to the lattice tilts each panel very slightly, so a roof
457    /// built from [`RoofVertex::position`] is planar only to within half a
458    /// unit. Use this when exact planarity matters — a renderer computing
459    /// normals, say. It mirrors [`Node::exact`], for the same reason.
460    ///
461    /// [`Node::exact`]: crate::Node::exact
462    pub exact: [f32; 3],
463    /// The skeleton node this corner was raised from.
464    ///
465    /// `None` for a corner a [`Profile::Mansard`]'s break line introduced,
466    /// which lies partway along a skeleton arc rather than at either end of it.
467    pub node: Option<NodeId>,
468}
469
470/// What part of a roof a [`Panel`] is.
471#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
472#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
473pub enum PanelKind {
474    /// A panel rising from one wall.
475    ///
476    /// # Gables
477    ///
478    /// Usually sloping, but not always. A wall given a limit of **zero** never
479    /// moves, so it sweeps no plan area at all, and its face is degenerate —
480    /// every corner sits on the wall's own line. Stood up at `z =
481    /// height_at(offset)` that degenerate face becomes a vertical triangle: a
482    /// **gable**. The neighbouring walls' corners slide along the frozen wall
483    /// rather than over it, so the ridge runs out to it instead of hipping away.
484    ///
485    /// It is the same panel with the same rule applied, so it gets no special
486    /// variant. A consumer that wants to tell the two apart can: a gable's
487    /// footprint has zero area, and nothing else's does. The `roof` example
488    /// names its OBJ groups that way.
489    Slope {
490        /// The wall it rises from.
491        ///
492        /// This is the traceability a straight skeleton gives for free: no
493        /// search, no nearest-neighbour query. The panel *is* the region that
494        /// wall's wavefront swept.
495        wall: EdgeId,
496        /// Which band of the [`Profile`] it belongs to, counting from the
497        /// eaves. A [`Profile::Hip`] only ever has band 0; a
498        /// [`Profile::Mansard`] has band 0 below the break and band 1 above.
499        band: u8,
500    },
501    /// The flat a truncated roof stops at, standing over the skeleton's
502    /// [residual].
503    ///
504    /// One of these per residual loop, wound like the input's rings:
505    /// counter-clockwise for a flat's outline, clockwise for a hole in one. So
506    /// a flat with a hole in it — a courtyard's, say — is two `Flat` panels, and
507    /// the winding is what says which is which.
508    ///
509    /// That is deliberately the raw material rather than a finished mesh. A
510    /// consumer that cannot express holes has to triangulate, and *how* is its
511    /// own business: the crate hands over the loops and their winding, which is
512    /// the part only it knows. The `roof` example shows one way, in about thirty
513    /// lines.
514    ///
515    /// [residual]: crate::Skeleton::residual
516    Flat,
517}
518
519/// One flat plane of roof.
520#[derive(Clone, Debug, PartialEq)]
521#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
522pub struct Panel {
523    /// What part of the roof this is.
524    pub kind: PanelKind,
525    /// The panel's corners, in order, as indices into [`Roof::verts`].
526    pub corners: Vec<RoofVertexId>,
527}
528
529impl Panel {
530    /// The wall this panel rises from, or `None` if it is a flat.
531    #[inline]
532    pub fn wall(&self) -> Option<EdgeId> {
533        match self.kind {
534            PanelKind::Slope { wall, .. } => Some(wall),
535            PanelKind::Flat => None,
536        }
537    }
538
539    /// Whether this is the flat rather than a slope.
540    #[inline]
541    pub fn is_flat(&self) -> bool {
542        matches!(self.kind, PanelKind::Flat)
543    }
544}
545
546/// A roof over a floor plan.
547///
548/// Build one with [`Roof::new`] for a hip, [`Roof::mansard`] for two pitches, or
549/// [`Roof::with_profile`] for either — and for the truncated versions of both,
550/// over a constrained skeleton.
551///
552/// Vertices are indexed by [`RoofVertexId`], and the first
553/// [`Skeleton::node_count`] of those stand directly over the skeleton nodes of
554/// the same number, so the two structures stay in step and provenance survives
555/// into 3D.
556///
557/// # Why every panel is flat
558///
559/// A panel is the region swept by one wall's wavefront, so every point on it is
560/// `offset` away from that wall's supporting line — an affine function of
561/// position. Each band of a [`Profile`] is affine in that distance, so the
562/// composition is affine too, and the panel is a plane.
563///
564/// That is why a [`Profile::Mansard`]'s break has to *cut* panels rather than
565/// merely bend them: the profile is only affine within a band, so a panel
566/// spanning the break would be a fold, not a plane. Cut at the break, both
567/// halves are planes again.
568///
569/// Planarity is therefore guaranteed by construction rather than fitted, and the
570/// crate's tests re-derive every corner's height from its wall to check it.
571///
572/// [`Skeleton::node_count`]: crate::Skeleton::node_count
573///
574/// # Examples
575///
576/// ```
577/// use straight_skeleton::{skeleton, Point, Polygon, Roof};
578///
579/// // An L-shaped house.
580/// let plan = Polygon::from_outer(&[
581///     Point::new(0, 0),
582///     Point::new(160, 0),
583///     Point::new(160, 70),
584///     Point::new(70, 70),
585///     Point::new(70, 150),
586///     Point::new(0, 150),
587/// ])?;
588///
589/// let roof = Roof::new(&skeleton(&plan)?, 0.6)?;
590///
591/// // Six walls, six panels.
592/// assert_eq!(roof.panels().len(), 6);
593///
594/// // Every panel knows which wall it rises from.
595/// for (i, panel) in roof.panels().iter().enumerate() {
596///     assert_eq!(panel.wall().unwrap().0 as usize, i);
597/// }
598///
599/// // Eaves sit at zero; nothing is below them.
600/// assert!(roof.verts().iter().all(|v| v.position.z >= 0));
601/// # Ok::<(), Box<dyn std::error::Error>>(())
602/// ```
603#[derive(Clone, Debug, PartialEq)]
604#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
605pub struct Roof {
606    verts: Vec<RoofVertex>,
607    panels: Vec<Panel>,
608    profile: Profile,
609}
610
611impl Roof {
612    /// Raises a hip roof over a skeleton.
613    ///
614    /// `pitch` is rise over run: 1.0 gives 45°, 0.5 gives a shallower roof
615    /// half as tall, 0.0 gives a flat one.
616    ///
617    /// Shorthand for [`Roof::with_profile`] with a [`Profile::Hip`]. Use
618    /// [`Roof::mansard`] for two pitches with a break between them, and see
619    /// [`Roof::with_profile`] for what a **constrained** skeleton does here.
620    ///
621    /// # Errors
622    ///
623    /// As [`Roof::with_profile`].
624    ///
625    /// # Examples
626    ///
627    /// ```
628    /// use straight_skeleton::{skeleton, skeleton_constrained, Point, Polygon, Roof, RoofError};
629    ///
630    /// let plan = Polygon::from_outer(&[
631    ///     Point::new(0, 0), Point::new(80, 0), Point::new(80, 80), Point::new(0, 80),
632    /// ])?;
633    /// let skel = skeleton(&plan)?;
634    ///
635    /// // A square plan gives a pyramid: its apex is 40 in from every wall.
636    /// assert_eq!(Roof::new(&skel, 1.0)?.ridge_height(), 40);
637    /// assert_eq!(Roof::new(&skel, 0.5)?.ridge_height(), 20);
638    /// assert_eq!(Roof::new(&skel, 0.0)?.ridge_height(), 0);
639    ///
640    /// // A pitch that would push the apex past i16 is refused, not clamped.
641    /// assert!(matches!(
642    ///     Roof::new(&skel, 1000.0),
643    ///     Err(RoofError::HeightOverflow { .. })
644    /// ));
645    ///
646    /// // Stopping every wall at 10 cuts the apex off, leaving a flat.
647    /// let truncated = skeleton_constrained(&plan, &[10.0; 4])?;
648    /// let roof = Roof::new(&truncated, 1.0)?;
649    /// assert_eq!(roof.ridge_height(), 10);
650    /// assert_eq!(roof.flat().count(), 1);
651    /// # Ok::<(), Box<dyn std::error::Error>>(())
652    /// ```
653    pub fn new(skeleton: &Skeleton, pitch: f32) -> Result<Roof, RoofError> {
654        Roof::with_profile(skeleton, Profile::Hip { pitch })
655    }
656
657    /// Raises a **mansard** roof: steep to `break_offset`, shallow above it.
658    ///
659    /// Shorthand for [`Roof::with_profile`] with a [`Profile::Mansard`]. See
660    /// there for what a mansard is and why the skeleton underneath is the same
661    /// one a hip roof uses.
662    ///
663    /// # Errors
664    ///
665    /// As [`Roof::with_profile`].
666    ///
667    /// # Examples
668    ///
669    /// ```
670    /// use straight_skeleton::{skeleton, Point, Polygon, Roof};
671    ///
672    /// // A 120 x 80 plan. Its ridge is 40 in from the long walls.
673    /// let plan = Polygon::from_outer(&[
674    ///     Point::new(0, 0), Point::new(120, 0), Point::new(120, 80), Point::new(0, 80),
675    /// ])?;
676    /// let skel = skeleton(&plan)?;
677    ///
678    /// // Steep (2:1) for the first 10, then shallow (1:4) to the ridge.
679    /// let roof = Roof::mansard(&skel, 2.0, 10.0, 0.25)?;
680    ///
681    /// // 10 * 2 = 20 at the kerb, then 30 more of run at 0.25 = 7.5 -> 28.
682    /// assert_eq!(roof.ridge_height(), 28);
683    ///
684    /// // Each of the four walls now carries two panels rather than one: the
685    /// // steep skirt, and the shallow slope above it.
686    /// assert_eq!(roof.panels().len(), 8);
687    /// assert_eq!(roof.panels_of(straight_skeleton::EdgeId(0)).count(), 2);
688    ///
689    /// // A hip roof of the same plan is much taller for the same lower pitch.
690    /// assert_eq!(Roof::new(&skel, 2.0)?.ridge_height(), 80);
691    /// # Ok::<(), Box<dyn std::error::Error>>(())
692    /// ```
693    pub fn mansard(
694        skeleton: &Skeleton,
695        lower_pitch: f32,
696        break_offset: f32,
697        upper_pitch: f32,
698    ) -> Result<Roof, RoofError> {
699        Roof::with_profile(
700            skeleton,
701            Profile::Mansard {
702                lower_pitch,
703                break_offset,
704                upper_pitch,
705            },
706        )
707    }
708
709    /// Raises a roof over a skeleton, with any [`Profile`].
710    ///
711    /// # Constrained skeletons
712    ///
713    /// A [`skeleton_constrained`] with one **uniform** limit gives a *truncated*
714    /// roof: the slopes rise to the limit and stop, and the [residual] the
715    /// wavefront stopped as becomes a [`PanelKind::Flat`] on top. That works
716    /// with any profile, so a truncated mansard is steep, then shallow, then
717    /// flat.
718    ///
719    /// **Uneven** limits are refused — see [`RoofError::UnevenLimits`], which
720    /// explains why there is no such roof rather than merely no implementation.
721    ///
722    /// # Errors
723    ///
724    /// - [`RoofError::InvalidPitch`] if a pitch is negative, NaN, or infinite.
725    /// - [`RoofError::InvalidBreak`] likewise for a mansard's break.
726    /// - [`RoofError::UnevenLimits`] if the skeleton's edges stopped at
727    ///   different distances.
728    /// - [`RoofError::BreakSplitsPanel`] if a mansard's break leaves a face in
729    ///   pieces that cannot be closed back up (a safety net that should not fire
730    ///   for the face of a valid polygon).
731    /// - [`RoofError::HeightOverflow`] if the plan is too wide for the pitch to
732    ///   fit in `i16`.
733    /// - [`RoofError::UnwalkableFace`] if a wall's face is not a closed region,
734    ///   which should not happen for a skeleton of a valid polygon.
735    ///
736    /// [`skeleton_constrained`]: crate::skeleton_constrained
737    /// [residual]: crate::Skeleton::residual
738    ///
739    /// # Examples
740    ///
741    /// ```
742    /// use straight_skeleton::{skeleton_constrained, PanelKind, Point, Polygon, Profile, Roof};
743    ///
744    /// let plan = Polygon::from_outer(&[
745    ///     Point::new(0, 0), Point::new(100, 0), Point::new(100, 100), Point::new(0, 100),
746    /// ])?;
747    ///
748    /// // Every wall stopped at 20: a hip roof with its apex cut off.
749    /// let skel = skeleton_constrained(&plan, &[20.0; 4])?;
750    /// let roof = Roof::with_profile(&skel, Profile::Hip { pitch: 0.5 })?;
751    ///
752    /// // Four slopes, and the flat they stop at.
753    /// assert_eq!(roof.panels().len(), 5);
754    /// assert_eq!(roof.flat().count(), 1);
755    ///
756    /// // The flat stands at 20 * 0.5, and that is the top of the roof.
757    /// assert_eq!(roof.ridge_height(), 10);
758    /// # Ok::<(), Box<dyn std::error::Error>>(())
759    /// ```
760    pub fn with_profile(skeleton: &Skeleton, profile: Profile) -> Result<Roof, RoofError> {
761        profile.validate()?;
762        check_limits_are_even(skeleton)?;
763
764        let mut build = Build {
765            skel: skeleton,
766            profile,
767            verts: Vec::with_capacity(skeleton.node_count()),
768            cuts: BTreeMap::new(),
769        };
770
771        // Every skeleton node becomes a roof vertex, in step, so that
772        // `RoofVertexId(i)` is `NodeId(i)` raised. The wavefront's travel *is*
773        // the run, so the rise follows from the offset alone.
774        for (i, node) in skeleton.nodes().iter().enumerate() {
775            let id = NodeId(i as u32);
776            let height = profile.height_at(node.offset);
777            let z = lattice_height(height).ok_or(RoofError::HeightOverflow { node: id, height })?;
778            build.verts.push(RoofVertex {
779                position: Point3::new(node.position.x, node.position.y, z),
780                exact: [node.exact[0], node.exact[1], height],
781                node: Some(id),
782            });
783        }
784
785        let mut panels = Vec::with_capacity(skeleton.input_edge_count());
786        for i in 0..skeleton.input_edge_count() as u16 {
787            let wall = EdgeId(i);
788            let face = skeleton
789                .face(wall)
790                .ok_or(RoofError::UnwalkableFace { wall })?;
791            build.push_slopes(wall, &face, &mut panels)?;
792        }
793
794        // The flat, where the wavefront stopped rather than collapsing. It sits
795        // at one height throughout, so it needs no cutting whatever the profile.
796        for loop_ in skeleton.residual() {
797            panels.push(Panel {
798                kind: PanelKind::Flat,
799                corners: loop_.nodes.iter().map(|&n| RoofVertexId(n.0)).collect(),
800            });
801        }
802
803        Ok(Roof {
804            verts: build.verts,
805            panels,
806            profile,
807        })
808    }
809
810    /// Every corner of the roof, indexed by [`RoofVertexId`].
811    #[inline]
812    pub fn verts(&self) -> &[RoofVertex] {
813        &self.verts
814    }
815
816    /// Every panel: the slopes in wall order, then any flats.
817    #[inline]
818    pub fn panels(&self) -> &[Panel] {
819        &self.panels
820    }
821
822    /// The profile this roof was raised with.
823    #[inline]
824    pub fn profile(&self) -> Profile {
825        self.profile
826    }
827
828    /// A corner of the roof.
829    ///
830    /// # Panics
831    ///
832    /// Panics if `v` does not belong to this roof.
833    #[inline]
834    pub fn vertex(&self, v: RoofVertexId) -> &RoofVertex {
835        &self.verts[v.0 as usize]
836    }
837
838    /// The corner standing over a given skeleton node.
839    ///
840    /// # Panics
841    ///
842    /// Panics if `n` does not belong to the skeleton this roof was built from.
843    #[inline]
844    pub fn vertex_at(&self, n: NodeId) -> &RoofVertex {
845        &self.verts[n.0 as usize]
846    }
847
848    /// The panels rising from a given wall, from the eaves up.
849    ///
850    /// A [`Profile::Hip`] gives exactly one; a [`Profile::Mansard`] gives two
851    /// where its break crosses the panel, and one where it does not reach.
852    ///
853    /// # Examples
854    ///
855    /// ```
856    /// use straight_skeleton::{skeleton, EdgeId, Point, Polygon, Roof};
857    ///
858    /// let plan = Polygon::from_outer(&[
859    ///     Point::new(0, 0), Point::new(120, 0), Point::new(120, 80), Point::new(0, 80),
860    /// ])?;
861    /// let skel = skeleton(&plan)?;
862    ///
863    /// assert_eq!(Roof::new(&skel, 0.5)?.panels_of(EdgeId(0)).count(), 1);
864    /// assert_eq!(Roof::mansard(&skel, 2.0, 10.0, 0.25)?.panels_of(EdgeId(0)).count(), 2);
865    /// # Ok::<(), Box<dyn std::error::Error>>(())
866    /// ```
867    pub fn panels_of(&self, wall: EdgeId) -> impl Iterator<Item = &Panel> + '_ {
868        self.panels.iter().filter(move |p| p.wall() == Some(wall))
869    }
870
871    /// The flat panels, if this roof is truncated. Empty otherwise.
872    ///
873    /// More than one only when the flat has a hole in it — see
874    /// [`PanelKind::Flat`].
875    pub fn flat(&self) -> impl Iterator<Item = &Panel> + '_ {
876        self.panels.iter().filter(|p| p.is_flat())
877    }
878
879    /// The height of the highest point: the ridge, or a pyramid's apex.
880    ///
881    /// # Examples
882    ///
883    /// ```
884    /// use straight_skeleton::{skeleton, Point, Polygon, Roof};
885    ///
886    /// // A 120-wide, 80-deep plan. The ridge runs down the middle of the long
887    /// // axis, 40 in from each long wall, so at pitch 0.5 it stands 20 high.
888    /// let plan = Polygon::from_outer(&[
889    ///     Point::new(0, 0), Point::new(120, 0), Point::new(120, 80), Point::new(0, 80),
890    /// ])?;
891    /// assert_eq!(Roof::new(&skeleton(&plan)?, 0.5)?.ridge_height(), 20);
892    /// # Ok::<(), Box<dyn std::error::Error>>(())
893    /// ```
894    pub fn ridge_height(&self) -> i16 {
895        self.verts.iter().map(|v| v.position.z).max().unwrap_or(0)
896    }
897
898    /// The corners of one panel, as positions.
899    ///
900    /// # Panics
901    ///
902    /// Panics if `panel` does not belong to this roof.
903    pub fn outline(&self, panel: &Panel) -> Vec<Point3> {
904        panel
905            .corners
906            .iter()
907            .map(|&v| self.vertex(v).position)
908            .collect()
909    }
910}
911
912/// Every edge either never stopped, or stopped level with the top of the roof.
913///
914/// See [`RoofError::UnevenLimits`] for why anything else has no roof at all.
915fn check_limits_are_even(skeleton: &Skeleton) -> Result<(), RoofError> {
916    let reaches = skeleton.max_offset();
917    for (i, node) in skeleton.nodes().iter().enumerate() {
918        if node.kind == NodeKind::LimitReached && node.offset < reaches - BREAK_EPS {
919            return Err(RoofError::UnevenLimits {
920                node: NodeId(i as u32),
921                stopped_at: node.offset,
922                reaches,
923            });
924        }
925    }
926    Ok(())
927}
928
929/// Builds a roof's vertices, minting new ones where a profile's break cuts an
930/// arc.
931struct Build<'a> {
932    skel: &'a Skeleton,
933    profile: Profile,
934    verts: Vec<RoofVertex>,
935    /// Break-line corners, keyed by the **unordered** pair of skeleton nodes
936    /// whose arc they sit on.
937    ///
938    /// Two panels share every arc, and both cut it at the same place. Minting a
939    /// corner per panel would leave two vertices at one point, and a roof with a
940    /// seam down every hip that a renderer would show as a crack. Keyed this way
941    /// they get the same one.
942    cuts: BTreeMap<(u32, u32), RoofVertexId>,
943}
944
945impl Build<'_> {
946    /// Which side of the break a node's offset falls: -1 below, +1 above, 0 on.
947    fn side(&self, n: NodeId, at: f32) -> i8 {
948        let o = self.skel.node(n).offset;
949        if o < at - BREAK_EPS {
950            -1
951        } else if o > at + BREAK_EPS {
952            1
953        } else {
954            0
955        }
956    }
957
958    /// The corner where the break crosses the arc between two nodes.
959    ///
960    /// Offset is an affine function of position, so the crossing is a simple
961    /// interpolation — and the break's level set across a whole panel is a
962    /// straight line, which is why cutting there leaves both halves flat.
963    fn cut_between(&mut self, a: NodeId, b: NodeId, at: f32) -> Result<RoofVertexId, RoofError> {
964        // Canonical order, so the two panels sharing this arc compute the very
965        // same point rather than two that differ in the last bit.
966        let (lo, hi) = if a.0 < b.0 { (a, b) } else { (b, a) };
967        if let Some(&v) = self.cuts.get(&(lo.0, hi.0)) {
968            return Ok(v);
969        }
970
971        let (nlo, nhi) = (self.skel.node(lo), self.skel.node(hi));
972        let span = nhi.offset - nlo.offset;
973        // Guarded, though a caller only asks when the ends straddle the break,
974        // which needs them at least 2 * BREAK_EPS apart.
975        let t = if span.abs() < f32::EPSILON {
976            0.0
977        } else {
978            (at - nlo.offset) / span
979        };
980        let x = nlo.exact[0] + (nhi.exact[0] - nlo.exact[0]) * t;
981        let y = nlo.exact[1] + (nhi.exact[1] - nlo.exact[1]) * t;
982        let height = self.profile.height_at(at);
983        let z = lattice_height(height).ok_or(RoofError::HeightOverflow { node: lo, height })?;
984
985        let p = Point::from_vec2_rounded(Vec2::new(x, y));
986        let id = RoofVertexId(self.verts.len() as u32);
987        self.verts.push(RoofVertex {
988            position: Point3::new(p.x, p.y, z),
989            exact: [x, y, height],
990            node: None,
991        });
992        self.cuts.insert((lo.0, hi.0), id);
993        Ok(id)
994    }
995
996    /// Turns one wall's face into its panels: one if the break misses it, or the
997    /// several the break cuts it into.
998    ///
999    /// The break is a level set of `offset`, and `offset` is affine across a
1000    /// single face (it is the distance to the wall's own line), so the break is a
1001    /// straight line **within this face**. Splitting a face is therefore
1002    /// splitting a simple polygon by a line — see [`Build::split_at_break`] for
1003    /// the general case, which cuts a face into as many lower and upper pieces as
1004    /// the line leaves it in.
1005    fn push_slopes(
1006        &mut self,
1007        wall: EdgeId,
1008        face: &[NodeId],
1009        out: &mut Vec<Panel>,
1010    ) -> Result<(), RoofError> {
1011        let Some(at) = self.profile.break_offset() else {
1012            out.push(Panel {
1013                kind: PanelKind::Slope { wall, band: 0 },
1014                corners: face.iter().map(|&n| RoofVertexId(n.0)).collect(),
1015            });
1016            return Ok(());
1017        };
1018
1019        let sides: Vec<i8> = face.iter().map(|&n| self.side(n, at)).collect();
1020
1021        // Wholly on one side: the break does not reach this panel, or clears it
1022        // entirely. Either way it stays in one piece, corners untouched. Corners
1023        // sitting *on* the break (side 0) do not cut it — they are the kerb the
1024        // panel runs up to, not a crossing of it.
1025        if sides.iter().all(|&s| s <= 0) || sides.iter().all(|&s| s >= 0) {
1026            let band = if sides.iter().any(|&s| s > 0) { 1 } else { 0 };
1027            out.push(Panel {
1028                kind: PanelKind::Slope { wall, band },
1029                corners: face.iter().map(|&n| RoofVertexId(n.0)).collect(),
1030            });
1031            return Ok(());
1032        }
1033
1034        self.split_at_break(wall, face, &sides, at, out)
1035    }
1036
1037    /// Splits a face the break genuinely crosses into its lower and upper pieces.
1038    ///
1039    /// A face can end up in more than two pieces: a wall whose wavefront was
1040    /// split apart by two reflex vertices has a face that dips below the break
1041    /// and back more than once, so the break cuts it into several lower pieces
1042    /// and several upper ones. This handles the general even-crossing partition,
1043    /// of which the ordinary "one lower, one upper" split is the two-crossing
1044    /// case. See [`Build::trace_band`] for the tracing itself.
1045    fn split_at_break(
1046        &mut self,
1047        wall: EdgeId,
1048        face: &[NodeId],
1049        sides: &[i8],
1050        at: f32,
1051        out: &mut Vec<Panel>,
1052    ) -> Result<(), RoofError> {
1053        // Order break-line corners along the break, which is parallel to the
1054        // wall. A face's first two nodes are the wall's own endpoints, so their
1055        // difference is the wall's direction — projecting onto it sorts corners
1056        // as they lie along the break.
1057        let a0 = self.skel.node(face[0]).exact;
1058        let a1 = self.skel.node(face[1]).exact;
1059        let (dx, dy) = (a1[0] - a0[0], a1[1] - a0[1]);
1060        let along = |v: RoofVertexId, verts: &[RoofVertex]| {
1061            let e = verts[v.0 as usize].exact;
1062            (e[0] - a0[0]) * dx + (e[1] - a0[1]) * dy
1063        };
1064
1065        // The augmented boundary: the face's own corners, with a new one spliced
1066        // in wherever an edge crosses the break strictly (a `-1`/`+1` pair). That
1067        // makes every crossing of the break pass through a corner *on* it, so
1068        // each piece is bounded by real corners rather than mid-edge points.
1069        let mut bound: Vec<Bv> = Vec::with_capacity(face.len() + 4);
1070        let n = face.len();
1071        for i in 0..n {
1072            let v = RoofVertexId(face[i].0);
1073            let key = along(v, &self.verts);
1074            bound.push(Bv {
1075                id: v,
1076                side: sides[i],
1077                key,
1078            });
1079
1080            let j = (i + 1) % n;
1081            if sides[i] * sides[j] < 0 {
1082                let c = self.cut_between(face[i], face[j], at)?;
1083                let key = along(c, &self.verts);
1084                bound.push(Bv {
1085                    id: c,
1086                    side: 0,
1087                    key,
1088                });
1089            }
1090        }
1091
1092        let lower = self.trace_band(&bound, true, wall)?;
1093        let upper = self.trace_band(&bound, false, wall)?;
1094
1095        // A sliver with fewer than three corners encloses nothing; dropping it
1096        // is what keeps a break laid exactly on a node from emitting a
1097        // degenerate panel. Lower pieces first, so a wall reads eaves-up.
1098        for (band, pieces) in [(0u8, lower), (1u8, upper)] {
1099            for corners in pieces {
1100                if corners.len() >= 3 {
1101                    out.push(Panel {
1102                        kind: PanelKind::Slope { wall, band },
1103                        corners,
1104                    });
1105                }
1106            }
1107        }
1108        Ok(())
1109    }
1110
1111    /// Traces the closed pieces of one band of a cut face.
1112    ///
1113    /// The band's boundary is made of two kinds of edge: stretches of the face's
1114    /// own outline that lie on this side of the break, and chords *along* the
1115    /// break that close those stretches back up. The outline stretches are known
1116    /// outright; the chords are the puzzle, and they are recovered from where the
1117    /// outline meets the break.
1118    ///
1119    /// The break is a straight line, so the band meets it along a set of disjoint
1120    /// intervals. Each interval's two ends are an outline stretch ending on the
1121    /// break and another starting off it again — so, sorted along the break, the
1122    /// loose ends pair up consecutively, and joining each pair is the chord.
1123    fn trace_band(
1124        &self,
1125        bound: &[Bv],
1126        lower: bool,
1127        wall: EdgeId,
1128    ) -> Result<Vec<Vec<RoofVertexId>>, RoofError> {
1129        let m = bound.len();
1130        let in_band = |s: i8| if lower { s <= 0 } else { s >= 0 };
1131
1132        // `next[i]` is the corner following corner `i` around this band. Seed it
1133        // from the outline: an edge is this band's boundary when both its ends
1134        // are on this side (and it is not a stretch lying *along* the break,
1135        // which is a chord's job, not an edge's).
1136        let mut next = alloc::vec![usize::MAX; m];
1137        let mut has_prev = alloc::vec![false; m];
1138        for i in 0..m {
1139            let j = (i + 1) % m;
1140            let (si, sj) = (bound[i].side, bound[j].side);
1141            if si == 0 && sj == 0 {
1142                continue;
1143            }
1144            if in_band(si) && in_band(sj) {
1145                next[i] = j;
1146                has_prev[j] = true;
1147            }
1148        }
1149
1150        // The loose ends on the break: corners on it that the outline leaves by
1151        // (need an outgoing chord) or arrives at (need an incoming one).
1152        let mut needs_out: Vec<usize> = Vec::new();
1153        let mut needs_in: Vec<usize> = Vec::new();
1154        for i in 0..m {
1155            if bound[i].side != 0 {
1156                continue;
1157            }
1158            match (has_prev[i], next[i] != usize::MAX) {
1159                (true, false) => needs_out.push(i),
1160                (false, true) => needs_in.push(i),
1161                // Fully joined already (a corner the break merely touches), or
1162                // not part of this band at all.
1163                _ => {}
1164            }
1165        }
1166
1167        // Sorted along the break, a loose end that needs a chord out and one that
1168        // needs a chord in alternate, and each adjacent pair bounds one interval
1169        // the band covers. Join them.
1170        let mut ends: Vec<usize> = needs_out.iter().chain(&needs_in).copied().collect();
1171        ends.sort_by(|&a, &b| bound[a].key.total_cmp(&bound[b].key));
1172        if ends.len() % 2 != 0 {
1173            return Err(RoofError::BreakSplitsPanel {
1174                wall,
1175                crossings: ends.len(),
1176            });
1177        }
1178        for pair in ends.chunks_exact(2) {
1179            let (a, b) = (pair[0], pair[1]);
1180            // One end must be an exit, the other an entry; anything else is
1181            // geometry this cannot make coherent pieces of.
1182            let out_in = match (next[a] == usize::MAX, next[b] == usize::MAX) {
1183                (true, false) => Some((a, b)),
1184                (false, true) => Some((b, a)),
1185                _ => None,
1186            };
1187            let Some((from, to)) = out_in else {
1188                return Err(RoofError::BreakSplitsPanel {
1189                    wall,
1190                    crossings: ends.len(),
1191                });
1192            };
1193            next[from] = to;
1194        }
1195
1196        // Every loose end is joined now, so following `next` walks closed loops.
1197        let mut pieces = Vec::new();
1198        let mut seen = alloc::vec![false; m];
1199        for start in 0..m {
1200            if seen[start] || next[start] == usize::MAX {
1201                continue;
1202            }
1203            let mut corners = Vec::new();
1204            let mut cur = start;
1205            while !seen[cur] {
1206                seen[cur] = true;
1207                corners.push(bound[cur].id);
1208                cur = next[cur];
1209                if cur == usize::MAX {
1210                    return Err(RoofError::BreakSplitsPanel {
1211                        wall,
1212                        crossings: ends.len(),
1213                    });
1214                }
1215            }
1216            pieces.push(corners);
1217        }
1218        Ok(pieces)
1219    }
1220}
1221
1222/// One corner of a face's augmented boundary while a break is being traced
1223/// through it: a face corner, or one the break spliced onto an edge.
1224struct Bv {
1225    /// The roof vertex this corner is.
1226    id: RoofVertexId,
1227    /// Which side of the break it falls: -1 below, +1 above, 0 on.
1228    side: i8,
1229    /// Its position projected along the break, for ordering the on-break corners.
1230    key: f32,
1231}
1232
1233/// Rounds a height onto the lattice, or `None` if it will not fit.
1234///
1235/// Unlike [`crate::Point`]'s rounding, this refuses rather than saturates. A
1236/// saturated `x`/`y` is a node nudged by a hair at the very edge of the
1237/// coordinate space; a saturated `z` is a roof with its ridge sliced flat, and
1238/// the caller needs to know.
1239fn lattice_height(h: f32) -> Option<i16> {
1240    if !h.is_finite() {
1241        return None;
1242    }
1243    let r = round_half_away_from_zero(h);
1244    if r < i16::MIN as f32 || r > i16::MAX as f32 {
1245        return None;
1246    }
1247    Some(r as i16)
1248}
1249
1250#[cfg(test)]
1251mod tests {
1252    use super::*;
1253
1254    #[test]
1255    fn lattice_height_rounds_to_nearest() {
1256        assert_eq!(lattice_height(0.0), Some(0));
1257        assert_eq!(lattice_height(2.4), Some(2));
1258        assert_eq!(lattice_height(2.5), Some(3));
1259        assert_eq!(lattice_height(2.6), Some(3));
1260    }
1261
1262    #[test]
1263    fn lattice_height_refuses_rather_than_saturating() {
1264        assert_eq!(lattice_height(32767.0), Some(i16::MAX));
1265        assert_eq!(lattice_height(32768.0), None);
1266        assert_eq!(lattice_height(1e9), None);
1267        assert_eq!(lattice_height(f32::INFINITY), None);
1268        assert_eq!(lattice_height(f32::NAN), None);
1269    }
1270
1271    #[test]
1272    fn point3_conversions() {
1273        let p = Point3::new(1, -2, 3);
1274        assert_eq!(<[i16; 3]>::from(p), [1, -2, 3]);
1275        assert_eq!(Point3::from([1, -2, 3]), p);
1276        assert_eq!(<(i16, i16, i16)>::from(p), (1, -2, 3));
1277        assert_eq!(Point3::ORIGIN, Point3::new(0, 0, 0));
1278    }
1279}