Skip to content

Add Fillet 2D#1727

Open
Trzeth wants to merge 3 commits into
elalish:masterfrom
Trzeth:feat-fillet2d-review
Open

Add Fillet 2D#1727
Trzeth wants to merge 3 commits into
elalish:masterfrom
Trzeth:feat-fillet2d-review

Conversation

@Trzeth
Copy link
Copy Markdown
Contributor

@Trzeth Trzeth commented May 20, 2026

This work aims to build a robust fillet algorithm for 2D polygons, inspired by the "rolling ball" method. Currently, the algorithm successfully processes 86 out of 108 test cases. Each test case consists of 12 subtests using different radius, which are scaled relative to the polygon's bounding box. As some cases are particularly complex, we are continuing to refine the algorithm to improve its overall robustness.

@Trzeth
Copy link
Copy Markdown
Contributor Author

Trzeth commented May 20, 2026

Problem

After collecting all locally-valid fillet circles, we still need to verify that each circle is globally valid. This is done by checking whether any other polygon edge intersects the fillet circle.

However, this check becomes numerically unstable in cases where three edges are close enough that the three fillet arcs almost form a complete circle.

The left side of the figure below shows this unstable case. The right side is the result after clustering.
image

Currently my best solution is to avoid validating these nearly-identical circles independently.

Instead, I first collect fillet circles whose geometry is close enough, then force their circle centers to be exactly the same point before running the global validity check. This makes the intersection test more stable for the near-complete-circle case.

The part I am least confident is when finding geometrically similar circles will introduce a manually chosen threshold.

Reproducing

The test in figure might be reproduce by running --gtest_filter=Polygon.Fillet.UShape with radius = 1.2368517001458557.
But since it's floating point related, I'm not quite sure if it can repoduce on other machine.

This branch has a predefined .vscode/tasks.json, so you can run the test by pressing Ctrl + B.
The output will be written to the Testing folder, and the figure should be generated as all_results.png under the working folder.

The key switches are:

// test/polygon_test.cpp L148
int idx = 0; // set to 2 can find more failure cases
// src/cross_section/fillet2d.cpp L839
const bool disableCircleCluster = true; // set to false to enable current solution

Relevant code

The most relevant code is in these two sections

Circle clustering logic:
https://github.com/elalish/manifold/pull/1727/changes#diff-0cdbbfe99708fe80cfb54efc2e08cd37699f0064b2d7cc4009caa8885ff92faeR842-R853

Global validity:
https://github.com/elalish/manifold/pull/1727/changes#diff-0cdbbfe99708fe80cfb54efc2e08cd37699f0064b2d7cc4009caa8885ff92faeR880-R965

@elalish
Copy link
Copy Markdown
Owner

elalish commented May 23, 2026

Okay, I've finally read through your code, more or less. I'm going to summarize the architecture I see, just hitting the high points.

  • BuildCollider: broad phase that produces pairs of edges that may need a fillet between them.
  • CalculateFilletArc: narrow phase, returning TopoConnectionPairs of actual fillets.
    • tests that edges are convex, and that the circle is tangent within each segment.
    • generates fillets (at least calculates the circle center).
    • finds circle clusters (with another collider) to determine if fillets are invalid due to a third edge too near.
  • Tracing: assembly of edge portions and fillets into output polygons.
    • a bit hard to follow, lots of hash map searching.

First, this code needs to be much shorter and simpler to be readable and debuggable. Make your primary named functions process single items, rather than vectors of them, and avoid lambdas to make it clearer what your function's inputs and outputs are.

There will be no epsilons in this code. The most important function you need is bool RadiusFits(ivec3 edgeIndices, float radius). The three edgeIndices must be in sorted order, so just order the three at the top of the function. These are the startVerts of the three edges we care about, indexing back into whatever polygon data structure you have, which in turn gives you their corresponding endVerts (next along the polygon). These three edges form a triangle - find the radius of the incircle. Return true if the triangle is CCW and the incircle radius > fillet radius.

Adjust your architecture:

  • BuildCollider: broad phase that produces pairs of edges that may need a fillet between them. (looks good)
  • FindFilletPairs: narrow phase, returning TopoConnectionPairs of actual fillets. (replace TopoConnectionPairs struct with simple std::pair<int, int> - left edge startVert, right edge startVert).
    • tests that edges are convex, and that the circle is tangent within each segment. (simplify)
    • for each pair that passes, search other pairs with the same first edge (using the existing broad phase result pairs). Each of these is a set of three edges - pass them to RadiusFits. If any don't fit, the input pair is invalid.
  • Tracing: assembly of edge portions and fillets into output polygons.
    • For each edge, make a vector of starting tangent points and ending tangent points for each fillet pair that connects to that edge.
    • Sort these tangents along the edge - the two vectors must be the same length, so pair them up into start-end sub-edge segments.
    • Walk these segments and fillets, generating the circular arcs as you go.

Note also that there's no need for epsilon in checking for convexity: we don't want nearly colinear angles anyway. circularSegments tells us the minimum angle we will generate in a fillet arc - so we have no need to build a fillet for any angle flatter than that.

@Trzeth
Copy link
Copy Markdown
Contributor Author

Trzeth commented May 25, 2026

Thanks for the detailed review!

It took me a little time to move past my original approach and think through whether this also applies to cases where four or more edges enclose a circle. After considering it, doing a set of three way is cleaner and more robust than clustering or detecting a closed-loop.

I’ll start implementing it right away.

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.

2 participants