2026-04-25 14:33:11 -04:00

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
}
}