Historia-Urbis/design.tex
Dane Sabo 0a029fe471 Doc restructure: split into design.tex (contract) and journal.tex (record)
design.tex carries the contract — invariants, algorithms, public API,
roadmap, the canonical D1..D19 decision index, and a new System
Walkthrough (§17) that derives the geometry from first principles for
a graduate-engineering reader: vector rotations and the inward-normal
formula, the shoelace signed area, segment intersection via Cramer,
Sutherland–Hodgman half-plane clipping, the DCEL next-pointer rule
with worked square example, per-edge depth-cap ray-cast, corner-parcel
construction in two flavors, frontage walk, the shared-vertex
registry, propose-then-apply deform, and end-to-end traces for both
subdivide_all and apply_road_edit(MoveNode).

journal.tex carries the live record — Self-Decisions Checklist at the
top, sessions 1-4 rewritten condensed with explanatory Rust snippets,
and a Spec Deviations Log indexed chronologically.

Makefile builds both PDFs (`make all`, `make design`, `make journal`,
`make watch-design`, `make watch-journal`, `make figs`).

Old single-file journal content lives in git history per user.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 13:19:03 -04:00

1388 lines
81 KiB
TeX

\documentclass[11pt,letterpaper]{article}
% ---- Packages ----
\usepackage[margin=1in]{geometry}
\usepackage{amsmath,amssymb,amsthm}
\usepackage{mathtools}
\usepackage{graphicx}
\usepackage{tikz}
\usetikzlibrary{calc,arrows.meta,positioning,shapes.geometric,decorations.pathmorphing}
\usepackage{booktabs}
\usepackage{longtable}
\usepackage{enumitem}
\usepackage{listings}
\usepackage{xcolor}
\usepackage{hyperref}
\usepackage{cleveref}
\usepackage{fancyhdr}
\usepackage{titlesec}
\usepackage{tcolorbox}
\tcbuselibrary{breakable, skins}
% ---- Code listing style for Rust ----
\definecolor{rustbg}{RGB}{248,248,248}
\definecolor{rustkw}{RGB}{175,0,75}
\definecolor{ruststr}{RGB}{0,128,0}
\definecolor{rustcom}{RGB}{120,120,120}
\definecolor{rusttype}{RGB}{0,75,150}
\lstdefinelanguage{Rust}{
keywords={fn,let,mut,pub,struct,enum,impl,trait,use,mod,as,async,await,return,if,else,match,for,while,loop,break,continue,in,where,move,ref,self,Self,crate,super,extern,unsafe,const,static,type,dyn,box},
keywordstyle=\color{rustkw}\bfseries,
ndkeywords={Vec,Result,Option,Some,None,Ok,Err,String,bool,u8,u16,u32,u64,usize,i8,i16,i32,i64,isize,f32,f64,DVec2,DMat2,HashMap,SlotMap,VertexId,ParcelId,RoadId,NodeId,HalfEdgeId,FaceId,Polygon,Parcel,ParcelSet,RoadGraph,Block,EdgeKind,SubdivisionParams,RoadEdit,EditOutcome},
ndkeywordstyle=\color{rusttype}\bfseries,
sensitive=true,
comment=[l]{//},
morecomment=[s]{/*}{*/},
commentstyle=\color{rustcom}\itshape,
string=[b]",
stringstyle=\color{ruststr},
morestring=[b]'
}
\lstset{
language=Rust,
basicstyle=\ttfamily\small,
backgroundcolor=\color{rustbg},
frame=single,
framesep=4pt,
rulecolor=\color{gray!40},
numbers=left,
numberstyle=\tiny\color{gray},
numbersep=8pt,
showstringspaces=false,
breaklines=true,
breakatwhitespace=true,
tabsize=4,
captionpos=b,
}
% ---- Custom environments ----
\newtcolorbox{invariant}[1][Invariant]{
colback=blue!4,
colframe=blue!50!black,
fonttitle=\bfseries,
title={#1},
breakable,
}
\newtcolorbox{decision}[1][Design Decision]{
colback=green!4,
colframe=green!50!black,
fonttitle=\bfseries,
title={#1},
breakable,
}
\newtcolorbox{openq}[1][Open Question]{
colback=orange!4,
colframe=orange!60!black,
fonttitle=\bfseries,
title={#1},
breakable,
}
\newtcolorbox{cccontract}[1][Claude Code Contract]{
colback=gray!5,
colframe=gray!50!black,
fonttitle=\bfseries,
title={#1},
breakable,
}
\newtcolorbox{milestonebox}[1][Milestone]{
colback=violet!4,
colframe=violet!60!black,
fonttitle=\bfseries,
title={#1},
breakable,
}
% ---- Header / footer ----
\pagestyle{fancy}
\fancyhf{}
\fancyhead[L]{\small Road Parceling System}
\fancyhead[R]{\small Design Document}
\fancyfoot[C]{\thepage}
% ---- Title block ----
\title{\textbf{Road Parceling System} \\ \large Design Document}
\author{Dane Sabo \\ {\small (with Claude Code)}}
\date{Started 2026-04-25}
\hypersetup{
colorlinks=true,
linkcolor=blue!50!black,
urlcolor=blue!50!black,
citecolor=blue!50!black,
pdftitle={Road Parceling System --- Design Document},
pdfauthor={Dane Sabo}
}
\begin{document}
\maketitle
\thispagestyle{empty}
\begin{abstract}
\noindent
This is the \emph{contract} for the road parceling system --- the
foundational geometric layer of a city simulation game where parcels
are arbitrary polygons drawn outward from road frontage rather than
cells in a fixed grid. It defines invariants, algorithms, the public
API, the testing strategy, and the milestone roadmap; it includes a
first-principles walkthrough of how the implementation works
(\cref{sec:walkthrough}) so a reader can understand the geometry
without spelunking through source. The companion \texttt{journal.tex}
is the running log of work, decisions, and deviations.
\end{abstract}
\tableofcontents
\newpage
% =======================================================
% PART I --- CONTRACT
% =======================================================
\section{Project Context and Motivation}
\label{sec:context}
\subsection{Why Build This}
Modern city simulation games suffer from two architectural choices that compound poorly. First, they tend toward rigid grid-based or cell-based zoning, which produces visually uniform cities that diverge from how real urban form develops parcel by parcel along road frontage. Second, they over-rely on bottom-up agent simulation: every citizen is an autonomous decision-maker rerolling actions on every tick. This scales badly --- \emph{Cities: Skylines II} is the canonical example of a game shipped with simulation costs that do not survive contact with a real player's city.
This project addresses the first problem directly: a parcel-based zoning model where parcels are arbitrary polygons drawn outward from roads, with user-configurable depth and frontage. The simulation architecture (which addresses the second problem via aggregate / hazard-rate modeling rather than per-agent rerolls) is documented separately and consumes the parcel system as a foundational layer.
\subsection{Scope of This Document}
This is the design document for the road parceling system: a pure-logic Rust crate with no rendering engine, no game loop, and no simulation behavior. Downstream systems --- buildings, zoning types, population dynamics, transit --- consume this crate's API but are out of scope here. \Cref{sec:roadmap} sketches what comes after the parceling crate is done.
\subsection{Audience}
The primary audience is future-me. The secondary audience is an autonomous coding agent (Claude Code) that implements the spec laid out in \cref{sec:contract}. Sections marked as \emph{Claude Code Contract} are written to be acted upon directly. \Cref{sec:walkthrough} is written for a graduate-level engineering reader who knows linear algebra and basic algorithmics but is not a computational-geometry specialist; it derives the geometric machinery from first principles.
% =======================================================
\section{Core Invariants}
\label{sec:invariants}
These are the load-bearing properties of the system. Every public function must preserve them, and every test suite must verify them after every operation. They are stated here once and referenced by number throughout the rest of the document.
\begin{invariant}[I1: Polygon validity]
Every parcel is a simple polygon: no self-intersections, no holes, vertices ordered counter-clockwise. No edge has length less than $\varepsilon_{\text{geom}}$. No three consecutive vertices are collinear within $\varepsilon_{\text{angle}}$.
\end{invariant}
\begin{invariant}[I2: Single frontage]
Each parcel has exactly one edge classified as \texttt{EdgeKind::Frontage}, lying coincident (within $\varepsilon_{\text{geom}}$) with a road segment in the network.
\end{invariant}
\begin{invariant}[I3: Non-overlap]
For any two parcels $P_i, P_j$ within the same block, the area of their interior intersection is zero within $\varepsilon_{\text{area}}$.
\end{invariant}
\begin{invariant}[I4: Edit persistence]
When a road edit modifies a segment $s$, parcels with frontage on $s$ recompute only their frontage edge. Non-frontage edges are preserved unless explicit geometric thresholds (\cref{sec:edit-handling}) force regeneration.
\end{invariant}
\begin{invariant}[I5: No degenerate output]
The public API never returns parcels violating I1--I3. Inputs that would produce such parcels are either gracefully merged with neighbors, regularized, or rejected with a typed error. The library never panics on invalid input.
\end{invariant}
\begin{invariant}[I6: Edit determinism]
Applying the same \texttt{RoadEdit} to the same \texttt{ParcelSet} twice produces identical output, byte-for-byte (modulo opaque IDs).
\end{invariant}
\begin{invariant}[I7: Edit reversibility]
Applying an edit and then its inverse restores the original parcel set within $\varepsilon_{\text{geom}}$ for all preserved parcels. Condemned parcels are not restored; this is a known asymmetry and acceptable. (See journal entry on minimum-change deformation: a centroid-bounded reading of I7 is what the implementation actually delivers.)
\end{invariant}
\begin{invariant}[I8: Shared-vertex consistency]
Two parcels whose polygons would coincide on a vertex hold the same \texttt{VertexId} for that point. \texttt{ParcelSet::move\_vertex} writes through to every referrer simultaneously, so adjacent parcels' shared boundaries cannot drift apart under repeated edits.
\end{invariant}
The numerical tolerances are crate-wide constants:
\begin{align*}
\varepsilon_{\text{geom}} &= 10^{-6} \text{ m} \\
\varepsilon_{\text{area}} &= 10^{-9} \text{ m}^2 \\
\varepsilon_{\text{angle}} &= 10^{-4} \text{ rad}
\end{align*}
% =======================================================
\section{Geometric Foundations}
\label{sec:geometry}
\subsection{Coordinate System and Numerical Type}
All geometry is 2D, in a flat Euclidean plane with units of meters. Coordinates use \texttt{glam::DVec2} (double-precision) throughout. Single-precision \texttt{Vec2} is rejected because parcel offset operations on long road segments accumulate error rapidly at \texttt{f32} resolution; at city scales of $10^4$\,m, an \texttt{f32} mantissa gives roughly $10^{-3}$\,m precision, which is insufficient for the cleanup passes described in \cref{sec:regularization}.
\subsection{Road Network as a Planar Graph}
The road network is represented as a planar graph $G = (V, E)$ where vertices are intersections and edges are road segments. We use a half-edge / DCEL (doubly-connected edge list) representation because it provides $O(1)$ access to:
\begin{itemize}[noitemsep]
\item the next edge around a face (block boundary traversal),
\item the twin edge across a road (parcels on the other side),
\item the edges incident to a vertex (intersection topology).
\end{itemize}
Faces of the planar graph correspond to blocks. The unbounded exterior face is excluded from subdivision. The DCEL construction algorithm and the next-pointer rule are derived from first principles in \cref{sec:walkthrough-dcel}.
\subsection{Block Extraction}
A block is a closed face of the planar graph other than the unbounded exterior. Extraction proceeds by:
\begin{enumerate}[noitemsep]
\item Identifying all faces of the DCEL via half-edge traversal.
\item Computing the signed area of each face; the unique face with negative area (under CCW convention) is the exterior.
\item Returning the remaining faces as block boundaries.
\end{enumerate}
\subsection{Inward Offsetting}
Given a block boundary $B$ as a CCW polygon and a setback distance $d_s$, the developable polygon $B'$ is the inward offset of $B$ by $d_s$. For convex inputs this is exact; for concave inputs it is the intersection of inward half-planes (a conservative approximation that may shave concave outer-corner detail but never produces an invalid polygon).
The offset can fail in two ways:
\begin{enumerate}[noitemsep]
\item For very narrow blocks, $B' = \emptyset$. The block is then unbuildable and produces zero parcels.
\item For non-convex blocks, the offset may produce multiple disjoint polygons. Each component is subdivided independently.
\end{enumerate}
% =======================================================
\section{Subdivision Algorithm}
\label{sec:subdivision}
\subsection{Frontage-First Subdivision}
The primary algorithm subdivides a block by walking along its road-facing boundary in increments of approximately the target frontage width, extruding perpendicular into the block interior. This produces parcels that face their road, which is the desired aesthetic and the realistic outcome.
\paragraph{Inputs.} A block boundary $B$ (CCW), the developable polygon $B' = \text{offset}_{-d_s}(B)$, and parameters:
\[
\theta = (w_f, \sigma_f, d_p, \sigma_d, \rho, w_{\min}, A_{\min}, \text{seed})
\]
where $w_f$ is target frontage width, $\sigma_f$ is frontage variance, $d_p$ is target depth, $\sigma_d$ is depth variance, $\rho \in [0, 1]$ is the regularity slider, $w_{\min}$ is minimum frontage, and $A_{\min}$ is minimum area.
\paragraph{Procedure (high level).}
\begin{enumerate}
\item Identify ``real corners'' of the block boundary --- vertices where the underlying road graph node has degree $\geq 3$, or vertices where the block boundary turns sharply enough to warrant a corner parcel rather than a continuous frontage walk. Acute corners (interior $< 60^\circ$) are flagged separately.
\item At each real corner, build a corner parcel: an axis-aligned (or skewed for non-90° corners) rectangle of dimensions $R$ by depth, anchored at the corner vertex, with frontage on the longer of the two adjacent block edges.
\item For each block boundary edge, walk the segment between the corner footprints (if any) at arc-length intervals $w_i = w_f + \sigma_f \cdot \xi_i$ where $\xi_i \sim \text{Uniform}(-1, 1)$ from a deterministic per-road RNG.
\item At each split point, extrude perpendicular into the block by a depth bounded by a per-edge ray-cast cap (half the perpendicular distance from the edge midpoint to the nearest other block edge).
\item Form quadrilateral parcels from consecutive split points and their extrusions.
\item Clip each parcel polygon against the block boundary's inward half-planes to ensure no parcel can extend past the block.
\item Reject parcels with frontage $< w_{\min}$ or area $< A_{\min}$.
\end{enumerate}
The algorithmic details, including the corner classification math, the ray-cast depth cap formulation, and the bisector-clip fallback for acute corners, are derived in \cref{sec:walkthrough-subdivision}.
\subsection{Edge Classification}
After subdivision, each parcel edge is classified:
\begin{table}[h]
\centering
\begin{tabular}{lll}
\toprule
\textbf{Kind} & \textbf{Definition} & \textbf{Color (figures)} \\
\midrule
\texttt{Frontage} & Lies within $\varepsilon_{\text{geom}}$ of a road segment & Blue \\
\texttt{Side} & Adjacent to the frontage edge in the polygon ring & Gray \\
\texttt{Back} & All other edges & Light gray, dashed \\
\bottomrule
\end{tabular}
\caption{Edge classification scheme.}
\end{table}
For non-quadrilateral parcels (e.g.\ pie slices in cul-de-sacs, sliver-merged parcels with extra vertices), the back classification absorbs all non-frontage, non-side edges.
\subsection{Regularization Pass}
\label{sec:regularization}
When $\rho > 0$, a regularization pass runs after subdivision. For each parcel:
\begin{enumerate}[noitemsep]
\item Compute the OBB (oriented bounding box) of the parcel, oriented to the frontage edge.
\item Linearly interpolate side-edge vertices toward their OBB-snapped positions with weight $\rho$.
\item Validate the result against I1; if validation fails, revert.
\end{enumerate}
At $\rho = 1$, parcels are forced to perfect rectangles aligned to their road. At $\rho = 0$, parcels carry whatever shape the raw subdivision produced. Intermediate values give partial cleanup, useful for cities where some neighborhoods should look planned and others organic.
% =======================================================
\section{Road Edit Handling}
\label{sec:edit-handling}
The most distinctive feature of this system is parcel persistence under road edits. Conventional city builders nuke and re-create parcels (and their buildings) when roads change. Here, parcels survive whenever geometrically reasonable, and shared boundary vertices update through the registry so adjacent parcels stay in lockstep.
\subsection{Edit Types}
\begin{lstlisting}[caption={Road edit enum.}]
pub enum RoadEdit {
MoveNode { node: NodeId, to: DVec2 },
SplitSegment { road: RoadId, at: DVec2 },
DeleteSegment { road: RoadId },
InsertSegment { from: NodeId, to: NodeId },
}
\end{lstlisting}
\subsection{Deformation Pipeline}
When \texttt{apply\_road\_edit} is invoked:
\begin{enumerate}
\item \textbf{Snapshot the graph} pre-edit so deform can re-project parcel frontages from the old geometry onto the new.
\item \textbf{Apply the topology mutation} and rebuild the DCEL.
\item \textbf{Identify affected parcels}: those with frontage on a modified road.
\item \textbf{Propose} a vertex-move set per affected parcel (no in-place mutation yet). Each parcel is categorized:
\begin{itemize}[noitemsep]
\item \emph{Untouched}: the road's \emph{line} didn't change (only its endpoints shifted along it) and the parcel's frontage is still inside the new segment. Skip.
\item \emph{Deformed}: project the frontage endpoints onto the new road via the original parameter mapping; validate against rotation/area thresholds.
\item \emph{Condemned}: validation fails (frontage too short, area too small, no longer on road).
\item \emph{Regenerate}: rotation threshold exceeded; the affected block is re-subdivided.
\end{itemize}
\item \textbf{Apply} all proposed vertex moves through \texttt{ParcelSet::move\_vertex}, which writes through the shared-vertex registry. Adjacent parcels' shared boundaries move atomically (I8).
\item \textbf{Building eviction}: for each surviving parcel with an attached building, call \texttt{BuildingFitCheck::fits\_in}; on \texttt{false} the building is evicted but the parcel survives.
\item \textbf{Drop} condemned parcels.
\item \textbf{Regenerate} marked blocks.
\end{enumerate}
The full derivation, including the parameter projection and the line-unchanged check, lives in \cref{sec:walkthrough-edits}.
\subsection{Regeneration Thresholds}
Deformation triggers regeneration of the affected block when any of the following hold for the deformed parcel:
\begin{table}[h]
\centering
\begin{tabular}{ll}
\toprule
\textbf{Condition} & \textbf{Outcome} \\
\midrule
Frontage length $< w_{\min}$ & Condemned \\
Side edge rotated $> \alpha_{\max}$ from original & Regenerated \\
Polygon self-intersects & Regenerated \\
Area $< A_{\min}$ & Condemned \\
Frontage no longer adjacent to any road & Condemned \\
\bottomrule
\end{tabular}
\caption{Thresholds that trigger regeneration or condemnation. $\alpha_{\max}$ defaults to $30^\circ$.}
\end{table}
\subsection{Building Footprint Preservation}
Parcels carry an opaque \texttt{Option<BuildingHandle>}. The crate does not define what a building is, but exposes a hook:
\begin{lstlisting}[caption={Building persistence hook.}]
pub trait BuildingFitCheck {
/// Returns true if the building still fits inside the deformed parcel.
fn fits_in(&self, parcel: &Parcel) -> bool;
}
\end{lstlisting}
If a building's \texttt{fits\_in} returns false during deformation, the parcel survives but its building is evicted. This is the key behavior that distinguishes the system from CS2's nuke-on-edit approach.
% =======================================================
\section{Degenerate Cases}
\label{sec:degenerate}
The correctness of this system is largely defined by how it handles degenerate inputs. Each case below has a named test in the suite.
\begin{longtable}{p{4.5cm}p{6cm}p{3.5cm}}
\toprule
\textbf{Test name} & \textbf{Scenario} & \textbf{Expected behavior} \\
\midrule
\endhead
\texttt{acute\_intersection\_15deg} & Two roads meet at $15^\circ$ & Sliver merged or rejected; I1--I3 hold \\
\texttt{acute\_intersection\_5deg} & Knife-edge angle & No panic; typed error or valid output \\
\texttt{colinear\_roads} & Two segments end-to-end, zero turn & Treated as one continuous frontage \\
\texttt{zero\_length\_segment} & Coincident endpoints & Returns \texttt{InvalidParams} or skips \\
\texttt{near\_duplicate\_nodes} & Nodes within $\varepsilon$ of each other & Merged or typed error \\
\texttt{self\_intersecting\_graph} & Roads cross with no node & Returns \texttt{NonPlanarGraph} \\
\texttt{cul\_de\_sac} & Single road into a bulb & Pie-slice parcels tile the bulb \\
\texttt{t\_intersection} & Standard T & All three blocks subdivide \\
\texttt{y\_intersection} & Three roads at $120^\circ$ & Corner parcels handled \\
\texttt{tiny\_block} & Perimeter $< 4 w_{\min}$ & 0 or 1 parcel; never invalid \\
\texttt{huge\_block} & 1\,km $\times$ 1\,km block & Sane parcel count; no explosion \\
\texttt{curved\_road\_high\_curv} & Road radius $< d_p$ & No self-intersection \\
\texttt{road\_edit\_micro\_move} & Move node by 0.01\,m & All parcels deformed; none regen \\
\texttt{road\_edit\_large\_move} & Move node by 50\,m & Mix of deformed/regen/condemned \\
\texttt{road\_edit\_inverse\_restores} & Apply edit then inverse & State matches initial within $\varepsilon$ (centroid-bounded post-D14) \\
\texttt{road\_delete\_condemns} & Delete a road segment & All frontage parcels condemned \\
\texttt{road\_split\_preserves} & Split segment with new node & Parcels deform; none regenerated \\
\texttt{building\_footprint\_persists} & Stub building, deform parcel & Building kept iff \texttt{fits\_in} true \\
\texttt{degenerate\_isolated\_node} & Graph node with no edges & Skipped; no panic \\
\texttt{disconnected\_graph} & Two components & Each subdivides independently \\
\texttt{numerical\_precision\_stress} & Coords near $10^{20}$ & I1--I3 still hold \\
\bottomrule
\caption{Required degenerate-case tests. Each must exist by name and pass.}
\label{tab:degenerate}
\end{longtable}
% =======================================================
\section{Crate Architecture}
\label{sec:architecture}
\subsection{Module Layout}
\begin{lstlisting}[language=, caption={Crate structure.}]
road_parceling/
|-- Cargo.toml
|-- src/
| |-- lib.rs // public API
| |-- geometry/
| | |-- polygon.rs // polygon ops, validation
| | |-- offset.rs // road edge offsetting
| | `-- skeleton.rs // straight skeleton (M0.5+)
| |-- network/
| | |-- graph.rs // DCEL road graph
| | `-- blocks.rs // block extraction
| |-- parcel/
| | |-- subdivide.rs // frontage-first subdivision
| | |-- classify.rs // edge classification
| | |-- deform.rs // deformation under edits
| | `-- regularize.rs // OBB snapping
| |-- config.rs // SubdivisionParams
| |-- error.rs // typed errors
| `-- viz/svg.rs // SVG output (feature-gated)
|-- tests/
|-- examples/
|-- benches/
`-- figures/ // generated SVG/PDF artifacts
\end{lstlisting}
\subsection{Public API Surface}
\begin{lstlisting}[caption={Public API in \texttt{lib.rs}.}]
pub use config::SubdivisionParams;
pub use error::{ParcelError, SubdivisionError};
pub use network::{RoadGraph, RoadId, NodeId};
pub use parcel::{
Parcel, ParcelId, ParcelSet, EdgeKind, VertexId,
BuildingFitCheck, BuildingHandle,
RoadEdit, EditOutcome,
SubdivisionStats,
};
pub fn subdivide_all(
graph: &RoadGraph,
params: &SubdivisionParams,
) -> Result<ParcelSet, SubdivisionError>;
pub fn subdivide_all_with_stats(
graph: &RoadGraph,
params: &SubdivisionParams,
) -> Result<(ParcelSet, SubdivisionStats), SubdivisionError>;
pub fn apply_road_edit(
parcels: &mut ParcelSet,
graph: &mut RoadGraph,
edit: RoadEdit,
params: &SubdivisionParams,
) -> Result<EditOutcome, ParcelError>;
pub struct EditOutcome {
pub deformed: Vec<ParcelId>,
pub regenerated: Vec<ParcelId>,
pub condemned: Vec<ParcelId>,
pub created: Vec<ParcelId>,
pub evicted_buildings: Vec<ParcelId>,
}
\end{lstlisting}
\subsection{Error Types}
All fallible operations return \texttt{Result}. No panics in library code outside of \texttt{debug\_assert!}.
\begin{lstlisting}[caption={Error enum.}]
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum SubdivisionError {
#[error("road graph is not planar at node {0:?}")]
NonPlanarGraph(NodeId),
#[error("block boundary is not closed")]
OpenBlock,
#[error("subdivision parameters invalid: {0}")]
InvalidParams(String),
#[error("geometric operation failed: {0}")]
GeometryFailure(String),
#[error("feature not yet implemented: {0}")]
Unimplemented(&'static str),
}
\end{lstlisting}
\subsection{Dependencies}
\begin{table}[h]
\centering
\begin{tabular}{lll}
\toprule
\textbf{Crate} & \textbf{Version} & \textbf{Purpose} \\
\midrule
\texttt{geo} & 0.28 & Polygon primitives, boolean ops (added in M0.5) \\
\texttt{glam} & 0.29 & \texttt{DVec2} math \\
\texttt{slotmap} & 1 & Stable IDs for graph entities \\
\texttt{thiserror} & 2 & Error types \\
\texttt{rand} & 0.8 & Deterministic RNG \\
\texttt{rand\_chacha} & 0.3 & Reproducible RNG backend \\
\texttt{svg} & 0.18 & SVG output (feature \texttt{viz}) \\
\texttt{serde} & 1 & Serialization (feature \texttt{serde}) \\
\midrule
\texttt{proptest} & 1 & Property-based testing (dev) \\
\texttt{insta} & 1 & Snapshot testing (dev) \\
\texttt{criterion} & 0.5 & Benchmarking (dev) \\
\bottomrule
\end{tabular}
\caption{Dependency manifest.}
\end{table}
% =======================================================
\section{Idiomatic Rust Requirements}
\label{sec:idioms}
The bar is high; this is a foundational crate that downstream code will depend on for years.
\begin{itemize}
\item No \texttt{unwrap()} or \texttt{expect()} outside tests and examples.
\item No \texttt{unsafe} without a \texttt{// SAFETY:} comment. None is expected.
\item Newtype IDs (\texttt{ParcelId}, \texttt{RoadId}, \texttt{NodeId}, \texttt{VertexId}); never expose raw indices.
\item \texttt{\#[must\_use]} on builders and on \texttt{EditOutcome}.
\item Iterator-first APIs where allocation is avoidable.
\item Borrowing over cloning; parcels and graphs are large.
\item \texttt{\#[non\_exhaustive]} on public enums likely to grow.
\item \texttt{cargo clippy --all-targets --all-features -- -D warnings} clean (\texttt{clippy::all} group; pedantic stays off, see D8).
\item \texttt{cargo fmt --check} clean.
\item \texttt{\#![deny(missing\_docs)]} at crate root; all public items documented.
\item Module-level docs at the top of each \texttt{mod.rs}, with example snippets where useful.
\item Feature flags: \texttt{serde}, \texttt{viz}.
\end{itemize}
% =======================================================
\section{Testing Strategy}
\label{sec:testing}
\subsection{Three Layers}
\paragraph{Unit tests} live in-module under \texttt{\#[cfg(test)]}. Every non-trivial geometric helper is tested directly.
\paragraph{Integration tests} live in \texttt{tests/}. They build a road graph, subdivide, and check invariants.
\paragraph{Property tests} use \texttt{proptest}. For each invariant I1--I8, a property test generates random valid road graphs and asserts the invariant. Generators produce graphs with 2--20 nodes, varying segment lengths, intersection angles in $[30^\circ, 150^\circ]$, and occasional degeneracies.
\subsection{Snapshot Testing}
Each example scenario in \texttt{examples/} renders to SVG and snapshots via \texttt{insta}. Visual regressions are caught when the SVG diff changes. Baseline SVGs are committed.
\subsection{Coverage Requirement}
Every named test in \cref{tab:degenerate} must exist and pass. There is no acceptable substitute. As of M0.4 all 21 are active.
% =======================================================
\section{Visualization and Figures}
\label{sec:figures}
The crate produces SVG output via the \texttt{viz} feature. A dedicated example, \texttt{generate\_figures}, regenerates every figure referenced in this document.
\subsection{Required Figures}
\begin{longtable}{ll}
\toprule
\textbf{Filename} & \textbf{Content} \\
\midrule
\endhead
\texttt{fig\_01\_grid\_block.svg} & Rectangular block subdivided \\
\texttt{fig\_02\_curved\_road.svg} & Parcels on a curved frontage \\
\texttt{fig\_03\_cul\_de\_sac.svg} & Pie-slice parcels around a bulb \\
\texttt{fig\_04\_y\_intersection.svg} & Three-way intersection corner lots \\
\texttt{fig\_05\_acute\_corner.svg} & Sliver-merge at sharp angle \\
\texttt{fig\_06a\_road\_edit\_before.svg} & Scene before road move \\
\texttt{fig\_06b\_road\_edit\_after.svg} & Same scene after; classes color-coded \\
\texttt{fig\_07\_regularity\_slider.svg} & $\rho \in \{0.0, 0.5, 1.0\}$ side by side \\
\texttt{plot\_subdivision\_perf.svg} & Criterion: parcels/s vs.\ block count \\
\texttt{plot\_parcel\_area\_hist.svg} & Histogram, 10k-parcel stress scene \\
\bottomrule
\caption{Required figure deliverables.}
\end{longtable}
\subsection{Color Conventions}
\begin{itemize}[noitemsep]
\item Roads: black, 2px stroke
\item Frontage edges: blue
\item Side edges: gray
\item Back edges: light gray, dashed
\item Parcel fill: pale yellow, 30\% opacity
\item Condemned parcels: red fill
\item Regenerated parcels: orange fill
\item Deformed parcels: green fill
\item Created parcels: blue fill
\end{itemize}
% =======================================================
\section{Performance Targets}
\label{sec:performance}
\begin{table}[h]
\centering
\begin{tabular}{lll}
\toprule
\textbf{Operation} & \textbf{Scale} & \textbf{Target (release)} \\
\midrule
\texttt{subdivide\_all} & 100 blocks & $< 50$ ms \\
\texttt{subdivide\_all} & 10\,000 blocks & $< 5$ s \\
\texttt{apply\_road\_edit} & 10k-parcel graph & $< 1$ ms per single-segment edit \\
\bottomrule
\end{tabular}
\caption{Performance targets. Measured via Criterion.}
\end{table}
The eventual interactive use case (M2 test harness, M3+ game) wants sub-millisecond response on typical edits so placing roads feels instant. Current measurements (M0.4): $\sim$0.4--0.7\,µs/parcel on M-series hardware, well under all targets.
% =======================================================
\section{Out of Scope}
\label{sec:oos}
Explicitly \emph{not} part of this milestone, to prevent scope creep:
\begin{itemize}[noitemsep]
\item Buildings (parcels carry an opaque handle; the crate does not define buildings).
\item Zoning types (residential / commercial / industrial). Parcels are typeless.
\item Population, agents, simulation tick logic.
\item Rendering beyond SVG export.
\item Game engine integration (Bevy, Godot).
\item 3D / terrain. Everything is 2D in a flat plane.
\item Persistence formats beyond optional \texttt{serde} derives.
\item Multi-threading. Single-threaded for the foundation; APIs designed not to preclude \texttt{Send}/\texttt{Sync} later.
\end{itemize}
% =======================================================
\section{Claude Code Contract}
\label{sec:contract}
This section is written to be acted on directly by an autonomous coding agent.
\begin{cccontract}[Working Style]
\begin{enumerate}
\item Work iteratively. Get a single rectangular block working end-to-end with tests and an SVG figure before adding complexity.
\item After each major feature, regenerate figures and verify by inspection.
\item Write tests \emph{before} fixing bugs. Every degenerate case starts as a failing test.
\item When a degenerate case is fundamentally unhandleable (e.g.\ truly self-intersecting input), the test asserts the correct typed error, not success.
\item Commit frequently with messages naming the feature or invariant addressed.
\item When a design decision has multiple reasonable answers, pick one, document it as a Design Decision in this document (\cref{sec:decisions}), and move on. Do not block.
\item If the spec is ambiguous or wrong, append a deviation note to the journal (\texttt{journal.tex}) listing the deviation. Resolve it later by updating this design document.
\end{enumerate}
\end{cccontract}
\begin{cccontract}[Definition of Done — Milestone 1]
The foundational parceling crate is complete (M1.0) when all of the following are simultaneously true:
\begin{enumerate}
\item \texttt{cargo build --all-features} succeeds with no warnings.
\item \texttt{cargo clippy --all-targets --all-features -- -D warnings} passes.
\item \texttt{cargo fmt --check} passes.
\item \texttt{cargo test --all-features} passes, including every named test in \cref{tab:degenerate}.
\item \texttt{cargo doc --all-features --no-deps} produces no warnings.
\item All figures listed in \cref{sec:figures} are generated and committed.
\item Performance targets in \cref{sec:performance} are met on a modern laptop.
\item This design document and the journal both compile with \texttt{make}.
\item OBB regularization actually does something at $\rho > 0$.
\end{enumerate}
\end{cccontract}
% =======================================================
% PART II --- LIVE PLANNING
% =======================================================
\section{Roadmap}
\label{sec:roadmap}
This section is the project's current direction. Updated as milestones land.
\begin{milestonebox}[M1 --- Foundational parceling crate]
\textbf{Goal.} \cref{sec:contract}'s Definition of Done.
\textbf{Status (2026-04-26).} $\sim$80\% complete. All 21 named tests active and passing as of M0.4; missing items are figures (\texttt{fig\_03\_cul\_de\_sac}, \texttt{fig\_05\_acute\_corner}, \texttt{plot\_subdivision\_perf}, \texttt{plot\_parcel\_area\_hist}) and a working OBB regularization. Slated to close at the end of M0.5.
\textbf{Sub-milestones.}
\begin{itemize}[noitemsep]
\item \textbf{M0.1} (DONE): Single-rectangle end-to-end. Crate compiles; SVG figure generated; 14 of 21 named tests passing.
\item \textbf{M0.2} (DONE): Corner parcels, sticky back edges (lite), preserve-on-deform, performance instrumentation, fig\_06a/b. 16 of 21 named tests.
\item \textbf{M0.3} (DONE): I3 fix at acute corners, minimum-change deformation, SplitSegment preserve, all 21 named tests active.
\item \textbf{M0.4} (DONE): Shared-vertex registry. No-drift contract under repeated edits.
\item \textbf{M0.5} (IN PROGRESS): Bulletproof overlap (rigorous polygon-polygon test, per-parcel depth caps, polygon-difference cleanup), Voronoi-experiment subdivision for intersections and cul-de-sacs, missing M1 figures, working OBB regularization. Closes M1.0.
\end{itemize}
\end{milestonebox}
\begin{milestonebox}[M2 --- Interactive test harness]
\textbf{Goal.} A clickable UI to place road nodes, drag roads, and see parcels regenerate live, so the API can be stress-tested by a human against arbitrary edits.
\textbf{Decision (M2 kick-off).} Implementation = a sibling Rust crate \texttt{road\_parceling\_studio/} using \texttt{egui} (immediate-mode UI). Builds for both native (fast iteration) and WASM (browser tab). Reasoning: keeps everything in Rust, no JS-side geometry duplication; \texttt{egui} is more productive for tooling than \texttt{bevy}.
\textbf{Out of scope.} Production game features (zoning, buildings, simulation). The harness is a developer/designer tool.
\end{milestonebox}
\begin{milestonebox}[M3+ --- TBD]
Candidates that come up in conversations:
\begin{itemize}[noitemsep]
\item Parcel \emph{merge} and \emph{split} operations (agent-driven). Vertex registry already supports the geometry; a parcel-layer DCEL with explicit edge identity is the next data-model upgrade.
\item Building system: a real \texttt{BuildingFitCheck} implementation, footprint preservation (Q4).
\item Game-engine integration (Bevy or similar).
\item Multi-threading: \texttt{subdivide\_all} parallelized per block.
\end{itemize}
\end{milestonebox}
% =======================================================
\section{Open Questions}
\label{sec:open}
A running list. Resolved questions migrate to \cref{sec:decisions} as Design Decisions and may also leave a note in the journal's session record where they were resolved.
\begin{openq}[Q1: Skeleton-based subdivision as a fallback]
Should the straight-skeleton-based subdivision algorithm be implemented as a fallback for blocks where frontage-first produces ugly results? Frontage-first handles 90\% of cases cleanly; skeleton handles irregular blocks better but adds significant complexity.
\textbf{Status.} Partially resolved (D4 commits to frontage-first as primary). The Voronoi-based experiment in M0.5 may serve the same role as the skeleton fallback; if it does, Q1 closes.
\end{openq}
\begin{openq}[Q2: Spatial index for affected-parcel lookup]
\texttt{apply\_road\_edit} needs to find all parcels with frontage on a given segment. Linear scan is $O(n)$ and fine for small cities; an R-tree or grid index becomes necessary at scale. When?
\textbf{Tentative.} Ship linear scan in M1 with a documented hot-path comment, swap in \texttt{rstar} when the benchmark in \cref{sec:performance} exceeds budget. The shared-vertex registry already uses a spatial hash for vertex lookup, so the scaffolding is partially in place.
\end{openq}
\begin{openq}[Q3: Block ownership of back edges]
For parcels facing roads on opposite sides of a block, who owns the back edge? Two options: (a) medial line, both deform symmetrically; (b) fixed at creation, one parcel grows while the other shrinks.
\textbf{Status.} Resolved (option a) for M0.1; per-edge ray-cast cap delivers symmetric extrusion. Revisited in M0.5 where per-parcel depth caps replace the per-edge variant for tighter no-overlap guarantees.
\end{openq}
\begin{openq}[Q4: Determinism of regeneration]
When a block is regenerated, the new parcel set differs from the old one. Should the regeneration be biased toward producing parcels that overlap maximally with the old ones, to preserve building footprints opportunistically?
\textbf{Tentative.} No for M1; revisit when the building system lands. The shared-vertex registry partially mitigates this --- vertices are reused on re-insertion at the same position.
\end{openq}
\begin{openq}[Q5: Voronoi vs.\ frontage-first at intersections]
M0.5 introduces an experimental Voronoi-based subdivision for intersection corners and cul-de-sac bulbs. Does it produce visually superior parcels? Should it become the default at high-degree intersections, or stay a fallback?
\textbf{Status.} Open; M0.5 implements it as an A/B-able alternative gated behind a \texttt{SubdivisionParams::corner\_method} flag.
\end{openq}
% =======================================================
\section{Design Decisions Index}
\label{sec:decisions}
Canonical record of every locked-in design choice, in order. The journal references these by D-number; this section is the authoritative copy.
\begin{decision}[D1, 2026-04-25 -- f64 throughout]
Use \texttt{glam::DVec2} (f64) crate-wide rather than \texttt{Vec2} (f32). Single-precision loses too much accuracy on offset operations at city scales. Cost: $\sim$2$\times$ memory for vertex storage. Worth it.
\end{decision}
\begin{decision}[D2, 2026-04-25 -- DCEL over adjacency list]
Half-edge / DCEL graph representation rather than an adjacency list. Block extraction (face traversal) is the dominant query and is $O(1)$ per step in DCEL. Cost: more complex insertion / deletion logic.
\end{decision}
\begin{decision}[D3, 2026-04-25 -- Parcels indexed by slotmap key]
\texttt{slotmap} for parcel storage rather than \texttt{Vec} indexing. Stable IDs are required for I4 (edit persistence): a parcel that survives a road edit must retain its identity for downstream consumers (buildings, agents).
\end{decision}
\begin{decision}[D4, 2026-04-25 -- Frontage-first as primary algorithm]
Implement frontage-first subdivision before any skeleton-based approach. Frontage-first handles the majority of real cases (rectangular blocks, gently curved roads, standard intersections). Skeleton-based subdivision is deferred (Q1).
\end{decision}
\begin{decision}[D5, 2026-04-25 -- BuildingHandle owns its BuildingFitCheck]
\texttt{BuildingHandle} wraps a \texttt{Box<dyn BuildingFitCheck>} so the deform pipeline can call \texttt{fits\_in} locally without forcing \texttt{apply\_road\_edit} to take a callback.
\end{decision}
\begin{decision}[D6, 2026-04-25 -- DCEL next/prev rule, explicit form]
\texttt{HalfEdge::next} = the \emph{predecessor} (CW neighbor) of \texttt{half\_edge.twin} in the target vertex's CCW-sorted outgoing list, with wrap. \texttt{HalfEdge::prev} = the twin of the \emph{successor} of the half-edge in its origin's list. The standard ``CCW after twin'' phrasing is ambiguous about rotation direction; this is the unambiguous form. (Derived in \cref{sec:walkthrough-dcel-next}.)
\end{decision}
\begin{decision}[D7, 2026-04-25 -- Polygon::new\_relaxed for block boundaries]
Block polygons may legitimately contain collinear-corner vertices (the \texttt{colinear\_roads} case). The strict \texttt{Polygon::new} rejects collinear triples; \texttt{new\_relaxed} skips that check. I1 applies to parcels, not blocks.
\end{decision}
\begin{decision}[D8, 2026-04-25 -- Clippy scope is all, not pedantic]
Crate enables \texttt{clippy::all} only. \texttt{pedantic} fights numerical-code conventions (single-letter coordinate names, struct-default reassignment) without buying real safety.
\end{decision}
\begin{decision}[D9, 2026-04-25 -- Build-first corner parcels]
At each real corner, the corner parcel is built before the frontage walk; the walk on each adjacent road then starts past the corner's footprint. (Voronoi delete-and-refill alternative considered and rejected: build-first has fewer edge-case surprises.)
\end{decision}
\begin{decision}[D10, 2026-04-25 -- Corner radius is the average frontage width]
Corner parcels extend $R = \texttt{params.frontage\_width}$ along the frontage-side adjacent road and \texttt{params.depth} along the other. 4-vertex parallelogram for 90° corners; 6-vertex L for $R \neq \texttt{depth}$ in some constructions.
\end{decision}
\begin{decision}[D11, 2026-04-25 -- ``Real corner'' definition]
Corner-parcel routine fires at any block-boundary vertex whose underlying graph node has degree $\geq 3$, \emph{or} degree 2 with a bend angle below $150^\circ$ (interior $> 60^\circ$). Acute corners are flagged separately and fall back to bisector-clip on adjacent regulars.
\end{decision}
\begin{decision}[D12, 2026-04-25 -- Road width deferred; setback is the placeholder]
Roads stay as zero-width centerlines through M1. The \texttt{setback} parameter folds in any visual road-thickness margin downstream consumers want. Revisit when a renderer needs literal road widths.
\end{decision}
\begin{decision}[D13, 2026-04-25 -- Sticky back edges, lite (now superseded by D17)]
Original M0.2 commitment: each parcel stores its full polygon in absolute world coords; deform pipeline only moves frontage vertices. Adjacent parcels' shared back edges happen to coincide. \emph{Superseded by D17}, which makes shared vertices explicit and atomic via the registry.
\end{decision}
\begin{decision}[D14, 2026-04-25 -- Minimum-change deformation (no-op preserve)]
When a road edit doesn't change a road's \emph{line} (only its endpoints shift along it), parcels whose frontage is still inside the new segment are reported as \texttt{Untouched} and skip the deformation. Trade-off: strict vertex-by-vertex inverse-restore is no longer guaranteed; centroid-bounded drift is the new contract.
\end{decision}
\begin{decision}[D15, 2026-04-25 -- Bisector-clip at acute corners, not obtuse]
Acute corners (interior $< 60^\circ$) get no corner parcel; instead, regular parcels along the two adjacent edges are bisector-clipped at the corner so their territories stay separated. Obtuse corners ($\geq 60^\circ$) keep their rectangle/parallelogram corner parcel and need no clip.
\end{decision}
\begin{decision}[D16, 2026-04-25 -- SplitSegment preserve on 4-vertex parcels]
On \texttt{SplitSegment}, parcels whose frontage is entirely on one side of the split point have their \texttt{frontage\_road} rebound (no geometric change). Parcels whose frontage spans the split are cut into two parcels along a perpendicular through the split point --- only for 4-vertex parcels; higher-vertex parcels fall back to Condemn. Buildings stay with the larger half.
\end{decision}
\begin{decision}[D17, 2026-04-25 -- Shared-vertex registry]
\texttt{ParcelSet} owns a \texttt{SlotMap<VertexId, VertexRecord>} plus a spatial-hash index. On parcel insertion, every polygon vertex is snapped to the registry --- existing matches within $\varepsilon_{\text{geom}}$ reuse the same \texttt{VertexId}; otherwise a new entry is created. Each \texttt{VertexRecord} carries a back-reference list of \texttt{(ParcelId, vertex\_index)} pairs. This is invariant I8.
\end{decision}
\begin{decision}[D18, 2026-04-25 -- move\_vertex write-through]
\texttt{ParcelSet::move\_vertex(vid, new\_pos)} updates the registry's stored position \emph{and} writes through to every referring parcel's polygon at the recorded index. Adjacent parcels' shared boundaries cannot drift.
\end{decision}
\begin{decision}[D19, 2026-04-25 -- Deform pipeline is propose-then-apply]
\texttt{deform\_parcel\_after\_road\_move} no longer mutates --- it returns a list of proposed \texttt{(VertexId, new\_pos)} moves. The outer loop validates each parcel, collects proposals, then applies them via \texttt{move\_vertex} after verdicts are in. Conflicting proposals on the same vertex are last-one-wins, but in practice the deform parameterization makes referrers agree by construction.
\end{decision}
% =======================================================
\section{System Walkthrough}
\label{sec:walkthrough}
This section is the ``how does this thing actually work'' tour. It is written for a graduate-level engineering reader who knows linear algebra and basic algorithmics but is not a computational-geometry specialist. Every formula is derived; every algorithmic step is justified; cross-references point to the file and function in the source where the math is implemented.
\subsection{Pipeline at 30,000 ft}
\label{sec:walkthrough-overview}
The crate consumes a planar road graph and produces a set of polygonal parcels with shared boundary vertices. The transformation is staged:
\begin{enumerate}[leftmargin=2em]
\item \textbf{Build the road graph topology.} Roads become a half-edge / DCEL data structure (\cref{sec:walkthrough-dcel}). Faces of the DCEL are blocks.
\item \textbf{Extract blocks.} Each bounded face becomes a \texttt{Block} polygon with metadata (\cref{sec:walkthrough-blocks}).
\item \textbf{Subdivide each block} into parcels via the frontage-first algorithm (\cref{sec:walkthrough-subdivision}).
\item \textbf{Snap vertices into the shared registry} so adjacent parcels share boundary points (\cref{sec:walkthrough-registry}).
\item \textbf{Edits} (move-node, split-segment, etc.) propose vertex moves; the outer pipeline applies them through the registry, validating each affected parcel (\cref{sec:walkthrough-edits}).
\end{enumerate}
The two end-to-end traces (\cref{sec:walkthrough-trace-subdivide,sec:walkthrough-trace-edit}) walk a single \texttt{subdivide\_all} call and a single \texttt{apply\_road\_edit(MoveNode)} call from the public API down to the geometric primitives.
\subsection{Mathematical primitives}
\label{sec:walkthrough-primitives}
We work in $\mathbb{R}^2$. Points are 2-vectors $\mathbf{p} = (p_x, p_y)$. The standard vector operations apply: addition, scaling, dot product $\mathbf{a} \cdot \mathbf{b} = a_x b_x + a_y b_y$, and the 2D ``cross product'' $\mathbf{a} \times \mathbf{b} = a_x b_y - a_y b_x$ which is the $z$-component of the 3D cross product (a scalar in 2D).
\subsubsection{Rotations}
\label{sec:walkthrough-rotations}
The standard 2D rotation matrix is
\[
R(\theta) = \begin{pmatrix} \cos\theta & -\sin\theta \\ \sin\theta & \cos\theta \end{pmatrix}.
\]
At $\theta = +\pi/2$ (a $90^\circ$ counter-clockwise rotation) this collapses to
\[
R(\pi/2) = \begin{pmatrix} 0 & -1 \\ 1 & 0 \end{pmatrix},
\]
so $R(\pi/2) \cdot (x, y)^T = (-y, x)^T$. Symbolically: \emph{rotate $90^\circ$ CCW takes $(x, y) \mapsto (-y, x)$}. Rotation by $-\pi/2$ (CW) is $(x, y) \mapsto (y, -x)$.
These two rotations are the crate's workhorses for computing perpendiculars. Given an edge direction $\mathbf{t} = \mathbf{p}_2 - \mathbf{p}_1$ (normalized), the inward normal of a CCW polygon at this edge is
\[
\mathbf{n}_{\text{in}} = R(\pi/2)\,\mathbf{t} = (-t_y, t_x).
\]
This is the direction pointing into the polygon's interior, because for a CCW polygon the interior lies to the left of each edge as you walk in the edge's direction, and ``left'' is exactly $R(\pi/2)$ applied to the forward direction.
In code (\texttt{src/parcel/subdivide.rs}, multiple sites):
\begin{lstlisting}
let edge_dir = (q - p) / (q - p).length();
let inward = DVec2::new(-edge_dir.y, edge_dir.x);
\end{lstlisting}
\subsubsection{Polygon orientation: the shoelace formula}
\label{sec:walkthrough-shoelace}
A polygon with vertices $\mathbf{p}_0, \mathbf{p}_1, \dots, \mathbf{p}_{n-1}$ has \emph{signed area}
\[
A = \frac{1}{2} \sum_{i=0}^{n-1} (x_i y_{i+1} - x_{i+1} y_i),
\]
where indices are taken modulo $n$. This is the \emph{shoelace formula}, named for the cross-multiplication pattern when you write the coordinates in two columns. The sign carries the orientation: $A > 0$ for CCW vertex order, $A < 0$ for CW.
\paragraph{Why this formula works.} Pick any reference point (call it the origin). For each edge $(\mathbf{p}_i, \mathbf{p}_{i+1})$, the triangle formed by origin, $\mathbf{p}_i$, and $\mathbf{p}_{i+1}$ has signed area $\frac{1}{2}(\mathbf{p}_i \times \mathbf{p}_{i+1}) = \frac{1}{2}(x_i y_{i+1} - x_{i+1} y_i)$ (the 2D cross product is twice the signed triangle area). Summing these triangles around a closed polygon, every interior region is covered the same net number of times by triangles of correct sign and zero net times by overlapping pieces of opposite sign --- the algebra works out so the sum equals the polygon's signed area. (This is a consequence of Stokes' theorem in 2D, but the elementary triangle-sum picture is enough here.)
We use this in \texttt{src/geometry/mod.rs}:
\begin{lstlisting}
pub fn signed_area(verts: &[DVec2]) -> f64 {
let n = verts.len();
if n < 3 { return 0.0; }
let mut a = 0.0;
for i in 0..n {
let p = verts[i];
let q = verts[(i + 1) % n];
a += p.x * q.y - q.x * p.y;
}
0.5 * a
}
\end{lstlisting}
\texttt{Polygon::new} uses the sign to detect CW input and flip it to CCW (so all internal code can assume CCW), and uses the magnitude to detect degenerate (zero-area) polygons.
\subsubsection{Segment intersection}
\label{sec:walkthrough-segint}
A segment from $\mathbf{p}_1$ to $\mathbf{p}_2$ is the set $\{\mathbf{p}_1 + t(\mathbf{p}_2 - \mathbf{p}_1) \mid t \in [0, 1]\}$. To find where two segments cross we solve the linear system
\[
\mathbf{p}_1 + t (\mathbf{p}_2 - \mathbf{p}_1) = \mathbf{p}_3 + s (\mathbf{p}_4 - \mathbf{p}_3)
\]
for $(t, s) \in [0,1]^2$. With $\mathbf{d}_1 = \mathbf{p}_2 - \mathbf{p}_1$ and $\mathbf{d}_2 = \mathbf{p}_4 - \mathbf{p}_3$, applying Cramer's rule gives
\[
t = \frac{(\mathbf{p}_3 - \mathbf{p}_1) \times \mathbf{d}_2}{\mathbf{d}_1 \times \mathbf{d}_2}, \qquad s = \frac{(\mathbf{p}_3 - \mathbf{p}_1) \times \mathbf{d}_1}{\mathbf{d}_1 \times \mathbf{d}_2}.
\]
The denominator $\mathbf{d}_1 \times \mathbf{d}_2$ is the determinant of the system; it vanishes iff the segments are parallel. The crate treats parallel segments as non-intersecting (they may overlap collinearly, but for our use cases that's not an interesting intersection).
\subsubsection{Polygon-vs-half-plane clipping (Sutherland--Hodgman)}
\label{sec:walkthrough-shclip}
Given a convex half-plane $H = \{\mathbf{p} \mid (\mathbf{p} - \mathbf{p}_0) \cdot \mathbf{n} \geq 0\}$ (where $\mathbf{n}$ is the inward normal and $\mathbf{p}_0$ is any point on the boundary line), and a polygon $P = (\mathbf{q}_0, \dots, \mathbf{q}_{m-1})$, the Sutherland--Hodgman algorithm computes $P \cap H$ in one pass:
\begin{enumerate}[leftmargin=2em]
\item Initialize an output list, empty.
\item For each edge $(\mathbf{q}_i, \mathbf{q}_{i+1})$ of $P$, classify both endpoints as \emph{inside} ($d \geq 0$) or \emph{outside} ($d < 0$) where $d = (\mathbf{q} - \mathbf{p}_0) \cdot \mathbf{n}$:
\begin{itemize}[noitemsep]
\item \emph{In--In}: append $\mathbf{q}_{i+1}$ to output.
\item \emph{In--Out}: compute the intersection of the edge with the half-plane boundary; append it to output. (No endpoint added.)
\item \emph{Out--In}: append the intersection, then $\mathbf{q}_{i+1}$.
\item \emph{Out--Out}: append nothing.
\end{itemize}
\item The output list is the clipped polygon (or empty if everything was outside).
\end{enumerate}
The intersection of edge $(\mathbf{a}, \mathbf{b})$ with the boundary line is a 1-D linear interpolation. Let $d_a = (\mathbf{a} - \mathbf{p}_0) \cdot \mathbf{n}$ and $d_b = (\mathbf{b} - \mathbf{p}_0) \cdot \mathbf{n}$. Then the intersection lies at
\[
t^* = \frac{d_a}{d_a - d_b}
\]
along the edge, giving point $\mathbf{a} + t^* (\mathbf{b} - \mathbf{a})$. (Derivation: the boundary is $\{\mathbf{p} \mid (\mathbf{p}-\mathbf{p}_0)\cdot\mathbf{n} = 0\}$. Substituting the parameterized edge and solving for $t$ yields the formula. The denominator vanishes only when $d_a = d_b$, which means the edge is parallel to the boundary --- a degenerate case we handle by returning ``no intersection''.)
\paragraph{Why this works for convex clip regions.} A convex region is the intersection of half-planes. If $H_1, \dots, H_k$ are the inward half-planes for an edge of the convex region, then $P \cap (H_1 \cap \dots \cap H_k) = (((P \cap H_1) \cap H_2) \cap \dots) \cap H_k$. Sutherland--Hodgman applies a sequence of half-plane clips, one per edge of the clip region. The crate uses this for two purposes: clipping a regular parcel against a corner bisector, and clipping every parcel against the block boundary's inward half-planes.
In code (\texttt{src/geometry/polygon.rs}, \texttt{Polygon::clip\_half\_plane}):
\begin{lstlisting}
pub fn clip_half_plane(&self, point: DVec2, inward_normal: DVec2) -> Option<Self> {
let n = inward_normal.normalize();
let inside = |p: DVec2| (p - point).dot(n) >= -EPS_GEOM;
let intersect = |a: DVec2, b: DVec2| -> Option<DVec2> {
let da = (a - point).dot(n);
let db = (b - point).dot(n);
let denom = da - db;
if denom.abs() < EPS_GEOM { return None; }
let t = da / denom;
Some(a + (b - a) * t)
};
// ... iterate edges, emit per the four-case rule above ...
}
\end{lstlisting}
\subsection{The road graph as a DCEL}
\label{sec:walkthrough-dcel}
\subsubsection{Why DCEL}
A planar graph drawn in the plane partitions the plane into \emph{faces} (the connected components of the complement of the edge set). The road graph's faces are exactly the city blocks plus one unbounded outer face. We need fast face queries: ``what's the boundary of this block?'', ``what's the block on the other side of this road?''.
A vanilla adjacency-list graph stores edges per vertex, which makes vertex-incidence and edge-existence cheap, but face traversal is $O(|E|)$ to discover faces from scratch each time. The Doubly-Connected Edge List (DCEL) augments the graph with explicit face structure. Every edge is split into two \emph{half-edges}, one for each direction, and pointer fields on each half-edge support face-walks in $O(\text{face perimeter})$ regardless of total graph size.
\subsubsection{Half-edge structure}
Each edge is represented as a pair of half-edges. A half-edge $h$ has:
\begin{itemize}[noitemsep]
\item \texttt{origin}: the vertex it leaves from.
\item \texttt{twin}: the half-edge for the same physical edge but opposite direction.
\item \texttt{next}: the next half-edge counterclockwise around the face on $h$'s left.
\item \texttt{prev}: the inverse of \texttt{next}.
\item \texttt{face}: the face $h$ bounds (on its left).
\end{itemize}
The convention we adopt: the face on a half-edge's \emph{left} (relative to the direction of travel) is the face that half-edge bounds. For a CCW polygon, walking around its boundary with the polygon's interior on your left, you traverse one half-edge per boundary edge in order.
\subsubsection{Constructing the DCEL: the next-pointer rule}
\label{sec:walkthrough-dcel-next}
Suppose we've created two half-edges per road, set up their \texttt{twin} pointers, and now want to fill in \texttt{next} so that following \texttt{next} from any half-edge walks the face on its left. The rule is most cleanly derived by asking: ``standing at the target vertex of $h$, with the face on $h$'s left, what's the next half-edge that bounds this same face?''
At a vertex $v$, the half-edges \emph{leaving} $v$ have specific compass-headings (angles from $v$ to their target). Sort them counter-clockwise by angle: in \texttt{src/network/graph.rs}, this is \texttt{sort\_outgoing\_by\_angle}, which uses \texttt{atan2(dir.y, dir.x)}.
Now consider half-edge $h$: $u \to v$. Its \emph{twin} $h^*$ leaves $v$ heading back toward $u$. In the CCW-sorted list of outgoing half-edges at $v$, $h^*$ sits at some position $k$. The face on $h$'s left is the face on $h^*$'s right, which is the face wedged between $h^*$ and the half-edge \emph{immediately clockwise} of $h^*$ in the angular ordering --- because the face wraps around $v$ between two consecutive outgoing half-edges. ``Immediately clockwise of $h^*$ in CCW-sorted order'' is the same as ``the predecessor of $h^*$ in the list, with wrap''.
So:
\begin{equation}
\boxed{\;\texttt{next}(h) = \text{predecessor of } \texttt{twin}(h) \text{ in } v\text{'s CCW-sorted outgoing list}, \text{ with wrap}\;}
\label{eq:dcel-next}
\end{equation}
where $v = $ target of $h$. The corresponding rule for \texttt{prev} is the dual:
\[
\texttt{prev}(h) = \texttt{twin}(\text{successor of } h \text{ in } u\text{'s CCW-sorted outgoing list}, \text{ with wrap})
\]
where $u = $ origin of $h$.
\paragraph{Worked example: square.} A unit square with vertices $A = (0,0)$, $B = (1,0)$, $C = (1,1)$, $D = (0,1)$ has 4 roads $AB, BC, CD, DA$ and 8 half-edges. At vertex $A$, the outgoing half-edges are $h_{AB}$ (heading $0^\circ$) and $h_{AD}$ (heading $90^\circ$). CCW-sorted: $[h_{AB}, h_{AD}]$.
Take $h_{DA}$: $D \to A$. Its twin is $h_{AD}$ ($A \to D$, heading $90^\circ$). At $A$, $h_{AD}$ is at position 1; its predecessor with wrap is at position 0 = $h_{AB}$. So $\texttt{next}(h_{DA}) = h_{AB}$. Continuing: $\texttt{next}(h_{AB}) = h_{BC}$, $\texttt{next}(h_{BC}) = h_{CD}$, $\texttt{next}(h_{CD}) = h_{DA}$, closing the cycle around the interior face.
\paragraph{Why the ``CCW after'' phrasing was ambiguous.} The literature sometimes states the rule as ``$\texttt{next}(h)$ is the half-edge immediately CCW after $\texttt{twin}(h)$''. The trap: ``after'' depends on which direction around $v$ you measure. CCW \emph{around the vertex} (= the angle ordering) gives \emph{successor} of twin; CCW \emph{around the face} (= the actual traversal we want) gives \emph{predecessor} of twin. The implementation gets this wrong once and you pay for it: the T-intersection unit test in M0.1 caught this when the T-stem face cycle enclosed the wrong region. (See journal session 1; locked in as D6.)
\subsubsection{Face extraction}
Once \texttt{next}/\texttt{prev}/\texttt{twin} are populated, every face is the cycle generated by repeatedly applying \texttt{next} from any one of its half-edges. Walk the cycle; the vertex positions trace out the face's boundary polygon. Use the shoelace formula (\cref{sec:walkthrough-shoelace}) on the vertex sequence: positive area = bounded face (a block), negative area = unbounded outer face (the exterior).
In \texttt{src/network/graph.rs}, \texttt{extract\_faces}:
\begin{lstlisting}
for h_start in edge_keys {
if self.half_edges[h_start].face.is_some() { continue; }
let mut cycle = Vec::new();
let mut h = h_start;
loop {
cycle.push(h);
let next = self.half_edges[h].next;
if next == h_start { break; }
h = next;
}
let pts: Vec<DVec2> = cycle.iter()
.map(|&hid| self.nodes[self.half_edges[hid].origin].pos)
.collect();
let signed = signed_area(&pts);
let face_id = self.faces.insert(Face {
boundary: h_start,
is_exterior: signed < 0.0 || signed.abs() < EPS_GEOM,
});
for h in cycle { self.half_edges[h].face = Some(face_id); }
}
\end{lstlisting}
\subsection{Block extraction}
\label{sec:walkthrough-blocks}
A \texttt{Block} is one bounded face of the road DCEL, packaged with metadata for subdivision. \texttt{src/network/blocks.rs}:
\begin{lstlisting}
pub struct Block {
pub(crate) face: FaceId,
pub polygon: Polygon, // CCW vertex ring
pub(crate) boundary_edges: Vec<HalfEdgeId>, // parallel to polygon vertices
pub roads: Vec<RoadId>, // parallel to polygon edges
}
\end{lstlisting}
The polygon is built from the face's vertex cycle; \texttt{Polygon::new\_relaxed} is used (not the strict \texttt{Polygon::new}) because block boundaries may legitimately contain collinear-corner vertices when adjacent road segments are end-to-end (the \texttt{colinear\_roads} case --- D7).
\subsection{Frontage-first subdivision}
\label{sec:walkthrough-subdivision}
Subdivision runs per block. The high-level flow:
\begin{enumerate}[leftmargin=2em]
\item Compute interior angles at each block-boundary vertex.
\item Classify each vertex: \emph{real corner} (build a corner parcel) vs. \emph{acute corner} (no corner parcel; bisector-clip adjacent regulars) vs. \emph{smooth continuation} (collinear; no corner treatment).
\item Compute per-edge depth caps (the maximum perpendicular extent of a parcel before it would hit the opposite side of the block).
\item For each real corner, build the corner parcel.
\item For each block boundary edge, walk the segment between corner footprints, place split points, and emit quad parcels.
\item Clip each parcel polygon against the block boundary's inward half-planes (defense in depth against any geometric oversight).
\end{enumerate}
\subsubsection{Interior angles and corner classification}
At vertex $v$ with neighbors $v_{\text{prev}}$ and $v_{\text{next}}$ in CCW boundary order, define
\[
\mathbf{t}_{\text{in}} = \frac{v_{\text{prev}} - v}{\|v_{\text{prev}} - v\|}, \qquad
\mathbf{t}_{\text{out}} = \frac{v_{\text{next}} - v}{\|v_{\text{next}} - v\|}.
\]
$\mathbf{t}_{\text{in}}$ points back along the previous edge; $\mathbf{t}_{\text{out}}$ points forward along the next edge. The \emph{arriving} direction (which is the previous edge's direction in CCW traversal) is $\mathbf{t}_{\text{arrive}} = -\mathbf{t}_{\text{in}}$.
The signed CCW turn from $\mathbf{t}_{\text{arrive}}$ to $\mathbf{t}_{\text{out}}$ is
\[
\theta_{\text{turn}} = \operatorname{atan2}(\mathbf{t}_{\text{arrive}} \times \mathbf{t}_{\text{out}}, \mathbf{t}_{\text{arrive}} \cdot \mathbf{t}_{\text{out}}).
\]
The interior angle at $v$ for a convex CCW polygon is then $\theta_{\text{int}} = \pi - \theta_{\text{turn}}$.
\paragraph{Classification rules.}
\begin{itemize}[noitemsep]
\item Underlying graph node has degree $\geq 3$, OR degree 2 with $\theta_{\text{int}} < 150^\circ$ ($\theta_{\text{turn}} > 30^\circ$): it's a real corner candidate.
\item Of those, $\theta_{\text{int}} > 60^\circ$\emph{obtuse real corner}: build a corner parcel.
\item $\theta_{\text{int}} \leq 60^\circ$\emph{acute corner}: no corner parcel, bisector-clip adjacent regulars.
\item Everything else (degree 2 with $\theta_{\text{int}} \geq 150^\circ$): smooth continuation, walk through.
\end{itemize}
(D11; the $60^\circ$ acute threshold was tuned empirically. Below it, the rectangle corner-parcel construction tries to extrude past the polygon boundary --- bisector-clip is the safer fallback.)
\subsubsection{Per-edge depth cap (ray-cast)}
\label{sec:walkthrough-depth-cap}
For each block boundary edge $e_i$, cast a ray from the edge midpoint $\mathbf{m}_i$ in the inward-normal direction $\mathbf{n}_i$ (\cref{sec:walkthrough-rotations}). The first intersection of this ray with another edge $e_j$ gives a distance $\rho_{ij}$. Define the per-edge depth cap as
\[
d_{\text{cap}}(e_i) = \frac{1}{2} \min_{j \neq i} \rho_{ij}.
\]
The factor of $\tfrac{1}{2}$ ensures parcels from $e_i$ extruding to $d_{\text{cap}}(e_i)$ and parcels from the closest opposite edge $e_j$ extruding to $d_{\text{cap}}(e_j)$ meet (or undershoot) at the midline between them, never overlap. (For convex blocks this is a tight bound under the assumption that the closest opposing edge remains the same across the entire span; for non-convex or strongly-tapered blocks, this is the milestone-0.5 weakness that we patch with per-parcel ray casts.)
Ray-segment intersection: parameterize the ray as $\mathbf{r}(t) = \mathbf{m}_i + t\mathbf{n}_i$ and the segment $e_j$ as $\mathbf{q}(s) = \mathbf{a} + s(\mathbf{b} - \mathbf{a})$ for $s \in [0, 1]$. Solving $\mathbf{r}(t) = \mathbf{q}(s)$ gives a $2 \times 2$ linear system whose solution (if the determinant is non-singular) yields $(t, s)$ via Cramer's rule. We accept solutions with $t > \varepsilon$ and $s \in [-\varepsilon, 1 + \varepsilon]$.
Code: \texttt{src/parcel/subdivide.rs}, \texttt{edge\_depth\_caps} and \texttt{ray\_segment\_distance}.
\subsubsection{Corner parcel construction}
\label{sec:walkthrough-corner}
At an obtuse real corner $v$, the corner parcel is a 4-vertex shape with corners derived as follows. Let $R = \texttt{params.frontage\_width}$ (the corner radius, D10) and $d = \texttt{params.depth}$.
We first decide which adjacent road wins frontage --- the longer one (with deterministic tie-break on \texttt{RoadId} bits). Two flavors:
\begin{itemize}[noitemsep]
\item \textbf{Frontage on next road.} Vertices: $v_0 = v$, $v_1 = v + R \mathbf{t}_{\text{out}}$, $v_2 = v_1 + d\mathbf{n}_{\text{out}}$, $v_3 = v + d \mathbf{t}_{\text{in}}$.
\item \textbf{Frontage on prev road.} Vertices: $v_0 = v$, $v_1 = v + d \mathbf{t}_{\text{out}}$, $v_2 = v_1 + R \mathbf{n}_{\text{out}}$, $v_3 = v + R \mathbf{t}_{\text{in}}$.
\end{itemize}
where $\mathbf{n}_{\text{out}} = R(\pi/2)\mathbf{t}_{\text{out}}$ is the inward normal of the next edge (\cref{sec:walkthrough-rotations}).
For a $90^\circ$ corner of a rectangle ($\mathbf{t}_{\text{in}} \perp \mathbf{t}_{\text{out}}$, with $\mathbf{n}_{\text{out}} = \mathbf{t}_{\text{in}}$), the corner parcel collapses to an axis-aligned $R \times d$ rectangle. For non-$90^\circ$ corners (e.g., the $120^\circ$ corner at the centre of a Y-intersection), it's a parallelogram.
The two flavors differ only in which side carries $R$ and which carries $d$; the geometry is otherwise symmetric. The frontage edge of the corner parcel is the one adjacent to the chosen winning road; \texttt{classify\_edges} (\texttt{src/parcel/classify.rs}) labels it \texttt{Frontage} and the rest \texttt{Side}/\texttt{Back}.
\subsubsection{The frontage walk}
Between corner footprints, regular parcels are emitted by walking the block edge at jittered intervals. For edge $e_i$ of length $L$ with start-consume $c_s$ (the corner footprint at the start vertex) and end-consume $c_e$ (at the end vertex), let $t_0 = c_s$ and $t_{\max} = L - c_e$. We generate split positions
\[
t_k = t_{k-1} + \max(w_{\min},\; w_f + \sigma_f \xi_k), \quad \xi_k \sim \text{Uniform}(-1, 1)
\]
from a per-road \texttt{ChaCha8Rng} until $t_k$ exceeds $t_{\max} - w_{\min}/2$, then append $t_{\max}$.
For each consecutive pair $(t_{k-1}, t_k)$, define
\[
\mathbf{p}_a = \mathbf{p}_{\text{start}} + t_{k-1} \mathbf{t}_{\text{edge}}, \qquad
\mathbf{p}_b = \mathbf{p}_{\text{start}} + t_k \mathbf{t}_{\text{edge}}.
\]
Pick a depth $d^* = \min(d_p + \sigma_d \eta_k,\; d_{\text{cap}}(e_i))$ from the same RNG, and form a quad parcel with vertices $(\mathbf{p}_a, \mathbf{p}_b, \mathbf{p}_b + d^* \mathbf{n}_{\text{in}}, \mathbf{p}_a + d^* \mathbf{n}_{\text{in}})$.
\subsubsection{Block-boundary defense clip}
After every parcel polygon is built (corner or regular), it is clipped against the block boundary's inward half-planes via Sutherland--Hodgman (\cref{sec:walkthrough-shclip}). For convex blocks this is a no-op for parcels generated correctly; for non-convex blocks (e.g., the Y-intersection sub-blocks, where the block triangle has acute outer corners) it ensures parcels can never extend outside their block face.
In code (\texttt{src/parcel/subdivide.rs}, \texttt{clip\_polygon\_to\_block}):
\begin{lstlisting}
fn clip_polygon_to_block(parcel: &Polygon, block: &Polygon) -> Option<Polygon> {
let mut current = parcel.clone();
let block_verts = block.vertices();
for i in 0..block_verts.len() {
let a = block_verts[i];
let b = block_verts[(i + 1) % block_verts.len()];
let edge = b - a;
let len = edge.length();
if len < EPS_GEOM { continue; }
let dir = edge / len;
let inward = DVec2::new(-dir.y, dir.x);
current = current.clip_half_plane(a, inward)?;
}
Some(current)
}
\end{lstlisting}
\subsection{Shared-vertex registry}
\label{sec:walkthrough-registry}
The registry is the data structure that prevents shared boundaries from drifting across edits. (D17, D18; invariant I8.)
\subsubsection{Data structure}
\begin{lstlisting}
pub struct ParcelSet {
parcels: SlotMap<ParcelId, Parcel>,
vertices: SlotMap<VertexId, VertexRecord>,
vertex_grid: HashMap<(i64, i64), Vec<VertexId>>,
// ...
}
struct VertexRecord {
pos: DVec2,
refs: Vec<(ParcelId, usize)>, // each parcel that references this vertex,
// and the index into that parcel's polygon ring.
}
\end{lstlisting}
The spatial grid is keyed at $\varepsilon_{\text{geom}}$ resolution: position $(x, y)$ maps to cell
\[
(\,\lfloor x / \varepsilon_{\text{geom}} + 0.5 \rfloor,\; \lfloor y / \varepsilon_{\text{geom}} + 0.5 \rfloor\,).
\]
Two positions within $\varepsilon_{\text{geom}}$ might fall into the same cell or into adjacent cells; lookup checks the cell and its 8 neighbors.
\subsubsection{Snap-on-insert}
When \texttt{ParcelSet::insert(parcel)} is called, every polygon vertex is passed through \texttt{find\_or\_create\_vertex(pos)}:
\begin{lstlisting}
fn find_or_create_vertex(&mut self, pos: DVec2) -> VertexId {
let key = vertex_key(pos);
for dx in -1..=1 {
for dy in -1..=1 {
let bucket = (key.0 + dx, key.1 + dy);
if let Some(ids) = self.vertex_grid.get(&bucket) {
for &vid in ids {
if let Some(rec) = self.vertices.get(vid) {
if (rec.pos - pos).length_squared() < EPS_GEOM * EPS_GEOM {
return vid;
}
}
}
}
}
}
let id = self.vertices.insert(VertexRecord { pos, refs: Vec::new() });
self.vertex_grid.entry(key).or_default().push(id);
id
}
\end{lstlisting}
The first hit (if any) within $\varepsilon_{\text{geom}}$ is reused; otherwise a new \texttt{VertexRecord} is created. The parcel's vertex-id list runs parallel to its polygon vertex list.
\subsubsection{Write-through propagation}
\texttt{ParcelSet::move\_vertex(vid, new\_pos)} updates the registry's stored position and walks every \texttt{(parcel\_id, vertex\_index)} reference to write the new position into the parcel polygon directly:
\begin{lstlisting}
pub fn move_vertex(&mut self, vid: VertexId, new_pos: DVec2) {
let refs = match self.vertices.get_mut(vid) {
Some(r) => { r.pos = new_pos; r.refs.clone() }
None => return,
};
for (pid, idx) in refs {
if let Some(p) = self.parcels.get_mut(pid) {
p.polygon.set_vertex_unchecked(idx, new_pos);
}
}
}
\end{lstlisting}
Validation (does the parcel's polygon stay simple after the move?) is the caller's responsibility, because validity depends on which combinations of vertices move together. The deform pipeline does this in its propose phase before any move is committed (\cref{sec:walkthrough-edits}).
\subsection{Edit pipeline}
\label{sec:walkthrough-edits}
\texttt{apply\_road\_edit(parcels, graph, edit, params)} is the single public entry for every road-network mutation. The pipeline is propose-then-apply (D19): no parcel is mutated until every affected parcel has been classified and its proposed vertex moves collected.
\subsubsection{Move-node case}
Given \texttt{RoadEdit::MoveNode \{ node, to \}}:
\begin{enumerate}[leftmargin=2em]
\item \textbf{Snapshot the graph} (\texttt{graph\_before = graph.clone()}). We need the old node positions to compute parcel-vertex parameters along the old roads.
\item \textbf{Apply the topology mutation} (move the node, re-run \texttt{rebuild\_topology}). The graph DCEL now reflects the new geometry.
\item \textbf{Identify incident roads} (those touching the moved node).
\item \textbf{For each parcel on each incident road}, run \texttt{deform\_parcel\_after\_road\_move(parcel, road, graph\_before, graph\_after, params)} which is a \emph{pure} function returning a verdict and (if Deformed) a list of proposed \texttt{(VertexId, new\_pos)} moves.
\end{enumerate}
The verdict logic:
\textbf{Untouched check (D14, line-unchanged).} Compute the road's old direction $\mathbf{d}_{\text{before}} = (\mathbf{p}_b^{\text{before}} - \mathbf{p}_a^{\text{before}})/L_{\text{before}}$ and new direction $\mathbf{d}_{\text{after}}$. The road's \emph{line} is unchanged iff:
\[
\big|\mathbf{d}_{\text{before}} \times \mathbf{d}_{\text{after}}\big| < \varepsilon_{\text{angle}}^{\text{small}}
\quad\text{and}\quad
\big|(\mathbf{p}_a^{\text{before}} - \mathbf{p}_a^{\text{after}}) \cdot \mathbf{n}_{\text{after}}\big| < \varepsilon_{\text{geom}}.
\]
The first condition says the directions are parallel; the second says the old start point lies on the new line. Combined, these say the line (as an infinite mathematical object) didn't change --- only the segment endpoints shifted along it. If additionally the parcel's frontage endpoints fall within the new segment's range $[0, L_{\text{after}}]$ (computing each as $\mathbf{p} \cdot \mathbf{d}_{\text{after}} - \mathbf{p}_a^{\text{after}} \cdot \mathbf{d}_{\text{after}}$), the parcel's frontage is still entirely on the new road and the parcel skips the deformation: it is reported as Untouched.
\textbf{Frontage projection.} If the line did change, project each of the parcel's two frontage endpoints. The projection uses the \emph{old} road's parameter:
\[
t_a = \frac{(\mathbf{p}_a^{\text{frontage,old}} - \mathbf{p}_a^{\text{road,before}}) \cdot \mathbf{d}_{\text{before}}}{L_{\text{before}}^2}\,L_{\text{before}},
\]
i.e., the dot product of the parcel-frontage-start-to-road-start vector with the road direction, normalized to the road's length parameter. (We use $\mathbf{d}_{\text{before}} L_{\text{before}} = \mathbf{p}_b^{\text{before}} - \mathbf{p}_a^{\text{before}}$ as the road vector and divide by $L_{\text{before}}^2$ to get the parameter.) Then
\[
\mathbf{p}_a^{\text{frontage,new}} = \mathbf{p}_a^{\text{road,after}} + t_a (\mathbf{p}_b^{\text{road,after}} - \mathbf{p}_a^{\text{road,after}}).
\]
Same for $\mathbf{p}_b^{\text{frontage}}$.
\textbf{Validate.} Build a \emph{hypothetical} new polygon with the proposed frontage endpoints (other vertices unchanged). Check:
\begin{itemize}[noitemsep]
\item The polygon validates as a simple CCW polygon.
\item Side-edge rotation: the angle between the old side direction and the new side direction is less than $\alpha_{\max} = \texttt{params.max\_side\_rotation}$ (default $30^\circ$).
\item Area $\geq A_{\min}$.
\item Frontage length $\geq w_{\min}$.
\end{itemize}
Failures map to \texttt{Regenerate} (rotation, simple-polygon failure) or \texttt{Condemned} (area, frontage too short). On success, the verdict is \texttt{Deformed} and we record the two proposed vertex moves: one for each frontage endpoint, looked up by \texttt{parcel.vertex\_ids[frontage\_index]}.
\textbf{Apply.} After all parcels are classified, the outer pipeline does:
\begin{enumerate}[noitemsep,leftmargin=2em]
\item For each $(vid, new\_pos) \in$ proposed moves: \texttt{parcels.move\_vertex(vid, new\_pos)}. This propagates atomically (\cref{sec:walkthrough-registry}) --- a shared frontage-end vertex between two adjacent regulars is updated once, and both polygons see it.
\item For each Deformed parcel, update its \texttt{frontage\_edge\_index} (because \texttt{Polygon::new} may have rotated the vertex order during proposal validation if the polygon happened to land in CW orientation).
\item For each Deformed parcel with an attached building, call \texttt{building.fits\_in(parcel)}; on \texttt{false}, evict.
\item Drop Condemned parcels.
\item Regenerate any block where some parcel asked for it.
\end{enumerate}
This propose-then-apply structure (D19) is what guarantees adjacent parcels stay in lockstep: two parcels sharing a vertex both propose the same new position (because the deform parameterization is a function of the vertex's position alone), so when \texttt{move\_vertex} fires on the shared \texttt{VertexId} the proposals agree.
\subsection{End-to-end trace: subdivide\_all}
\label{sec:walkthrough-trace-subdivide}
A single call \texttt{subdivide\_all(\&graph, \&params)} flows as follows.
\begin{enumerate}[leftmargin=2em]
\item \texttt{params.validate()} (\texttt{src/config.rs}) --- range-checks every \texttt{SubdivisionParams} field, returns \texttt{InvalidParams} on failure.
\item If \texttt{!graph.topology\_valid}, clone the graph and run \texttt{rebuild\_topology()} on the clone (the public API is \texttt{\&RoadGraph}, so we can't mutate it). \texttt{rebuild\_topology()} runs \texttt{check\_planarity}, \texttt{build\_half\_edges}, \texttt{sort\_outgoing\_by\_angle}, \texttt{link\_next\_and\_prev}, \texttt{extract\_faces}.
\item \texttt{extract\_blocks(graph)} walks the DCEL faces, skipping the exterior, and constructs \texttt{Block} structs with their CCW vertex polygons + per-edge \texttt{RoadId}s.
\item For each block: \texttt{subdivide\_block(graph, block, params, block\_idx)}.
\begin{enumerate}[label=\alph*),leftmargin=2em]
\item Compute \texttt{interior\_angles} per vertex.
\item Classify vertices: \texttt{real\_corner[i]}, \texttt{acute\_corner[i]} (\cref{sec:walkthrough-subdivision}).
\item Compute \texttt{depth\_caps[i]} for each block edge via ray-cast.
\item Decide \texttt{frontage\_on\_next[i]} per real corner (longer adjacent road wins).
\item For each real corner, build the corner-parcel polygon; clip against the block boundary; if valid and area $\geq A_{\min}$, push to output.
\item For each block edge, compute walk bounds (subtracting corner footprints), generate split positions via a deterministic per-road \texttt{ChaCha8Rng}, and emit each quad parcel (with bisector clip at acute-corner ends, then block clip, then frontage-index recovery).
\end{enumerate}
\item For each generated parcel, \texttt{ParcelSet::insert(parcel)} which snaps every polygon vertex to the registry, builds \texttt{vertex\_ids}, and updates \texttt{by\_block} and \texttt{by\_road} indexes.
\item Return \texttt{ParcelSet} (or with stats when called via \texttt{subdivide\_all\_with\_stats}).
\end{enumerate}
\subsection{End-to-end trace: apply\_road\_edit(MoveNode)}
\label{sec:walkthrough-trace-edit}
A single call \texttt{apply\_road\_edit(\&mut parcels, \&mut graph, RoadEdit::MoveNode \{node, to\}, \&params)} flows as follows.
\begin{enumerate}[leftmargin=2em]
\item \texttt{params.validate()}.
\item \texttt{graph\_before = graph.clone()}.
\item Apply mutation: \texttt{graph.nodes[node].pos = to}; \texttt{graph.topology\_valid = false}; \texttt{graph.rebuild\_topology()}.
\item \texttt{move\_node\_path(parcels, \&graph\_before, graph, node, params, \&mut outcome)}:
\begin{enumerate}[label=\alph*),leftmargin=2em]
\item Collect \texttt{incident\_roads} (those with \texttt{node} as endpoint in the new graph).
\item For each road, list parcels on it via \texttt{parcels.parcels\_on\_road(road)}.
\item For each parcel, call \texttt{deform\_parcel\_after\_road\_move} (pure --- no mutation):
\begin{enumerate}[label=\roman*),noitemsep,leftmargin=2em]
\item Untouched check: line unchanged + frontage in new range → return Untouched.
\item Compute proposed new frontage endpoints via parameter projection.
\item Validate hypothetical polygon (rotation, area, frontage length, polygon validity).
\item Return verdict; if Deformed, include the two proposed moves and the new frontage edge index.
\end{enumerate}
\item Bookkeeping: append parcel-id to one of \texttt{outcome.deformed} / \texttt{outcome.condemned}; record blocks needing regeneration.
\end{enumerate}
\item Apply phase:
\begin{enumerate}[label=\alph*),noitemsep,leftmargin=2em]
\item For each $(vid, new\_pos)$ in proposed moves: \texttt{parcels.move\_vertex(vid, new\_pos)} (writes through the registry).
\item Update \texttt{frontage\_edge\_index} on each Deformed parcel.
\item Building fit-check loop: evict where \texttt{!building.fits\_in(parcel)}.
\item Drop Condemned parcels.
\item For each block in \texttt{to\_regenerate}: drop its surviving parcels (mark Regenerated), re-run \texttt{subdivide\_block}, insert each new parcel (mark Created).
\end{enumerate}
\item Return \texttt{outcome}.
\end{enumerate}
% =======================================================
\appendix
\section{Notation Reference}
\label{app:notation}
\begin{tabular}{ll}
\toprule
\textbf{Symbol} & \textbf{Meaning} \\
\midrule
$G = (V, E)$ & Road graph (planar) \\
$B$ & Block boundary polygon \\
$B'$ & Developable polygon, $B$ offset inward by $d_s$ \\
$d_s$ & Road setback distance \\
$w_f, \sigma_f$ & Target frontage width, variance \\
$d_p, \sigma_d$ & Target parcel depth, variance \\
$\rho$ & Regularity slider, $[0, 1]$ \\
$w_{\min}, A_{\min}$ & Minimum frontage and area \\
$\alpha_{\max}$ & Maximum side-edge rotation before regeneration \\
$\varepsilon_{\text{geom}}$ & Geometric tolerance, $10^{-6}$ m \\
$\varepsilon_{\text{area}}$ & Area tolerance, $10^{-9}$ m$^2$ \\
$\varepsilon_{\text{angle}}$ & Angular tolerance, $10^{-4}$ rad \\
$\mathbf{t}_{\text{in}}, \mathbf{t}_{\text{out}}$ & Unit tangents back along prev / forward along next edge at a corner \\
$\mathbf{n}_{\text{in}}, \mathbf{n}_{\text{out}}$ & Inward normals at prev / next edge \\
$R$ & Corner radius (default $= w_f$) \\
$d_{\text{cap}}(e_i)$ & Per-edge depth cap (\cref{sec:walkthrough-depth-cap}) \\
$\theta_{\text{int}}$ & Interior angle at a polygon vertex \\
$\theta_{\text{turn}}$ & Signed CCW turn at a polygon vertex (= $\pi - \theta_{\text{int}}$) \\
\bottomrule
\end{tabular}
\end{document}