Where a line meets a shape
Clipping a connector to a rectangle, an ellipse, and an arbitrary polygon — two closed forms and one honest loop.
In the code: foundation/geometry.py
A connector between two boxes should not start at their centres; it should start where it leaves the first shape and stop where it enters the second. Get this wrong and every arrow either floats in space or buries its head in the box.
The problem: given a shape's centre and a direction, find where the ray exits the outline. Each shape family has its own trick.
<diagram padding="16">
<row gap="34" align="center">
<rect id="r">rect</rect>
<ellipse id="e">ellipse</ellipse>
<hexagon id="h">polygon</hexagon>
</row>
<connect from="r" to="e"/>
<connect from="e" to="h"/>
</diagram>
Rectangle: scale until you hit an edge
Take the direction (dx, dy) from the centre. Ask how far you could go before
crossing the vertical edges, and before crossing the horizontal ones:
tx = |dx| / halfWidth
ty = |dy| / halfHeight
Whichever is larger is the constraint that binds — that is the edge you leave
through. Scale the direction by 1 / max(tx, ty) and you land exactly on the
boundary. No branching on which of four edges, no line-segment intersection: one
max and one divide.
This is the same slab test used in ray/AABB intersection in renderers, minus the near/far bookkeeping, because here the ray starts inside the box.
Ellipse: squash the problem into a circle
For an ellipse the elegant move is a change of variables. Divide by the radii and the ellipse becomes a unit circle:
scale = 1 / sqrt((dx/rx)² + (dy/ry)²)
Multiply the direction by that and you are on the ellipse. It is the rectangle trick with a Euclidean norm instead of a max norm — which is exactly the relationship between a circle and a square in the first place.
Polygon: there is no trick, and that's fine
An arbitrary polygon has no closed form, so you do the honest thing: walk the edges and intersect the ray with each segment, keeping the nearest hit in front of you. It is O(edges) per connector, and since diagram shapes have a handful of vertices, that cost never shows up.
What makes this worth it is that the outline is the only definition of the shape. Sauvignon defines a mark as vertices in the unit box, and both the rendered path and the connector clipping derive from those same points. Add a triangle and its arrows meet its slanted sides correctly, with no extra code:
from sauvignon import PolygonMark, register_mark
register_mark(PolygonMark("triangle", [(0.5, 0), (1, 1), (0, 1)]))
That is the payoff of treating shapes as data. The alternative — a switch over
shape names in the routing code — means every new shape touches the connector
logic, and it is how you end up with arrows that work for rectangles and quietly
misbehave for everything else.
What clipping alone cannot know
Clipping tells you where a line meets its own endpoints' shapes. It says nothing about the shapes in between — so on its own it would happily run a connector straight through an unrelated box.
The engine layers obstacle-avoiding routing on top of it: a straight connector
whose direct line is blocked is rerouted around the boxes in its way, described
in drawing a graph of the code. Pinning a
from-anchor or to-anchor opts out, for the cases where a line is meant to
pass through what it crosses — a circuit's wires through its gates.
Further reading. Any ray-tracing text on slab-based AABB intersection covers the rectangle case; the polygon case is the standard ray/segment intersection found in every computational-geometry introduction.