Skip to content

shape: cycle - trace arcs to shape borders (close #1578)#2742

Open
yasumorishima wants to merge 6 commits into
terrastruct:masterfrom
yasumorishima:feat/cycle-border-tracing
Open

shape: cycle - trace arcs to shape borders (close #1578)#2742
yasumorishima wants to merge 6 commits into
terrastruct:masterfrom
yasumorishima:feat/cycle-border-tracing

Conversation

@yasumorishima
Copy link
Copy Markdown

Summary

Closes #1578.

This is a continuation of #2362 by @alixander. That PR introduced the shape: cycle layout but the curves did not start or stop on shape borders, which the bounty on #1578 asks to fix. This branch is based on alixander:cycle (the head of #2362) and adds the missing border-tracing on top, plus tests and a couple of small geometry fixes.

If preferred, I am happy to either land this as the supersede PR or rebase the four new commits onto a fresh branch off master. Glad to follow whichever workflow suits the maintainers best.

Problem

In #2362 the cycle edges were sampled as a 30-point arc between source and destination shape centers, then handed to Edge.TraceToShape to clip the route to the shape borders. With small shapes on a layout circle, every arc sample near the source center fell inside the source rectangle, so the segment-edge clipper found no crossing and left the curve starting at the shape center instead of on the border.

Fix

Solve the layout circle against each shape analytically and limit the arc to the angles where it crosses the source and destination borders, then refine the endpoints onto the actual shape outline:

  • New Segment.IntersectCircle(center, radius) helper in lib/geo solves a quadratic for segment-circle crossings (single point on a tangent contact).
  • d2cycle.createCircularArc now intersects the layout circle with the four edges of each shape's bounding box, picks the angles where the arc enters / exits, and samples between them.
  • The endpoints are then passed through shape.TraceToShapeBorder so non-rectangular shapes (circle, oval, hexagon, cloud, ...) end on their actual outline rather than on the bounding box border. For rectangles this is a no-op.
  • When the analytic step degenerates (zero sweep, no crossing in arc range), fallbackStraightRoute produces a straight connection whose endpoints are also clipped to the shape border via the same helper, instead of returning the shape centers.

The renderer side (cubic-bezier emission for IsCurve) is left exactly as in #2362, so the visual style of the curves matches that PR.

Tests

  • Regenerated e2etests/testdata/txtar/cycle-diagram/{dagre,elk}/{board.exp.json,sketch.exp.svg} with TA=1. The new SVGs render every cycle connection starting and ending on a shape border.
  • New TestSegmentIntersectCircle in lib/geo/segment_test.go covers eight cases (chord, tangent, off-origin circle, endpoint on circle, both endpoints inside / outside, vertical chord, degenerate zero-length segment).
  • go test ./d2layouts/... ./lib/geo/... all green.
  • Full go test ./e2etests/... was run; only failures are the existing Go 1.26 url.URL JSON field-order shifts that already affect master (e.g. icon-label, cycle-order, sql-table-reserved) and are unrelated to this change.

Self-review

Both findings from CodeRabbit on the equivalent fork PR were addressed:

  • Endpoints are now clipped to the actual shape outline (not just the bounding box) via shape.TraceToShapeBorder.
  • IntersectCircle no longer reports a tangent contact twice (disc > 0 guard added with regression test).
  • The fallback paths now produce shape-border endpoints instead of returning the shape centers.

Bounty

/claim #1578

alixander and others added 6 commits February 19, 2025 08:04
…ction

Closes terrastruct#1578.

The previous cycle layout sampled an arc between shape centers and relied
on TraceToShape to clip the route. With small shapes sitting on a large
radius, every arc sample near the source center fell inside the rectangle,
so the segment-based clipper never found an edge crossing and left the
curve starting at the shape center.

Now the layout solves the layout circle against each rectangle's four
edges analytically and limits the arc to the angles where it crosses the
source and destination borders, so every connection starts and ends
exactly on a shape boundary.

Also adds Segment.IntersectCircle to lib/geo and removes the unused
clamp helpers from d2cycle/layout.go.
Addresses CodeRabbit review feedback on #1.

Major: my analytic intersection only used `geo.Box` edges, which is the
shape's bounding box. For non-rectangular shapes (circle, oval, hexagon,
cloud, ...) the bounding box border is not the actual shape outline, so
connections were landing on the bounding box rather than on the shape.
Fixed by passing each arc endpoint through `shape.TraceToShapeBorder`,
which walks the line from the shape center to the box-border point and
returns the intersection with the shape's perimeter (a no-op for
rectangular shapes).

Minor: `Segment.IntersectCircle` appended both quadratic roots even when
the discriminant was zero (a tangent contact), so a graze was reported
as two identical points. Fixed by emitting the second root only when
the discriminant is strictly positive, and added a tangent regression
test case.
Addresses CodeRabbit second-review feedback on #1.

When `nextBoundaryAngle` could not find an arc-range crossing (very
narrow sweep, degenerate geometry, ...) the previous fallbacks let
`startAngle = srcAngle` / `endAngle = dstAngle` and emitted a route
whose first/last point coincided with the shape center, since the
center lies on the layout circle. That regressed the very fix the PR
introduces and also fed `TraceToShapeBorder` a center-as-rectBorder
input.

Now the createCircularArc flow falls through to the straight-line
fallback whenever either boundary angle is missing or the resulting
range is empty, and `fallbackStraightRoute` itself runs each endpoint
through a new `clipToShapeBorder` helper. The helper extends a ray
from the shape center toward the other endpoint, intersects the
bounding box, and refines via `TraceToShapeBorder` for non-rectangular
shapes. Both fallbacks therefore emit shape-border endpoints, matching
the arc-success path.

The cycle-diagram fixture is unchanged because rectangle shapes never
hit the fallback in the existing test cases.
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.

shape: cycle

2 participants