Historia-Urbis/journal.tex
2026-04-25 14:33:11 -04:00

1317 lines
60 KiB
TeX
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

\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},
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][]{
colback=orange!4,
colframe=orange!60!black,
fonttitle=\bfseries,
title=Open Question,
breakable,
#1
}
\newtcolorbox{cccontract}[1][]{
colback=gray!5,
colframe=gray!50!black,
fonttitle=\bfseries,
title=Claude Code Contract,
breakable,
#1
}
% ---- Header / footer ----
\pagestyle{fancy}
\fancyhf{}
\fancyhead[L]{\small Road Parceling System}
\fancyhead[R]{\small Design Journal}
\fancyfoot[C]{\thepage}
% ---- Title block ----
\title{\textbf{Road Parceling System} \\ \large A Design Journal}
\author{Dane Sabo}
\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 Journal},
pdfauthor={Dane Sabo}
}
\begin{document}
\maketitle
\thispagestyle{empty}
\begin{abstract}
\noindent
This journal documents the design and implementation of a road-frontage-based parcel subdivision system, intended as the foundational geometric layer of a city simulation game. The system rejects the rigid grid-based zoning of conventional city builders in favor of arbitrary polygonal parcels drawn outward from road frontage, with explicit handling for parcel persistence under road edits. This document serves three purposes: (1) a record of design decisions and their justifications, (2) a specification precise enough to drive autonomous implementation, and (3) a running notebook for open questions, dead ends, and revisions. It is intended to be read and extended over the lifetime of the project.
\end{abstract}
\tableofcontents
\newpage
% =======================================================
\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 journal covers \emph{only} the road parceling system. It is 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.
\subsection{Audience}
The primary audience is future-me. The secondary audience is an autonomous coding agent (Claude Code) that will implement the spec laid out in \cref{sec:contract}. Sections marked as \emph{Claude Code Contract} are written to be acted upon directly.
% =======================================================
\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}
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.
\begin{decision}[Stable IDs via slotmap]
Graph nodes, edges, and parcels are addressed by newtype-wrapped \texttt{slotmap} keys. This gives stable IDs that survive insertion and deletion, which is essential for the edit-persistence invariant (I4): a parcel's identity must outlive perturbations to its surrounding geometry.
\end{decision}
\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$. We use the \texttt{geo} crate's offset operation, with a fallback to a manual implementation for cases where \texttt{geo} produces invalid output (concave blocks with sharp inner corners are the failure mode).
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$, 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.}
\begin{enumerate}
\item For each road segment $r$ on the boundary of $B$, compute the offset segment $r' \subset \partial B'$.
\item Walk $r'$ from one end to the other, placing split points at arc-length intervals $w_i$ where:
\[
w_i = w_f + \sigma_f \cdot \xi_i, \quad \xi_i \sim \text{Uniform}(-1, 1)
\]
drawn from a deterministic RNG seeded by \texttt{seed} and the road segment ID.
\item At each split point $p_i$, extrude perpendicular into $B'$ to depth $d_i = d_p + \sigma_d \cdot \eta_i$, producing an interior point $q_i$.
\item Form quadrilateral parcels with vertices $(p_i, p_{i+1}, q_{i+1}, q_i)$.
\item Resolve interior collisions where extrusions from opposite sides of the block meet. Two strategies, used in order:
\begin{enumerate}[label=(\alph*)]
\item If the block depth (perpendicular distance between opposing road edges) is less than $2 d_p$, clip extrusions to the medial axis of $B'$.
\item If extrusions still overlap after clipping, shrink $d_i$ on the longer of the two until disjoint.
\end{enumerate}
\item Reject any parcel with frontage $< w_{\min}$ or area $< A_{\min}$. Merge rejected parcels into their larger neighbor when geometrically possible.
\end{enumerate}
\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.
\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{Identify affected parcels.} Query the spatial index for parcels with frontage on the modified segment(s).
\item \textbf{Attempt deformation.} For each affected parcel:
\begin{enumerate}[label=(\alph*)]
\item Recompute the frontage edge by re-offsetting the new road geometry.
\item Translate frontage vertices to their new positions.
\item Hold side and back vertices fixed.
\item Reconnect the polygon and validate.
\end{enumerate}
\item \textbf{Categorize outcome.} Each parcel ends in one of four states:
\begin{itemize}[noitemsep]
\item \emph{Deformed} (success): the parcel persists with new frontage.
\item \emph{Regenerated}: deformation violated thresholds; the block is re-subdivided.
\item \emph{Condemned}: the parcel cannot exist in the new geometry; the building (if any) is evicted.
\item \emph{Created}: a new parcel from a regenerated block.
\end{itemize}
\end{enumerate}
\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{Determinism and Idempotence}
\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.
\end{invariant}
\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$ \\
\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 (optional)
| |-- 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, EdgeKind};
pub fn subdivide_all(
graph: &RoadGraph,
params: &SubdivisionParams,
) -> Result<ParcelSet, 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>,
}
\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 \\
\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}); 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.
\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}.
\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--I7, 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.
% =======================================================
\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
\end{itemize}
\subsection{Inclusion in This Journal}
As figures are produced, they should be checked into \texttt{figures/} and included in this journal via:
\begin{lstlisting}[language=TeX, caption={Including a generated figure.}]
\begin{figure}[h]
\centering
\includegraphics[width=0.8\linewidth]{figures/fig_01_grid_block.pdf}
\caption{Frontage-first subdivision of a rectangular block.}
\label{fig:grid-block}
\end{figure}
\end{lstlisting}
SVG figures should be converted to PDF via \texttt{rsvg-convert} or similar in a build script (\texttt{scripts/figs\_to\_pdf.sh}) for clean inclusion.
% =======================================================
\section{Performance Targets}
\label{sec:performance}
Not the primary goal of milestone one, but tracked to catch regressions:
\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}
% =======================================================
\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 milestone one; 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}[title=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 journal, and move on. Do not block.
\item If the spec is ambiguous or wrong, append a note to \cref{sec:revisions} listing what changed and why. Do not silently deviate.
\end{enumerate}
\end{cccontract}
\begin{cccontract}[title=Definition of Done]
The milestone is complete 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 journal compiles with \texttt{latexmk -pdf journal.tex} and includes all generated figures.
\item \cref{sec:revisions} contains an entry per design decision made during implementation that deviated from or extended the spec.
\end{enumerate}
\end{cccontract}
% =======================================================
\section{Open Questions}
\label{sec:open}
A running list. Resolved questions migrate to \cref{sec:revisions} as Design Decisions.
\begin{openq}[title=Q1: Skeleton-based subdivision as a fallback]
Should the straight-skeleton-based subdivision algorithm be implemented in milestone one as a fallback for blocks where frontage-first produces ugly results, or deferred to milestone two? Frontage-first handles 90\% of cases cleanly; skeleton handles irregular blocks better but adds significant complexity. \textbf{Tentative}: defer to milestone two; stub returns \texttt{Unimplemented}.
\end{openq}
\begin{openq}[title=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 milestone one with a documented hot-path comment, swap in \texttt{rstar} when the benchmark in \cref{sec:performance} exceeds budget.
\end{openq}
\begin{openq}[title=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{Tentative}: (a) for milestone one; revisit when buildings need parcel-area stability.
\end{openq}
\begin{openq}[title=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 milestone one; this is a milestone-two optimization.
\end{openq}
% =======================================================
\section{Design Decisions}
\label{sec:decisions}
A record of decisions made during design and implementation. Each entry is dated and references the section it affects.
\begin{decision}[D1, 2026-04-25 -- f64 throughout]
Decided to 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]
Decided on a 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]
Decided to use \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]
Decided to 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}
% =======================================================
\section{Revisions and Deviations}
\label{sec:revisions}
This section is the project's running implementation log. It is written
during implementation, in chronological session entries. Each session
narrates what was built, what tests landed, what design decisions had
to be locked in, where the spec was extended or reinterpreted, and
what is queued for the next session. The intent is that future-me (or
another contributor) can read straight through and understand why the
code looks the way it does.
\subsection*{Entry template}
Sessions are dated subsections. Within a session, individual deviations
from the spec follow:
\begin{verbatim}
\subsubsection*{YYYY-MM-DD --- Short title}
What changed:
Why:
Affected sections:
\end{verbatim}
Newly-locked design decisions are recorded in the same colored
\texttt{decision} boxes as \cref{sec:decisions}, numbered continuing
from D4. Open questions that move forward are noted with the same Q
prefix as \cref{sec:open}.
% -----------------------------------------------------------------
\subsection{2026-04-25 --- Session 1: Milestone 0.1, rectangle end-to-end}
\label{sec:session-1}
\paragraph{Goal of the session.}
\Cref{sec:contract}'s working-style contract is unambiguous: ``Get a
single rectangular block working end-to-end with tests and an SVG
figure before adding complexity.'' That is the floor for this
session. The aspiration is that, by the end, the public API surface
of \cref{sec:architecture} compiles, the rectangle case in
\cref{fig:grid-block} below is generated by the crate (not drawn by
hand), the tooling gates of \cref{sec:contract}'s Definition-of-Done
are green, and the named tests of \cref{tab:degenerate} either pass or
have an explicit, dated milestone-0.2 marker explaining what they
need.
\paragraph{What got built.}
The crate at \texttt{road\_parceling/} now ships every module listed in
\cref{sec:architecture}: \texttt{geometry/} (polygon validation, half-plane
clipping, inward offsetting), \texttt{network/} (half-edge / DCEL graph
with stable slotmap ids, planar-graph validation, face extraction),
\texttt{parcel/} (subdivide, classify, regularize, deform), and the
feature-gated \texttt{viz/} (SVG renderer + figure generator). Public
API is exactly the surface declared in \cref{sec:architecture}:
\texttt{subdivide\_all}, \texttt{apply\_road\_edit}, the IDs, and the
\texttt{BuildingFitCheck} trait. Total: $\approx 2{,}500$ lines of
library code plus $\approx 400$ of integration tests.
The frontage-first algorithm of \cref{sec:subdivision} is implemented
end-to-end. The corner-overlap problem (where parcels generated from
two adjacent frontages would intrude into each other's territory at
the shared block corner) is resolved by clipping each parcel against
the inward-pointing angle bisector of its two end corners. The
opposite-frontage problem is bounded by a per-edge depth cap derived
from a ray cast across the block from the edge midpoint --- this is
\cref{sec:open}'s Q3 option (a), now locked in (see below).
\begin{figure}[h]
\centering
\includegraphics[width=0.8\linewidth]{figures/fig_01_grid_block.pdf}
\caption{The first figure produced by the crate: a $200 \times 100$
rectangular block subdivided at default parameters
($w_f=20$, $d_p=30$, $d_s=1$, $\rho=0$). Roads in black; parcel
frontage edges in blue, side edges gray, back edges light-gray
dashed. Corner pie-slice parcels are the bisector-clipped triangles
at each block corner. The empty central strip is the medial gap:
the block's vertical extent ($100$\,m) exceeds $2 d_p$, so the
depth cap stops parcels short of the centerline. Generated by
\texttt{cargo run --example generate\_figures --features viz}.}
\label{fig:grid-block}
\end{figure}
\paragraph{Tooling gates.}
\texttt{cargo build --all-features},
\texttt{cargo clippy --all-targets --all-features -- -D warnings},
\texttt{cargo fmt --check}, and
\texttt{cargo doc --all-features --no-deps} are all clean.
\paragraph{Test status.}
22 unit tests + 14 integration tests + 1 doc test pass (37 total).
Seven of \cref{tab:degenerate}'s 21 named tests are
\texttt{\#[ignore]}-d behind explicit milestone-0.2 markers:
\texttt{acute\_intersection\_15deg}, \texttt{acute\_intersection\_5deg},
\texttt{cul\_de\_sac}, \texttt{curved\_road\_high\_curv},
\texttt{road\_edit\_inverse\_restores},
\texttt{road\_split\_preserves}, and
\texttt{building\_footprint\_persists}. The remaining 14 named tests
pass --- including the T- and Y-junctions, the huge ($1\,\text{km}$)
and tiny blocks, the disconnected graph, near-duplicate nodes,
isolated nodes, the planarity violation, and the colinear-roads case.
\texttt{numerical\_precision\_stress} runs at coordinates near $10^6$
rather than the spec's $10^{20}$; this is documented in the test as a
practical f64 ceiling and revisited in milestone 0.2.
\paragraph{Design decisions locked in this session.}
The spec's \cref{sec:decisions} ends at D4. Continuing:
\begin{decision}[D5, 2026-04-25 -- BuildingHandle owns its
\texttt{BuildingFitCheck}]
\Cref{sec:edit-handling} describes \texttt{BuildingHandle} as
``opaque'', leaving room for either a bare id or an owning wrapper.
Locked in: \texttt{BuildingHandle} wraps a
\texttt{Box<dyn BuildingFitCheck>} so the deform pipeline can call
\texttt{fits\_in} locally. A bare-id design would have required
\texttt{apply\_road\_edit} to take a callback or side table, which
would change the spec'd signature in \cref{sec:architecture}.
\end{decision}
\begin{decision}[D6, 2026-04-25 -- DCEL next/prev rule, explicit form]
The standard DCEL ``next is CCW after twin'' phrasing is ambiguous
--- whether the rotation goes CW or CCW around the target vertex
depends on whose perspective you take. The implementation locks in:
\texttt{half\_edge.next} is the \emph{predecessor} (CW neighbor) of
\texttt{half\_edge.twin} in the target vertex's CCW-sorted outgoing
list, with wrap; \texttt{half\_edge.prev} is the twin of the
\emph{successor} of the half-edge in its origin's list. The
\texttt{t\_intersection} test was the catalyst: with the rule
inverted, the T-stem face cycle enclosed the wrong region, so the
left and right blocks fused into one and parcels collapsed. See
\texttt{src/network/graph.rs} : \texttt{link\_next\_and\_prev} for the
derivation.
\end{decision}
\begin{decision}[D7, 2026-04-25 -- \texttt{Polygon::new\_relaxed} for
block boundaries]
Invariant I1 forbids collinear triples in \emph{parcel} polygons. But
\cref{sec:degenerate}'s \texttt{colinear\_roads} case requires that
two end-to-end road segments still bound a valid block, which means
the block polygon legitimately has a collinear-corner vertex at the
shared node. Locked in: a relaxed polygon constructor (with the
collinear check skipped) for block boundaries; parcel construction
still calls the strict constructor. I1 applies to parcels, not blocks.
\end{decision}
\begin{decision}[D8, 2026-04-25 -- Clippy scope is \texttt{all}, not
\texttt{pedantic}]
\Cref{sec:idioms} requires \texttt{cargo clippy --all-targets
--all-features -- -D warnings} clean. The crate's
\texttt{[lints.clippy]} enables the \texttt{all} group only.
\texttt{pedantic} fights numerical-code conventions (single-letter
coordinate names, struct-default reassignment) without buying real
safety, so it stays off. The \cref{sec:contract} gate is satisfied
verbatim.
\end{decision}
\paragraph{Spec deviations recorded this session.}
The following entries follow the template above.
\subsubsection*{2026-04-25 --- \texttt{apply\_road\_edit} is
regenerate-only in 0.1}
What changed: The milestone-0.1 implementation of
\texttt{apply\_road\_edit} condemns every parcel touching an affected
road and re-subdivides every block from scratch. The
\texttt{Deformed} bucket of \texttt{EditOutcome} is always empty;
outcomes split between \texttt{Condemned}, \texttt{Created}, and (a
degenerate copy of) \texttt{Regenerated}.
Why: A correct preserve-on-deform pipeline --- re-projecting frontage
endpoints onto the new road geometry, holding side and back vertices
fixed, applying \cref{sec:edit-handling}'s rotation/area thresholds,
calling \texttt{BuildingFitCheck::fits\_in} for every surviving
parcel --- is the headline of milestone 0.2. Shipping the
regenerate-only fallback first lets every other system (the edit
enum, the outcome categorization, the affected-roads lookup, the
edit-time invariants check) get exercised end-to-end, so the
preserve work in 0.2 is a pure addition rather than a rewrite. The
regenerate path is always safe; no parcel is silently corrupted, and
post-edit I1--I3 still hold.
Affected sections: \cref{sec:edit-handling}. Invariants I6 and I7
(determinism and reversibility) hold \emph{vacuously} for the
regenerate path. Three named tests
(\texttt{road\_edit\_inverse\_restores},
\texttt{road\_split\_preserves},
\texttt{building\_footprint\_persists}) are \texttt{\#[ignore]}-d
with markers pointing at this entry.
\subsubsection*{2026-04-25 --- Setback is metadata-only depth}
What changed: \Cref{sec:subdivision}'s algorithm walks the offset
segment $r' \subset \partial B'$, implying that a parcel's frontage
edge sits $d_s$ inside the block. But \cref{sec:subdivision}'s edge
classification requires the frontage edge to lie within
$\varepsilon_{\text{geom}}$ of a road segment. With
$\varepsilon_{\text{geom}} = 10^{-6}\,\text{m}$ and a default
$d_s = 1\,\text{m}$, these are mutually exclusive.
Resolution: parcels touch the road. The frontage edge is on the road
within $\varepsilon_{\text{geom}}$, satisfying the I2 reading
literally. The setback parameter is folded into the parcel depth
($\text{total\_depth} = d_s + d_p$), and the front-most $d_s$ of the
parcel is treated as an unbuildable margin held in metadata --- not
as an extra geometric edge. (A six-vertex strip with the setback as
a real edge would violate I1's no-collinear-triple rule, since the
three vertices along each side of the setback strip are collinear
with the road tangent.)
Why: I2 is the load-bearing invariant, and the spec text in
\cref{sec:invariants} pins it specifically to ``coincident with a
road segment''. Reinterpreting setback as a depth offset is the
minimum change that keeps I2 honest while still exposing the knob
the spec defines.
Affected sections: \cref{sec:invariants} (I2 reading clarified);
\cref{sec:subdivision} (algorithm step 1 reinterpreted).
\subsubsection*{2026-04-25 --- Regularization pass is a stub}
What changed: \texttt{parcel/regularize.rs} is a no-op for milestone
0.1. \texttt{SubdivisionParams::regularity} default is $0$, so the
non-regularized output is what the figures show. The three panels of
\texttt{fig\_07\_regularity\_slider\_*.svg} are therefore
byte-identical until the OBB snap lands.
Why: Frontage-first subdivision already produces orthogonal parcels
on straight road frontage, so the visible payoff of OBB-snapping
only appears once curved or skew frontages are in play
(\cref{sec:degenerate}'s \texttt{curved\_road\_high\_curv}).
Sequencing the regularization pass after curved-road support avoids
implementing it twice --- once against the milestone-0.1 input
shapes and again against the milestone-0.2 ones.
Affected sections: \cref{sec:regularization}.
\subsubsection*{2026-04-25 --- tcolorbox preamble fix}
What changed: The original env definitions for
\texttt{invariant} and \texttt{decision} took an optional argument
\texttt{[\#1]} and passed it through to tcolorbox keys. Calls like
\verb|\begin{invariant}[I1: Polygon validity]| therefore tried to
set a tcb key named \texttt{I1: Polygon validity}, which fails on
modern \texttt{tcolorbox} (the same call is silently ignored on
older versions). The envs now interpret \texttt{\#1} as the title,
defaulting to ``Invariant'' / ``Design Decision'' when omitted.
Why: Without this, \texttt{latexmk -pdf journal.tex} fails on the
first invariant box and \cref{sec:contract}'s
``This journal compiles'' clause cannot be met. The fix preserves
every existing call site and changes only the preamble macros.
Affected sections: preamble only; visible call sites in
\cref{sec:invariants} and \cref{sec:decisions} render unchanged.
\paragraph{Open questions touched.}
\begin{decision}[Q1 \texttt{->} closed for milestone 0.1, deferred to 0.2]
\Cref{sec:open}'s Q1 (skeleton-based subdivision as a fallback)
remains stubbed at \texttt{Unimplemented}. The frontage-first
algorithm is complete enough for the rectangle, T, Y, and
disconnected-graph cases that drive the milestone-0.1 deliverables.
Skeleton fallback is now bundled with \texttt{cul\_de\_sac} and
\texttt{curved\_road\_high\_curv} into the milestone-0.2 queue.
\end{decision}
\begin{decision}[Q3 \texttt{->} option (a), confirmed]
\Cref{sec:open}'s Q3 (block ownership of back edges) is option (a)
in the implementation: each frontage's parcels extrude to a depth
bounded by a per-edge ray-cast cap (half the perpendicular distance
to the nearest other block edge). Symmetric. The default-parameter
rectangle in \cref{fig:grid-block} hits the cap on every parcel and
shows the expected medial gap. No revision to the spec's tentative
answer.
\end{decision}
\paragraph{Next session --- milestone 0.2 queue.}
The priority order for the next session, in roughly increasing
implementation cost:
\begin{enumerate}[noitemsep]
\item Preserve-on-deform pipeline for \texttt{MoveNode}: project
frontage endpoints onto the new road, hold side+back fixed,
validate against the rotation/area thresholds in
\cref{sec:edit-handling}. Unlocks \texttt{road\_edit\_micro\_move}'s
``all parcels deformed; none regen'' assertion and starts on I7.
\item \texttt{SplitSegment} preserve path. Splitting a parcel's
frontage at the new node should produce two parcels sharing the
side edges. Unlocks \texttt{road\_split\_preserves}.
\item \texttt{BuildingFitCheck} eviction in the deform path. Trait
is already wired (D5); just needs to be called. Unlocks
\texttt{building\_footprint\_persists}.
\item Sliver-merge for acute corners ($<$ some threshold, probably
$30^\circ$). Detect during subdivision and merge with neighbor.
Unlocks \texttt{acute\_intersection\_15deg/5deg}.
\item Curved-road support (depth cap on tight curvature; arc
walking). Unlocks \texttt{curved\_road\_high\_curv} and starts on
\texttt{cul\_de\_sac}.
\item Pie-slice parcels for cul-de-sac bulbs. Unlocks
\texttt{cul\_de\_sac}.
\item OBB regularization pass (\cref{sec:regularization}). Now
meaningful given curved-road parcels.
\item Inverse-restore round trip
(\texttt{road\_edit\_inverse\_restores}). Falls out of items 1--2
plus a known-pristine snapshot.
\end{enumerate}
% Future sessions land below this line as new \subsection entries.
% -----------------------------------------------------------------
\subsection{2026-04-25 --- Session 2: Milestone 0.2 (corner parcels,
sticky back edges, preserve-on-deform)}
\label{sec:session-2}
\paragraph{Goal of the session.}
Three big rocks for milestone 0.2, set during the kickoff conversation:
fix the corner parcels (today's bisector-clipped triangles are
unionized into proper 4--6 sided corner parcels), make road moves
preserve back edges and only change a single parcel (instead of
rippling to its back-to-back neighbor), and start tracking
performance --- per-phase wall-clock timing surfaced in this section,
since real-time placement is the eventual gameplay target. Deferred
to milestone 0.3: literal road width (the \texttt{setback} parameter
is the working placeholder), curved-road handling, sliver-merge for
acute corners, OBB regularization.
\paragraph{Decisions locked in at session kickoff.}
\begin{decision}[D9, 2026-04-25 -- Build-first corner parcels]
At each ``real'' corner, the corner parcel is built before the
frontage walk and the walk on each adjacent road then starts past
the corner's extent. (The Voronoi delete-and-refill alternative was
considered and rejected --- build-first has fewer edge-case
surprises and is naturally deterministic without a second pass.)
\end{decision}
\begin{decision}[D10, 2026-04-25 -- Corner radius is the average
frontage width]
The corner extends \(R = \texttt{params.frontage\_width}\) along each
adjacent road and \(\texttt{params.depth}\) perpendicular into the
block. Result: a 4-vertex parallelogram when \(R=\texttt{depth}\), a
6-vertex L-shape otherwise. Kept malleable so future tuning can
trade off corner footprint vs. mid-block parcel count.
\end{decision}
\begin{decision}[D11, 2026-04-25 -- ``Real corner'' definition]
The corner-parcel routine fires at any block-boundary vertex whose
underlying graph node has degree \(\geq 3\) (T, Y, +) \emph{or}
degree 2 with a bend angle below \(150^\circ\) (so the four \(90^\circ\)
corners of a rectangle qualify; a near-collinear continuation does
not).
\end{decision}
\begin{decision}[D12, 2026-04-25 -- Road width deferred to 0.3;
\texttt{setback} is the placeholder]
A literal \texttt{road\_width} parameter on \texttt{Road} would
ripple through block extraction, frontage geometry, and the
visualization at once. Instead we keep roads as centerlines and
treat \texttt{setback} as the catch-all for ``space the road plus
its surrounding right-of-way actually consumes'' --- downstream
consumers (a future game renderer) draw the road on top of the
centerline at whatever width they like, and tune \texttt{setback}
upward to give the visual road room. Revisited in milestone 0.3
when curved-road handling forces re-touching this code anyway.
\end{decision}
\begin{decision}[D13, 2026-04-25 -- Sticky back edges, lite]
Each parcel stores its full polygon in absolute world coordinates.
The deform pipeline is responsible for moving \emph{only} the
frontage vertices when a road edit fires; back and side vertices
stay put. No explicit shared-edge tracking between adjacent parcels.
This achieves the user-requested invariant (a road move changes one
parcel, not two) without a parcel-layer DCEL refactor. Trade-off:
when adjacent parcels' back edges drift apart by more than
\(\varepsilon_{\text{geom}}\) (e.g. the back-to-back neighbor was
condemned and re-created from a different generator state), a sliver
gap can open between them. I3 still holds (parcels never overlap);
gap detection is added to the milestone-0.3 backlog.
\end{decision}
\paragraph{Status during writing.}
Session in progress; sub-paragraphs below are written as work lands.
Performance numbers and figures are filled in once the corresponding
features compile.
\paragraph{Performance instrumentation.}
\texttt{SubdivisionStats} lands as a public type alongside
\texttt{ParcelSet}, returned by the new
\texttt{subdivide\_all\_with\_stats} entry point (the existing
\texttt{subdivide\_all} is now a thin wrapper that drops the stats).
Per-phase wall-clock timing is collected for topology rebuild, block
extraction, and the cumulative per-block subdivision; aggregate
parcels-per-second and time-per-parcel are derived properties.
Numbers will be folded into this section once the rest of 0.2 lands.
\paragraph{Corner parcel rework.}
The bisector-clipped triangles of milestone 0.1 are gone. Each real
corner now produces a four-vertex parcel whose frontage edge lies on
the longer of the two adjacent block edges (length \(R\)) and whose
side2 lies along the other adjacent road (length \texttt{depth}). The
construction has two flavors --- ``frontage on next'' or ``frontage
on prev'' --- depending on which road wins; in both cases the parcel
is the rectangle (or skewed parallelogram for non-90° corners)
\(R\)-by-\texttt{depth} with one corner pinned at the intersection
vertex.
The frontage walk on each block edge now starts past the corner
footprint and ends past the next corner's footprint. Per-edge
consumption is asymmetric: the corner whose frontage lies on this
edge consumes \(R\); the corner whose side2 lies on this edge
consumes \texttt{depth}. The bisector-clip pass is removed entirely.
For the default 200×100 rectangle the figure now shows four corner
rectangles each 20×30, eight 20×30 middle parcels along the long
roads, two 20×30 middle parcels along each short road, and the
expected medial gap inside.
\paragraph{Preserve-on-deform pipeline.}
\texttt{apply\_road\_edit} now has three code paths:
(a) for \texttt{MoveNode}, it re-projects each affected parcel's
frontage endpoints onto the new road geometry, holding side and back
vertices fixed in absolute coordinates (D13). Validation against
the rotation threshold and the area/frontage minimums decides
whether the parcel is \texttt{Deformed}, \texttt{Condemned}, or
needs the block re-subdivided. Surviving parcels with attached
buildings get their \texttt{BuildingFitCheck::fits\_in} called; on
\texttt{false} the building is evicted and the parcel id is added
to a new \texttt{evicted\_buildings} bucket on \texttt{EditOutcome}
(extension to spec §4.2).
(b) for \texttt{DeleteSegment}, all parcels on the deleted road are
condemned and any face left without parcels is re-subdivided.
(c) for \texttt{SplitSegment} and \texttt{InsertSegment}, the
milestone-0.1 regenerate-only fallback still applies — split-preserve
is the headline of milestone 0.3.
The previously-\texttt{\#[ignore]}-d
\texttt{road\_edit\_inverse\_restores} and
\texttt{building\_footprint\_persists} tests are now active and pass.
\paragraph{New figures.}
\Cref{fig:y-intersection} shows three blocks formed by a Y at 120°
inside an equilateral triangle. The 30°-interior outer corners are
below the acute-skip threshold, so no corner parcel is built there;
the block-clip pass keeps every regular parcel inside its block.
\Cref{fig:edit-before} and \cref{fig:edit-after} show a 200×100
rectangle before and after a 8\,m \texttt{MoveNode} on its
bottom-right corner; surviving parcels in the after-figure are
tinted by their \texttt{EditOutcome} category (deformed = green).
\begin{figure}[h]
\centering
\includegraphics[width=0.8\linewidth]{figures/fig_01_grid_block.pdf}
\caption{Default 200×100 rectangle subdivided by milestone 0.2.
Compare with milestone 0.1: the four corners are now proper
rectangle corner parcels rather than bisector-clipped triangles.}
\label{fig:grid-block-v02}
\end{figure}
\begin{figure}[h]
\centering
\includegraphics[width=0.6\linewidth]{figures/fig_04_y_intersection.pdf}
\caption{Y intersection. Three roads at 120° from the origin meet
three outer triangle vertices; corner parcels appear at the inner
120° corners (origin) and are skipped at the 30° outer corners.}
\label{fig:y-intersection}
\end{figure}
\begin{figure}[h]
\centering
\begin{minipage}{0.48\textwidth}
\includegraphics[width=\linewidth]{figures/fig_06a_road_edit_before.pdf}
\caption{Before: \cref{fig:grid-block-v02}'s rectangle.}
\label{fig:edit-before}
\end{minipage}\hfill
\begin{minipage}{0.48\textwidth}
\includegraphics[width=\linewidth]{figures/fig_06b_road_edit_after.pdf}
\caption{After: bottom-right corner moved 8\,m right; deformed
parcels are green.}
\label{fig:edit-after}
\end{minipage}
\end{figure}
\paragraph{Deviations from spec.}
\subsubsection*{2026-04-25 --- Acute-corner skip + per-parcel
block-clip}
What changed: For block-boundary vertices with interior angle
\(< 60^\circ\), no corner parcel is built. Instead, the regular
frontage walks at those vertices proceed as if the corner were
non-real, and every parcel produced (corner or regular) is then
clipped against the inward half-planes of the block boundary. This
keeps parcels strictly inside the block at acute corners while the
sliver-merge logic catches up in milestone 0.3.
Why: The R×depth corner construction implicitly assumes interior
\(\geq 60^\circ\). Below that threshold the perpendicular extension
of stripe-prev exits the block on the t\_in side. The block-clip
pass is conservative (correct for convex blocks; acceptable
over-clipping for mild concavities) and does the right thing for
the Y-intersection figure.
Affected sections: \cref{sec:subdivision} (algorithm gains a
post-clip step); \cref{sec:degenerate}'s acute tests still
\texttt{\#[ignore]}-d pending sliver-merge.
\subsubsection*{2026-04-25 --- \texttt{EditOutcome.evicted\_buildings}}
What changed: Added a \texttt{evicted\_buildings: Vec<ParcelId>}
field to \texttt{EditOutcome}, enumerating parcels whose attached
building was dropped during deformation because
\texttt{BuildingFitCheck::fits\_in} returned false.
Why: Spec §4.2 lists four outcome buckets but doesn't separate
``parcel survived but its building did not''. Without this bucket,
caller-side game logic would have to walk every deformed parcel and
check its building presence against a pre-edit snapshot. A separate
bucket is cheap and removes that walk.
Affected sections: \cref{sec:edit-handling}.
\paragraph{Performance.}
Numbers from \texttt{cargo run --release --example perfprobe} on a
M-series Mac, after a few warmup iterations:
\begin{center}
\begin{tabular}{lrrrr}
\toprule
\textbf{Scene} & \textbf{Blocks} & \textbf{Parcels} & \textbf{Total time} & \textbf{µs / parcel} \\
\midrule
Single 200×100 rectangle & 1 & 24 & 15 µs & 0.63 \\
5×5 grid of disjoint rectangles & 25 & 605 & 220 µs & 0.36 \\
\bottomrule
\end{tabular}
\end{center}
Both well under the spec §9 targets (100 blocks $<$ 50\,ms; 10\,000
blocks $<$ 5\,s). At roughly 2--3 million parcels per second on this
hardware, the gameplay constraint of ``feels instant when a road is
placed'' (informal user requirement) is satisfied with margin --- a
typical road placement that touches a hundred parcels lands in well
under a millisecond.
\paragraph{Test status.}
24 unit tests, 16 integration tests, 1 doc test, and the new
\texttt{stats\_report\_nonzero\_phases} unit test all pass. Five
named tests remain \texttt{\#[ignore]}-d for milestone 0.3
(\texttt{acute\_intersection\_15deg/5deg},
\texttt{cul\_de\_sac}, \texttt{curved\_road\_high\_curv},
\texttt{road\_split\_preserves}). \texttt{cargo clippy
--all-targets --all-features -- -D warnings} and
\texttt{cargo fmt --check} are clean.
\paragraph{What's next --- milestone 0.3 queue.}
\begin{enumerate}[noitemsep]
\item \texttt{SplitSegment} preserve path: split the parcel whose
frontage spans the new node into two parcels sharing the side
edges. Unlocks \texttt{road\_split\_preserves}.
\item Sliver-merge for acute corners: detect interior \(< 60^\circ\),
merge the would-be corner parcel territory with its longer
neighbor along the same road. Unlocks the two
\texttt{acute\_intersection} tests and tidies up
\cref{fig:y-intersection}.
\item Curved-road support: discretize curves into polylines with
variable depth caps based on local radius. Unlocks
\texttt{curved\_road\_high\_curv} and starts on \texttt{cul\_de\_sac}.
\item Pie-slice parcels for cul-de-sac bulbs.
\item OBB regularization pass (\cref{sec:regularization}).
\item \cref{sec:open}'s Q4: regeneration biased to preserve
building footprints.
\item Q2: spatial index (rstar) for affected-parcel lookup, once
the linear scan in \texttt{move\_node\_path} starts to bite.
\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 \\
$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 regen \\
$\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 \\
\bottomrule
\end{tabular}
\end{document}