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}