Skip to content

d2cycle: trace arcs to shape borders (close #1578)#1

Open
yasumorishima wants to merge 4 commits into
alixander-cycle-basefrom
feat/cycle-border-tracing
Open

d2cycle: trace arcs to shape borders (close #1578)#1
yasumorishima wants to merge 4 commits into
alixander-cycle-basefrom
feat/cycle-border-tracing

Conversation

@yasumorishima
Copy link
Copy Markdown
Owner

@yasumorishima yasumorishima commented May 2, 2026

Fork-only Draft PR for CodeRabbit / CodeQL review of the d2cycle border-tracing fix on top of alixander/d2:cycle (upstream PR terrastruct#2362).

Scope of this PR

This branch was forked from alixander/d2:cycle (the head of the open upstream PR terrastruct#2362), then a single new commit was added:

d2cycle: trace arcs to shape borders via analytic circle-rect intersection

So the diff against master contains:

Reviewers should focus on the latest commit (d2cycle: trace arcs to shape borders ...).

Problem

In upstream PR terrastruct#2362, cycle edges were sampled as a 30-point arc between source and destination centers, then handed to Edge.TraceToShape to clip to 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, not on the border.

The bounty (terrastruct#1578, $50) asks for the curves to start and stop exactly on the source / destination shape borders.

Fix

Solve the layout circle against each rectangle's four edges analytically (Segment.IntersectCircle, new helper in lib/geo/segment.go) and limit the arc to the angles where it crosses the source and destination borders. The arc's first and last sample then sit exactly on the shape boundaries.

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 the shape boundaries (visually verified).
  • d2layouts/... and lib/geo/... unit tests pass.

Why this PR is on a fork only

Summary by CodeRabbit

  • Bug Fixes

    • Cycle-diagram edges now use circular-arc routing with improved clipping to shape borders and a safe straight-line fallback when arcs are invalid.
  • Tests

    • Updated expected cycle-diagram outputs to reflect the new routing geometry.
    • Added tests covering segment–circle intersection cases for robust geometric behavior.

…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.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 2, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f65f863c-5350-4342-b8b8-42fe008cd0c4

📥 Commits

Reviewing files that changed from the base of the PR and between bc6bb7e and e032e56.

📒 Files selected for processing (1)
  • d2layouts/d2cycle/layout.go

📝 Walkthrough

Walkthrough

Reworks circular-arc edge routing to sample arcs on a computed layout circle and clip endpoints to shape outlines; introduces segment–circle intersection support; updates e2e fixture route coordinates to match the new routing outputs.

Changes

Circular arc routing & geometry

Layer / File(s) Summary
Geometric Utility
lib/geo/segment.go, lib/geo/segment_test.go
Adds Segment.IntersectCircle(center *Point, radius float64) []*Point and unit tests covering two/one/zero intersections, tangent, off-center, vertical chord, endpoint-on-circle, and degenerate zero-length segment.
Radius calculation
d2layouts/d2cycle/layout.go
calculateRadius replaced: computes min radius from object max size and PADDING using (maxSize/2 + PADDING) / sin(pi/numObjects) and clamps to MIN_RADIUS.
Arc Routing Core
d2layouts/d2cycle/layout.go
createCircularArc(edge, radius) rewritten to compute source/destination angles, find boundary angles via box–circle intersections, sample arc points at ARC_STEPS on the layout circle, clip first/last points to true shape borders using shape.TraceToShapeBorder, set edge.Route/edge.IsCurve, and fallback to fallbackStraightRoute (with clipToShapeBorder) when sweep/angles are invalid. Removed prior center-to-center sampling and related clamping.
Straight-route clipping
d2layouts/d2cycle/layout.go
Introduces fallbackStraightRoute and clipToShapeBorder to produce straight segments with endpoints clipped to shapes when arc routing is not applicable.
Tests / Fixtures
e2etests/testdata/txtar/cycle-diagram/dagre/board.exp.json, e2etests/testdata/txtar/cycle-diagram/elk/board.exp.json
Updated route coordinate arrays for connections 1.(a -> b)[0], 1.(b -> c)[0], 1.(c -> d)[0], 2.(a -> b)[0], 2.(b -> c)[0], and 3.(a -> b)[0] to reflect the recomputed arc sampling; other fixture fields unchanged.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

"I hop along the circle's rim,
I trace the arcs both faint and trim.
Endpoints tucked to shapes so neat,
New routes sampled, curves complete.
A rabbit cheers — diagrams meet."

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'test' is vague and does not meaningfully describe the specific change.
Description check ✅ Passed The PR provides comprehensive context, problem statement, solution approach, and testing details despite not following the repository template format.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/cycle-border-tracing

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

@yasumorishima yasumorishima changed the base branch from master to alixander-cycle-base May 2, 2026 06:06
@yasumorishima yasumorishima marked this pull request as ready for review May 2, 2026 06:53
@yasumorishima
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 2, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@d2layouts/d2cycle/layout.go`:
- Around line 163-184: The problem is boxCircleIntersectionAngles uses boxEdges
(via boxEdges) so intersections are computed against the bounding box instead of
the actual node outline; update boxCircleIntersectionAngles to accept or derive
the real shape boundary and compute intersections against that boundary (e.g.,
call a shape-specific IntersectCircle or build edges from the node's actual path
instead of geo.Box), and remove/replace boxEdges usage; change or add a helper
(instead of boxEdges) that returns the true shape segments for
circles/ovals/polygons (or use an existing shape.IntersectCircle method) so arc
endpoints lie on the rendered shape outline rather than the bounding rectangle.

In `@lib/geo/segment.go`:
- Around line 145-159: The quadratic root loop can append the same intersection
twice when disc == 0 (tangent); modify the logic in the intersection code that
computes disc, sqrtDisc and iterates the two t values so duplicate roots are
deduplicated before appending to pts (e.g., only consider each t once or skip
appending if a previously appended point with the same coordinates already
exists within a small epsilon); keep references to the existing symbols (disc,
sqrtDisc, pts, NewPoint, s.Start, dx, dy) and ensure IntersectCircle returns
only unique intersection points.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 16518446-a1e4-4dfe-89c1-237617d7b906

📥 Commits

Reviewing files that changed from the base of the PR and between 89bd2e8 and 00778a6.

⛔ Files ignored due to path filters (2)
  • e2etests/testdata/txtar/cycle-diagram/dagre/sketch.exp.svg is excluded by !**/*.svg
  • e2etests/testdata/txtar/cycle-diagram/elk/sketch.exp.svg is excluded by !**/*.svg
📒 Files selected for processing (5)
  • d2layouts/d2cycle/layout.go
  • e2etests/testdata/txtar/cycle-diagram/dagre/board.exp.json
  • e2etests/testdata/txtar/cycle-diagram/elk/board.exp.json
  • lib/geo/segment.go
  • lib/geo/segment_test.go

Comment thread d2layouts/d2cycle/layout.go
Comment thread lib/geo/segment.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.
@yasumorishima
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 2, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@yasumorishima
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 2, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@d2layouts/d2cycle/layout.go`:
- Around line 88-103: The fallback logic uses node centers (srcCenter/dstCenter)
when nextBoundaryAngle misses, causing edges to originate at centers and
breaking TraceToShapeBorder; instead, compute border-clipped points using the
existing shape-border helper (e.g., TraceToShapeBorder or the same border
clipping used elsewhere) for both source and destination whenever startAngle or
endAngle fall back to srcAngle/dstAngle or when sweep <= 0 or the end<=start
case; update the branches around nextBoundaryAngle and the calls in
fallbackStraightRoute to return points produced by tracing/clipping to the shape
border (using the edge.Src.Box and edge.Dst.Box with the respective angle or
center-to-circle intersection) rather than returning srcCenter/dstCenter so all
three failing cases preserve shape-border endpoints.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 525dd801-bf6c-4ad7-af6e-d9e96941a86b

📥 Commits

Reviewing files that changed from the base of the PR and between 00778a6 and bc6bb7e.

📒 Files selected for processing (3)
  • d2layouts/d2cycle/layout.go
  • lib/geo/segment.go
  • lib/geo/segment_test.go
✅ Files skipped from review due to trivial changes (1)
  • lib/geo/segment.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/geo/segment_test.go

Comment thread d2layouts/d2cycle/layout.go
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.
@yasumorishima
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 2, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@yasumorishima
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 2, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@yasumorishima
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 2, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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