214 lines
5.9 KiB
Rust
214 lines
5.9 KiB
Rust
//! Parcel data model and subdivision/edit entry points (spec §3, §4).
|
|
//!
|
|
//! A [`Parcel`] is a simple polygon with exactly one frontage edge
|
|
//! coincident with a road segment (invariant **I2**). Parcels are
|
|
//! grouped by their containing block (face), and the whole crate's
|
|
//! public output is a [`ParcelSet`].
|
|
//!
|
|
//! Subdivision and edit operations are exposed via the free
|
|
//! functions [`subdivide_all`] and [`apply_road_edit`].
|
|
|
|
pub mod classify;
|
|
pub mod deform;
|
|
pub mod regularize;
|
|
pub mod subdivide;
|
|
|
|
use std::collections::HashMap;
|
|
use std::fmt;
|
|
|
|
use glam::DVec2;
|
|
use slotmap::{new_key_type, SlotMap};
|
|
|
|
use crate::geometry::Polygon;
|
|
use crate::network::graph::FaceId;
|
|
use crate::network::RoadId;
|
|
|
|
pub use deform::{apply_road_edit, EditOutcome, RoadEdit};
|
|
pub use subdivide::{subdivide_all, subdivide_all_with_stats, SubdivisionStats};
|
|
|
|
new_key_type! {
|
|
/// Stable identifier for a [`Parcel`].
|
|
pub struct ParcelId;
|
|
}
|
|
|
|
/// Kind of a parcel boundary edge (spec §3.2).
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
|
pub enum EdgeKind {
|
|
/// Edge lies coincident with a road segment.
|
|
Frontage,
|
|
/// Edge is adjacent to the frontage edge in the parcel ring.
|
|
Side,
|
|
/// All other edges.
|
|
Back,
|
|
}
|
|
|
|
/// Trait that downstream code implements for buildings to participate
|
|
/// in deform-time fit checks (spec §4.6).
|
|
pub trait BuildingFitCheck: 'static {
|
|
/// True iff the building still fits inside `parcel`.
|
|
fn fits_in(&self, parcel: &Parcel) -> bool;
|
|
}
|
|
|
|
/// Owning wrapper around a `BuildingFitCheck`. Stored on parcels via
|
|
/// `Option<BuildingHandle>`.
|
|
///
|
|
/// Note: the spec describes `BuildingHandle` as "opaque". This crate
|
|
/// implements it as a heap-allocated trait object so that the
|
|
/// deformation pipeline can call `fits_in` without taking a callback
|
|
/// from the user. Recorded as revision §11 (R1).
|
|
pub struct BuildingHandle {
|
|
inner: Box<dyn BuildingFitCheck>,
|
|
}
|
|
|
|
impl BuildingHandle {
|
|
/// Wrap a building implementation.
|
|
#[must_use]
|
|
pub fn new<B: BuildingFitCheck>(b: B) -> Self {
|
|
Self { inner: Box::new(b) }
|
|
}
|
|
|
|
/// Forward to the inner [`BuildingFitCheck::fits_in`].
|
|
#[must_use]
|
|
pub fn fits_in(&self, parcel: &Parcel) -> bool {
|
|
self.inner.fits_in(parcel)
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for BuildingHandle {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.debug_struct("BuildingHandle").finish_non_exhaustive()
|
|
}
|
|
}
|
|
|
|
/// A single parcel.
|
|
#[derive(Debug)]
|
|
pub struct Parcel {
|
|
pub(crate) polygon: Polygon,
|
|
pub(crate) edge_kinds: Vec<EdgeKind>,
|
|
pub(crate) frontage_road: RoadId,
|
|
pub(crate) frontage_edge_index: usize,
|
|
pub(crate) block: FaceId,
|
|
pub(crate) building: Option<BuildingHandle>,
|
|
}
|
|
|
|
impl Parcel {
|
|
/// Vertex ring of the parcel polygon (CCW, not closed).
|
|
#[must_use]
|
|
pub fn vertices(&self) -> &[DVec2] {
|
|
self.polygon.vertices()
|
|
}
|
|
|
|
/// Polygon view.
|
|
#[must_use]
|
|
pub fn polygon(&self) -> &Polygon {
|
|
&self.polygon
|
|
}
|
|
|
|
/// Edge classification, parallel to `vertices()`.
|
|
#[must_use]
|
|
pub fn edge_kinds(&self) -> &[EdgeKind] {
|
|
&self.edge_kinds
|
|
}
|
|
|
|
/// The single road this parcel faces (invariant **I2**).
|
|
#[must_use]
|
|
pub fn frontage_road(&self) -> RoadId {
|
|
self.frontage_road
|
|
}
|
|
|
|
/// Endpoints of the frontage edge.
|
|
#[must_use]
|
|
pub fn frontage_edge(&self) -> (DVec2, DVec2) {
|
|
let v = self.polygon.vertices();
|
|
let i = self.frontage_edge_index;
|
|
(v[i], v[(i + 1) % v.len()])
|
|
}
|
|
|
|
/// Frontage length, meters.
|
|
#[must_use]
|
|
pub fn frontage_length(&self) -> f64 {
|
|
let (a, b) = self.frontage_edge();
|
|
(b - a).length()
|
|
}
|
|
|
|
/// Parcel area, m².
|
|
#[must_use]
|
|
pub fn area(&self) -> f64 {
|
|
self.polygon.area()
|
|
}
|
|
|
|
/// Attach a building. Returns the previously-attached building, if
|
|
/// any.
|
|
pub fn attach_building(&mut self, b: BuildingHandle) -> Option<BuildingHandle> {
|
|
self.building.replace(b)
|
|
}
|
|
|
|
/// Detach (and return) the currently attached building, if any.
|
|
pub fn detach_building(&mut self) -> Option<BuildingHandle> {
|
|
self.building.take()
|
|
}
|
|
|
|
/// Whether a building is currently attached.
|
|
#[must_use]
|
|
pub fn has_building(&self) -> bool {
|
|
self.building.is_some()
|
|
}
|
|
}
|
|
|
|
/// The output of `subdivide_all`: a stable-ID indexed collection of
|
|
/// parcels, organized by their containing block.
|
|
#[derive(Debug, Default)]
|
|
pub struct ParcelSet {
|
|
pub(crate) parcels: SlotMap<ParcelId, Parcel>,
|
|
pub(crate) by_block: HashMap<FaceId, Vec<ParcelId>>,
|
|
pub(crate) by_road: HashMap<RoadId, Vec<ParcelId>>,
|
|
}
|
|
|
|
impl ParcelSet {
|
|
/// Number of parcels.
|
|
#[must_use]
|
|
pub fn len(&self) -> usize {
|
|
self.parcels.len()
|
|
}
|
|
|
|
/// True if there are no parcels.
|
|
#[must_use]
|
|
pub fn is_empty(&self) -> bool {
|
|
self.parcels.is_empty()
|
|
}
|
|
|
|
/// Iterate over `(ParcelId, &Parcel)` pairs.
|
|
pub fn iter(&self) -> impl Iterator<Item = (ParcelId, &Parcel)> {
|
|
self.parcels.iter()
|
|
}
|
|
|
|
/// Look up a parcel by id.
|
|
#[must_use]
|
|
pub fn get(&self, id: ParcelId) -> Option<&Parcel> {
|
|
self.parcels.get(id)
|
|
}
|
|
|
|
/// Look up a parcel mutably by id.
|
|
pub fn get_mut(&mut self, id: ParcelId) -> Option<&mut Parcel> {
|
|
self.parcels.get_mut(id)
|
|
}
|
|
|
|
/// Iterate over the parcel ids within a single road's frontage.
|
|
pub fn parcels_on_road(&self, road: RoadId) -> impl Iterator<Item = ParcelId> + '_ {
|
|
self.by_road
|
|
.get(&road)
|
|
.into_iter()
|
|
.flat_map(|v| v.iter().copied())
|
|
}
|
|
|
|
pub(crate) fn insert(&mut self, parcel: Parcel) -> ParcelId {
|
|
let block = parcel.block;
|
|
let road = parcel.frontage_road;
|
|
let id = self.parcels.insert(parcel);
|
|
self.by_block.entry(block).or_default().push(id);
|
|
self.by_road.entry(road).or_default().push(id);
|
|
id
|
|
}
|
|
}
|