10 Commits

Author SHA1 Message Date
Dane Sabo
fb50885e7f M2 part 2: WASM build target for road_parceling_studio
Refactored Studio + impl App into lib.rs (was main.rs). Added a
#[wasm_bindgen] start_in_canvas entry plus index.html/Trunk.toml so
the same code runs in a browser tab via:

  rustup target add wasm32-unknown-unknown
  cargo install trunk
  cd road_parceling_studio && trunk serve

Native still builds with `cargo run -p road_parceling_studio`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 13:51:06 -04:00
Dane Sabo
334e6c84ed M2 part 1: road_parceling_studio interactive harness (native)
Sibling crate using egui/eframe. Click empty space to drop a node and
start a road from it; click a second node to close the road. Drag a
node to live-apply MoveNode (with snap-back on rejection). Side panel
exposes every SubdivisionParams knob, view toggles, and timing stats.

- Workspace Cargo.toml at repo root.
- RoadGraph::nodes() iterator added (needed to hit-test isolated
  just-placed nodes that have no incident road yet).
- WASM target list is configured in Cargo.toml but native is the only
  shipped frontend in this commit — WASM bundling lands next.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 13:48:30 -04:00
Dane Sabo
43df8f3ceb M0.5 part 2: OBB regularization + remaining figures
- regularize.rs: ρ=0 no-op, 0<ρ<1 lerp non-frontage verts toward
  OBB-snapped target, ρ=1 full snap to oriented bounding rectangle.
- viz: add fig_03_cul_de_sac, fig_05_acute_corner, plot_subdivision_perf
  (~125 µs/parcel at 25×25 scale), plot_parcel_area_hist (varied-block
  scene so the distribution is actually distributed). fig_07 panels at
  ρ ∈ {0, 0.5, 1.0} on a Y intersection (rectangles are no-ops for OBB).
- subdivide.rs cleanup pass: drop parcels on diff/union/conversion
  fallback paths instead of pushing untracked claims (was admitting
  overlaps). find_frontage_edge_after_clip now takes a tolerance — the
  cleanup pass uses a few-mm window since its inputs are 1mm-snapped.
- tests: bump assert_no_overlapping_parcels tol to 1cm² (the snap grid
  produces ~mm-scale slivers between adjacent parcels at intersection
  centers; still well below min_area).

Closes M1.0 DoD: all named tests pass, all spec figures present, OBB
regularization working, perf at ~0.13 ms/parcel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 13:43:16 -04:00
Dane Sabo
0968572184 M0.5 part 1: geo dep + polygon-difference cleanup, Y overlap fixed
The Y intersection had a real I3 violation that the M0.3 centroid-
only check missed (parcels 0 and 7 overlapped by ~64.5 m²). This
commit:

- adds geo 0.28 as a dependency
- replaces y_intersection_no_overlaps's centroid-in-polygon check
  with a rigorous polygon-polygon intersection test using
  geo::BooleanOps; adds rectangle_no_overlaps_rigorous as a
  positive-control test for the rectangle case
- adds cleanup_block_parcel_overlaps pass at the end of
  subdivide_block: iterate parcels in placement order (corners
  first, regulars after), subtract previously-claimed territory
  from each via geo's polygon difference, drop empties, recover
  frontage edge index and edge_kinds for survivors
