All notes

· 6 min read

A road network that only worked on rectangles

Replacing a global construction with one that only ever relies on two neighbours at a time, built so the well-behaved case provably reduces to the old output, and one measured trade-off I took anyway.

What the old layout assumed

Every geometric construction assumes something about the plots it's going to meet. That assumption usually holds, or you'd have noticed, so I think the more useful question is whether anything guarantees it. If it relies on something the input doesn't have to have, it's only working by luck, and the tests just haven't hit the unlucky case.

The engine here packs fixed-size rectangular units into an irregular plot and leaves a connected drivable network between them. That's parallel lanes at a fixed pitch, clipped to the usable area, with rows of units flanking each one. A unit only counts as reachable if the point in front of its access side lies on that network, and the network is pruned by a search from the entrance, so anything unreachable gets deleted.

An x that every lane shares

The lanes were stitched together by two global end connectors, full-height strips at both extremes of the local x axis. For those to cross every lane, there has to be one x value at which every lane exists.

A rectangle gives you that for free, since every lane clips to the full width. A tapered plot doesn't. Lanes at different heights clip to different x ranges, and two of them can end up sharing no x at all.

When that happens the connectors reach some lanes and miss others, the network fragments, and the prune does its job and deletes every piece the entrance can't reach. On four of the eight orientations of a pointed kite, coverage collapsed to a strip of around 7%. It was a valid, fully connected, tiny layout that passed every check.

Two neighbours at a time

The replacement is adjacent-pair bridges. I sort the lanes by cross-axis position and, for each consecutive pair, bridge them wherever those two share x. That never needs an x range shared by every lane, just one shared by two neighbours at a time. It's a far weaker requirement, and a taper wears it down gradually instead of breaking it outright.

Where a pair shares at least a lane's width of x, two bridges go in, one near each end of the shared span and each spanning both lanes, so adjacent lanes loop instead of leaving a dead-end spur. Where a pair shares less, one bridge goes in at the midpoint of the shared span, and only if that strip has usable area. An apex sliver that can't be bridged is left to the prune, which costs a small remnant but leaves the main chain connected.

Rectangles come out the same as before

Bridges landing at the same x across different pairs are merged along the cross axis before anything is emitted, and that merge step is why the change was safe to make. On a rectangle every pair's shared span is the whole bounding box, so every pair's bridges land at the same two x values, and the merge collapses the stack into the same two full-height spines the old connectors produced. You get the same rectangles, not just an equivalent network.

I care about that more than I care about the kite. A general fix that also perturbs the easy cases is hard to evaluate, because every number moves and a real gain looks the same as a wash. Here the easy case reduces to the old output by construction, so any difference measured on a rectangle has one explanation left. That's a short entrance stub I added in the same change, which pulled two entrances on an L-shaped plot from 6.1m off-network to 0.0m and costs a rectangle a little coverage.

Fixing the row angle

A row flanking a lane needs a bearing, meaning the direction across the row, pointing into the lane. It was derived as the direction from the edge's midpoint toward the polygon's centroid.

For a rectangle the centroid lies on the perpendicular bisector of every edge, so that direction is perpendicular. For anything else it's an approximation, and the polygon here is a lane piece clipped to the usable area. A connector clipped by a tapered boundary came out as a five-vertex polygon, with its vertex-average centroid skewed toward one end.

The bearing came out about 64° off perpendicular. The row ran at the wrong angle, its footprints stopped lining up with the edge, and every candidate along it failed containment or the lane-overlap check. So it produced nothing, and nothing flagged it.

The fix takes the two unit perpendiculars from the edge itself, which are exact whatever the shape, and only uses the crude centroid to choose which one points inward.

const nx = -dy / len;
const ny = dx / len;
const sign = (centroid.x - mid.x) * nx + (centroid.y - mid.y) * ny >= 0 ? 1 : -1;
return (Math.atan2(ny * sign, nx * sign) * 180) / Math.PI;

So the centroid went from supplying an angle to supplying a sign. A sign only needs the centroid on the correct side of the edge, and a lopsided vertex average manages that reliably. On the tapered fixture, coverage went from 0.503 to 0.537, and smaller units placed rose from one to seven.

The trade-off I took

Both of those fixes left a related problem, a residual band across the middle where the lane pitch doesn't divide the span evenly. Where the leftover is big enough that an inserted lane's flanking rows are guaranteed clear depth beside the existing rows, a lane goes in. That took the diamond from 53.4% to 59.4% and the mid-size rectangle from 58.1% to 64.2%, and left the large rectangle unchanged at 67.9%.

Below that threshold a new lane recovers nothing, so the existing lanes are redistributed evenly across the span instead. There's nothing new to collide with, the lanes just widen to absorb an even share. On a trapezoid fixture that closed the void and took coverage from 60.2% to 58.2%.

That's worse, and I kept it. The wider lanes displace a few stray units a later pass had squeezed into the margins, but I think wider lanes with no dead band down the middle beat a better number with a strip nobody can use. If I'd special-cased the residual so the metric didn't move, I'd have been adding a rule just to protect a score.

One diamond fixture held flat at 0.539 throughout. Before writing that off as a fix that hadn't worked, I measured its residual. It was 2.77m, against a smallest unit 2.991m long, so nothing fits there at any angle and there was nothing for the fix to recover.

Published geometry · invariants · trade-offs