Skip to content

feat: libavoid routes after a drop, edges on one port as one path; a dragged node's edges wait for the drop - #115

Merged
endrix merged 4 commits into
mainfrom
feat/libavoid-after-drop-on-main
Nov 6, 2026
Merged

endrix merged 4 commits into
mainfrom
feat/libavoid-after-drop-on-main

Conversation

@endrix

@endrix endrix commented Nov 6, 2026

Copy link
Copy Markdown
Owner

Stacked on #112 (the base branch); GitHub retargets it to main once #112 merges.

What

  • After a drop, the routes an edge cannot keep are drawn by libavoid, with the edges on one port as one shared path. The layout draws the edges into a port as a trunk; the built-in router gives every edge its own channel and drew a comb. libavoid did the same because the wrapper started every edge on a port at its own staggered point, which is why it was left opt-in. Now they start at one point and libavoid keeps them on one path for as long as they go the same way: a trunk with a junction. The half-nudge kink the stagger was for is taken out after routing.
  • libavoid is the router from now on. The built-in one is the fallback when libavoid is unavailable or cannot solve an edge, the choice for a reroute of every edge in a diagram of more than 200 edges, and available throughout with WORKFLOW_DIAGRAM_ROUTER=manhattan. The vendored build is found from the sources, so dev runs and tests use it without an environment variable.
  • The edges of a node being dragged are not drawn while it moves. They come back with the model update that carries the drop's routes, the way a schematic editor redraws a block's nets after it is placed. A move by arrow key hides nothing. If no routes arrive within 1.5s, or the drag is cancelled, or the reroute fails, the edges are drawn as they were.

Checked

  • gemmini state document: a 60px and a 400px move of "The loop configuration" keep ELK's trunk; the one straight edge libavoid routes joins the port cleanly.
  • rob:Com fixture (fixtures-shared-port.json): the two edges off one port share a stub and part at a junction; before, the first had an 8px kink.
  • New tests: shared path on a fan-in; the two rob:Com tests rewritten for the shared design, with the staggered one kept behind sharedPortPaths: false; the trunk tests assert libavoid draws the refused routes; four client tests for hide and reveal. Server 440, client 358 tests, typecheck pass.

https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv

…e path; a dragged node's edges wait for the drop

The routes an edge cannot keep after a move (see the slide) are now
drawn by libavoid, with every edge on a port started at one point so
that libavoid routes them as one shared path: a trunk with a junction,
as the layout draws a fan-in. The staggered start the wrapper used gave
every edge on a port its own channel, which is why libavoid drew the
same comb as the built-in router and was left opt-in. The half-nudge
kink the stagger was for is taken out after routing instead: a step
smaller than the nudging distance at a port goes back onto the port's
row. libavoid is the default router now; the built-in one is the
fallback when libavoid is unavailable or cannot solve an edge, the
choice for a reroute of every edge in a diagram of more than 200 edges,
and available throughout with WORKFLOW_DIAGRAM_ROUTER=manhattan. The
vendored build is found from the sources too, so a dev run and the
tests route with libavoid without an environment variable.

On the client, the edges of a node being dragged are not drawn while it
moves. Before, they were drawn on their stored routes with a stub
trailing the pointer, then jumped at the drop. They come back with the
model update that follows the drop's reroute, or after 1.5s if none
comes, or at once when a drag is cancelled or the reroute fails. A move
that is finished as it arrives (an arrow key) hides nothing.

Verified on the gemmini state document: a 60px and a 400px move of
"The loop configuration" keep ELK's trunk, and the one straight edge
libavoid routes joins the port cleanly; on the rob:Com fixture the two
edges off one port share a stub and part at a junction, where the first
had an 8px kink before.

Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv
…rts; reroutes log their time

Reported: after a drop the edges took a second or more to come back.
The hidden edges waited for a model update by name, the one after the
final reroute, and when that was not the update that reached the drag
router they stayed hidden until the 1.5s fallback.

An edge is now hidden only while its stored route does not start and
end at its ports as they stand: the first update with a route for the
node's new place draws it, whichever server round trip brings it (the
ChangeBounds answer already carries one), and a cancelled drag draws
it at once, since the node goes back to where the route is. No model
update handler, so the container parity baseline is as it was. The
fallback stays for a route that never comes.

The reroute handler logs one always-on `[dialogram perf] reroute` line
per persisted reroute, like the layout's: edges kept, edges routed, the
router and the time taken, so a slow drop can be read from the output
channel.

Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv
A pause of more than 0.7s in a drag made the client's quiet timer take the
drag for over: it sent the final reroute, armed the 1.5s fallback, and the
fallback drew the edges again while the pointer was still down, where the
client's live router drew them on their old routes. The quiet timer keeps
its 0.7s only after a finished move; while the pointer is down it waits 5s,
long enough to tell a node held still from a drag that never finishes.

Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv
…again

Whether an edge is hidden for a drag was decided by the model being drawn,
and crossed the edge off the hidden set once its route reached its ports.
A bounds pass the server asks for mid-drag draws its own model in the hidden
layer, with the node still where the server last saw it and the routes
reaching its ports, so that pass crossed the edges off for the rest of the
drag: a drag started before the pass had answered showed its edges on the
live router. The question is now asked without a side effect, a drag starting
within a drop's reveal cancels that reveal, and a reveal that no model update
follows asks for a render, so the edges come back at the fallback too.

Found with a headless probe of the real client: a synthetic drag with a
RequestBoundsAction injected mid-drag.

Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv
@endrix
endrix merged commit b87fa84 into main Nov 6, 2026
1 check passed
@endrix
endrix deleted the feat/libavoid-after-drop-on-main branch November 6, 2026 08:41
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