Skip to main content

axioval_engine/
walking_surface.rs

1//! Stair flights and ramps: treads, risers, goings, sloped runs and the
2//! headroom above a walking surface.
3//!
4//! ADR 0004: this seam measures. Whether a riser is too high, a flight too
5//! irregular or a ramp too steep for its length is a rule's judgement over
6//! these measurements.
7//!
8//! A service reports **positions**, never the derived lengths: tread
9//! elevations, the positions of each tread's front and back edge along the
10//! flight's walking line, a run's lowest and highest points and its extent
11//! along its own ascending direction. Risers, goings, tread depths,
12//! nosings, rises, run lengths, slopes, tread widths and winder angles are
13//! computed here from those positions as intervals sure to hold the exact
14//! value, so no adapter can report a riser that disagrees with its treads.
15//! A position is an interval; the evidence is exact exactly when every
16//! position is a point.
17//!
18//! Widths come the same way: a tread or a run may report the positions of
19//! its two sides across its walking direction ([`across`]), stated only
20//! where it fills the rectangle between its ends and those sides. A landing
21//! ([`LandingEvidence`]) is the level surface a request's candidate carries
22//! at one end of a flight or run, reported as the positions of its far side
23//! and its sides along the direction leaving that end; its depth is taken
24//! from the end's arrival line. The clearance below a subject
25//! ([`ClearanceBelow`]) is its height above the floors of the spaces a
26//! request names.
27//!
28//! A flight climbs along its **walking line** ([`WalkingLine`]): a straight
29//! flight along one horizontal direction, a turning flight (winders, a
30//! quarter turn) along a plan polyline with a vertex on every tread. The
31//! request says where a turning flight's line runs
32//! ([`WalkingLinePlacement`]): midway across the treads, or at a stated
33//! distance from the side the flight turns towards. Goings are measured
34//! along that line, nosing to nosing. A turning flight's tread walks along
35//! its own direction, square to its nosing, and a winder fills no rectangle
36//! along any, so it has no sides and the flight no width. Treads may also
37//! carry their nosing edge ([`PlanSegment`]), from which winder angles are
38//! derived, and whether the riser below them is closed ([`RiserClosure`]).
39//!
40//! Handrails ([`HandrailEvidence`]) along a flight or a run are the rule's
41//! selection, reported as positions along and across the walking direction
42//! and as the height of their top above the pitch line: the nosing line of a
43//! flight, the surface of a run. A turning flight's handrails are measured
44//! part by part ([`StretchPart`]): each rail along one of its straight runs
45//! of treads, in that run's frame. The rails along one side are the pieces of
46//! its handrail, ordered bottom to top here ([`HandrailEvidence::side_rail`])
47//! with the gaps between them ([`HandrailEvidence::gap`]), never by an
48//! adapter.
49//!
50//! A service refuses a shape it cannot decide rather than approximate it.
51
52use std::sync::Arc;
53
54use axioval_ir::{Evidence, ObjectId};
55use thiserror::Error;
56
57use crate::{ConvexPlanRegion, ElevationInterval, MetricDirection};
58
59/// Failure to measure a stair flight, a ramp or headroom.
60#[derive(Clone, Debug, Error, PartialEq, Eq)]
61pub enum WalkingSurfaceError {
62    /// The service holds no geometry for this object.
63    #[error("no geometry for `{0}`")]
64    UnknownObject(ObjectId),
65    /// The geometry could not be measured: a bodiless object, a body the
66    /// host could not mesh, or a mesh that cannot be read.
67    #[error("walking surface unavailable: {0}")]
68    Unavailable(String),
69    /// The body is not a shape this service can decide, such as a winder,
70    /// a flight in several pieces or a warped ramp. Never a verdict.
71    #[error("walking surface unsupported: {0}")]
72    Unsupported(String),
73    /// A tessellation of curved faces could change the answer, so the
74    /// service refuses rather than report an estimate as a measurement.
75    #[error("walking surface is inexact: {0}")]
76    InexactGeometry(String),
77    /// Positions are unordered, a direction is not horizontal, or the
78    /// measurement names another object or request.
79    #[error("walking surface measurement is invalid")]
80    InvalidMeasurement,
81    /// Evidence reported as exact for intervals, or as inexact for points.
82    #[error("walking surface evidence does not match its exactness")]
83    InexactEvidence,
84}
85
86/// A length or ratio known to lie in `[lower, upper]`.
87#[derive(Clone, Copy, Debug, PartialEq)]
88pub struct MeasuredInterval {
89    lower: f64,
90    upper: f64,
91}
92
93impl MeasuredInterval {
94    /// An interval; bounds must be finite and ordered.
95    pub fn try_new(lower: f64, upper: f64) -> Result<Self, WalkingSurfaceError> {
96        if !lower.is_finite() || !upper.is_finite() || lower > upper {
97            return Err(WalkingSurfaceError::InvalidMeasurement);
98        }
99        Ok(Self { lower, upper })
100    }
101
102    /// Smallest possible value.
103    #[must_use]
104    pub fn lower(&self) -> f64 {
105        self.lower
106    }
107
108    /// Largest possible value.
109    #[must_use]
110    pub fn upper(&self) -> f64 {
111        self.upper
112    }
113
114    /// Whether the value is a single number.
115    #[must_use]
116    #[allow(clippy::float_cmp)]
117    pub fn is_point(&self) -> bool {
118        self.lower == self.upper
119    }
120}
121
122/// `a - b` for positions, as an interval sure to hold the exact difference
123/// of any two values the positions may take.
124fn between(a: ElevationInterval, b: ElevationInterval) -> MeasuredInterval {
125    let low = subtract_down(a.lower_metres(), b.upper_metres());
126    let high = subtract_up(a.upper_metres(), b.lower_metres());
127    MeasuredInterval {
128        lower: low,
129        upper: high.max(low),
130    }
131}
132
133/// `x - y` rounded towards negative infinity.
134fn subtract_down(x: f64, y: f64) -> f64 {
135    let (rounded, error) = two_difference(x, y);
136    if error < 0.0 {
137        rounded.next_down()
138    } else {
139        rounded
140    }
141}
142
143/// `x - y` rounded towards positive infinity.
144fn subtract_up(x: f64, y: f64) -> f64 {
145    let (rounded, error) = two_difference(x, y);
146    if error > 0.0 {
147        rounded.next_up()
148    } else {
149        rounded
150    }
151}
152
153/// The rounded difference and its rounding error: `x - y = rounded + error`
154/// exactly (two-sum).
155fn two_difference(x: f64, y: f64) -> (f64, f64) {
156    let rounded = x - y;
157    let back = rounded - x;
158    let error = (x - (rounded - back)) + (-y - back);
159    (rounded, error)
160}
161
162/// `x / y` for positive `y`, rounded towards negative (`up == false`) or
163/// positive infinity.
164fn divide(x: f64, y: f64, up: bool) -> f64 {
165    let rounded = x / y;
166    // `rounded * y - x` exactly, with one rounding: its sign says on which
167    // side of the exact quotient `rounded` lies.
168    let residual = rounded.mul_add(y, -x);
169    match (up, residual) {
170        (true, residual) if residual < 0.0 => rounded.next_up(),
171        (false, residual) if residual > 0.0 => rounded.next_down(),
172        _ => rounded,
173    }
174}
175
176/// A straight edge in plan whose ends are known within `radius` metres.
177///
178/// A tread's nosing is one: the edge the walking line climbs onto it
179/// across. An exact mesh gives its ends as points (radius zero); a
180/// tessellation widens them by its chord deviation.
181#[derive(Clone, Copy, Debug, PartialEq)]
182pub struct PlanSegment {
183    from: [f64; 2],
184    to: [f64; 2],
185    radius: f64,
186}
187
188impl PlanSegment {
189    /// The edge from `from` to `to`, each end within `radius` of the given
190    /// point. Coordinates must be finite, the ends distinct and the radius
191    /// finite and not negative.
192    pub fn try_new(from: [f64; 2], to: [f64; 2], radius: f64) -> Result<Self, WalkingSurfaceError> {
193        let finite = from.iter().chain(&to).all(|value| value.is_finite());
194        #[allow(clippy::float_cmp)]
195        if !finite || from == to || !radius.is_finite() || radius < 0.0 {
196            return Err(WalkingSurfaceError::InvalidMeasurement);
197        }
198        Ok(Self { from, to, radius })
199    }
200
201    /// One end.
202    #[must_use]
203    pub fn from(&self) -> [f64; 2] {
204        self.from
205    }
206
207    /// The other end.
208    #[must_use]
209    pub fn to(&self) -> [f64; 2] {
210        self.to
211    }
212
213    /// How far each true end may lie from the given one.
214    #[must_use]
215    pub fn radius(&self) -> f64 {
216        self.radius
217    }
218
219    /// The angle between the lines of two edges, in radians from `0`
220    /// (parallel) to `π/2` (square), as an interval sure to hold the angle
221    /// between any two edges whose ends lie within the radii. `None` when an
222    /// edge is too short for its radius to fix a direction.
223    #[must_use]
224    pub fn angle_to(&self, other: &PlanSegment) -> Option<MeasuredInterval> {
225        // Moving each end of an edge of length `L` by at most `r` turns it
226        // by at most `asin(2r / L)`. The subtraction below rounds each
227        // component by at most `ε·|coordinate|`, which counts as radius.
228        let turn = |segment: &PlanSegment| -> Option<([f64; 2], f64)> {
229            let vector = [
230                segment.to[0] - segment.from[0],
231                segment.to[1] - segment.from[1],
232            ];
233            let magnitude = segment
234                .from
235                .iter()
236                .chain(&segment.to)
237                .fold(0.0_f64, |most, value| most.max(value.abs()));
238            let radius = 4.0f64.mul_add(f64::EPSILON * magnitude, segment.radius);
239            let length = vector[0].hypot(vector[1]) * 4.0f64.mul_add(-f64::EPSILON, 1.0);
240            if length <= 2.0 * radius {
241                return None;
242            }
243            Some((vector, (2.0 * radius / length).asin()))
244        };
245        let (u, first) = turn(self)?;
246        let (v, second) = turn(other)?;
247        let cross = u[0].mul_add(v[1], -(u[1] * v[0]));
248        let dot = u[0].mul_add(v[0], u[1] * v[1]);
249        let angle = cross.abs().atan2(dot.abs());
250        // The products and `atan2` round by a few units in the last place
251        // of an angle no larger than π/2.
252        let spread = first + second + 16.0 * f64::EPSILON;
253        let lower = (angle - spread).max(0.0);
254        let upper = (angle + spread).min(std::f64::consts::FRAC_PI_2).max(lower);
255        MeasuredInterval::try_new(lower, upper).ok()
256    }
257
258    fn is_exact(&self) -> bool {
259        self.radius == 0.0
260    }
261}
262
263/// Whether the riser below a tread closes the step.
264#[derive(Clone, Copy, Debug, PartialEq, Eq)]
265pub enum RiserClosure {
266    /// A riser rises from the tread below across the whole step.
267    Closed,
268    /// The step is open: the tread below ends in a face falling away from
269    /// it rather than in a riser climbing to this tread.
270    Open,
271    /// The service did not decide it, such as the first riser of a flight
272    /// whose front face does not reach the level it starts from.
273    NotMeasured,
274}
275
276/// One tread: an upward-facing horizontal face of a flight.
277///
278/// `front` and `back` are the positions along the flight's walking line
279/// where the line climbs onto the tread and leaves it; the front edge is
280/// the nosing. A service may add the nosing edge itself, the tread's extent
281/// across the walking line and whether the riser below it is closed.
282#[derive(Clone, Copy, Debug, PartialEq)]
283pub struct Tread {
284    elevation: ElevationInterval,
285    front: ElevationInterval,
286    back: ElevationInterval,
287    sides: Option<Sides>,
288    nosing: Option<PlanSegment>,
289    riser_below: RiserClosure,
290}
291
292/// The positions of a surface's two sides across a walking direction, along
293/// [`across`] it: `left` the lower, `right` the higher.
294type Sides = (ElevationInterval, ElevationInterval);
295
296/// The horizontal direction a quarter turn anticlockwise from `direction` in
297/// plan: the axis widths and sides are measured along.
298#[must_use]
299pub fn across(direction: MetricDirection) -> MetricDirection {
300    let [x, y, _] = direction.components();
301    // A quarter turn swaps and negates the components: still a unit vector,
302    // so the normalization leaves it as it is.
303    MetricDirection::try_new([-y, x, 0.0]).unwrap_or(direction)
304}
305
306fn sides(left: ElevationInterval, right: ElevationInterval) -> Result<Sides, WalkingSurfaceError> {
307    if left.lower_metres() > right.lower_metres() || left.upper_metres() > right.upper_metres() {
308        return Err(WalkingSurfaceError::InvalidMeasurement);
309    }
310    Ok((left, right))
311}
312
313fn sides_exact(sides: Option<Sides>) -> bool {
314    sides.is_none_or(|(left, right)| left.is_exact() && right.is_exact())
315}
316
317/// The least of some widths: an interval sure to hold the narrowest.
318fn least(widths: impl Iterator<Item = MeasuredInterval>) -> Option<MeasuredInterval> {
319    widths.reduce(|least, width| MeasuredInterval {
320        lower: least.lower.min(width.lower),
321        upper: least.upper.min(width.upper),
322    })
323}
324
325impl Tread {
326    /// A tread at `elevation` spanning `front` to `back` along the walking
327    /// line.
328    pub fn try_new(
329        elevation: ElevationInterval,
330        front: ElevationInterval,
331        back: ElevationInterval,
332    ) -> Result<Self, WalkingSurfaceError> {
333        if front.lower_metres() > back.lower_metres() || front.upper_metres() > back.upper_metres()
334        {
335            return Err(WalkingSurfaceError::InvalidMeasurement);
336        }
337        Ok(Self {
338            elevation,
339            front,
340            back,
341            sides: None,
342            nosing: None,
343            riser_below: RiserClosure::NotMeasured,
344        })
345    }
346
347    /// The tread with the positions of its sides along [`across`] its
348    /// walking direction: the flight's for a straight flight, the direction
349    /// square to its nosing for a turning flight's tread. A service states
350    /// them only where the tread fills the rectangle between its front,
351    /// back and sides, so the width holds all along its depth; a winder has
352    /// none.
353    pub fn with_sides(
354        mut self,
355        left: ElevationInterval,
356        right: ElevationInterval,
357    ) -> Result<Self, WalkingSurfaceError> {
358        self.sides = Some(sides(left, right)?);
359        Ok(self)
360    }
361
362    /// The tread with its nosing edge in plan.
363    #[must_use]
364    pub fn with_nosing(mut self, nosing: PlanSegment) -> Self {
365        self.nosing = Some(nosing);
366        self
367    }
368
369    /// The tread with whether the riser below it closes the step.
370    #[must_use]
371    pub fn with_riser_below(mut self, riser: RiserClosure) -> Self {
372        self.riser_below = riser;
373        self
374    }
375
376    /// The positions of the tread's sides across its walking direction,
377    /// when measured.
378    #[must_use]
379    pub fn sides(&self) -> Option<(ElevationInterval, ElevationInterval)> {
380        self.sides
381    }
382
383    /// The tread's width across its walking direction, when its sides were
384    /// measured. Nothing is deducted for handrails.
385    #[must_use]
386    pub fn width(&self) -> Option<MeasuredInterval> {
387        self.sides.map(|(left, right)| between(right, left))
388    }
389
390    /// Elevation of the tread's surface.
391    #[must_use]
392    pub fn elevation(&self) -> ElevationInterval {
393        self.elevation
394    }
395
396    /// Position of the front edge (the nosing) along the walking line.
397    #[must_use]
398    pub fn front(&self) -> ElevationInterval {
399        self.front
400    }
401
402    /// Position of the back edge along the walking line.
403    #[must_use]
404    pub fn back(&self) -> ElevationInterval {
405        self.back
406    }
407
408    /// The nosing edge in plan, when measured.
409    #[must_use]
410    pub fn nosing(&self) -> Option<PlanSegment> {
411        self.nosing
412    }
413
414    /// Whether the riser below the tread closes the step.
415    #[must_use]
416    pub fn riser_below(&self) -> RiserClosure {
417        self.riser_below
418    }
419
420    /// The tread's depth along the walking line, back less front.
421    #[must_use]
422    pub fn depth(&self) -> MeasuredInterval {
423        between(self.back, self.front)
424    }
425
426    /// Whether every position of the tread is known exactly.
427    #[must_use]
428    pub fn is_exact(&self) -> bool {
429        self.elevation.is_exact()
430            && self.front.is_exact()
431            && self.back.is_exact()
432            && sides_exact(self.sides)
433            && self.nosing.is_none_or(|nosing| nosing.is_exact())
434    }
435}
436
437/// Where a turning flight's walking line runs across its treads.
438#[derive(Clone, Copy, Debug, PartialEq)]
439pub enum WalkingLinePlacement {
440    /// Midway across each tread: the flight's centre line.
441    Centre,
442    /// This many metres from the side the flight turns towards, across
443    /// each tread. Built through [`TreadFlightRequest::from_inner_side`].
444    FromInnerSide(f64),
445}
446
447/// A request for an object's stair flight, with where its walking line
448/// runs should it turn. A straight flight's goings are the same on every
449/// line along it, so it ignores the placement.
450#[derive(Clone, Debug, PartialEq)]
451pub struct TreadFlightRequest {
452    object: ObjectId,
453    walking_line: WalkingLinePlacement,
454}
455
456impl TreadFlightRequest {
457    /// The flight of `object`, its walking line on its centre line.
458    #[must_use]
459    pub fn new(object: ObjectId) -> Self {
460        Self {
461            object,
462            walking_line: WalkingLinePlacement::Centre,
463        }
464    }
465
466    /// The flight of `object`, its walking line `offset` metres from the
467    /// side it turns towards. The offset must be finite and positive.
468    pub fn from_inner_side(object: ObjectId, offset: f64) -> Result<Self, WalkingSurfaceError> {
469        if !offset.is_finite() || offset <= 0.0 {
470            return Err(WalkingSurfaceError::InvalidMeasurement);
471        }
472        Ok(Self {
473            object,
474            walking_line: WalkingLinePlacement::FromInnerSide(offset),
475        })
476    }
477
478    /// The object whose flight is measured.
479    #[must_use]
480    pub fn object(&self) -> &ObjectId {
481        &self.object
482    }
483
484    /// Where the walking line runs.
485    #[must_use]
486    pub fn walking_line(&self) -> WalkingLinePlacement {
487        self.walking_line
488    }
489}
490
491/// The line a flight is walked along, in plan.
492#[derive(Clone, Debug, PartialEq)]
493pub enum WalkingLine {
494    /// A straight flight climbs along one horizontal direction; positions
495    /// along it are projections onto that direction.
496    Straight(MetricDirection),
497    /// A turning flight climbs along a polyline with a vertex on every
498    /// tread, bottom to top; positions along it are arc lengths from its
499    /// first vertex, before that vertex and after its last along its end
500    /// segments extended.
501    Turning(Vec<[f64; 2]>),
502}
503
504impl WalkingLine {
505    /// Whether the line turns.
506    #[must_use]
507    pub fn is_turning(&self) -> bool {
508        matches!(self, Self::Turning(_))
509    }
510
511    #[allow(clippy::float_cmp)]
512    fn is_valid(&self) -> bool {
513        match self {
514            Self::Straight(direction) => direction.components()[2] == 0.0,
515            Self::Turning(vertices) => {
516                vertices.len() >= 2
517                    && vertices.iter().flatten().all(|value| value.is_finite())
518                    && vertices.windows(2).all(|pair| pair[0] != pair[1])
519            }
520        }
521    }
522}
523
524/// A stair flight measured from its body.
525///
526/// The flight rises from `base`, its body's lowest point, through its treads
527/// in ascending order, to `top`, its body's highest point. The first riser
528/// runs from the base to the first tread: the flight is taken to stand on
529/// the level it starts from. When the top lies above the last tread, the
530/// flight ends in a riser from the last tread to the top, meeting the upper
531/// floor; when it lies no higher than the last tread's surface, the last
532/// tread is the top step. A top the intervals cannot place either way is
533/// refused.
534#[derive(Clone, Debug, PartialEq)]
535pub struct TreadFlight {
536    request: TreadFlightRequest,
537    walking_line: WalkingLine,
538    base: ElevationInterval,
539    top: ElevationInterval,
540    treads: Vec<Tread>,
541    ends_in_riser: bool,
542    final_riser: RiserClosure,
543    evidence: Evidence,
544}
545
546impl TreadFlight {
547    /// The flight answering `request`, climbing along `walking_line`.
548    ///
549    /// Treads must be given bottom to top, strictly ascending in elevation
550    /// and in front position; the base must lie below the first tread and
551    /// the top at or above the last, decidably. A straight line must be
552    /// horizontal, a turning one at least two distinct plan points. The
553    /// evidence is exact exactly when every position is a point.
554    pub fn try_new(
555        request: TreadFlightRequest,
556        walking_line: WalkingLine,
557        base: ElevationInterval,
558        top: ElevationInterval,
559        treads: Vec<Tread>,
560        evidence: Evidence,
561    ) -> Result<Self, WalkingSurfaceError> {
562        if !walking_line.is_valid() {
563            return Err(WalkingSurfaceError::InvalidMeasurement);
564        }
565        let (Some(first), Some(last)) = (treads.first(), treads.last()) else {
566            return Err(WalkingSurfaceError::InvalidMeasurement);
567        };
568        if base.upper_metres() >= first.elevation.lower_metres() {
569            return Err(WalkingSurfaceError::InvalidMeasurement);
570        }
571        for pair in treads.windows(2) {
572            if pair[0].elevation.upper_metres() >= pair[1].elevation.lower_metres()
573                || pair[0].front.upper_metres() >= pair[1].front.lower_metres()
574            {
575                return Err(WalkingSurfaceError::InvalidMeasurement);
576            }
577        }
578        let ends_in_riser = if top.lower_metres() > last.elevation.upper_metres() {
579            true
580        } else if top.upper_metres() <= last.elevation.upper_metres()
581            && top.upper_metres() >= last.elevation.lower_metres()
582        {
583            // The top lies on the last tread's own surface.
584            false
585        } else {
586            // Whether the flight ends in a riser cannot be decided.
587            return Err(WalkingSurfaceError::InvalidMeasurement);
588        };
589        let exact = base.is_exact() && top.is_exact() && treads.iter().all(Tread::is_exact);
590        if evidence.exact != exact || evidence.locator.trim().is_empty() {
591            return Err(WalkingSurfaceError::InexactEvidence);
592        }
593        Ok(Self {
594            request,
595            walking_line,
596            base,
597            top,
598            treads,
599            ends_in_riser,
600            final_riser: RiserClosure::NotMeasured,
601            evidence,
602        })
603    }
604
605    /// The flight with whether its final riser, from the last tread to the
606    /// top, closes the step; ignored when the flight ends on its last
607    /// tread.
608    #[must_use]
609    pub fn with_final_riser(mut self, riser: RiserClosure) -> Self {
610        self.final_riser = riser;
611        self
612    }
613
614    /// The request this answers.
615    #[must_use]
616    pub fn request(&self) -> &TreadFlightRequest {
617        &self.request
618    }
619
620    /// The measured object.
621    #[must_use]
622    pub fn object(&self) -> &ObjectId {
623        &self.request.object
624    }
625
626    /// The line the flight climbs along.
627    #[must_use]
628    pub fn walking_line(&self) -> &WalkingLine {
629        &self.walking_line
630    }
631
632    /// Elevation of the body's lowest point, where the first riser starts.
633    #[must_use]
634    pub fn base(&self) -> ElevationInterval {
635        self.base
636    }
637
638    /// Elevation of the body's highest point.
639    #[must_use]
640    pub fn top(&self) -> ElevationInterval {
641        self.top
642    }
643
644    /// The treads, bottom to top.
645    #[must_use]
646    pub fn treads(&self) -> &[Tread] {
647        &self.treads
648    }
649
650    /// Whether the flight ends in a riser above its last tread.
651    #[must_use]
652    pub fn ends_in_riser(&self) -> bool {
653        self.ends_in_riser
654    }
655
656    /// Riser heights, bottom to top: from the base to the first tread,
657    /// between consecutive treads, and from the last tread to the top when
658    /// the flight ends in a riser. Their number is the flight's number of
659    /// risers (steps).
660    #[must_use]
661    pub fn risers(&self) -> Vec<MeasuredInterval> {
662        let mut levels = vec![self.base];
663        levels.extend(self.treads.iter().map(|tread| tread.elevation));
664        if self.ends_in_riser {
665            levels.push(self.top);
666        }
667        levels
668            .windows(2)
669            .map(|pair| between(pair[1], pair[0]))
670            .collect()
671    }
672
673    /// Whether each riser closes its step, in the order of
674    /// [`Self::risers`]: the riser below each tread, then the final riser
675    /// when the flight ends in one.
676    #[must_use]
677    pub fn riser_closures(&self) -> Vec<RiserClosure> {
678        let mut closures: Vec<RiserClosure> =
679            self.treads.iter().map(|tread| tread.riser_below).collect();
680        if self.ends_in_riser {
681            closures.push(self.final_riser);
682        }
683        closures
684    }
685
686    /// Goings, bottom to top: the distance along the walking line from
687    /// each tread's nosing to the next one's. A flight of `n` treads has
688    /// `n - 1` goings.
689    #[must_use]
690    pub fn goings(&self) -> Vec<MeasuredInterval> {
691        self.treads
692            .windows(2)
693            .map(|pair| between(pair[1].front, pair[0].front))
694            .collect()
695    }
696
697    /// Nosing projections, bottom to top: how far each tread above the
698    /// first reaches over the tread below it, the lower tread's back less
699    /// the upper tread's front. Zero or less where it does not overhang.
700    #[must_use]
701    pub fn nosings(&self) -> Vec<MeasuredInterval> {
702        self.treads
703            .windows(2)
704            .map(|pair| between(pair[0].back, pair[1].front))
705            .collect()
706    }
707
708    /// Winder angles, bottom to top: for each tread below the last, the
709    /// plan angle between its nosing and the next tread's, `0` for a
710    /// straight tread. `None` where a nosing is not measured or too short
711    /// for its uncertainty to fix a direction.
712    #[must_use]
713    pub fn winder_angles(&self) -> Vec<Option<MeasuredInterval>> {
714        self.treads
715            .windows(2)
716            .map(|pair| match (pair[0].nosing, pair[1].nosing) {
717                (Some(lower), Some(upper)) => lower.angle_to(&upper),
718                _ => None,
719            })
720            .collect()
721    }
722
723    /// The flight's rise: from the base to the top of its last riser.
724    #[must_use]
725    pub fn rise(&self) -> MeasuredInterval {
726        let summit = if self.ends_in_riser {
727            self.top
728        } else {
729            self.treads.last().map_or(self.top, |tread| tread.elevation)
730        };
731        between(summit, self.base)
732    }
733
734    /// The flight's width: its narrowest tread's, when every tread's sides
735    /// were measured.
736    #[must_use]
737    pub fn width(&self) -> Option<MeasuredInterval> {
738        let widths: Option<Vec<MeasuredInterval>> = self.treads.iter().map(Tread::width).collect();
739        least(widths?.into_iter())
740    }
741
742    /// Whether every position is known exactly.
743    #[must_use]
744    pub fn is_exact(&self) -> bool {
745        self.evidence.exact
746    }
747
748    /// Reviewable provenance of the measurement.
749    #[must_use]
750    pub fn evidence(&self) -> &Evidence {
751        &self.evidence
752    }
753}
754
755/// One sloped run of a ramp: a planar upward-facing walking face.
756///
757/// `start` and `end` are the positions of its nearest and farthest points
758/// along `direction`, the run's horizontal direction of steepest ascent;
759/// `bottom` and `top` the elevations of its lowest and highest points.
760#[derive(Clone, Copy, Debug, PartialEq)]
761pub struct SlopedRun {
762    direction: MetricDirection,
763    bottom: ElevationInterval,
764    top: ElevationInterval,
765    start: ElevationInterval,
766    end: ElevationInterval,
767    sides: Option<Sides>,
768}
769
770impl SlopedRun {
771    /// A run rising from `bottom` to `top` between `start` and `end` along
772    /// the horizontal `direction`. Both must be decidably ordered: a run
773    /// with no rise or no length is no sloped run.
774    pub fn try_new(
775        direction: MetricDirection,
776        bottom: ElevationInterval,
777        top: ElevationInterval,
778        start: ElevationInterval,
779        end: ElevationInterval,
780    ) -> Result<Self, WalkingSurfaceError> {
781        #[allow(clippy::float_cmp)]
782        if direction.components()[2] != 0.0
783            || bottom.upper_metres() >= top.lower_metres()
784            || start.upper_metres() >= end.lower_metres()
785        {
786            return Err(WalkingSurfaceError::InvalidMeasurement);
787        }
788        Ok(Self {
789            direction,
790            bottom,
791            top,
792            start,
793            end,
794            sides: None,
795        })
796    }
797
798    /// The run with the positions of its sides along [`across`] its
799    /// direction, stated only where its plan fills the rectangle between its
800    /// ends and those sides.
801    pub fn with_sides(
802        mut self,
803        left: ElevationInterval,
804        right: ElevationInterval,
805    ) -> Result<Self, WalkingSurfaceError> {
806        self.sides = Some(sides(left, right)?);
807        Ok(self)
808    }
809
810    /// The positions of the run's sides across its direction, when measured.
811    #[must_use]
812    pub fn sides(&self) -> Option<(ElevationInterval, ElevationInterval)> {
813        self.sides
814    }
815
816    /// The run's width across its direction, when its sides were measured.
817    #[must_use]
818    pub fn width(&self) -> Option<MeasuredInterval> {
819        self.sides.map(|(left, right)| between(right, left))
820    }
821
822    /// The run's horizontal direction of steepest ascent.
823    #[must_use]
824    pub fn direction(&self) -> MetricDirection {
825        self.direction
826    }
827
828    /// Elevation of the run's lowest point.
829    #[must_use]
830    pub fn bottom(&self) -> ElevationInterval {
831        self.bottom
832    }
833
834    /// Elevation of the run's highest point.
835    #[must_use]
836    pub fn top(&self) -> ElevationInterval {
837        self.top
838    }
839
840    /// Position of the run's lowest end along its direction.
841    #[must_use]
842    pub fn start(&self) -> ElevationInterval {
843        self.start
844    }
845
846    /// Position of the run's highest end along its direction.
847    #[must_use]
848    pub fn end(&self) -> ElevationInterval {
849        self.end
850    }
851
852    /// The run's rise, top less bottom.
853    #[must_use]
854    pub fn rise(&self) -> MeasuredInterval {
855        between(self.top, self.bottom)
856    }
857
858    /// The run's horizontal length along its direction, end less start.
859    #[must_use]
860    pub fn length(&self) -> MeasuredInterval {
861        between(self.end, self.start)
862    }
863
864    /// The run's slope, rise over horizontal length. On a planar face the
865    /// height changes linearly along the direction of steepest ascent, so
866    /// this is the face's gradient.
867    #[must_use]
868    pub fn slope(&self) -> MeasuredInterval {
869        let (rise, length) = (self.rise(), self.length());
870        let lower = divide(rise.lower.max(0.0), length.upper, false);
871        let upper = divide(rise.upper, length.lower, true);
872        MeasuredInterval {
873            lower,
874            upper: upper.max(lower),
875        }
876    }
877
878    /// Whether every position of the run is known exactly.
879    #[must_use]
880    pub fn is_exact(&self) -> bool {
881        self.bottom.is_exact()
882            && self.top.is_exact()
883            && self.start.is_exact()
884            && self.end.is_exact()
885            && sides_exact(self.sides)
886    }
887}
888
889/// A ramp measured from its body: its sloped runs, ordered by bottom
890/// elevation. Horizontal faces between them (landings) are not runs.
891#[derive(Clone, Debug, PartialEq)]
892pub struct SlopedSurface {
893    object: ObjectId,
894    runs: Vec<SlopedRun>,
895    evidence: Evidence,
896}
897
898impl SlopedSurface {
899    /// The runs of `object`, at least one. The evidence is exact exactly
900    /// when every position is a point.
901    pub fn try_new(
902        object: ObjectId,
903        runs: Vec<SlopedRun>,
904        evidence: Evidence,
905    ) -> Result<Self, WalkingSurfaceError> {
906        if runs.is_empty() {
907            return Err(WalkingSurfaceError::InvalidMeasurement);
908        }
909        let exact = runs.iter().all(SlopedRun::is_exact);
910        if evidence.exact != exact || evidence.locator.trim().is_empty() {
911            return Err(WalkingSurfaceError::InexactEvidence);
912        }
913        Ok(Self {
914            object,
915            runs,
916            evidence,
917        })
918    }
919
920    /// The measured object.
921    #[must_use]
922    pub fn object(&self) -> &ObjectId {
923        &self.object
924    }
925
926    /// The sloped runs.
927    #[must_use]
928    pub fn runs(&self) -> &[SlopedRun] {
929        &self.runs
930    }
931
932    /// Whether every position is known exactly.
933    #[must_use]
934    pub fn is_exact(&self) -> bool {
935        self.evidence.exact
936    }
937
938    /// Reviewable provenance of the measurement.
939    #[must_use]
940    pub fn evidence(&self) -> &Evidence {
941        &self.evidence
942    }
943}
944
945/// A request for the headroom above an object's walking surface.
946///
947/// The obstacles are the rule's selection, never the service's: sorted,
948/// deduplicated, and without the subject.
949#[derive(Clone, Debug, PartialEq, Eq)]
950pub struct HeadroomRequest {
951    subject: ObjectId,
952    obstacles: Vec<ObjectId>,
953}
954
955impl HeadroomRequest {
956    /// Headroom above `subject` against `obstacles`.
957    #[must_use]
958    pub fn new(subject: ObjectId, obstacles: impl IntoIterator<Item = ObjectId>) -> Self {
959        let mut obstacles: Vec<ObjectId> = obstacles
960            .into_iter()
961            .filter(|obstacle| *obstacle != subject)
962            .collect();
963        obstacles.sort();
964        obstacles.dedup();
965        Self { subject, obstacles }
966    }
967
968    /// The object whose walking surface is measured.
969    #[must_use]
970    pub fn subject(&self) -> &ObjectId {
971        &self.subject
972    }
973
974    /// The objects that may stand above it.
975    #[must_use]
976    pub fn obstacles(&self) -> &[ObjectId] {
977        &self.obstacles
978    }
979}
980
981/// The vertical clearance above an object's walking surface.
982///
983/// The walking surface is the subject's upward-facing faces no steeper than
984/// 45°: treads, ramp slopes and landings. The clearance is the least
985/// vertical distance from a point of it to an obstacle's body directly
986/// above; `None` when no requested obstacle stands above it anywhere.
987#[derive(Clone, Debug, PartialEq)]
988pub struct Headroom {
989    request: HeadroomRequest,
990    clearance: Option<MeasuredInterval>,
991    governing: Vec<ObjectId>,
992    evidence: Evidence,
993}
994
995impl Headroom {
996    /// The headroom answering `request`. `governing` names the obstacles
997    /// whose clearance may be the least one: at least one when there is a
998    /// clearance, none otherwise, every one requested. A clearance interval
999    /// is never exact evidence.
1000    pub fn try_new(
1001        request: HeadroomRequest,
1002        clearance: Option<MeasuredInterval>,
1003        governing: Vec<ObjectId>,
1004        evidence: Evidence,
1005    ) -> Result<Self, WalkingSurfaceError> {
1006        let governing = governed(&request.obstacles, clearance, governing, &evidence)?;
1007        Ok(Self {
1008            request,
1009            clearance,
1010            governing,
1011            evidence,
1012        })
1013    }
1014
1015    /// The request this answers.
1016    #[must_use]
1017    pub fn request(&self) -> &HeadroomRequest {
1018        &self.request
1019    }
1020
1021    /// The least vertical clearance, or `None` when nothing stands above.
1022    #[must_use]
1023    pub fn clearance(&self) -> Option<MeasuredInterval> {
1024        self.clearance
1025    }
1026
1027    /// The obstacles whose clearance may be the least.
1028    #[must_use]
1029    pub fn governing(&self) -> &[ObjectId] {
1030        &self.governing
1031    }
1032
1033    /// Reviewable provenance of the measurement.
1034    #[must_use]
1035    pub fn evidence(&self) -> &Evidence {
1036        &self.evidence
1037    }
1038}
1039
1040/// One end of a stair flight or of a ramp's run.
1041///
1042/// A flight's end is placed along the direction its end tread climbs: the
1043/// flight's own for a straight flight, the direction square to the end
1044/// tread's nosing for a turning flight, whose landing positions are then
1045/// projections onto that direction in plan, never arc lengths along its
1046/// walking line. A turning flight ending on a winder has no such direction,
1047/// and a service refuses that end.
1048#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
1049pub enum WalkingEnd {
1050    /// Where a flight starts, in front of its first riser.
1051    FlightBottom,
1052    /// Where a flight arrives, beyond its last riser.
1053    FlightTop,
1054    /// The lower end of a ramp's run, by its index in
1055    /// [`SlopedSurface::runs`].
1056    RunBottom(usize),
1057    /// The upper end of a ramp's run, by its index.
1058    RunTop(usize),
1059}
1060
1061/// A request for the landing at one end of a flight or run.
1062///
1063/// The candidates are the objects that may carry it, the rule's selection
1064/// (slabs, landings, floors): sorted, deduplicated and without the subject.
1065/// A service may also take a ramp's own level faces as its landing, never a
1066/// flight's own treads.
1067#[derive(Clone, Debug, PartialEq, Eq)]
1068pub struct LandingRequest {
1069    subject: ObjectId,
1070    end: WalkingEnd,
1071    candidates: Vec<ObjectId>,
1072}
1073
1074impl LandingRequest {
1075    /// The landing at `end` of `subject` among `candidates`.
1076    #[must_use]
1077    pub fn new(
1078        subject: ObjectId,
1079        end: WalkingEnd,
1080        candidates: impl IntoIterator<Item = ObjectId>,
1081    ) -> Self {
1082        let mut candidates: Vec<ObjectId> = candidates
1083            .into_iter()
1084            .filter(|candidate| *candidate != subject)
1085            .collect();
1086        candidates.sort();
1087        candidates.dedup();
1088        Self {
1089            subject,
1090            end,
1091            candidates,
1092        }
1093    }
1094
1095    /// The flight or ramp whose end is measured.
1096    #[must_use]
1097    pub fn subject(&self) -> &ObjectId {
1098        &self.subject
1099    }
1100
1101    /// Which end.
1102    #[must_use]
1103    pub fn end(&self) -> WalkingEnd {
1104        self.end
1105    }
1106
1107    /// The objects that may carry the landing.
1108    #[must_use]
1109    pub fn candidates(&self) -> &[ObjectId] {
1110        &self.candidates
1111    }
1112}
1113
1114/// The rectangle of a landing along the direction leaving the flight or
1115/// run: the position of its far side and of its two sides along [`across`]
1116/// that direction. A service states it only where the landing's level
1117/// surface fills that rectangle.
1118#[derive(Clone, Copy, Debug, PartialEq)]
1119pub struct LandingExtent {
1120    far: ElevationInterval,
1121    left: ElevationInterval,
1122    right: ElevationInterval,
1123}
1124
1125impl LandingExtent {
1126    /// A landing reaching `far` along the leaving direction, between the
1127    /// sides `left` and `right` across it.
1128    pub fn try_new(
1129        far: ElevationInterval,
1130        left: ElevationInterval,
1131        right: ElevationInterval,
1132    ) -> Result<Self, WalkingSurfaceError> {
1133        let (left, right) = sides(left, right)?;
1134        Ok(Self { far, left, right })
1135    }
1136
1137    /// Position of the landing's far side along the leaving direction.
1138    #[must_use]
1139    pub fn far(&self) -> ElevationInterval {
1140        self.far
1141    }
1142
1143    /// Positions of the landing's sides across the leaving direction.
1144    #[must_use]
1145    pub fn sides(&self) -> (ElevationInterval, ElevationInterval) {
1146        (self.left, self.right)
1147    }
1148
1149    fn is_exact(&self) -> bool {
1150        self.far.is_exact() && sides_exact(Some((self.left, self.right)))
1151    }
1152}
1153
1154/// The object carrying a landing and, when it is a rectangle along the
1155/// leaving direction, its extent.
1156#[derive(Clone, Debug, PartialEq)]
1157pub struct Landing {
1158    carrier: ObjectId,
1159    extent: Option<LandingExtent>,
1160}
1161
1162impl Landing {
1163    /// A landing on `carrier`, measured when `extent` is given.
1164    #[must_use]
1165    pub fn new(carrier: ObjectId, extent: Option<LandingExtent>) -> Self {
1166        Self { carrier, extent }
1167    }
1168
1169    /// The object whose level surface meets the end: a requested candidate,
1170    /// or the subject itself for a ramp's own landing.
1171    #[must_use]
1172    pub fn carrier(&self) -> &ObjectId {
1173        &self.carrier
1174    }
1175
1176    /// The landing's rectangle, `None` when its surface is no rectangle
1177    /// along the leaving direction and so was not measured.
1178    #[must_use]
1179    pub fn extent(&self) -> Option<LandingExtent> {
1180        self.extent
1181    }
1182}
1183
1184/// The landing at one end of a flight or run.
1185///
1186/// `direction` is the horizontal direction leaving the subject at that end
1187/// (back down the flight's direction at its bottom, on at its top); `edge`
1188/// the position along it of the end's arrival line: the first riser at a
1189/// flight's bottom, the last riser at its top, a run's end. A landing's
1190/// positions are along the same direction, so its depth is its far side
1191/// less the edge. `landing` is `None` when no candidate's level surface at
1192/// the end's elevation meets the end: nothing selected carries it.
1193#[derive(Clone, Debug, PartialEq)]
1194pub struct LandingEvidence {
1195    request: LandingRequest,
1196    direction: MetricDirection,
1197    edge: ElevationInterval,
1198    landing: Option<Landing>,
1199    evidence: Evidence,
1200}
1201
1202impl LandingEvidence {
1203    /// The landing answering `request`. The carrier must be the subject or a
1204    /// requested candidate, and a measured landing's far side must lie
1205    /// decidably beyond the edge; the evidence is exact exactly when every
1206    /// position is a point.
1207    pub fn try_new(
1208        request: LandingRequest,
1209        direction: MetricDirection,
1210        edge: ElevationInterval,
1211        landing: Option<Landing>,
1212        evidence: Evidence,
1213    ) -> Result<Self, WalkingSurfaceError> {
1214        #[allow(clippy::float_cmp)]
1215        if direction.components()[2] != 0.0 {
1216            return Err(WalkingSurfaceError::InvalidMeasurement);
1217        }
1218        if let Some(landing) = &landing {
1219            let named = landing.carrier == request.subject
1220                || request.candidates.binary_search(&landing.carrier).is_ok();
1221            let beyond = landing
1222                .extent
1223                .is_none_or(|extent| extent.far.lower_metres() > edge.upper_metres());
1224            if !named || !beyond {
1225                return Err(WalkingSurfaceError::InvalidMeasurement);
1226            }
1227        }
1228        let exact = edge.is_exact()
1229            && landing
1230                .as_ref()
1231                .and_then(|landing| landing.extent)
1232                .is_none_or(|extent| extent.is_exact());
1233        if evidence.exact != exact || evidence.locator.trim().is_empty() {
1234            return Err(WalkingSurfaceError::InexactEvidence);
1235        }
1236        Ok(Self {
1237            request,
1238            direction,
1239            edge,
1240            landing,
1241            evidence,
1242        })
1243    }
1244
1245    /// The request this answers.
1246    #[must_use]
1247    pub fn request(&self) -> &LandingRequest {
1248        &self.request
1249    }
1250
1251    /// The horizontal direction leaving the subject at the end.
1252    #[must_use]
1253    pub fn direction(&self) -> MetricDirection {
1254        self.direction
1255    }
1256
1257    /// Position of the end's arrival line along the leaving direction.
1258    #[must_use]
1259    pub fn edge(&self) -> ElevationInterval {
1260        self.edge
1261    }
1262
1263    /// The landing, or `None` when nothing requested carries one.
1264    #[must_use]
1265    pub fn landing(&self) -> Option<&Landing> {
1266        self.landing.as_ref()
1267    }
1268
1269    /// The landing's depth along the leaving direction, from the arrival
1270    /// line to its far side, when its extent was measured.
1271    #[must_use]
1272    pub fn depth(&self) -> Option<MeasuredInterval> {
1273        let extent = self.landing.as_ref()?.extent?;
1274        Some(between(extent.far, self.edge))
1275    }
1276
1277    /// The landing's width across the leaving direction, when its extent was
1278    /// measured.
1279    #[must_use]
1280    pub fn width(&self) -> Option<MeasuredInterval> {
1281        let extent = self.landing.as_ref()?.extent?;
1282        Some(between(extent.right, extent.left))
1283    }
1284
1285    /// Reviewable provenance of the measurement.
1286    #[must_use]
1287    pub fn evidence(&self) -> &Evidence {
1288        &self.evidence
1289    }
1290}
1291
1292/// A request for the clearance below a flight or ramp: its height above the
1293/// floors of the spaces people walk in beneath it.
1294///
1295/// The spaces are the rule's selection, sorted, deduplicated and without the
1296/// subject.
1297#[derive(Clone, Debug, PartialEq, Eq)]
1298pub struct ClearanceBelowRequest {
1299    subject: ObjectId,
1300    spaces: Vec<ObjectId>,
1301}
1302
1303impl ClearanceBelowRequest {
1304    /// The clearance below `subject` over the floors of `spaces`.
1305    #[must_use]
1306    pub fn new(subject: ObjectId, spaces: impl IntoIterator<Item = ObjectId>) -> Self {
1307        let mut spaces: Vec<ObjectId> = spaces
1308            .into_iter()
1309            .filter(|space| *space != subject)
1310            .collect();
1311        spaces.sort();
1312        spaces.dedup();
1313        Self { subject, spaces }
1314    }
1315
1316    /// The object whose underside is measured.
1317    #[must_use]
1318    pub fn subject(&self) -> &ObjectId {
1319        &self.subject
1320    }
1321
1322    /// The spaces whose floors may lie beneath it.
1323    #[must_use]
1324    pub fn spaces(&self) -> &[ObjectId] {
1325        &self.spaces
1326    }
1327}
1328
1329/// The clearance below a flight or ramp.
1330///
1331/// A space's floor is its body's downward-facing level faces. The clearance
1332/// is the least vertical distance from a point of a requested space's floor
1333/// up to the subject's underside directly above it, leaving out where the
1334/// subject rests on that floor; `None` when the subject stands above no
1335/// requested floor.
1336#[derive(Clone, Debug, PartialEq)]
1337pub struct ClearanceBelow {
1338    request: ClearanceBelowRequest,
1339    clearance: Option<MeasuredInterval>,
1340    governing: Vec<ObjectId>,
1341    evidence: Evidence,
1342}
1343
1344impl ClearanceBelow {
1345    /// The clearance answering `request`. `governing` names the spaces whose
1346    /// floor may lie closest below: at least one when there is a clearance,
1347    /// none otherwise, every one requested. A clearance interval is never
1348    /// exact evidence.
1349    pub fn try_new(
1350        request: ClearanceBelowRequest,
1351        clearance: Option<MeasuredInterval>,
1352        governing: Vec<ObjectId>,
1353        evidence: Evidence,
1354    ) -> Result<Self, WalkingSurfaceError> {
1355        let governing = governed(&request.spaces, clearance, governing, &evidence)?;
1356        Ok(Self {
1357            request,
1358            clearance,
1359            governing,
1360            evidence,
1361        })
1362    }
1363
1364    /// The request this answers.
1365    #[must_use]
1366    pub fn request(&self) -> &ClearanceBelowRequest {
1367        &self.request
1368    }
1369
1370    /// The least vertical clearance, or `None` when no requested floor lies
1371    /// beneath.
1372    #[must_use]
1373    pub fn clearance(&self) -> Option<MeasuredInterval> {
1374        self.clearance
1375    }
1376
1377    /// The spaces whose floor may lie closest below.
1378    #[must_use]
1379    pub fn governing(&self) -> &[ObjectId] {
1380        &self.governing
1381    }
1382
1383    /// Reviewable provenance of the measurement.
1384    #[must_use]
1385    pub fn evidence(&self) -> &Evidence {
1386        &self.evidence
1387    }
1388}
1389
1390/// `governing` sorted and checked against the requested `named` objects, a
1391/// clearance and its evidence, as [`Headroom`] and [`ClearanceBelow`] share.
1392fn governed(
1393    named: &[ObjectId],
1394    clearance: Option<MeasuredInterval>,
1395    mut governing: Vec<ObjectId>,
1396    evidence: &Evidence,
1397) -> Result<Vec<ObjectId>, WalkingSurfaceError> {
1398    governing.sort();
1399    governing.dedup();
1400    let requested = governing
1401        .iter()
1402        .all(|object| named.binary_search(object).is_ok());
1403    if !requested || clearance.is_some() == governing.is_empty() {
1404        return Err(WalkingSurfaceError::InvalidMeasurement);
1405    }
1406    if clearance.is_some_and(|clearance| clearance.lower < 0.0) {
1407        return Err(WalkingSurfaceError::InvalidMeasurement);
1408    }
1409    if evidence.locator.trim().is_empty()
1410        || (evidence.exact && clearance.is_some_and(|clearance| !clearance.is_point()))
1411    {
1412        return Err(WalkingSurfaceError::InexactEvidence);
1413    }
1414    Ok(governing)
1415}
1416
1417/// The stretch of walking surface a handrail is measured along: a stair
1418/// flight, or one run of a ramp by its index in [`SlopedSurface::runs`].
1419#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
1420pub enum WalkingStretch {
1421    /// A stair flight, along its nosing line: in one part when straight, in
1422    /// its straight parts when it turns ([`StretchPart`]).
1423    Flight,
1424    /// A ramp's run, along its surface.
1425    Run(usize),
1426}
1427
1428/// A request for the handrails along a flight or a run.
1429///
1430/// The rails are the rule's selection (railings of a handrail type, say):
1431/// sorted, deduplicated and without the subject. `reach` is how far outside
1432/// the walking surface's sides a rail may run and still be measured along
1433/// it, `above` how far above the pitch line's highest point its lowest point
1434/// may lie (so the rail of a flight stacked above is not taken for this
1435/// one's), and `extension` how far beyond each end of the pitch line the
1436/// rise of a rail's top is measured, zero for none.
1437#[derive(Clone, Debug, PartialEq)]
1438pub struct HandrailRequest {
1439    subject: ObjectId,
1440    stretch: WalkingStretch,
1441    rails: Vec<ObjectId>,
1442    reach: f64,
1443    above: f64,
1444    extension: f64,
1445}
1446
1447impl HandrailRequest {
1448    /// The handrails along `stretch` of `subject` among `rails`. `reach`,
1449    /// `above` and `extension` must be finite and not negative.
1450    pub fn try_new(
1451        subject: ObjectId,
1452        stretch: WalkingStretch,
1453        rails: impl IntoIterator<Item = ObjectId>,
1454        (reach, above): (f64, f64),
1455        extension: f64,
1456    ) -> Result<Self, WalkingSurfaceError> {
1457        if [reach, above, extension]
1458            .iter()
1459            .any(|length| !length.is_finite() || *length < 0.0)
1460        {
1461            return Err(WalkingSurfaceError::InvalidMeasurement);
1462        }
1463        let mut rails: Vec<ObjectId> = rails.into_iter().filter(|rail| *rail != subject).collect();
1464        rails.sort();
1465        rails.dedup();
1466        Ok(Self {
1467            subject,
1468            stretch,
1469            rails,
1470            reach,
1471            above,
1472            extension,
1473        })
1474    }
1475
1476    /// The flight or ramp the rails run along.
1477    #[must_use]
1478    pub fn subject(&self) -> &ObjectId {
1479        &self.subject
1480    }
1481
1482    /// Which stretch of it.
1483    #[must_use]
1484    pub fn stretch(&self) -> WalkingStretch {
1485        self.stretch
1486    }
1487
1488    /// The objects that may be its handrails.
1489    #[must_use]
1490    pub fn rails(&self) -> &[ObjectId] {
1491        &self.rails
1492    }
1493
1494    /// How far outside the walking surface's sides a rail is still measured.
1495    #[must_use]
1496    pub fn reach(&self) -> f64 {
1497        self.reach
1498    }
1499
1500    /// How far above the pitch line's highest point a rail's lowest point
1501    /// may lie and still be measured.
1502    #[must_use]
1503    pub fn above(&self) -> f64 {
1504        self.above
1505    }
1506
1507    /// How far beyond each end of the pitch line a rail's rise is measured.
1508    #[must_use]
1509    pub fn extension(&self) -> f64 {
1510        self.extension
1511    }
1512}
1513
1514/// A request for the clear width of a flight or a ramp's run: the free
1515/// width across it that the requested obstacles leave (handrails, walls,
1516/// anything standing beside or over the walking surface) between two
1517/// heights above its pitch line.
1518///
1519/// The obstacles are the rule's selection, sorted, deduplicated and without
1520/// the subject. `band` is `(from, to)`, how far above the pitch line (a
1521/// flight's nosing line, a run's surface) the band starts and ends, `0 <=
1522/// from < to`.
1523#[derive(Clone, Debug, PartialEq)]
1524pub struct ClearWidthRequest {
1525    subject: ObjectId,
1526    stretch: WalkingStretch,
1527    obstacles: Vec<ObjectId>,
1528    band: (f64, f64),
1529}
1530
1531impl ClearWidthRequest {
1532    /// The clear width along `stretch` of `subject` that `obstacles` leave
1533    /// within `band` above its pitch line.
1534    pub fn try_new(
1535        subject: ObjectId,
1536        stretch: WalkingStretch,
1537        obstacles: impl IntoIterator<Item = ObjectId>,
1538        band: (f64, f64),
1539    ) -> Result<Self, WalkingSurfaceError> {
1540        let (from, to) = band;
1541        if !from.is_finite() || !to.is_finite() || from < 0.0 || to <= from {
1542            return Err(WalkingSurfaceError::InvalidMeasurement);
1543        }
1544        let mut obstacles: Vec<ObjectId> = obstacles
1545            .into_iter()
1546            .filter(|obstacle| *obstacle != subject)
1547            .collect();
1548        obstacles.sort();
1549        obstacles.dedup();
1550        Ok(Self {
1551            subject,
1552            stretch,
1553            obstacles,
1554            band,
1555        })
1556    }
1557
1558    /// The flight or ramp measured.
1559    #[must_use]
1560    pub fn subject(&self) -> &ObjectId {
1561        &self.subject
1562    }
1563
1564    /// Which stretch of it.
1565    #[must_use]
1566    pub fn stretch(&self) -> WalkingStretch {
1567        self.stretch
1568    }
1569
1570    /// The objects that may narrow it.
1571    #[must_use]
1572    pub fn obstacles(&self) -> &[ObjectId] {
1573        &self.obstacles
1574    }
1575
1576    /// How far above the pitch line the band starts and ends.
1577    #[must_use]
1578    pub fn band(&self) -> (f64, f64) {
1579        self.band
1580    }
1581}
1582
1583/// The narrowest clear width along a flight or run.
1584///
1585/// At each position along the stretch, between its ends, the free width is
1586/// the distance across between the innermost points the requested
1587/// obstacles reach within the band from the left and from the right, where
1588/// no obstacle reaches in, the walking surface's own side; the clear width
1589/// is the least of these. `governing` names the obstacles bounding the
1590/// narrowest place, none when the walking surface's own sides do.
1591#[derive(Clone, Debug, PartialEq)]
1592pub struct ClearWidthEvidence {
1593    request: ClearWidthRequest,
1594    width: MeasuredInterval,
1595    governing: Vec<ObjectId>,
1596    evidence: Evidence,
1597}
1598
1599impl ClearWidthEvidence {
1600    /// The clear width answering `request`: never negative, `governing`
1601    /// among the requested obstacles, and exact evidence only for a point.
1602    pub fn try_new(
1603        request: ClearWidthRequest,
1604        width: MeasuredInterval,
1605        mut governing: Vec<ObjectId>,
1606        evidence: Evidence,
1607    ) -> Result<Self, WalkingSurfaceError> {
1608        governing.sort();
1609        governing.dedup();
1610        if width.lower < 0.0
1611            || !governing
1612                .iter()
1613                .all(|object| request.obstacles.binary_search(object).is_ok())
1614        {
1615            return Err(WalkingSurfaceError::InvalidMeasurement);
1616        }
1617        if evidence.locator.trim().is_empty() || (evidence.exact && !width.is_point()) {
1618            return Err(WalkingSurfaceError::InexactEvidence);
1619        }
1620        Ok(Self {
1621            request,
1622            width,
1623            governing,
1624            evidence,
1625        })
1626    }
1627
1628    /// The request this answers.
1629    #[must_use]
1630    pub fn request(&self) -> &ClearWidthRequest {
1631        &self.request
1632    }
1633
1634    /// The narrowest clear width.
1635    #[must_use]
1636    pub fn width(&self) -> MeasuredInterval {
1637        self.width
1638    }
1639
1640    /// The obstacles bounding the narrowest place.
1641    #[must_use]
1642    pub fn governing(&self) -> &[ObjectId] {
1643        &self.governing
1644    }
1645
1646    /// Reviewable provenance of the measurement.
1647    #[must_use]
1648    pub fn evidence(&self) -> &Evidence {
1649        &self.evidence
1650    }
1651}
1652
1653/// The side of a flight or run a handrail runs along, as seen by someone
1654/// climbing it. [`across`] points to the climber's left, so the left side
1655/// lies at the higher positions across.
1656#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
1657pub enum RailSide {
1658    /// The climber's right: the lower half of the positions across.
1659    Right,
1660    /// The climber's left: the higher half.
1661    Left,
1662}
1663
1664/// One straight part of a stretch a handrail is measured along: the
1665/// horizontal direction it climbs and the positions of the walking
1666/// surface's sides along [`across`] it.
1667///
1668/// A straight flight or a ramp's run is one part. A turning flight's parts
1669/// are its runs of consecutive treads filling rectangles square to parallel
1670/// nosings, bottom to top; its winders belong to none, and a rail beside
1671/// them is measured in the frame of the part it runs on from.
1672#[derive(Clone, Copy, Debug, PartialEq)]
1673pub struct StretchPart {
1674    direction: MetricDirection,
1675    sides: (ElevationInterval, ElevationInterval),
1676}
1677
1678impl StretchPart {
1679    /// A part climbing along the horizontal `direction`, its walking
1680    /// surface between the ordered sides `left` and `right` across it.
1681    pub fn try_new(
1682        direction: MetricDirection,
1683        (left, right): (ElevationInterval, ElevationInterval),
1684    ) -> Result<Self, WalkingSurfaceError> {
1685        #[allow(clippy::float_cmp)]
1686        if direction.components()[2] != 0.0 {
1687            return Err(WalkingSurfaceError::InvalidMeasurement);
1688        }
1689        Ok(Self {
1690            direction,
1691            sides: sides(left, right)?,
1692        })
1693    }
1694
1695    /// The horizontal direction the part climbs.
1696    #[must_use]
1697    pub fn direction(&self) -> MetricDirection {
1698        self.direction
1699    }
1700
1701    /// Positions of the walking surface's sides across the direction.
1702    #[must_use]
1703    pub fn sides(&self) -> (ElevationInterval, ElevationInterval) {
1704        self.sides
1705    }
1706}
1707
1708/// One handrail measured along a stretch.
1709///
1710/// A rail is measured in the frame of one part of the stretch, the first
1711/// unless stated ([`Self::in_part`]). `start` and `end` are the positions
1712/// of its body's nearest and farthest points along that part's direction,
1713/// `left` and `right` of its lowest and highest points across it. `lowest`
1714/// and `highest` bound the height of the top of its body above the pitch
1715/// line (the nosing line of a flight, the surface of a run) where both run:
1716/// the least and the greatest height along it, each as an interval sure to
1717/// hold it. `bottom_rise` and `top_rise` are how much the top of its body
1718/// rises and falls over the requested extension beyond each end of the
1719/// pitch line, `None` where it does not reach that far or no extension was
1720/// requested.
1721#[derive(Clone, Copy, Debug, PartialEq)]
1722pub struct RailMeasurement {
1723    part: usize,
1724    start: ElevationInterval,
1725    end: ElevationInterval,
1726    left: ElevationInterval,
1727    right: ElevationInterval,
1728    lowest: MeasuredInterval,
1729    highest: MeasuredInterval,
1730    bottom_rise: Option<MeasuredInterval>,
1731    top_rise: Option<MeasuredInterval>,
1732}
1733
1734impl RailMeasurement {
1735    /// A rail spanning `start` to `end` along the stretch and `left` to
1736    /// `right` across it, its top `lowest` to `highest` above the pitch
1737    /// line. Each pair must be ordered.
1738    pub fn try_new(
1739        (start, end): (ElevationInterval, ElevationInterval),
1740        (left, right): (ElevationInterval, ElevationInterval),
1741        lowest: MeasuredInterval,
1742        highest: MeasuredInterval,
1743    ) -> Result<Self, WalkingSurfaceError> {
1744        let (start, end) = sides(start, end)?;
1745        let (left, right) = sides(left, right)?;
1746        if lowest.lower > highest.lower || lowest.upper > highest.upper {
1747            return Err(WalkingSurfaceError::InvalidMeasurement);
1748        }
1749        Ok(Self {
1750            part: 0,
1751            start,
1752            end,
1753            left,
1754            right,
1755            lowest,
1756            highest,
1757            bottom_rise: None,
1758            top_rise: None,
1759        })
1760    }
1761
1762    /// The rail measured in the frame of the stretch's part `part`, by its
1763    /// index in [`HandrailEvidence::parts`].
1764    #[must_use]
1765    pub fn in_part(mut self, part: usize) -> Self {
1766        self.part = part;
1767        self
1768    }
1769
1770    /// The index of the part the rail is measured in.
1771    #[must_use]
1772    pub fn part(&self) -> usize {
1773        self.part
1774    }
1775
1776    /// The rail with the rise of its top over the extension beyond the
1777    /// bottom and the top of the pitch line. A rise is never negative.
1778    pub fn with_rises(
1779        mut self,
1780        bottom: Option<MeasuredInterval>,
1781        top: Option<MeasuredInterval>,
1782    ) -> Result<Self, WalkingSurfaceError> {
1783        if [bottom, top].iter().flatten().any(|rise| rise.lower < 0.0) {
1784            return Err(WalkingSurfaceError::InvalidMeasurement);
1785        }
1786        self.bottom_rise = bottom;
1787        self.top_rise = top;
1788        Ok(self)
1789    }
1790
1791    /// Position of the rail's nearest point along the stretch.
1792    #[must_use]
1793    pub fn start(&self) -> ElevationInterval {
1794        self.start
1795    }
1796
1797    /// Position of the rail's farthest point along the stretch.
1798    #[must_use]
1799    pub fn end(&self) -> ElevationInterval {
1800        self.end
1801    }
1802
1803    /// Positions of the rail's lowest and highest points across the stretch.
1804    #[must_use]
1805    pub fn sides(&self) -> (ElevationInterval, ElevationInterval) {
1806        (self.left, self.right)
1807    }
1808
1809    /// The least height of the rail's top above the pitch line.
1810    #[must_use]
1811    pub fn lowest(&self) -> MeasuredInterval {
1812        self.lowest
1813    }
1814
1815    /// The greatest height of the rail's top above the pitch line.
1816    #[must_use]
1817    pub fn highest(&self) -> MeasuredInterval {
1818        self.highest
1819    }
1820
1821    /// How much the rail's top rises and falls over the extension beyond
1822    /// the bottom of the pitch line, when it reaches that far.
1823    #[must_use]
1824    pub fn bottom_rise(&self) -> Option<MeasuredInterval> {
1825        self.bottom_rise
1826    }
1827
1828    /// The same beyond the top of the pitch line.
1829    #[must_use]
1830    pub fn top_rise(&self) -> Option<MeasuredInterval> {
1831        self.top_rise
1832    }
1833
1834    /// The rectangle the rail fills in plan along `direction`: the one
1835    /// holding every rectangle its positions allow (`outer`), or the one
1836    /// every such rectangle holds. `None` when that is empty.
1837    fn plan(&self, direction: MetricDirection, outer: bool) -> Option<ConvexPlanRegion> {
1838        let ((near, far), (low, high)) = if outer {
1839            (
1840                (self.start.lower_metres(), self.end.upper_metres()),
1841                (self.left.lower_metres(), self.right.upper_metres()),
1842            )
1843        } else {
1844            (
1845                (self.start.upper_metres(), self.end.lower_metres()),
1846                (self.left.upper_metres(), self.right.lower_metres()),
1847            )
1848        };
1849        if near >= far || low >= high {
1850            return None;
1851        }
1852        let [dx, dy, _] = direction.components();
1853        let [ax, ay, _] = across(direction).components();
1854        let at = |along: f64, beside: f64| {
1855            [
1856                along.mul_add(dx, beside * ax),
1857                along.mul_add(dy, beside * ay),
1858            ]
1859        };
1860        // Along, then across a quarter turn anticlockwise: anticlockwise.
1861        ConvexPlanRegion::try_new(vec![
1862            at(near, low),
1863            at(far, low),
1864            at(far, high),
1865            at(near, high),
1866        ])
1867        .ok()
1868    }
1869}
1870
1871/// The handrails along a flight or run.
1872///
1873/// `parts` are the stretch's straight parts ([`StretchPart`]), bottom to
1874/// top: one for a straight flight or a run. `pitch` holds the positions
1875/// where the pitch line starts, along the first part's direction (a
1876/// flight's first nosing, a run's lower end), and where it ends, along the
1877/// last part's (the last nosing or the upper floor's edge, a run's upper
1878/// end). `rails` names every requested rail whose body runs along the
1879/// stretch, each measured in the frame of one part: it overlaps the
1880/// stretch along that part's direction and lies within the request's reach
1881/// of its sides across it. A rail's heights are computed, so the evidence
1882/// is never exact.
1883#[derive(Clone, Debug, PartialEq)]
1884pub struct HandrailEvidence {
1885    request: HandrailRequest,
1886    parts: Vec<StretchPart>,
1887    pitch: (ElevationInterval, ElevationInterval),
1888    rails: Vec<(ObjectId, RailMeasurement)>,
1889    evidence: Evidence,
1890}
1891
1892impl HandrailEvidence {
1893    /// The handrails answering `request` along one straight part: the
1894    /// horizontal `direction` and the walking surface's sides across it.
1895    /// Every rail must be requested, named once and measured in that part;
1896    /// they are kept in identity order.
1897    pub fn try_new(
1898        request: HandrailRequest,
1899        direction: MetricDirection,
1900        pitch: (ElevationInterval, ElevationInterval),
1901        walking_sides: (ElevationInterval, ElevationInterval),
1902        rails: Vec<(ObjectId, RailMeasurement)>,
1903        evidence: Evidence,
1904    ) -> Result<Self, WalkingSurfaceError> {
1905        let part = StretchPart::try_new(direction, walking_sides)?;
1906        Self::try_in_parts(request, vec![part], pitch, rails, evidence)
1907    }
1908
1909    /// The handrails answering `request` along the straight `parts`, bottom
1910    /// to top, at least one. Every rail must be requested, named once and
1911    /// measured in one of the parts; they are kept in identity order.
1912    pub fn try_in_parts(
1913        request: HandrailRequest,
1914        parts: Vec<StretchPart>,
1915        pitch: (ElevationInterval, ElevationInterval),
1916        mut rails: Vec<(ObjectId, RailMeasurement)>,
1917        evidence: Evidence,
1918    ) -> Result<Self, WalkingSurfaceError> {
1919        if parts.is_empty() {
1920            return Err(WalkingSurfaceError::InvalidMeasurement);
1921        }
1922        if parts.len() == 1 {
1923            // Both ends lie along the one direction, so they are ordered.
1924            sides(pitch.0, pitch.1)?;
1925        }
1926        rails.sort_by(|a, b| a.0.cmp(&b.0));
1927        let unique = rails.windows(2).all(|pair| pair[0].0 != pair[1].0);
1928        let requested = rails.iter().all(|(rail, measurement)| {
1929            request.rails.binary_search(rail).is_ok() && measurement.part < parts.len()
1930        });
1931        if !unique || !requested {
1932            return Err(WalkingSurfaceError::InvalidMeasurement);
1933        }
1934        if evidence.exact || evidence.locator.trim().is_empty() {
1935            return Err(WalkingSurfaceError::InexactEvidence);
1936        }
1937        Ok(Self {
1938            request,
1939            parts,
1940            pitch,
1941            rails,
1942            evidence,
1943        })
1944    }
1945
1946    /// The request this answers.
1947    #[must_use]
1948    pub fn request(&self) -> &HandrailRequest {
1949        &self.request
1950    }
1951
1952    /// The stretch's straight parts, bottom to top.
1953    #[must_use]
1954    pub fn parts(&self) -> &[StretchPart] {
1955        &self.parts
1956    }
1957
1958    /// The first part's horizontal walking direction: the stretch's, when
1959    /// it is one part.
1960    #[must_use]
1961    pub fn direction(&self) -> MetricDirection {
1962        self.first().direction
1963    }
1964
1965    /// Where the pitch line starts, along the first part's direction, and
1966    /// where it ends, along the last part's.
1967    #[must_use]
1968    pub fn pitch(&self) -> (ElevationInterval, ElevationInterval) {
1969        self.pitch
1970    }
1971
1972    /// Positions of the walking surface's sides across the first part's
1973    /// direction: the stretch's, when it is one part.
1974    #[must_use]
1975    pub fn sides(&self) -> (ElevationInterval, ElevationInterval) {
1976        self.first().sides
1977    }
1978
1979    fn first(&self) -> &StretchPart {
1980        &self.parts[0]
1981    }
1982
1983    fn part(&self, rail: &RailMeasurement) -> &StretchPart {
1984        &self.parts[rail.part]
1985    }
1986
1987    /// The rails running along the stretch, in identity order.
1988    #[must_use]
1989    pub fn rails(&self) -> &[(ObjectId, RailMeasurement)] {
1990        &self.rails
1991    }
1992
1993    /// How far a rail reaches beyond the bottom of the pitch line, along
1994    /// the first part's direction: negative where it starts above it.
1995    /// `None` for a rail measured along a later part, which does not run
1996    /// along the first.
1997    #[must_use]
1998    pub fn bottom_extension(&self, rail: &RailMeasurement) -> Option<MeasuredInterval> {
1999        (rail.part == 0).then(|| between(self.pitch.0, rail.start))
2000    }
2001
2002    /// How far a rail reaches beyond the top of the pitch line, along the
2003    /// last part's direction; `None` for a rail measured along an earlier
2004    /// part.
2005    #[must_use]
2006    pub fn top_extension(&self, rail: &RailMeasurement) -> Option<MeasuredInterval> {
2007        (rail.part + 1 == self.parts.len()).then(|| between(rail.end, self.pitch.1))
2008    }
2009
2010    /// The pieces of the handrail along `side`: every measured rail running
2011    /// along it ([`Self::side`]), bottom to top, part by part. Within a part,
2012    /// pieces are consecutive when each starts and ends decidably further
2013    /// along than the one before; two starting or ending where the
2014    /// positions cannot tell apart, or one lying within another's stretch
2015    /// (a second rail beside or below it), leave the order undecided, and
2016    /// the pieces are returned as `Err`, named. No rail along the side is an
2017    /// empty rail.
2018    pub fn side_rail(
2019        &self,
2020        side: RailSide,
2021    ) -> Result<Vec<&(ObjectId, RailMeasurement)>, Vec<ObjectId>> {
2022        let mut pieces: Vec<&(ObjectId, RailMeasurement)> = self
2023            .rails
2024            .iter()
2025            .filter(|(_, rail)| self.side(rail) == Some(side))
2026            .collect();
2027        pieces.sort_by(|a, b| {
2028            a.1.part
2029                .cmp(&b.1.part)
2030                .then_with(|| {
2031                    a.1.start
2032                        .lower_metres()
2033                        .total_cmp(&b.1.start.lower_metres())
2034                })
2035                .then_with(|| a.0.cmp(&b.0))
2036        });
2037        let ordered = pieces.windows(2).all(|pair| {
2038            let (lower, upper) = (&pair[0].1, &pair[1].1);
2039            lower.part < upper.part
2040                || (lower.start.upper_metres() < upper.start.lower_metres()
2041                    && lower.end.upper_metres() < upper.end.lower_metres())
2042        });
2043        if ordered {
2044            Ok(pieces)
2045        } else {
2046            Err(pieces.into_iter().map(|(rail, _)| rail.clone()).collect())
2047        }
2048    }
2049
2050    /// The gap in plan between two rails: the least horizontal distance
2051    /// between the rectangles their bodies fill, each along its own part,
2052    /// zero where they touch or overlap, as an interval sure to hold it.
2053    /// `None` when a rail's positions leave no rectangle it surely fills,
2054    /// so no upper bound.
2055    #[must_use]
2056    pub fn gap(&self, a: &RailMeasurement, b: &RailMeasurement) -> Option<MeasuredInterval> {
2057        let (along_a, along_b) = (self.part(a).direction, self.part(b).direction);
2058        let (outer_a, inner_a) = (a.plan(along_a, true)?, a.plan(along_a, false));
2059        let (outer_b, inner_b) = (b.plan(along_b, true)?, b.plan(along_b, false));
2060        let (inner_a, inner_b) = (inner_a?, inner_b?);
2061        let scale = [&outer_a, &outer_b]
2062            .iter()
2063            .flat_map(|region| region.ring())
2064            .flatten()
2065            .fold(1.0_f64, |scale, value| scale.max(value.abs()));
2066        // Placing the corners rounds by a few units in the last place of
2067        // the positions, and so does the separation.
2068        let margin = 64.0 * f64::EPSILON * scale;
2069        let lower = (outer_a.separation(&outer_b) - margin).max(0.0);
2070        let upper = inner_a.separation(&inner_b).max(0.0) + margin;
2071        MeasuredInterval::try_new(lower, upper.max(lower)).ok()
2072    }
2073
2074    /// The side a rail runs along: the one whose half of its part's walking
2075    /// surface holds it wholly across, `None` for a rail that may reach over
2076    /// the middle.
2077    #[must_use]
2078    pub fn side(&self, rail: &RailMeasurement) -> Option<RailSide> {
2079        let (left, right) = self.part(rail).sides;
2080        // The midpoint rounds once, by at most half a unit in the last
2081        // place, which the neighbouring value covers.
2082        let low = f64::midpoint(left.lower_metres(), right.lower_metres()).next_down();
2083        let high = f64::midpoint(left.upper_metres(), right.upper_metres()).next_up();
2084        if rail.right.upper_metres() < low {
2085            Some(RailSide::Right)
2086        } else if rail.left.lower_metres() > high {
2087            Some(RailSide::Left)
2088        } else {
2089            None
2090        }
2091    }
2092
2093    /// Reviewable provenance of the measurement.
2094    #[must_use]
2095    pub fn evidence(&self) -> &Evidence {
2096        &self.evidence
2097    }
2098}
2099
2100/// Measures stair flights, ramps and the headroom above them.
2101pub trait WalkingSurfaceService: Send + Sync + 'static {
2102    /// The treads, base and top of the request's object as a stair flight,
2103    /// walked along the requested line should it turn.
2104    fn measure_tread_flight(
2105        &self,
2106        request: &TreadFlightRequest,
2107    ) -> Result<TreadFlight, WalkingSurfaceError>;
2108
2109    /// The sloped runs of `object` as a ramp.
2110    fn measure_sloped_runs(&self, object: &ObjectId) -> Result<SlopedSurface, WalkingSurfaceError>;
2111
2112    /// The headroom above the request subject's walking surface.
2113    fn measure_headroom(&self, request: &HeadroomRequest) -> Result<Headroom, WalkingSurfaceError>;
2114
2115    /// The landing at the requested end of a flight or run. The default
2116    /// refuses: a service that does not look for landings never answers
2117    /// that there is none.
2118    fn measure_landing(
2119        &self,
2120        request: &LandingRequest,
2121    ) -> Result<LandingEvidence, WalkingSurfaceError> {
2122        Err(WalkingSurfaceError::Unsupported(format!(
2123            "landings of {} are not measured by this service",
2124            request.subject()
2125        )))
2126    }
2127
2128    /// The clearance below the request subject over the requested spaces'
2129    /// floors. The default refuses.
2130    fn measure_clearance_below(
2131        &self,
2132        request: &ClearanceBelowRequest,
2133    ) -> Result<ClearanceBelow, WalkingSurfaceError> {
2134        Err(WalkingSurfaceError::Unsupported(format!(
2135            "the clearance below {} is not measured by this service",
2136            request.subject()
2137        )))
2138    }
2139
2140    /// The handrails along the requested stretch. The default refuses: a
2141    /// service that does not look for handrails never answers that there is
2142    /// none.
2143    fn measure_handrails(
2144        &self,
2145        request: &HandrailRequest,
2146    ) -> Result<HandrailEvidence, WalkingSurfaceError> {
2147        Err(WalkingSurfaceError::Unsupported(format!(
2148            "handrails along {} are not measured by this service",
2149            request.subject()
2150        )))
2151    }
2152
2153    /// The clear width along the requested stretch. The default refuses: a
2154    /// service that does not measure clear widths never answers with the
2155    /// walking surface's own width.
2156    fn measure_clear_width(
2157        &self,
2158        request: &ClearWidthRequest,
2159    ) -> Result<ClearWidthEvidence, WalkingSurfaceError> {
2160        Err(WalkingSurfaceError::Unsupported(format!(
2161            "the clear width along {} is not measured by this service",
2162            request.subject()
2163        )))
2164    }
2165}
2166
2167/// Registry handle for a [`WalkingSurfaceService`].
2168#[derive(Clone)]
2169pub struct WalkingSurfaceServiceHandle(Arc<dyn WalkingSurfaceService>);
2170
2171impl WalkingSurfaceServiceHandle {
2172    /// Wraps a trusted walking-surface service.
2173    #[must_use]
2174    pub fn new(service: Arc<dyn WalkingSurfaceService>) -> Self {
2175        Self(service)
2176    }
2177
2178    /// The flight answering `request`; an answer to another request is
2179    /// refused.
2180    pub fn measure_tread_flight(
2181        &self,
2182        request: &TreadFlightRequest,
2183    ) -> Result<TreadFlight, WalkingSurfaceError> {
2184        let flight = self.0.measure_tread_flight(request)?;
2185        if flight.request() != request {
2186            return Err(WalkingSurfaceError::InvalidMeasurement);
2187        }
2188        Ok(flight)
2189    }
2190
2191    /// The ramp of `object`; one naming another object is refused.
2192    pub fn measure_sloped_runs(
2193        &self,
2194        object: &ObjectId,
2195    ) -> Result<SlopedSurface, WalkingSurfaceError> {
2196        let ramp = self.0.measure_sloped_runs(object)?;
2197        if ramp.object() != object {
2198            return Err(WalkingSurfaceError::InvalidMeasurement);
2199        }
2200        Ok(ramp)
2201    }
2202
2203    /// The headroom answering `request`; an answer to another request is
2204    /// refused.
2205    pub fn measure_headroom(
2206        &self,
2207        request: &HeadroomRequest,
2208    ) -> Result<Headroom, WalkingSurfaceError> {
2209        let headroom = self.0.measure_headroom(request)?;
2210        if headroom.request() != request {
2211            return Err(WalkingSurfaceError::InvalidMeasurement);
2212        }
2213        Ok(headroom)
2214    }
2215
2216    /// The landing answering `request`; an answer to another request is
2217    /// refused.
2218    pub fn measure_landing(
2219        &self,
2220        request: &LandingRequest,
2221    ) -> Result<LandingEvidence, WalkingSurfaceError> {
2222        let landing = self.0.measure_landing(request)?;
2223        if landing.request() != request {
2224            return Err(WalkingSurfaceError::InvalidMeasurement);
2225        }
2226        Ok(landing)
2227    }
2228
2229    /// The clearance below answering `request`; an answer to another request
2230    /// is refused.
2231    pub fn measure_clearance_below(
2232        &self,
2233        request: &ClearanceBelowRequest,
2234    ) -> Result<ClearanceBelow, WalkingSurfaceError> {
2235        let below = self.0.measure_clearance_below(request)?;
2236        if below.request() != request {
2237            return Err(WalkingSurfaceError::InvalidMeasurement);
2238        }
2239        Ok(below)
2240    }
2241
2242    /// The handrails answering `request`; an answer to another request is
2243    /// refused.
2244    pub fn measure_handrails(
2245        &self,
2246        request: &HandrailRequest,
2247    ) -> Result<HandrailEvidence, WalkingSurfaceError> {
2248        let rails = self.0.measure_handrails(request)?;
2249        if rails.request() != request {
2250            return Err(WalkingSurfaceError::InvalidMeasurement);
2251        }
2252        Ok(rails)
2253    }
2254
2255    /// The clear width answering `request`; an answer to another request is
2256    /// refused.
2257    pub fn measure_clear_width(
2258        &self,
2259        request: &ClearWidthRequest,
2260    ) -> Result<ClearWidthEvidence, WalkingSurfaceError> {
2261        let width = self.0.measure_clear_width(request)?;
2262        if width.request() != request {
2263            return Err(WalkingSurfaceError::InvalidMeasurement);
2264        }
2265        Ok(width)
2266    }
2267}
2268
2269#[cfg(test)]
2270mod tests {
2271    use super::*;
2272    use axioval_ir::SourceId;
2273
2274    fn source() -> SourceId {
2275        SourceId::new("cad", "m").unwrap()
2276    }
2277
2278    fn id(local: &str) -> ObjectId {
2279        ObjectId::new(source(), local).unwrap()
2280    }
2281
2282    fn point(value: f64) -> ElevationInterval {
2283        ElevationInterval::exact(value).unwrap()
2284    }
2285
2286    fn request(local: &str) -> TreadFlightRequest {
2287        TreadFlightRequest::new(id(local))
2288    }
2289
2290    fn x() -> MetricDirection {
2291        MetricDirection::try_new([1.0, 0.0, 0.0]).unwrap()
2292    }
2293
2294    fn tread(z: f64, front: f64, back: f64) -> Tread {
2295        Tread::try_new(point(z), point(front), point(back)).unwrap()
2296    }
2297
2298    fn evidence(exact: bool) -> Evidence {
2299        Evidence {
2300            source: source(),
2301            locator: "tread-flight:a".into(),
2302            exact,
2303        }
2304    }
2305
2306    /// Whether `interval` holds the decimal `value`, up to the binary
2307    /// rounding of the decimal positions it was computed from.
2308    fn contains(interval: MeasuredInterval, value: f64) -> bool {
2309        interval.lower() - 1e-15 <= value && value <= interval.upper() + 1e-15
2310    }
2311
2312    #[test]
2313    fn risers_goings_and_nosings_come_from_the_positions() {
2314        let treads = vec![
2315            tread(0.17, 0.0, 0.30),
2316            tread(0.34, 0.28, 0.58),
2317            tread(0.55, 0.56, 0.86),
2318        ];
2319        let flight = TreadFlight::try_new(
2320            request("a"),
2321            WalkingLine::Straight(x()),
2322            point(0.0),
2323            point(0.72),
2324            treads,
2325            evidence(true),
2326        )
2327        .unwrap();
2328        assert!(flight.ends_in_riser());
2329        let risers = flight.risers();
2330        assert_eq!(risers.len(), 4);
2331        // One closure per riser, the final one as the service states it.
2332        let closures = flight
2333            .clone()
2334            .with_final_riser(RiserClosure::Open)
2335            .riser_closures();
2336        assert_eq!(closures.len(), 4);
2337        assert_eq!(closures[3], RiserClosure::Open);
2338        assert_eq!(flight.riser_closures()[3], RiserClosure::NotMeasured);
2339        for (riser, expected) in risers.iter().zip([0.17, 0.17, 0.21, 0.17]) {
2340            assert!(contains(*riser, expected), "{riser:?} {expected}");
2341            assert!(riser.upper() - riser.lower() <= 2.0 * f64::EPSILON);
2342        }
2343        let goings = flight.goings();
2344        assert_eq!(goings.len(), 2);
2345        assert!(goings.iter().all(|going| contains(*going, 0.28)));
2346        assert!(
2347            flight
2348                .nosings()
2349                .iter()
2350                .all(|nosing| contains(*nosing, 0.02))
2351        );
2352        assert!(contains(flight.rise(), 0.72));
2353        assert!(contains(flight.treads()[0].depth(), 0.30));
2354    }
2355
2356    #[test]
2357    fn a_flight_whose_top_is_its_last_tread_has_no_final_riser() {
2358        let treads = vec![tread(0.2, 0.0, 0.3), tread(0.4, 0.3, 0.6)];
2359        let flight = TreadFlight::try_new(
2360            request("a"),
2361            WalkingLine::Straight(x()),
2362            point(0.0),
2363            point(0.4),
2364            treads,
2365            evidence(true),
2366        )
2367        .unwrap();
2368        assert!(!flight.ends_in_riser());
2369        assert_eq!(flight.risers().len(), 2);
2370        assert!(contains(flight.rise(), 0.4));
2371    }
2372
2373    #[test]
2374    fn incoherent_flights_are_refused() {
2375        let ordered = || vec![tread(0.2, 0.0, 0.3), tread(0.4, 0.3, 0.6)];
2376        // Treads out of order.
2377        let reversed = vec![tread(0.4, 0.3, 0.6), tread(0.2, 0.0, 0.3)];
2378        assert_eq!(
2379            TreadFlight::try_new(
2380                request("a"),
2381                WalkingLine::Straight(x()),
2382                point(0.0),
2383                point(0.4),
2384                reversed,
2385                evidence(true)
2386            ),
2387            Err(WalkingSurfaceError::InvalidMeasurement)
2388        );
2389        // A base above the first tread, and a top below the last.
2390        assert_eq!(
2391            TreadFlight::try_new(
2392                request("a"),
2393                WalkingLine::Straight(x()),
2394                point(0.3),
2395                point(0.4),
2396                ordered(),
2397                evidence(true)
2398            ),
2399            Err(WalkingSurfaceError::InvalidMeasurement)
2400        );
2401        assert_eq!(
2402            TreadFlight::try_new(
2403                request("a"),
2404                WalkingLine::Straight(x()),
2405                point(0.0),
2406                point(0.3),
2407                ordered(),
2408                evidence(true)
2409            ),
2410            Err(WalkingSurfaceError::InvalidMeasurement)
2411        );
2412        // No treads, or a sloped direction.
2413        assert_eq!(
2414            TreadFlight::try_new(
2415                request("a"),
2416                WalkingLine::Straight(x()),
2417                point(0.0),
2418                point(0.4),
2419                vec![],
2420                evidence(true)
2421            ),
2422            Err(WalkingSurfaceError::InvalidMeasurement)
2423        );
2424        let sloped = MetricDirection::try_new([1.0, 0.0, 1.0]).unwrap();
2425        assert_eq!(
2426            TreadFlight::try_new(
2427                request("a"),
2428                WalkingLine::Straight(sloped),
2429                point(0.0),
2430                point(0.4),
2431                ordered(),
2432                evidence(true)
2433            ),
2434            Err(WalkingSurfaceError::InvalidMeasurement)
2435        );
2436        // Exactness must match the positions.
2437        assert_eq!(
2438            TreadFlight::try_new(
2439                request("a"),
2440                WalkingLine::Straight(x()),
2441                point(0.0),
2442                point(0.4),
2443                ordered(),
2444                evidence(false)
2445            ),
2446            Err(WalkingSurfaceError::InexactEvidence)
2447        );
2448        let widened = ElevationInterval::try_new(0.39, 0.41).unwrap();
2449        assert_eq!(
2450            TreadFlight::try_new(
2451                request("a"),
2452                WalkingLine::Straight(x()),
2453                point(0.0),
2454                widened,
2455                ordered(),
2456                evidence(true)
2457            ),
2458            Err(WalkingSurfaceError::InvalidMeasurement)
2459        );
2460    }
2461
2462    #[test]
2463    fn a_run_slope_is_its_rise_over_its_length() {
2464        let run = SlopedRun::try_new(x(), point(0.0), point(0.5), point(1.0), point(7.0)).unwrap();
2465        assert!(contains(run.slope(), 0.5 / 6.0));
2466        assert!(run.slope().upper() - run.slope().lower() <= 4.0 * f64::EPSILON);
2467        assert!(contains(run.length(), 6.0) && contains(run.rise(), 0.5));
2468        assert_eq!(
2469            SlopedRun::try_new(x(), point(0.5), point(0.5), point(1.0), point(7.0)),
2470            Err(WalkingSurfaceError::InvalidMeasurement)
2471        );
2472        let ramp = SlopedSurface::try_new(id("a"), vec![run], evidence(true)).unwrap();
2473        assert!(ramp.is_exact());
2474        assert_eq!(
2475            SlopedSurface::try_new(id("a"), vec![], evidence(true)),
2476            Err(WalkingSurfaceError::InvalidMeasurement)
2477        );
2478    }
2479
2480    #[test]
2481    fn headroom_names_requested_obstacles_only() {
2482        let request = HeadroomRequest::new(id("a"), [id("c"), id("a"), id("b"), id("c")]);
2483        assert_eq!(request.obstacles(), &[id("b"), id("c")]);
2484        let clearance = MeasuredInterval::try_new(2.0, 2.0 + 1e-9).ok();
2485        assert!(
2486            Headroom::try_new(request.clone(), clearance, vec![id("b")], evidence(false)).is_ok()
2487        );
2488        assert_eq!(
2489            Headroom::try_new(request.clone(), clearance, vec![id("d")], evidence(false)),
2490            Err(WalkingSurfaceError::InvalidMeasurement)
2491        );
2492        assert_eq!(
2493            Headroom::try_new(request.clone(), clearance, vec![], evidence(false)),
2494            Err(WalkingSurfaceError::InvalidMeasurement)
2495        );
2496        assert_eq!(
2497            Headroom::try_new(request.clone(), clearance, vec![id("b")], evidence(true)),
2498            Err(WalkingSurfaceError::InexactEvidence)
2499        );
2500        assert!(Headroom::try_new(request, None, vec![], evidence(false)).is_ok());
2501    }
2502
2503    struct Other;
2504    impl WalkingSurfaceService for Other {
2505        fn measure_tread_flight(
2506            &self,
2507            _: &TreadFlightRequest,
2508        ) -> Result<TreadFlight, WalkingSurfaceError> {
2509            TreadFlight::try_new(
2510                request("b"),
2511                WalkingLine::Straight(x()),
2512                point(0.0),
2513                point(0.2),
2514                vec![tread(0.2, 0.0, 0.3)],
2515                evidence(true),
2516            )
2517        }
2518        fn measure_sloped_runs(&self, _: &ObjectId) -> Result<SlopedSurface, WalkingSurfaceError> {
2519            let run = SlopedRun::try_new(x(), point(0.0), point(0.5), point(0.0), point(6.0))?;
2520            SlopedSurface::try_new(id("b"), vec![run], evidence(true))
2521        }
2522        fn measure_headroom(&self, _: &HeadroomRequest) -> Result<Headroom, WalkingSurfaceError> {
2523            Headroom::try_new(
2524                HeadroomRequest::new(id("b"), []),
2525                None,
2526                vec![],
2527                evidence(false),
2528            )
2529        }
2530    }
2531
2532    #[test]
2533    fn answers_about_another_object_are_refused() {
2534        let handle = WalkingSurfaceServiceHandle::new(Arc::new(Other));
2535        assert_eq!(
2536            handle.measure_tread_flight(&request("a")),
2537            Err(WalkingSurfaceError::InvalidMeasurement)
2538        );
2539        assert_eq!(
2540            handle.measure_sloped_runs(&id("a")),
2541            Err(WalkingSurfaceError::InvalidMeasurement)
2542        );
2543        assert_eq!(
2544            handle.measure_headroom(&HeadroomRequest::new(id("a"), [])),
2545            Err(WalkingSurfaceError::InvalidMeasurement)
2546        );
2547        assert!(handle.measure_tread_flight(&request("b")).is_ok());
2548        // Landings and the clearance below are refused by default, never
2549        // answered empty.
2550        assert!(matches!(
2551            handle.measure_landing(&LandingRequest::new(id("b"), WalkingEnd::FlightTop, [])),
2552            Err(WalkingSurfaceError::Unsupported(_))
2553        ));
2554        assert!(matches!(
2555            handle.measure_clearance_below(&ClearanceBelowRequest::new(id("b"), [])),
2556            Err(WalkingSurfaceError::Unsupported(_))
2557        ));
2558        let rails =
2559            HandrailRequest::try_new(id("b"), WalkingStretch::Flight, [], (0.1, 1.5), 0.3).unwrap();
2560        assert!(matches!(
2561            handle.measure_handrails(&rails),
2562            Err(WalkingSurfaceError::Unsupported(_))
2563        ));
2564    }
2565
2566    fn rail(left: f64, right: f64, lowest: f64) -> RailMeasurement {
2567        RailMeasurement::try_new(
2568            (point(-0.3), point(1.5)),
2569            (point(left), point(right)),
2570            MeasuredInterval::try_new(lowest, lowest + 1e-9).unwrap(),
2571            MeasuredInterval::try_new(lowest + 0.01, lowest + 0.01 + 1e-9).unwrap(),
2572        )
2573        .unwrap()
2574    }
2575
2576    #[test]
2577    fn handrails_are_requested_rails_on_a_side_with_extensions() {
2578        let request = HandrailRequest::try_new(
2579            id("a"),
2580            WalkingStretch::Flight,
2581            [id("r"), id("a"), id("l"), id("r")],
2582            (0.2, 1.5),
2583            0.3,
2584        )
2585        .unwrap();
2586        assert_eq!(request.rails(), &[id("l"), id("r")]);
2587        assert!(
2588            HandrailRequest::try_new(id("a"), WalkingStretch::Flight, [], (-0.1, 1.5), 0.0)
2589                .is_err()
2590        );
2591        let evidence = |exact: bool| Evidence {
2592            source: source(),
2593            locator: "handrails:a".into(),
2594            exact,
2595        };
2596        let measure = |rails: Vec<(ObjectId, RailMeasurement)>, exact: bool| {
2597            HandrailEvidence::try_new(
2598                request.clone(),
2599                x(),
2600                (point(0.0), point(1.12)),
2601                (point(0.0), point(1.2)),
2602                rails,
2603                evidence(exact),
2604            )
2605        };
2606        let rails = vec![
2607            (id("r"), rail(-0.05, 0.0, 0.9)),
2608            (id("l"), rail(1.2, 1.25, 0.85)),
2609        ];
2610        let measured = measure(rails.clone(), false).unwrap();
2611        assert_eq!(measured.rails()[0].0, id("l"));
2612        let (left, right) = (measured.rails()[0].1, measured.rails()[1].1);
2613        assert_eq!(measured.side(&left), Some(RailSide::Left));
2614        assert_eq!(measured.side(&right), Some(RailSide::Right));
2615        assert_eq!(measured.side(&rail(0.5, 0.7, 0.9)), None);
2616        assert!(contains(measured.bottom_extension(&left).unwrap(), 0.3));
2617        assert!(contains(measured.top_extension(&left).unwrap(), 0.38));
2618        // Exact evidence, an unrequested rail and one named twice are
2619        // refused.
2620        assert_eq!(
2621            measure(rails, true),
2622            Err(WalkingSurfaceError::InexactEvidence)
2623        );
2624        for rails in [
2625            vec![(id("x"), rail(0.0, 0.1, 0.9))],
2626            vec![
2627                (id("l"), rail(0.0, 0.1, 0.9)),
2628                (id("l"), rail(0.0, 0.1, 0.9)),
2629            ],
2630        ] {
2631            assert_eq!(
2632                measure(rails, false),
2633                Err(WalkingSurfaceError::InvalidMeasurement)
2634            );
2635        }
2636        // A negative rise or unordered heights are refused.
2637        let rise = MeasuredInterval::try_new(-0.1, 0.0).ok();
2638        assert!(rail(0.0, 0.1, 0.9).with_rises(rise, None).is_err());
2639        assert!(
2640            RailMeasurement::try_new(
2641                (point(0.0), point(1.0)),
2642                (point(0.0), point(0.1)),
2643                MeasuredInterval::try_new(1.0, 1.0).unwrap(),
2644                MeasuredInterval::try_new(0.9, 0.9).unwrap(),
2645            )
2646            .is_err()
2647        );
2648    }
2649
2650    fn piece(start: f64, end: f64, left: f64, right: f64) -> RailMeasurement {
2651        RailMeasurement::try_new(
2652            (point(start), point(end)),
2653            (point(left), point(right)),
2654            MeasuredInterval::try_new(0.9, 0.9 + 1e-9).unwrap(),
2655            MeasuredInterval::try_new(0.9, 0.9 + 1e-9).unwrap(),
2656        )
2657        .unwrap()
2658    }
2659
2660    #[test]
2661    fn the_pieces_along_a_side_are_ordered_with_their_gaps() {
2662        let request = HandrailRequest::try_new(
2663            id("a"),
2664            WalkingStretch::Flight,
2665            [id("p"), id("q"), id("r"), id("s"), id("m")],
2666            (0.2, 1.5),
2667            0.3,
2668        )
2669        .unwrap();
2670        let measure = |rails: Vec<(ObjectId, RailMeasurement)>| {
2671            HandrailEvidence::try_new(
2672                request.clone(),
2673                x(),
2674                (point(0.0), point(0.84)),
2675                (point(0.0), point(1.2)),
2676                rails,
2677                evidence(false),
2678            )
2679            .unwrap()
2680        };
2681        // Left: `q` then `p` with a 0.1 m gap; right: `r` then `s` meeting
2682        // end to end; `m` reaches over the middle.
2683        let measured = measure(vec![
2684            (id("p"), piece(0.5, 1.14, 1.25, 1.3)),
2685            (id("q"), piece(-0.3, 0.4, 1.25, 1.3)),
2686            (id("r"), piece(-0.3, 0.4, -0.1, -0.05)),
2687            (id("s"), piece(0.4, 1.14, -0.1, -0.05)),
2688            (id("m"), piece(-0.3, 1.14, 0.55, 0.65)),
2689        ]);
2690        let left = measured.side_rail(RailSide::Left).unwrap();
2691        let names: Vec<&ObjectId> = left.iter().map(|(rail, _)| rail).collect();
2692        assert_eq!(names, [&id("q"), &id("p")]);
2693        let gap = measured.gap(&left[0].1, &left[1].1).unwrap();
2694        assert!(
2695            contains(gap, 0.1) && gap.upper() - gap.lower() < 1e-12,
2696            "{gap:?}"
2697        );
2698        let right = measured.side_rail(RailSide::Right).unwrap();
2699        let gap = measured.gap(&right[0].1, &right[1].1).unwrap();
2700        assert!(gap.lower() == 0.0 && gap.upper() < 1e-12, "{gap:?}");
2701        // Offset across, pieces are apart diagonally.
2702        let apart = measured
2703            .gap(&piece(-0.3, 0.4, 1.25, 1.3), &piece(0.7, 1.14, 1.6, 1.65))
2704            .unwrap();
2705        assert!(contains(apart, 0.3_f64.hypot(0.3)), "{apart:?}");
2706        // A second rail within the first's stretch leaves the order open.
2707        let nested = measure(vec![
2708            (id("p"), piece(-0.3, 1.14, 1.25, 1.3)),
2709            (id("q"), piece(0.0, 0.84, 1.3, 1.35)),
2710        ]);
2711        assert_eq!(
2712            nested.side_rail(RailSide::Left),
2713            Err(vec![id("p"), id("q")])
2714        );
2715        assert_eq!(nested.side_rail(RailSide::Right), Ok(vec![]));
2716        // A piece too uncertain to surely fill a rectangle has no upper
2717        // bound on its gap.
2718        let blurred = RailMeasurement::try_new(
2719            (around(0.5, 0.2), around(0.6, 0.2)),
2720            (point(1.25), point(1.3)),
2721            MeasuredInterval::try_new(0.9, 0.9).unwrap(),
2722            MeasuredInterval::try_new(0.9, 0.9).unwrap(),
2723        )
2724        .unwrap();
2725        assert_eq!(measured.gap(&left[0].1, &blurred), None);
2726    }
2727
2728    #[test]
2729    fn a_turning_flights_rails_are_measured_part_by_part() {
2730        // A quarter turn: along +x over y 0 .. 1, then along +y over x
2731        // 0.84 .. 1.84, whose sides across +y (towards -x) lie at -1.84 and
2732        // -0.84. The outer rail is two pieces meeting at the corner.
2733        let y = MetricDirection::try_new([0.0, 1.0, 0.0]).unwrap();
2734        let parts = vec![
2735            StretchPart::try_new(x(), (point(0.0), point(1.0))).unwrap(),
2736            StretchPart::try_new(y, (point(-1.84), point(-0.84))).unwrap(),
2737        ];
2738        let request = HandrailRequest::try_new(
2739            id("a"),
2740            WalkingStretch::Flight,
2741            [id("p"), id("q"), id("r")],
2742            (0.2, 1.5),
2743            0.3,
2744        )
2745        .unwrap();
2746        let measure = |rails: Vec<(ObjectId, RailMeasurement)>| {
2747            HandrailEvidence::try_in_parts(
2748                request.clone(),
2749                parts.clone(),
2750                (point(0.0), point(1.56)),
2751                rails,
2752                evidence(false),
2753            )
2754        };
2755        // `p` along +x at y -0.1 .. -0.05 up to the corner, `q` along +y at
2756        // x 1.84 .. 1.89 from it, `r` along the inner side of the top part.
2757        let measured = measure(vec![
2758            (id("p"), piece(-0.3, 1.89, -0.1, -0.05)),
2759            (id("q"), piece(-0.1, 1.86, -1.89, -1.84).in_part(1)),
2760            (id("r"), piece(1.0, 1.86, -0.84, -0.79).in_part(1)),
2761        ])
2762        .unwrap();
2763        assert_eq!(measured.parts().len(), 2);
2764        let (p, q, r) = (
2765            &measured.rails()[0].1,
2766            &measured.rails()[1].1,
2767            &measured.rails()[2].1,
2768        );
2769        assert_eq!(measured.side(p), Some(RailSide::Right));
2770        assert_eq!(measured.side(q), Some(RailSide::Right));
2771        assert_eq!(measured.side(r), Some(RailSide::Left));
2772        let outer = measured.side_rail(RailSide::Right).unwrap();
2773        let names: Vec<&ObjectId> = outer.iter().map(|(rail, _)| rail).collect();
2774        assert_eq!(names, [&id("p"), &id("q")]);
2775        // They meet at the corner.
2776        let gap = measured.gap(p, q).unwrap();
2777        assert!(gap.lower() == 0.0 && gap.upper() < 1e-12, "{gap:?}");
2778        // The first piece reaches beyond the bottom, the last beyond the
2779        // top; neither is measured beyond the other end.
2780        assert!(contains(measured.bottom_extension(p).unwrap(), 0.3));
2781        assert_eq!(measured.top_extension(p), None);
2782        assert!(contains(measured.top_extension(q).unwrap(), 0.3));
2783        assert_eq!(measured.bottom_extension(q), None);
2784        // A rail in a part the stretch does not have is refused.
2785        assert_eq!(
2786            measure(vec![(id("p"), piece(0.0, 1.0, -0.1, -0.05).in_part(2))]),
2787            Err(WalkingSurfaceError::InvalidMeasurement)
2788        );
2789        assert_eq!(
2790            HandrailEvidence::try_in_parts(
2791                request.clone(),
2792                vec![],
2793                (point(0.0), point(1.56)),
2794                vec![],
2795                evidence(false)
2796            ),
2797            Err(WalkingSurfaceError::InvalidMeasurement)
2798        );
2799        let sloped = MetricDirection::try_new([1.0, 0.0, 1.0]).unwrap();
2800        assert!(StretchPart::try_new(sloped, (point(0.0), point(1.0))).is_err());
2801    }
2802
2803    fn around(value: f64, margin: f64) -> ElevationInterval {
2804        ElevationInterval::try_new(value - margin, value + margin).unwrap()
2805    }
2806
2807    #[test]
2808    fn widths_come_from_the_sides_and_the_narrowest_tread_governs() {
2809        let sided = |z: f64, front: f64, right: f64| {
2810            tread(z, front, front + 0.3)
2811                .with_sides(point(0.0), point(right))
2812                .unwrap()
2813        };
2814        let treads = vec![sided(0.2, 0.0, 1.2), sided(0.4, 0.3, 1.1)];
2815        let flight = TreadFlight::try_new(
2816            request("a"),
2817            WalkingLine::Straight(x()),
2818            point(0.0),
2819            point(0.4),
2820            treads,
2821            evidence(true),
2822        )
2823        .unwrap();
2824        assert!(contains(flight.width().unwrap(), 1.1));
2825        // One tread without sides leaves the flight's width unknown.
2826        let treads = vec![sided(0.2, 0.0, 1.2), tread(0.4, 0.3, 0.6)];
2827        let flight = TreadFlight::try_new(
2828            request("a"),
2829            WalkingLine::Straight(x()),
2830            point(0.0),
2831            point(0.4),
2832            treads,
2833            evidence(true),
2834        )
2835        .unwrap();
2836        assert_eq!(flight.width(), None);
2837        // Sides out of order, and widened sides reported as exact.
2838        assert_eq!(
2839            tread(0.2, 0.0, 0.3).with_sides(point(1.0), point(0.0)),
2840            Err(WalkingSurfaceError::InvalidMeasurement)
2841        );
2842        let widened = tread(0.2, 0.0, 0.3)
2843            .with_sides(point(0.0), ElevationInterval::try_new(1.0, 1.1).unwrap())
2844            .unwrap();
2845        assert!(!widened.is_exact());
2846        assert_eq!(
2847            TreadFlight::try_new(
2848                request("a"),
2849                WalkingLine::Straight(x()),
2850                point(0.0),
2851                point(0.2),
2852                vec![widened],
2853                evidence(true)
2854            ),
2855            Err(WalkingSurfaceError::InexactEvidence)
2856        );
2857        let run = SlopedRun::try_new(x(), point(0.0), point(0.5), point(1.0), point(7.0))
2858            .unwrap()
2859            .with_sides(point(-0.75), point(0.75))
2860            .unwrap();
2861        assert!(contains(run.width().unwrap(), 1.5) && run.is_exact());
2862        let [ax, ay, _] = across(x()).components();
2863        assert!(ax.abs() < f64::EPSILON && (ay - 1.0).abs() < f64::EPSILON);
2864    }
2865
2866    fn landing_evidence(
2867        request: &LandingRequest,
2868        landing: Option<Landing>,
2869        exact: bool,
2870    ) -> Result<LandingEvidence, WalkingSurfaceError> {
2871        LandingEvidence::try_new(
2872            request.clone(),
2873            x(),
2874            point(1.0),
2875            landing,
2876            Evidence {
2877                source: source(),
2878                locator: "landing:a".into(),
2879                exact,
2880            },
2881        )
2882    }
2883
2884    #[test]
2885    fn a_landing_is_carried_by_a_requested_object_beyond_the_edge() {
2886        let request = LandingRequest::new(id("a"), WalkingEnd::FlightTop, [id("c"), id("a")]);
2887        assert_eq!(request.candidates(), &[id("c")]);
2888        let extent = LandingExtent::try_new(point(2.5), point(-0.1), point(1.4)).unwrap();
2889        let found =
2890            landing_evidence(&request, Some(Landing::new(id("c"), Some(extent))), true).unwrap();
2891        assert!(contains(found.depth().unwrap(), 1.5));
2892        assert!(contains(found.width().unwrap(), 1.5));
2893        // The subject may carry its own landing (a ramp's).
2894        assert!(
2895            landing_evidence(&request, Some(Landing::new(id("a"), Some(extent))), true).is_ok()
2896        );
2897        // An unrequested carrier, a far side short of the edge, and
2898        // exactness that does not match the positions are refused.
2899        assert_eq!(
2900            landing_evidence(&request, Some(Landing::new(id("d"), None)), true),
2901            Err(WalkingSurfaceError::InvalidMeasurement)
2902        );
2903        let short = LandingExtent::try_new(point(0.5), point(0.0), point(1.0)).unwrap();
2904        assert_eq!(
2905            landing_evidence(&request, Some(Landing::new(id("c"), Some(short))), true),
2906            Err(WalkingSurfaceError::InvalidMeasurement)
2907        );
2908        assert_eq!(
2909            landing_evidence(&request, None, false),
2910            Err(WalkingSurfaceError::InexactEvidence)
2911        );
2912        assert_eq!(
2913            LandingExtent::try_new(point(2.0), point(1.0), point(0.0)),
2914            Err(WalkingSurfaceError::InvalidMeasurement)
2915        );
2916        // Found but not a rectangle: no size.
2917        let unmeasured =
2918            landing_evidence(&request, Some(Landing::new(id("c"), None)), true).unwrap();
2919        assert_eq!(unmeasured.depth(), None);
2920        assert_eq!(unmeasured.width(), None);
2921    }
2922
2923    #[test]
2924    fn the_clearance_below_names_requested_spaces_only() {
2925        let request = ClearanceBelowRequest::new(id("a"), [id("s"), id("a"), id("s")]);
2926        assert_eq!(request.spaces(), &[id("s")]);
2927        let clearance = MeasuredInterval::try_new(1.5, 1.5 + 1e-9).ok();
2928        assert!(
2929            ClearanceBelow::try_new(request.clone(), clearance, vec![id("s")], evidence(false))
2930                .is_ok()
2931        );
2932        assert_eq!(
2933            ClearanceBelow::try_new(request.clone(), clearance, vec![id("t")], evidence(false)),
2934            Err(WalkingSurfaceError::InvalidMeasurement)
2935        );
2936        assert_eq!(
2937            ClearanceBelow::try_new(request.clone(), clearance, vec![id("s")], evidence(true)),
2938            Err(WalkingSurfaceError::InexactEvidence)
2939        );
2940        assert!(ClearanceBelow::try_new(request, None, vec![], evidence(false)).is_ok());
2941    }
2942
2943    #[test]
2944    fn winder_angles_and_widths_come_from_nosings_and_sides() {
2945        // A quarter turn over three winders: nosings at 0°, 30°, 60° and
2946        // 90° about the inner corner at the origin.
2947        let nosing = |degrees: f64| {
2948            let (sin, cos) = degrees.to_radians().sin_cos();
2949            PlanSegment::try_new([0.0, 0.0], [cos, sin], 0.0).unwrap()
2950        };
2951        let treads: Vec<Tread> = [0.0, 30.0, 60.0, 90.0]
2952            .iter()
2953            .enumerate()
2954            .map(|(step, degrees)| {
2955                #[allow(clippy::cast_precision_loss)]
2956                let step = step as f64;
2957                tread(0.18 * (step + 1.0), 0.3 * step, 0.3 * step + 0.3)
2958                    .with_nosing(nosing(*degrees))
2959                    .with_sides(point(-0.4), point(0.5))
2960                    .unwrap()
2961                    .with_riser_below(RiserClosure::Closed)
2962            })
2963            .collect();
2964        let line = WalkingLine::Turning(vec![[0.5, 0.1], [0.4, 0.3], [0.3, 0.4], [0.1, 0.5]]);
2965        let flight = TreadFlight::try_new(
2966            request("a"),
2967            line.clone(),
2968            point(0.0),
2969            point(0.72),
2970            treads,
2971            evidence(false),
2972        )
2973        .unwrap_err();
2974        // Exact positions and nosings make exact evidence.
2975        assert_eq!(flight, WalkingSurfaceError::InexactEvidence);
2976        let treads: Vec<Tread> = [0.0, 30.0, 60.0, 90.0]
2977            .iter()
2978            .enumerate()
2979            .map(|(step, degrees)| {
2980                #[allow(clippy::cast_precision_loss)]
2981                let step = step as f64;
2982                let tread = tread(0.18 * (step + 1.0), 0.3 * step, 0.3 * step + 0.3)
2983                    .with_nosing(nosing(*degrees));
2984                // Only the first tread fills a rectangle; the winders taper.
2985                if step == 0.0 {
2986                    tread.with_sides(point(-0.4), point(0.5)).unwrap()
2987                } else {
2988                    tread
2989                }
2990            })
2991            .collect();
2992        let flight = TreadFlight::try_new(
2993            request("a"),
2994            line,
2995            point(0.0),
2996            point(0.72),
2997            treads,
2998            evidence(true),
2999        )
3000        .unwrap();
3001        assert!(flight.walking_line().is_turning());
3002        let angles = flight.winder_angles();
3003        assert_eq!(angles.len(), 3);
3004        for angle in angles {
3005            let angle = angle.unwrap();
3006            assert!(contains(angle, 30.0_f64.to_radians()), "{angle:?}");
3007            assert!(angle.upper() - angle.lower() < 1e-12);
3008        }
3009        let width = flight.treads()[0].width().unwrap();
3010        assert!(contains(width, 0.9));
3011        // A winder has no sides, so the flight has no width.
3012        assert_eq!(flight.treads()[1].width(), None);
3013        assert_eq!(flight.width(), None);
3014        assert_eq!(flight.treads()[0].riser_below(), RiserClosure::NotMeasured);
3015    }
3016
3017    #[test]
3018    fn an_uncertain_nosing_widens_its_angle_and_a_short_one_has_none() {
3019        let a = PlanSegment::try_new([0.0, 0.0], [1.0, 0.0], 0.001).unwrap();
3020        let b = PlanSegment::try_new([0.0, 0.0], [0.0, 1.0], 0.001).unwrap();
3021        let square = a.angle_to(&b).unwrap();
3022        assert!(square.upper() <= std::f64::consts::FRAC_PI_2);
3023        assert!(square.lower() < std::f64::consts::FRAC_PI_2 - 0.0039);
3024        let parallel = a.angle_to(&a).unwrap();
3025        assert!(parallel.lower() == 0.0 && parallel.upper() > 0.0039);
3026        let short = PlanSegment::try_new([0.0, 0.0], [0.001, 0.0], 0.001).unwrap();
3027        assert_eq!(short.angle_to(&a), None);
3028        assert_eq!(
3029            PlanSegment::try_new([0.0, 0.0], [0.0, 0.0], 0.0),
3030            Err(WalkingSurfaceError::InvalidMeasurement)
3031        );
3032        assert_eq!(
3033            PlanSegment::try_new([0.0, 0.0], [1.0, 0.0], -1.0),
3034            Err(WalkingSurfaceError::InvalidMeasurement)
3035        );
3036    }
3037
3038    #[test]
3039    fn clear_widths_name_requested_obstacles_and_a_band_above_the_pitch_line() {
3040        let request = ClearWidthRequest::try_new(
3041            id("f"),
3042            WalkingStretch::Flight,
3043            [id("rail"), id("f"), id("rail")],
3044            (0.5, 1.5),
3045        )
3046        .unwrap();
3047        assert_eq!(request.obstacles(), [id("rail")]);
3048        assert_eq!(request.band(), (0.5, 1.5));
3049        for band in [(1.5, 0.5), (-0.1, 1.0), (0.5, f64::NAN)] {
3050            assert!(ClearWidthRequest::try_new(id("f"), WalkingStretch::Flight, [], band).is_err());
3051        }
3052        let width = MeasuredInterval::try_new(0.99, 1.01).unwrap();
3053        let measured =
3054            ClearWidthEvidence::try_new(request.clone(), width, vec![id("rail")], evidence(false))
3055                .unwrap();
3056        assert_eq!(measured.governing(), [id("rail")]);
3057        assert_eq!(
3058            ClearWidthEvidence::try_new(request.clone(), width, vec![id("wall")], evidence(false)),
3059            Err(WalkingSurfaceError::InvalidMeasurement)
3060        );
3061        assert_eq!(
3062            ClearWidthEvidence::try_new(request, width, vec![], evidence(true)),
3063            Err(WalkingSurfaceError::InexactEvidence)
3064        );
3065    }
3066
3067    #[test]
3068    fn walking_lines_and_offsets_are_checked() {
3069        assert!(TreadFlightRequest::from_inner_side(id("a"), 0.0).is_err());
3070        assert!(TreadFlightRequest::from_inner_side(id("a"), f64::NAN).is_err());
3071        let request = TreadFlightRequest::from_inner_side(id("a"), 0.4).unwrap();
3072        assert_eq!(
3073            request.walking_line(),
3074            WalkingLinePlacement::FromInnerSide(0.4)
3075        );
3076        let treads = || vec![tread(0.2, 0.0, 0.3), tread(0.4, 0.3, 0.6)];
3077        for line in [
3078            WalkingLine::Turning(vec![[0.0, 0.0]]),
3079            WalkingLine::Turning(vec![[0.0, 0.0], [0.0, 0.0]]),
3080            WalkingLine::Turning(vec![[0.0, 0.0], [f64::INFINITY, 0.0]]),
3081        ] {
3082            assert_eq!(
3083                TreadFlight::try_new(
3084                    request.clone(),
3085                    line,
3086                    point(0.0),
3087                    point(0.4),
3088                    treads(),
3089                    evidence(true)
3090                ),
3091                Err(WalkingSurfaceError::InvalidMeasurement)
3092            );
3093        }
3094        assert!(
3095            tread(0.2, 0.0, 0.3)
3096                .with_sides(point(1.0), point(0.0))
3097                .is_err()
3098        );
3099    }
3100}