Skip to main content

axioval_engine/
walkability.rs

1//! Source-neutral walkable-region topology and service contracts.
2use crate::door_leaves::{SweptDoor, tidy_swept};
3use crate::{LengthInterval, ServiceRegistry, ServiceRegistryError};
4use axioval_ir::{Evidence, ObjectId};
5use std::{
6    collections::{BTreeMap, BTreeSet, VecDeque},
7    sync::Arc,
8};
9use thiserror::Error;
10#[derive(Clone, Debug, Error, PartialEq)]
11pub enum WalkabilityError {
12    #[error("minimum width must be finite and positive")]
13    InvalidMinimumWidth,
14    #[error("walkability region identifier is blank")]
15    InvalidRegionId,
16    #[error("passage joins a region to itself")]
17    SelfPassage,
18    #[error("passage evidence is not exact and reviewable")]
19    InexactPassage,
20    #[error("walkability evidence is incomplete")]
21    IncompleteEvidence,
22    #[error("duplicate walkability region")]
23    DuplicateRegion,
24    #[error("passage names an unknown region")]
25    UnknownRegion,
26    #[error("duplicate walkable passage")]
27    DuplicatePassage,
28    #[error("region maps an object outside the request universe")]
29    UnexpectedMappedObject,
30    #[error("portal passage violates the request portal policy")]
31    ForbiddenPortalPassage,
32    #[error("walkability object is not mapped to a region")]
33    ObjectUnavailable,
34    #[error("backend returned another request")]
35    ResponseRequestMismatch,
36    #[error("vertical connector is declared twice with different kinds")]
37    ConflictingConnector,
38    #[error("passage is both a portal and a vertical connector")]
39    PortalConnectorPassage,
40    #[error("connector passage violates the request connector declaration")]
41    ForbiddenConnectorPassage,
42    /// A stated clear width is not finite and positive, names an object
43    /// that is not a requested entrance, or is stated twice.
44    #[error("stated clear width is invalid or names no requested entrance")]
45    InvalidStatedClearWidth,
46    /// One door is given twice with different swept sectors.
47    #[error("a swept door is given twice with different sectors")]
48    ConflictingSweptDoors,
49    /// An obstruction depth or surface gap is not finite and non-negative.
50    #[error("a walkability tolerance must be finite and non-negative")]
51    InvalidTolerance,
52    /// A stretch lies on a portal or connector passage, is not narrower
53    /// than the request's width, or names an object the request does not.
54    #[error("a narrow stretch does not fit its passage or the request")]
55    InvalidStretch,
56    /// The backend refused: evidence it would need is missing, approximate
57    /// or outside what it can measure. Never a negative verdict.
58    #[error("walkability unavailable: {0}")]
59    Unavailable(String),
60}
61/// What kind of vertical connector joins walkable regions on different levels.
62///
63/// The kind is the rule's (or its source's) classification, carried in the
64/// request, never inferred from geometry. A rule forbids a kind by routing
65/// with [`WalkabilitySnapshot::route_between_avoiding`], so a route that
66/// needs a stair is unreachable for it.
67#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
68pub enum VerticalConnectorKind {
69    Lift,
70    Ramp,
71    Stair,
72}
73impl VerticalConnectorKind {
74    pub fn as_str(self) -> &'static str {
75        match self {
76            Self::Lift => "lift",
77            Self::Ramp => "ramp",
78            Self::Stair => "stair",
79        }
80    }
81}
82/// A selected object that joins levels, and its kind.
83#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
84pub struct VerticalConnector {
85    object: ObjectId,
86    kind: VerticalConnectorKind,
87}
88impl VerticalConnector {
89    pub fn new(object: ObjectId, kind: VerticalConnectorKind) -> Self {
90        Self { object, kind }
91    }
92    pub fn object(&self) -> &ObjectId {
93        &self.object
94    }
95    pub fn kind(&self) -> VerticalConnectorKind {
96        self.kind
97    }
98}
99#[derive(Clone, Debug, PartialEq)]
100pub struct WalkabilityRequest {
101    surfaces: Vec<ObjectId>,
102    entrances: Vec<ObjectId>,
103    obstacles: Vec<ObjectId>,
104    minimum_width: f64,
105    elevation_band: Option<LengthInterval>,
106    traverse_verified_portals: bool,
107    include_motion_envelopes: bool,
108    connectors: Vec<VerticalConnector>,
109    stated_clear_widths: BTreeMap<ObjectId, f64>,
110    swept: Vec<SweptDoor>,
111    obstruction_depth: f64,
112    surface_gap: f64,
113}
114impl WalkabilityRequest {
115    pub fn try_new(
116        mut surfaces: Vec<ObjectId>,
117        mut entrances: Vec<ObjectId>,
118        mut obstacles: Vec<ObjectId>,
119        minimum_width: f64,
120        elevation_band: Option<LengthInterval>,
121        traverse_verified_portals: bool,
122        include_motion_envelopes: bool,
123    ) -> Result<Self, WalkabilityError> {
124        if !minimum_width.is_finite() || minimum_width <= 0.0 {
125            return Err(WalkabilityError::InvalidMinimumWidth);
126        }
127        surfaces.sort();
128        surfaces.dedup();
129        entrances.sort();
130        entrances.dedup();
131        obstacles.sort();
132        obstacles.dedup();
133        Ok(Self {
134            surfaces,
135            entrances,
136            obstacles,
137            minimum_width,
138            elevation_band,
139            traverse_verified_portals,
140            include_motion_envelopes,
141            connectors: Vec::new(),
142            stated_clear_widths: BTreeMap::new(),
143            swept: Vec::new(),
144            obstruction_depth: 0.0,
145            surface_gap: 0.0,
146        })
147    }
148    /// Tolerates obstacles intruding up to `metres` into a surface from its
149    /// boundary: whatever an obstacle occupies within that distance of the
150    /// surface's plan boundary (a skirting, a pipe along the wall) does not
151    /// obstruct it. Zero, the default, tolerates nothing.
152    ///
153    /// # Errors
154    ///
155    /// [`WalkabilityError::InvalidTolerance`] when `metres` is not finite
156    /// and non-negative.
157    pub fn with_obstruction_depth(mut self, metres: f64) -> Result<Self, WalkabilityError> {
158        self.obstruction_depth = tolerance(metres)?;
159        Ok(self)
160    }
161    /// The depth, in metres, up to which obstacles may intrude.
162    pub fn obstruction_depth_metres(&self) -> f64 {
163        self.obstruction_depth
164    }
165    /// Joins surfaces whose plans lie at most `metres` apart at overlapping
166    /// heights as if they touched, with the gap between them walkable: a
167    /// modelling gap between two spaces does not cut a route. Obstacles in
168    /// the gap still obstruct. Zero, the default, joins only surfaces that
169    /// touch.
170    ///
171    /// # Errors
172    ///
173    /// [`WalkabilityError::InvalidTolerance`] when `metres` is not finite
174    /// and non-negative.
175    pub fn with_surface_gap(mut self, metres: f64) -> Result<Self, WalkabilityError> {
176        self.surface_gap = tolerance(metres)?;
177        Ok(self)
178    }
179    /// The widest gap, in metres, joined between surfaces.
180    pub fn surface_gap_metres(&self) -> f64 {
181        self.surface_gap
182    }
183    /// Counts the sectors `swept` doors sweep as obstacles on the surfaces
184    /// they stand on (see [`SweptDoor`]): a definite passage stays clear of
185    /// their circumscribed polygons, a proof that none exists holds against
186    /// their inscribed ones. A requested entrance is walked through, so a
187    /// backend never counts its own swing against a passage through it,
188    /// only against passages past it.
189    ///
190    /// # Errors
191    ///
192    /// [`WalkabilityError::ConflictingSweptDoors`] when one door is given
193    /// twice with different sectors.
194    pub fn with_swept_doors(mut self, swept: Vec<SweptDoor>) -> Result<Self, WalkabilityError> {
195        self.swept = tidy_swept(swept).ok_or(WalkabilityError::ConflictingSweptDoors)?;
196        Ok(self)
197    }
198    /// The doors whose swept sectors are obstacles, sorted by door.
199    pub fn swept_doors(&self) -> &[SweptDoor] {
200        &self.swept
201    }
202    /// States, in metres, the clear width a requested entrance's leaf and
203    /// lining leave, as the rule reads it from its source (a door's stated
204    /// clear width, say). A backend bounds the entrance's crossing by it
205    /// from above and may use it to admit the body; it never widens what
206    /// the geometry shows.
207    ///
208    /// # Errors
209    ///
210    /// [`WalkabilityError::InvalidStatedClearWidth`] when a width is not
211    /// finite and positive, an object is not a requested entrance, or one
212    /// entrance is stated twice.
213    pub fn with_stated_clear_widths(
214        mut self,
215        widths: impl IntoIterator<Item = (ObjectId, f64)>,
216    ) -> Result<Self, WalkabilityError> {
217        let mut stated = BTreeMap::new();
218        for (object, metres) in widths {
219            if !metres.is_finite()
220                || metres <= 0.0
221                || self.entrances.binary_search(&object).is_err()
222                || stated.insert(object, metres).is_some()
223            {
224                return Err(WalkabilityError::InvalidStatedClearWidth);
225            }
226        }
227        self.stated_clear_widths = stated;
228        Ok(self)
229    }
230    /// The clear width stated for `entrance`, if any.
231    pub fn stated_clear_width(&self, entrance: &ObjectId) -> Option<f64> {
232        self.stated_clear_widths.get(entrance).copied()
233    }
234    /// Every stated clear width, ordered by entrance.
235    pub fn stated_clear_widths(&self) -> &BTreeMap<ObjectId, f64> {
236        &self.stated_clear_widths
237    }
238    /// Declares the vertical connectors the backend may join levels through.
239    ///
240    /// # Errors
241    ///
242    /// [`WalkabilityError::ConflictingConnector`] when one object is given
243    /// two kinds.
244    pub fn with_connectors(
245        mut self,
246        mut connectors: Vec<VerticalConnector>,
247    ) -> Result<Self, WalkabilityError> {
248        connectors.sort();
249        connectors.dedup();
250        if connectors
251            .windows(2)
252            .any(|pair| pair[0].object == pair[1].object)
253        {
254            return Err(WalkabilityError::ConflictingConnector);
255        }
256        self.connectors = connectors;
257        Ok(self)
258    }
259    pub fn connectors(&self) -> &[VerticalConnector] {
260        &self.connectors
261    }
262    pub fn surfaces(&self) -> &[ObjectId] {
263        &self.surfaces
264    }
265    pub fn entrances(&self) -> &[ObjectId] {
266        &self.entrances
267    }
268    pub fn obstacles(&self) -> &[ObjectId] {
269        &self.obstacles
270    }
271    pub fn minimum_width_metres(&self) -> f64 {
272        self.minimum_width
273    }
274    pub fn elevation_band(&self) -> Option<LengthInterval> {
275        self.elevation_band
276    }
277    pub fn traverses_verified_portals(&self) -> bool {
278        self.traverse_verified_portals
279    }
280    pub fn includes_motion_envelopes(&self) -> bool {
281        self.include_motion_envelopes
282    }
283}
284fn tolerance(metres: f64) -> Result<f64, WalkabilityError> {
285    if metres.is_finite() && metres >= 0.0 {
286        Ok(metres)
287    } else {
288        Err(WalkabilityError::InvalidTolerance)
289    }
290}
291/// What keeps a body from passing a stretch of one surface.
292#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
293pub enum StretchLimit {
294    /// The surface itself is too narrow there: its boundary alone stops the
295    /// body.
296    Narrow,
297    /// Obstacles standing on the floor, or swung doors, stop the body; the
298    /// surface alone would not.
299    Obstructed,
300    /// Obstacles hanging above the floor, inside the headroom band, stop
301    /// the body: without them it would not be stopped there.
302    Low,
303}
304impl StretchLimit {
305    pub fn as_str(self) -> &'static str {
306        match self {
307            Self::Narrow => "narrow",
308            Self::Obstructed => "obstructed",
309            Self::Low => "low",
310        }
311    }
312}
313/// Where, inside one walkable surface, a body of the request's width cannot
314/// pass, and why.
315///
316/// The backend proves the passage it marks too narrow; the position only
317/// locates it for a reviewer (where the parts it separates come closest, in
318/// the model's coordinates, at the floor's elevation) and is never a
319/// measurement. The obstacles are those found there that the limit depends
320/// on, possibly none.
321#[derive(Clone, Debug, PartialEq)]
322pub struct WalkableStretch {
323    surface: ObjectId,
324    limit: StretchLimit,
325    at: [f64; 3],
326    obstacles: Vec<ObjectId>,
327    headroom: Option<LengthInterval>,
328}
329impl WalkableStretch {
330    /// A stretch of `surface` at `at`.
331    ///
332    /// # Errors
333    ///
334    /// [`WalkabilityError::InvalidStretch`] when a coordinate is not finite.
335    pub fn try_new(
336        surface: ObjectId,
337        limit: StretchLimit,
338        at: [f64; 3],
339        mut obstacles: Vec<ObjectId>,
340    ) -> Result<Self, WalkabilityError> {
341        if at.iter().any(|value| !value.is_finite()) {
342            return Err(WalkabilityError::InvalidStretch);
343        }
344        obstacles.sort();
345        obstacles.dedup();
346        Ok(Self {
347            surface,
348            limit,
349            at,
350            obstacles,
351            headroom: None,
352        })
353    }
354    /// The headroom the obstacles of a [`StretchLimit::Low`] stretch leave
355    /// above the floor.
356    ///
357    /// # Errors
358    ///
359    /// [`WalkabilityError::InvalidStretch`] for any other limit.
360    pub fn with_headroom(mut self, headroom: LengthInterval) -> Result<Self, WalkabilityError> {
361        if self.limit != StretchLimit::Low {
362            return Err(WalkabilityError::InvalidStretch);
363        }
364        self.headroom = Some(headroom);
365        Ok(self)
366    }
367    pub fn surface(&self) -> &ObjectId {
368        &self.surface
369    }
370    pub fn limit(&self) -> StretchLimit {
371        self.limit
372    }
373    /// Where the stretch lies: plan position and floor elevation, metres.
374    pub fn at(&self) -> [f64; 3] {
375        self.at
376    }
377    /// The obstacles (or swept doors) the limit depends on, sorted.
378    pub fn obstacles(&self) -> &[ObjectId] {
379        &self.obstacles
380    }
381    pub fn headroom(&self) -> Option<LengthInterval> {
382        self.headroom
383    }
384}
385#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
386pub struct WalkabilityRegionId(String);
387impl WalkabilityRegionId {
388    pub fn new(value: impl Into<String>) -> Result<Self, WalkabilityError> {
389        let value = value.into();
390        if value.trim().is_empty() {
391            Err(WalkabilityError::InvalidRegionId)
392        } else {
393            Ok(Self(value))
394        }
395    }
396    pub fn as_str(&self) -> &str {
397        &self.0
398    }
399}
400#[derive(Clone, Debug, PartialEq)]
401pub struct WalkabilityRegion {
402    id: WalkabilityRegionId,
403    objects: Vec<ObjectId>,
404}
405impl WalkabilityRegion {
406    pub fn new(id: WalkabilityRegionId, mut objects: Vec<ObjectId>) -> Self {
407        objects.sort();
408        objects.dedup();
409        Self { id, objects }
410    }
411    pub fn id(&self) -> &WalkabilityRegionId {
412        &self.id
413    }
414    pub fn objects(&self) -> &[ObjectId] {
415        &self.objects
416    }
417}
418#[derive(Clone, Debug, PartialEq)]
419pub struct VerifiedWalkablePassage {
420    a: WalkabilityRegionId,
421    b: WalkabilityRegionId,
422    portal: Option<ObjectId>,
423    connector: Option<VerticalConnector>,
424    stretch: Option<WalkableStretch>,
425    clear_width: LengthInterval,
426    evidence: Evidence,
427}
428impl VerifiedWalkablePassage {
429    pub fn try_new(
430        mut a: WalkabilityRegionId,
431        mut b: WalkabilityRegionId,
432        portal: Option<ObjectId>,
433        clear_width: LengthInterval,
434        evidence: Evidence,
435    ) -> Result<Self, WalkabilityError> {
436        if a == b {
437            return Err(WalkabilityError::SelfPassage);
438        }
439        if !evidence.exact || evidence.locator.trim().is_empty() {
440            return Err(WalkabilityError::InexactPassage);
441        }
442        if b < a {
443            std::mem::swap(&mut a, &mut b);
444        }
445        Ok(Self {
446            a,
447            b,
448            portal,
449            connector: None,
450            stretch: None,
451            clear_width,
452            evidence,
453        })
454    }
455    /// Marks this passage as a climb through a vertical connector.
456    ///
457    /// # Errors
458    ///
459    /// [`WalkabilityError::PortalConnectorPassage`] when the passage already
460    /// crosses a portal.
461    pub fn with_connector(
462        mut self,
463        connector: VerticalConnector,
464    ) -> Result<Self, WalkabilityError> {
465        if self.portal.is_some() {
466            return Err(WalkabilityError::PortalConnectorPassage);
467        }
468        if self.stretch.is_some() {
469            return Err(WalkabilityError::InvalidStretch);
470        }
471        self.connector = Some(connector);
472        Ok(self)
473    }
474    /// Marks this passage as a stretch of one surface no body of the
475    /// request's width passes, located by `stretch`. The snapshot accepts
476    /// it only with an upper width bound below the request's width.
477    ///
478    /// # Errors
479    ///
480    /// [`WalkabilityError::InvalidStretch`] when the passage crosses a
481    /// portal or climbs a connector.
482    pub fn with_stretch(mut self, stretch: WalkableStretch) -> Result<Self, WalkabilityError> {
483        if self.portal.is_some() || self.connector.is_some() {
484            return Err(WalkabilityError::InvalidStretch);
485        }
486        self.stretch = Some(stretch);
487        Ok(self)
488    }
489    /// Where this passage is too narrow inside a surface, when it is.
490    pub fn stretch(&self) -> Option<&WalkableStretch> {
491        self.stretch.as_ref()
492    }
493    pub fn connector(&self) -> Option<&VerticalConnector> {
494        self.connector.as_ref()
495    }
496    pub fn endpoints(&self) -> (&WalkabilityRegionId, &WalkabilityRegionId) {
497        (&self.a, &self.b)
498    }
499    pub fn portal(&self) -> Option<&ObjectId> {
500        self.portal.as_ref()
501    }
502    pub fn clear_width(&self) -> LengthInterval {
503        self.clear_width
504    }
505    pub fn evidence(&self) -> &Evidence {
506        &self.evidence
507    }
508}
509#[derive(Clone, Debug, PartialEq)]
510pub struct WalkabilitySnapshot {
511    request: WalkabilityRequest,
512    regions: Vec<WalkabilityRegion>,
513    passages: Vec<VerifiedWalkablePassage>,
514    object_regions: BTreeMap<ObjectId, Vec<WalkabilityRegionId>>,
515    evidence: Evidence,
516}
517impl WalkabilitySnapshot {
518    pub fn try_new(
519        request: WalkabilityRequest,
520        mut regions: Vec<WalkabilityRegion>,
521        mut passages: Vec<VerifiedWalkablePassage>,
522        evidence: Evidence,
523    ) -> Result<Self, WalkabilityError> {
524        if !evidence.exact || evidence.locator.trim().is_empty() {
525            return Err(WalkabilityError::IncompleteEvidence);
526        }
527        regions.sort_by(|a, b| a.id.cmp(&b.id));
528        if regions.windows(2).any(|w| w[0].id == w[1].id) {
529            return Err(WalkabilityError::DuplicateRegion);
530        }
531        let ids: BTreeSet<_> = regions.iter().map(|r| r.id.clone()).collect();
532        if passages
533            .iter()
534            .any(|p| !ids.contains(&p.a) || !ids.contains(&p.b))
535        {
536            return Err(WalkabilityError::UnknownRegion);
537        }
538        let universe: BTreeSet<_> = request
539            .surfaces()
540            .iter()
541            .chain(request.entrances())
542            .chain(request.obstacles())
543            .chain(request.connectors().iter().map(VerticalConnector::object))
544            .cloned()
545            .collect();
546        if regions
547            .iter()
548            .flat_map(|region| region.objects.iter())
549            .any(|object| !universe.contains(object))
550        {
551            return Err(WalkabilityError::UnexpectedMappedObject);
552        }
553        if passages.iter().any(|passage| {
554            passage.portal.as_ref().is_some_and(|portal| {
555                !request.traverse_verified_portals
556                    || request.entrances.binary_search(portal).is_err()
557            })
558        }) {
559            return Err(WalkabilityError::ForbiddenPortalPassage);
560        }
561        if passages.iter().any(|passage| {
562            passage
563                .connector
564                .as_ref()
565                .is_some_and(|connector| request.connectors.binary_search(connector).is_err())
566        }) {
567            return Err(WalkabilityError::ForbiddenConnectorPassage);
568        }
569        let swept: BTreeSet<&ObjectId> = request.swept.iter().map(SweptDoor::door).collect();
570        if passages.iter().any(|passage| {
571            passage.stretch.as_ref().is_some_and(|stretch| {
572                passage.clear_width.upper_metres() >= request.minimum_width
573                    || request.surfaces.binary_search(&stretch.surface).is_err()
574                    || stretch.obstacles.iter().any(|object| {
575                        request.obstacles.binary_search(object).is_err() && !swept.contains(object)
576                    })
577            })
578        }) {
579            return Err(WalkabilityError::InvalidStretch);
580        }
581        let key = |p: &VerifiedWalkablePassage| {
582            (
583                p.a.clone(),
584                p.b.clone(),
585                p.portal.clone(),
586                p.connector.clone(),
587            )
588        };
589        passages.sort_by_key(key);
590        if passages
591            .windows(2)
592            .any(|window| key(&window[0]) == key(&window[1]))
593        {
594            return Err(WalkabilityError::DuplicatePassage);
595        }
596        let mut object_regions: BTreeMap<ObjectId, Vec<WalkabilityRegionId>> = BTreeMap::new();
597        for region in &regions {
598            for object in &region.objects {
599                object_regions
600                    .entry(object.clone())
601                    .or_default()
602                    .push(region.id.clone());
603            }
604        }
605        for mapped in object_regions.values_mut() {
606            mapped.sort();
607            mapped.dedup();
608        }
609        Ok(Self {
610            request,
611            regions,
612            passages,
613            object_regions,
614            evidence,
615        })
616    }
617    pub fn request(&self) -> &WalkabilityRequest {
618        &self.request
619    }
620    pub fn regions(&self) -> &[WalkabilityRegion] {
621        &self.regions
622    }
623    pub fn passages(&self) -> &[VerifiedWalkablePassage] {
624        &self.passages
625    }
626    pub fn evidence(&self) -> &Evidence {
627        &self.evidence
628    }
629    pub fn route_between(
630        &self,
631        from: &ObjectId,
632        to: &ObjectId,
633    ) -> Result<WalkabilityRouteOutcome, WalkabilityError> {
634        self.route_between_avoiding(from, to, &[])
635    }
636    /// Routes as [`Self::route_between`], but never through a passage whose
637    /// vertical connector is of a `forbidden` kind. Forbidding
638    /// [`VerticalConnectorKind::Stair`] makes a stairs-only connection
639    /// unreachable.
640    ///
641    /// # Errors
642    ///
643    /// [`WalkabilityError::ObjectUnavailable`] when an endpoint is mapped to
644    /// no region.
645    pub fn route_between_avoiding(
646        &self,
647        from: &ObjectId,
648        to: &ObjectId,
649        forbidden: &[VerticalConnectorKind],
650    ) -> Result<WalkabilityRouteOutcome, WalkabilityError> {
651        self.route_between_admitting(from, to, |passage| {
652            if passage
653                .connector
654                .as_ref()
655                .is_some_and(|connector| forbidden.contains(&connector.kind))
656            {
657                PassageAdmission::Refused
658            } else {
659                PassageAdmission::Admitted
660            }
661        })
662    }
663    /// Routes as [`Self::route_between`], with a rule's own judgement of
664    /// each passage on top of the width: the definite graph keeps only
665    /// passages it [admits](PassageAdmission::Admitted), the possible graph
666    /// drops only those it [refuses](PassageAdmission::Refused). A rule
667    /// that cannot decide a passage (a door whose required width it cannot
668    /// read) can therefore only turn a verdict `Indeterminate`.
669    ///
670    /// # Errors
671    ///
672    /// [`WalkabilityError::ObjectUnavailable`] when an endpoint is mapped to
673    /// no region.
674    pub fn route_between_admitting(
675        &self,
676        from: &ObjectId,
677        to: &ObjectId,
678        admit: impl Fn(&VerifiedWalkablePassage) -> PassageAdmission,
679    ) -> Result<WalkabilityRouteOutcome, WalkabilityError> {
680        let (starts, goals) = self.endpoints(from, to)?;
681        if let Some(path) = self.path(starts, goals, false, &admit) {
682            return Ok(WalkabilityRouteOutcome::Reachable(path));
683        }
684        if self.path(starts, goals, true, &admit).is_some() {
685            Ok(WalkabilityRouteOutcome::Indeterminate)
686        } else {
687            Ok(WalkabilityRouteOutcome::Unreachable)
688        }
689    }
690    /// The passages that block every route from `from` to `to`: those
691    /// leaving the regions the possible graph reaches from `from` (too
692    /// narrow, or refused by `admit`) towards a region from which `to` can
693    /// be reached, widths and admission ignored, without re-entering them.
694    /// Empty when a possible route exists, and when nothing joins the two
695    /// at all.
696    ///
697    /// Any route, once it last leaves the reached regions, crosses a
698    /// returned passage: they form a cut, ordered as the snapshot orders
699    /// its passages. A block that only guards some other region is left
700    /// out.
701    ///
702    /// # Errors
703    ///
704    /// [`WalkabilityError::ObjectUnavailable`] when an endpoint is mapped to
705    /// no region.
706    pub fn blocking_passages(
707        &self,
708        from: &ObjectId,
709        to: &ObjectId,
710        admit: impl Fn(&VerifiedWalkablePassage) -> PassageAdmission,
711    ) -> Result<Vec<&VerifiedWalkablePassage>, WalkabilityError> {
712        let (starts, goals) = self.endpoints(from, to)?;
713        let reached = self.reach(starts, |edge| self.usable(edge, true, &admit));
714        if goals.iter().any(|goal| reached.contains(goal)) {
715            return Ok(Vec::new());
716        }
717        // Regions the goal reaches, ignoring widths and admission, without
718        // passing through the reached side: a block behind another
719        // reached region is not this goal's.
720        let graph = self.graph(|_| true);
721        let mut leads_to_goal: BTreeSet<WalkabilityRegionId> = goals.iter().cloned().collect();
722        let mut queue: VecDeque<WalkabilityRegionId> = leads_to_goal.iter().cloned().collect();
723        while let Some(node) = queue.pop_front() {
724            for next in graph.get(&node).into_iter().flatten() {
725                if !reached.contains(next) && leads_to_goal.insert(next.clone()) {
726                    queue.push_back(next.clone());
727                }
728            }
729        }
730        Ok(self
731            .passages
732            .iter()
733            .filter(|edge| {
734                let (a, b) = (reached.contains(&edge.a), reached.contains(&edge.b));
735                (a && !b && leads_to_goal.contains(&edge.b))
736                    || (b && !a && leads_to_goal.contains(&edge.a))
737            })
738            .collect())
739    }
740    fn endpoints(
741        &self,
742        from: &ObjectId,
743        to: &ObjectId,
744    ) -> Result<(&[WalkabilityRegionId], &[WalkabilityRegionId]), WalkabilityError> {
745        let starts = self
746            .object_regions
747            .get(from)
748            .ok_or(WalkabilityError::ObjectUnavailable)?;
749        let goals = self
750            .object_regions
751            .get(to)
752            .ok_or(WalkabilityError::ObjectUnavailable)?;
753        Ok((starts, goals))
754    }
755    /// Whether `edge` belongs to the possible or the definite graph.
756    fn usable(
757        &self,
758        edge: &VerifiedWalkablePassage,
759        possible: bool,
760        admit: &impl Fn(&VerifiedWalkablePassage) -> PassageAdmission,
761    ) -> bool {
762        let minimum = self.request.minimum_width;
763        if possible {
764            edge.clear_width.upper_metres() >= minimum && admit(edge) != PassageAdmission::Refused
765        } else {
766            edge.clear_width.lower_metres() >= minimum && admit(edge) == PassageAdmission::Admitted
767        }
768    }
769    /// Every region reachable from `starts` through passages `keep` keeps.
770    fn reach(
771        &self,
772        starts: &[WalkabilityRegionId],
773        keep: impl Fn(&VerifiedWalkablePassage) -> bool,
774    ) -> BTreeSet<WalkabilityRegionId> {
775        let graph = self.graph(keep);
776        let mut seen: BTreeSet<WalkabilityRegionId> = starts.iter().cloned().collect();
777        let mut queue: VecDeque<WalkabilityRegionId> = seen.iter().cloned().collect();
778        while let Some(node) = queue.pop_front() {
779            for next in graph.get(&node).into_iter().flatten() {
780                if seen.insert(next.clone()) {
781                    queue.push_back(next.clone());
782                }
783            }
784        }
785        seen
786    }
787    fn graph(
788        &self,
789        keep: impl Fn(&VerifiedWalkablePassage) -> bool,
790    ) -> BTreeMap<WalkabilityRegionId, Vec<WalkabilityRegionId>> {
791        let mut graph: BTreeMap<WalkabilityRegionId, Vec<WalkabilityRegionId>> = BTreeMap::new();
792        for edge in self.passages.iter().filter(|edge| keep(edge)) {
793            graph
794                .entry(edge.a.clone())
795                .or_default()
796                .push(edge.b.clone());
797            graph
798                .entry(edge.b.clone())
799                .or_default()
800                .push(edge.a.clone());
801        }
802        for neighbors in graph.values_mut() {
803            neighbors.sort();
804            neighbors.dedup();
805        }
806        graph
807    }
808    fn path(
809        &self,
810        starts: &[WalkabilityRegionId],
811        goals: &[WalkabilityRegionId],
812        possible: bool,
813        admit: &impl Fn(&VerifiedWalkablePassage) -> PassageAdmission,
814    ) -> Option<Vec<WalkabilityRegionId>> {
815        let graph = self.graph(|edge| self.usable(edge, possible, admit));
816        let goal_set: BTreeSet<_> = goals.iter().cloned().collect();
817        let mut queue = VecDeque::new();
818        let mut parent: BTreeMap<WalkabilityRegionId, Option<WalkabilityRegionId>> =
819            BTreeMap::new();
820        for start in starts {
821            if parent.insert(start.clone(), None).is_none() {
822                queue.push_back(start.clone());
823            }
824        }
825        while let Some(node) = queue.pop_front() {
826            if goal_set.contains(&node) {
827                let mut path = vec![node.clone()];
828                let mut cursor = node;
829                while let Some(Some(prev)) = parent.get(&cursor) {
830                    path.push(prev.clone());
831                    cursor = prev.clone();
832                }
833                path.reverse();
834                return Some(path);
835            }
836            for next in graph.get(&node).into_iter().flatten() {
837                if !parent.contains_key(next) {
838                    parent.insert(next.clone(), Some(node.clone()));
839                    queue.push_back(next.clone());
840                }
841            }
842        }
843        None
844    }
845}
846/// A rule's judgement of one passage, on top of its width (see
847/// [`WalkabilitySnapshot::route_between_admitting`]).
848#[derive(Clone, Copy, Debug, Eq, PartialEq)]
849pub enum PassageAdmission {
850    /// The rule accepts the passage.
851    Admitted,
852    /// The rule cannot decide: the passage stays possible, never definite.
853    Undecided,
854    /// The rule rejects the passage: it is in neither graph.
855    Refused,
856}
857#[derive(Clone, Debug, PartialEq)]
858pub enum WalkabilityRouteOutcome {
859    Reachable(Vec<WalkabilityRegionId>),
860    Unreachable,
861    Indeterminate,
862}
863pub trait WalkabilityService: Send + Sync {
864    fn snapshot(
865        &self,
866        request: &WalkabilityRequest,
867    ) -> Result<WalkabilitySnapshot, WalkabilityError>;
868}
869#[derive(Clone)]
870pub struct WalkabilityServiceHandle(Arc<dyn WalkabilityService>);
871impl WalkabilityServiceHandle {
872    pub fn new(service: Arc<dyn WalkabilityService>) -> Self {
873        Self(service)
874    }
875    pub fn snapshot(
876        &self,
877        request: &WalkabilityRequest,
878    ) -> Result<WalkabilitySnapshot, WalkabilityError> {
879        let snapshot = self.0.snapshot(request)?;
880        if snapshot.request() != request {
881            return Err(WalkabilityError::ResponseRequestMismatch);
882        }
883        Ok(snapshot)
884    }
885    pub fn register(self, services: &mut ServiceRegistry) -> Result<(), ServiceRegistryError> {
886        services.register(self)
887    }
888}