Skip to content

Optimal network finder (EMST): shortest links joining a set of points - #16

Merged
valentinps merged 1 commit into
world-borderfrom
emst-network-finder
Aug 1, 2026
Merged

Optimal network finder (EMST): shortest links joining a set of points#16
valentinps merged 1 commit into
world-borderfrom
emst-network-finder

Conversation

@valentinps

Copy link
Copy Markdown
Owner

Search-bar-only planning tool: give it a set of points and it draws the
shortest set of links joining them all. For working out where belts, trains
or power lines should actually run.

No permanent UI — it is reached by typing "Optimal network finder (EMST)"
(or just "emst") in the search bar, via a new tool kind in the search
catalog. Points come from four places, all landing in the same list: clicking
empty map, clicking a building (which takes that building's exact position and
name), the whole rectangle selection in bulk, or typed X/Y metres.

The maths (map/static/map/emst.js)

Both link styles are exact and both O(n log n). 100,000 points solve in
about a second.

Style What it minimises How it stays n log n
Straight Total length, point to point (Euclidean MST) The MST sits inside the Delaunay triangulation, so a sweep-hull triangulation cuts the candidate set from n²/2 edges to under 3n, then Kruskal
X / Y only Total length with every link an L of two axis-aligned legs (rectilinear/L1 MST) The MST sits inside the octant graph, and inside one octant the L1 distance collapses to a linear function of the far endpoint — so four Fenwick sweeps find all eight nearest neighbours

X/Y-only is not the straight tree with corners drawn on: changing the
metric changes which points the optimal tree even connects, so it is solved
from scratch.

Everything is computed in map pixels. The world → map projection is a single
uniform scale with no rotation, so a tree that is optimal there is optimal in
metres, and "along X" on screen is "along X" in the world.

Destination (optional)

Mark one point as the destination and a slider trades total network length
against how far every point has to travel through the tree to reach it.
That is the Prim–Dijkstra tradeoff: Prim's algorithm with the key changed to
alpha * (tree distance from the destination) + edge weight.

  • alpha = 0 is literally Prim → the same exact MST as before.
  • alpha = 1 is literally Dijkstra over a candidate set augmented with the
    destination's own edges → every point joins it directly, which is the exact
    minimum of summed trip distance.
  • In between it is a genuine trade, and a heuristic — "shortest network whose
    trips sum to at most X" is NP-hard. The panel wording says so rather than
    implying an optimum.

Measured on 5,700 machines from a real save: 40% along the slider costs 9%
more network
and cuts total travel by 47% (16.4 → 17.9 km of links,
5,496 → 2,923 km of travel).

With no destination set, nothing changes from the plain shortest-network
behaviour — no slider, no extra readout.

Correctness gate (tools/check_emst.js)

The thing that makes both algorithms fast is that they only ever hand Kruskal
a small candidate set that provably contains the MST. If either set is ever
short of an edge, the result is still a spanning tree — just a silently longer
one. Nothing about the output looks wrong, so it is diffed against a
brute-force O(n²) Prim over the complete graph:

node tools/check_emst.js [iterations]

Point sets are chosen to break naive geometry: grids (endlessly co-circular),
points on a circle, collinear runs, exact and near duplicates, tight clusters,
and the map's real coordinate range. With a destination it additionally checks
that alpha=0 is still the exact MST, that alpha=1 is the exact star, and
that the trip distances the panel reports match a walk of the tree it actually
drew. It earned its keep by catching an inverted in-circle sign on its first
run.

Two fixes found while using it

  • Selected pins were not outlined (selection.js). A pin — vehicles,
    collectables, players, crash sites — is drawn as a circle floating above its
    coordinate with a tail pointing down at it, so marking the bare coordinate
    marked the tail's tip and left the pin itself looking untouched. It is now
    ringed as well, using the same geometry _paintPin and the hover highlight
    already use.
  • The same object could be added twice. Points now carry the object's save
    id, so clicking a 100 m foundation's far corner counts as the same object
    rather than a second point 100 m away. Clicking it again removes it.

Verified

Driven end to end in the real app with Playwright: search → panel → map clicks
→ object clicks → typed coordinates → compute → mode switch → per-link hover
tooltips → bulk add of a 29,000-object selection → destination and slider →
copy → close/reopen. No console errors, at three window sizes.

Scale: the panel lists at most 200 rows (with an explicit "and N more"), so a
29,000-point network adds in 0.1 s and draws in 2.4 s while the tree itself
takes 77 ms.

🤖 Generated with Claude Code

A planning tool for "where should the belts/trains/power actually run".
Reachable only by typing its name in the search bar (a new "tool" catalog
entry, so it costs no permanent UI), it takes a set of points -- clicked
empty map, clicked buildings, the whole rectangle selection in bulk, or
typed X/Y metres -- and draws the shortest set of links joining them.

Two link styles, both exact and both O(n log n) (map/static/map/emst.js):

  Straight    Euclidean MST. Sits inside the Delaunay triangulation, so a
              sweep-hull triangulation cuts the candidate set from n^2/2
              edges to under 3n and Kruskal does the rest.
  X / Y only  Rectilinear (L1) MST -- NOT the straight tree with corners
              drawn on: changing the metric changes which points the optimal
              tree even connects. Sits inside the octant graph, and inside
              one octant the L1 distance collapses to a linear function of
              the far endpoint, so four Fenwick sweeps find all eight
              nearest neighbours at once.

Optionally one point is a DESTINATION, and a slider trades total network
length against how far every point has to travel through the tree to reach
it (Prim-Dijkstra: Prim with the key alpha * tree-distance + edge weight).
alpha = 0 is Prim, so still the exact MST; alpha = 1 is Dijkstra over a
candidate set augmented with the destination's own edges, so every point
joins it directly -- the exact minimum of summed trip distance. Between the
two it is a heuristic trade, and the panel says so rather than implying an
optimum that is NP-hard to find. On 5,700 real machines, 40% along the
slider costs 9% more network and cuts total travel by 47%.

Everything is computed in map pixels: the world projection is one uniform
scale with no rotation, so an optimal tree there is optimal in metres, and
"along X" on screen is "along X" in the world.

tools/check_emst.js is the gate. A short candidate set still yields a
spanning tree, just a silently longer one, so every result is diffed
against brute-force Prim over grids, circles, collinear runs, duplicates
and clusters -- plus, with a destination, that alpha=0 is still the MST,
that alpha=1 is the exact star, and that the trip distances the panel
reports match a walk of the tree it actually drew. It caught an inverted
in-circle sign on its first run. 100,000 points solve in about a second.

Also here, both found by using the tool:

- selection.js: selecting a PIN (vehicles, collectables, players, crash
  sites) marked the bare coordinate, which is where the tail points -- the
  pin itself, floating above it, looked untouched. Now ringed, with the
  same geometry the hover highlight already uses.
- The same object can no longer be added twice: points carry the object's
  save id, so clicking a 100m foundation's far corner counts as the same
  object rather than a second point. Clicking it again removes it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@valentinps
valentinps merged commit 1b80a61 into world-border Aug 1, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant