fix(map): every overlap is resolved, not negotiated - #586
Conversation
#585 stopped asking where containers were concerned: `separateBoxes` runs after `computeBoxes` and translates whole clusters until no two boxes are closer than `BOX_GAP`, because a force settles wherever it balances whatever pulls the other way and a contact constraint does not. Every other overlap on the map was still a force, and lost the same argument. #584's constraint is the clearest case. `STRAY_PUSH` is 6; an org-level service is held to the shared ring by the strongest target weight there is. They meet with the service parked partway inside a container and stop. And the ring it is held to clears the *targets* of everyone's owned services by a radial constant that knows nothing about box padding, caption width or the band a name chip hangs in — so a service landing on a container is the ordinary case. The pass is now general. Everything it separates reduces to a rectangle, so one contact rule serves a container, the chip a folded container leaves behind, and a node that belongs to no open container. A container's *member* is deliberately not one of them: its cluster's rectangle already encloses it, so a member intruding on another container means the two rectangles intersect, which is said by moving both clusters whole rather than by shearing one ball out of the fleet it is drawn with. That also drops two things the stray force did: pushing a node hidden inside a folded container, and pushing one member of a cluster on its own. `separateNodes` is the other half — every visible pair, measured on what is drawn. The repulsion reasons in `radiusOf`, a circle around the ball, while a node is the ball *and* the caption under it, and the caption is the wider of the two: siblings whose minimum is `pad + 18` routinely have their labels lying across each other. It runs before `computeBoxes`, because a container is built to hug its members and a correction made after the rectangle was measured would leave `draw` stroking an outline a frame behind the cluster inside it. ## Why the springs had to change too A positional pass that fights a spring is a map that twitches forever, and that is what this did at first: measured on the fixture below, chi's cluster never stopped moving — 2.5px of travel with 0.03px to show for it, the signature of a correction being made and undone. Three changes make the forces want what the pass enforces, so that by the time it runs there is nothing left to correct. `clearance` is the distance along a given direction at which two nodes clear each other, and it is now the floor under both fixed-distance springs and under the repulsion's minimum. A spring that asks for a distance at which its pair overlaps is one the pass argues with every frame. This is #583's reasoning a level down: a container has to be big enough for its own name, and a ring has to be wide enough for the names on it. `restLength` is the rest of it. A spoke can be packed tighter than its captions fit — a user, an agent, a subagent and a service on radii chosen without reference to text — and no per-pair floor fixes that, because the pair that overlaps is not the pair the spring joins. So above the layout's number the *targets* have the say: `separateNodes` already moves a corrected node's target with it, and reading the rest length back off the targets is what stops the correction being argued with. `setGraph` re-seeds them, so a new fleet starts from the layout's numbers again. ## Verification `dashboard/scripts/screenshot-live-map-crowd.mjs`, a real-stack fixture of the reported shape: clusters made of services rather than agents, one user owning six, org-level instances on a ring that crosses the containers, two long agent names on one ring, and a folded cluster whose chip is loose on the map. It asserts rather than illustrates. Containers are drawn on the canvas, but nodes and chips are DOM, so "nothing overlaps anything" is checkable — and so is "it settles", which is the failure a positional constraint invites. On the same fixture: | | overlapping pairs | motion over 5s | |---|---|---| | origin/dev | 12 | none | | this branch | none | none | Positions, not pixels: the live indicator breathes and the canvas repaints regardless, so comparing two screenshots would say nothing about the layout.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Code diff size+415 / −87 across 1 file (net +328)
Source files under |
The check paired the two snapshots by index, and nothing said they had to be the same length. A node leaving between them — a fade-out completing, a fold — makes `after` the shorter array, and `before[i].x - undefined` is `NaN`, which is not greater than 0.5, so the check passes by failing to compute. A settle check that goes quiet exactly when the map is doing something is worse than no check. The two snapshots now have to describe the same shapes in the same order, and say so out loud when they do not: a settled map does not change what it is drawing, so that is a failure in its own right rather than a reason to skip the comparison. Not keyed by label instead, because labels repeat — three users can each own an instance called `notion`. Raised by Seer on #586.
|
Seer's finding was right: the settle check paired the snapshots by index with nothing requiring equal length, so a node leaving between them made Fixed in b4bcb5a. Not keyed by label as suggested, because labels repeat here — three users each own an instance called Re-ran on the real stack: 26 shapes, no overlap, nothing moved over 5s. |
#585 stopped asking where containers were concerned.
separateBoxesruns aftercomputeBoxesand translates whole clusters until no two boxes are closer thanBOX_GAP, because a force settles wherever it balances whatever pulls the otherway and a contact constraint does not. Every other overlap on the map was still
a force, and lost the same argument.
What was still being negotiated
#584's constraint.
STRAY_PUSHis 6; an org-level service is held to theshared ring by the strongest target weight on the map. They meet with the
service parked partway inside a container and stop. And the ring it is held to
clears the targets of everyone's owned services by a radial constant that
knows nothing about box padding, caption width, or the band a name chip hangs
in — so a service landing on a container is the ordinary case, not the edge one.
Node against node. The repulsion reasons in
radiusOf, a circle around theball, while a node is the ball and the caption under it, and the caption is
the wider of the two. Siblings on one ring, whose minimum is
pad + 18,routinely have their labels lying across each other.
A folded cluster's chip was in no constraint at all.
The pass is now general
Everything it separates reduces to a rectangle, so one contact rule serves a
container, the chip a folded container leaves behind, and a node that belongs to
no open container. A container's member is deliberately not one of them: its
cluster's rectangle already encloses it, so a member intruding on another
container means the two rectangles intersect — which is said by moving both
clusters whole rather than by shearing one ball out of the fleet it is drawn
with. That also drops two things the stray force did: pushing a node hidden
inside a folded container, and pushing one member of a cluster on its own.
separateNodesis the other half, and runs beforecomputeBoxes: a containeris built to hug its members, so a correction made after the rectangle was
measured would leave
drawstroking an outline a frame behind the clusterinside it.
Why the springs had to change too
A positional pass that fights a spring is a map that twitches forever, and that
is what this did at first. On the fixture below, chi's cluster never stopped
moving: 2.5px of travel with 0.03px to show for it, which is the signature of a
correction being made and undone. Three changes make the forces want what the
pass enforces, so that by the time it runs there is nothing left to correct.
clearance— the distance along a given direction at which two nodes clear eachother — is now the floor under both fixed-distance springs and under the
repulsion's minimum. A spring asking for a distance at which its pair overlaps
is one the pass argues with every frame. This is #583's reasoning a level down:
a container has to be big enough for its own name, and a ring has to be wide
enough for the names on it.
restLengthis the rest of it. A spoke can be packed tighter than its captionsfit — a user, an agent, a subagent and a service on radii chosen without
reference to text — and no per-pair floor fixes that, because the pair that
overlaps is not the pair the spring joins. So above the layout's number the
targets have the say:
separateNodesalready moves a corrected node's targetwith it, and reading the rest length back off the targets is what stops the
correction being argued with.
setGraphre-seeds them, so a new fleet startsfrom the layout's numbers again.
Verification
dashboard/scripts/screenshot-live-map-crowd.mjsseeds the reported shape onthe real stack: clusters made of services rather than agents, one user owning
six, org-level instances on a ring that crosses the containers, two long agent
names on one ring, and a folded cluster whose chip is loose on the map.
It asserts rather than illustrates. Containers are drawn on the canvas, but
nodes and chips are DOM, so "nothing overlaps anything" is checkable — and so is
"it settles", which is the failure a positional constraint invites.
origin/devPositions, not pixels: the live indicator breathes and the canvas repaints
regardless, so comparing two screenshots would say nothing about the layout.
Before —
origin/dev, same fixturebilling-sync's caption lies acrossnotion;eventbrite's crossesrelease-cutter;release-cutter.verifier's crosseshubspot; both of bruno'sservices sit under their agents' labels.
After
One container folded
dagmar.olsen +9is loose on the map and stays clear of everything.Full frames:
before ·
after
Also re-ran
screenshot-live-map.mjsunchanged, for the traffic and collapseshots this does not aim at.
🤖 Generated with Claude Code
https://claude.ai/code/session_018A3Bie3VNycPZC1bEU4jom