- snaps polygon coords to a 1mm grid before handing to geo (helps
  geo's sweep-line invariants); strips collinear-triple and
  near-zero-length-edge artifacts from boolean output before
  feeding back into Polygon::new strict
- wraps difference/union calls in catch_unwind so geo's
  occasional sweep-line panic on degenerate inputs falls back to
  a no-op instead of crashing subdivision

Test status: 24 unit + 24 integration + 1 doc passing. Y figure
visually cleaner — every parcel sits inside its own sub-block, no
visible overlap between sub-blocks.

Self-decisions checklist progress: rigorous I3 testing landed
(checklist item ✓).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 13:26:43 -04:00
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
Dane Sabo
d8d5dd9c17 Shared-vertex registry on ParcelSet (no-drift guarantee)
The first half of the eventual full DCEL — vertex identity. Every
parcel polygon vertex now resolves to a stable VertexId via a
spatial-hash lookup at EPS_GEOM resolution. Coincident positions
across two parcels resolve to the same VertexId, so adjacent
parcels share one physical vertex.

ParcelSet::move_vertex(vid, new_pos) updates the registry's
position AND writes through to every parcel's polygon at the
recorded index. Adjacent parcels' shared boundaries can never drift
apart — they are the same vertex, mutated once.

The deform pipeline now propose-then-apply: deform_parcel_after_road_move
returns proposed (VertexId, new_pos) moves rather than mutating in
place. The outer loop validates each parcel's hypothetical
post-move polygon, then applies all proposed moves via
move_vertex. Conflicting proposals on the same vertex are
last-one-wins, but in practice the deform parameterization makes
all referrers agree by construction.

New regression test: shared_vertex_no_drift_under_repeated_edits.
50 small random node moves plus an inverse; asserts every shared
boundary vertex is bit-for-bit identical across every parcel that
references it.

Edge identity (parcel-layer half-edges) is the next milestone — it
enables split/merge ergonomics. Vertex identity alone is enough
for the no-drift contract this session was scoped to deliver.

Journal §11 session 4 entry adds D17 (registry), D18
(write-through), D19 (propose-then-apply), and the milestone-0.5
queue.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 15:23:13 -04:00
Dane Sabo
c6f2f01818 All 21 named tests now active: cul_de_sac + curved_road_high_curv
Wrote real test bodies for the last two ignored tests using polyline
approximations of their curved geometries. Both verify I1-I3 hold
and don't panic. True pie-slice subdivision and proper tight-radius
depth clamping are still milestone-0.4 work, but the contract from
spec table tab:degenerate is satisfied — every named test exists,
runs, and passes.

Final count: 24 unit + 22 integration + 1 doc test = 47 tests, zero
ignored. Up from 14 active out of 21 named at the end of milestone
0.1.

Journal §11 session 3 numbers updated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 14:55:39 -04:00
Dane Sabo
4b0eae9caf Milestone 0.3: I3 fix at acute corners, min-change deform, SplitSegment preserve
Y-intersection had a real I3 overlap at acute corners, caught by a
new programmatic centroid-in-other-polygon test
(y_intersection_no_overlaps). Fix: bisector-clip regular parcels
adjacent to acute corners (interior < 60°). Obtuse corners keep
their rectangle/parallelogram corner parcels and need no clip.

Minimum-change deformation: when a road's *line* doesn't change —
only its endpoints shift along the same line, e.g., the bottom road
gets longer when its right endpoint moves outward — parcels whose
frontage is still on the new segment are reported as Untouched and
keep their absolute coordinates. Only parcels on a road that
actually rotated get re-projected. Trade-off: vertex-exact
inverse-restore is no longer guaranteed (centroid drift bounded by
edit delta is the new contract).

SplitSegment preserve: rebind frontage_road for parcels whose
frontage is entirely on one side of the split point, or split into
two parcels along a perpendicular through the split point for
parcels that span. road_split_preserves test is now active and
passing. acute_intersection_15deg/5deg also active.

Test status: 24 unit + 20 integration + 1 doc passing; only
cul_de_sac and curved_road_high_curv still ignored (need real curved
roads). Journal §11 session 3 entry added with D14, D15, D16, two
spec deviations, and the milestone-0.4 queue.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 14:51:22 -04:00
Dane Sabo
95b69eddac Untrack build artifacts; add .gitignore; milestone 0.2 + 0.3 progress
Tracked files dropped from 11,756 to ~40 by adding .gitignore and
removing target/, *.aux/log/etc, and .DS_Store from the index.

Code changes since the initial commit:
- Milestone 0.2 (corner parcel rework, sticky back edges,
  preserve-on-deform pipeline, performance instrumentation, new
  Y-intersection and edit before/after figures, journal §11
  session 2 entry).
- Milestone 0.3 in progress: I3 overlap fix at acute corners
  (programmatic test caught a real I3 violation in the Y figure;
  fixed by bisector-clipping regular parcels adjacent to acute
  corners), minimum-change deformation (parcels on a road whose
  *line* didn't change are skipped — only parcels on a road whose
  direction changed are deformed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 14:45:03 -04:00
Dane Sabo
86b246ead2 initial commit 2026-04-25 14:33:11 -04:00