Server-side viewport culling for LiveFlow.
Filters nodes and edges to only render those visible within the current viewport, dramatically reducing DOM size for large flows.
How It Works
The viewport transform maps flow-space to screen-space:
screen_x = flow_x * zoom + viewport.x
screen_y = flow_y * zoom + viewport.yInverting gives us the visible flow-space rectangle:
flow_left = -viewport.x / zoom
flow_top = -viewport.y / zoom
flow_right = (-viewport.x + container_width) / zoom
flow_bottom = (-viewport.y + container_height) / zoomA node is visible if its bounding box overlaps this rectangle (with a margin). Unmeasured nodes use an estimated size (200x100) for the visibility check, so they render when near the viewport and get measured by JS on first appearance.
When zoomed out far enough that more than max_visible nodes would be in view,
nodes are sampled by proximity to the viewport center to cap DOM size.
Summary
Functions
Filters a list of edges to only those with at least one visible endpoint.
Filters a list of nodes to only those visible in the viewport.
Checks if a node is visible within the given flow-space rectangle.
Computes the visible rectangle in flow-space coordinates.
Functions
Filters a list of edges to only those with at least one visible endpoint.
An edge is kept if its source OR target node is in the visible set. This prevents edges from "popping" in when both endpoints are just outside the viewport.
@spec cull_nodes(list(), LiveFlow.Viewport.t(), number(), number(), keyword()) :: {list(), MapSet.t()}
Filters a list of nodes to only those visible in the viewport.
Returns {visible_nodes, visible_node_ids} where visible_node_ids
is a MapSet used for edge culling.
When more than max_visible nodes fall within the viewport (e.g. when
zoomed out very far), nodes closest to the viewport center are prioritized
to cap DOM size and prevent browser freezes.
Checks if a node is visible within the given flow-space rectangle.
Unmeasured nodes use their position as a point check (with a generous default size estimate) so they only render when near the viewport. This prevents rendering thousands of off-screen unmeasured nodes at once.
@spec visible_rect(LiveFlow.Viewport.t(), number(), number(), number()) :: map()
Computes the visible rectangle in flow-space coordinates.
Returns %{left: float, top: float, right: float, bottom: float}.
The margin (in flow-space pixels) extends the rectangle in all directions
to preload nodes just outside the viewport for smooth panning.