Skip to content

NURBS curve intersections in 2D#1869

Merged
kennyweiss merged 8 commits into
developfrom
feature/copeland/nurbs_intersect
May 21, 2026
Merged

NURBS curve intersections in 2D#1869
kennyweiss merged 8 commits into
developfrom
feature/copeland/nurbs_intersect

Conversation

@dylan-copeland
Copy link
Copy Markdown
Contributor

Summary

  • This PR adds a small function to find intersections of two NURBS curves in 2D, assuming finitely many intersections.
  • A unit test is included for two simple curves with a single intersection.

@dylan-copeland dylan-copeland self-assigned this May 18, 2026
@dylan-copeland dylan-copeland added the enhancement New feature or request label May 18, 2026
@dylan-copeland dylan-copeland requested a review from kennyweiss May 18, 2026 17:35
Comment thread src/axom/primal/operators/detail/intersect_bezier_impl.hpp Outdated
Copy link
Copy Markdown
Member

@kennyweiss kennyweiss left a comment

Choose a reason for hiding this comment

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

Thanks for the contribution @dylan-copeland !

Please add some additional doxygen/code comments and some additional tests per my PR comments.

Comment on lines +1464 to +1465
* \param [out] p1 The array of parameters for intersection points in n1.
* \param [out] p2 The array of parameters for intersection points in n2.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Presumably, the entries in p1 and p2 correspond to the same point in space. Please mention this in the doxygen.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 9ddf55b.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It's probably also worth mentioning in the doxygen that this method also parameterizes the curves as half-open (since the underlying bezier-bezier intersection routine does too), such that intersections at either terminal endpoint will be ignored.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I added a comment about endpoints in abdb66d.

Comment thread src/axom/primal/operators/detail/intersect_bezier_impl.hpp
Comment on lines +573 to +574
T u_full = axom::utilities::lerp(knots1[i], knots1[i + 1], u_local[k]);
T v_full = axom::utilities::lerp(knots2[j], knots2[j + 1], v_local[k]);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Consider adding a comment that it is inside the range of knots[i] and knots[i+1] because the know array was created with getUniqueKnots()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 9ddf55b.

}

//------------------------------------------------------------------------------
TEST(primal_nurbscurve, nurbscurve_intersections)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please add some more tests, including:

  • curves that don't intersect
  • curves that intersect more than once
  • curves that intersect more than once at the same physical points
  • curves that intersect at a knot vector

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I added tests for the first two suggestions in 9ddf55b. For the third, I am not sure of a good way to make curves in axom intersecting more than once at the same points. There is a way to make a circle up to 2*pi. Making something more interesting would need to be done manually (to find control points, weights, etc), unless someone has suggestions. For the fourth, I made a test that seems to expose issues with intersect_bezier_curves. The test was two circles of radius 1, centered at (0,0) and (2,0), which should touch at (1,0), which should be a knot for both circles. The circles need not be complete (can be arcs). For several variations of this, it failed to find any intersection. Unless I am missing something, intersect_bezier_curves needs to be made more robust. It seems to recursively split curves in half to check each half, using a linear approximation when the curve is within the prescribed tolerance of being linear. This misses some intersections, e.g. two circles that touch at one point.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't know if we expect intersect_bezier_curves to find intersections at points of tangencies. It says as much in the doxygen for intersect( bezier, bezier ), but not for intersect_bezier_curves, so I think that's an absence in the documentation more than anything. That's not to say that there's philosophically wrong with tangent intersections, it's just a considerably hard edge case to handle, and we've been kicking it down the road for a while now.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I added more comments in abdb66d. I also tried to create a test with intersection of two circular arcs of 180 degrees, which have knots 0, 0.5, 1. The arcs intersect at their midpoints, and the intersection is found correctly, but it is not at the expected parameter of 0.5, matching a knot. Evaluating the arcs at points in [0,1] shows the parameterization is unusual, and the location of knot 0.5 is some point far from the midpoint. So it wasn't easy to make a test intersecting at a knot, but it should be possible for a manually defined curve. I'm not sure it is worth the effort to make these tests.

Copy link
Copy Markdown
Member

@kennyweiss kennyweiss May 19, 2026

Choose a reason for hiding this comment

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

I was thinking of something like this:
image

NURBSCurve2D make_cubic_shape()
{
  // Open cubic NURBS curve (degree 3), non-rational.
  // 24 control points, 28 knots (n + p + 2: 24 + 3 + 1 = 28).
  return NURBSCurve2D(
    axom::Array<Point2D> {
      Point2D {-5.0000000000, +3.6000000000}, Point2D {-3.4000000000, +3.0000000000},
      Point2D {-1.8000000000, +2.2000000000}, Point2D {-0.4000000000, +1.0000000000},
      Point2D {+1.0000000000, -0.4000000000}, Point2D {+2.4000000000, -1.6000000000},
      Point2D {+3.6000000000, -2.4000000000}, Point2D {+4.4000000000, -1.0000000000},
      Point2D {+3.6000000000, +0.8000000000}, Point2D {+2.0000000000, +2.0000000000},
      Point2D {+0.0000000000, +2.8000000000}, Point2D {-2.0000000000, +2.0000000000},
      Point2D {-3.2000000000, +0.8000000000}, Point2D {-2.6000000000, -0.8000000000},
      Point2D {-1.0000000000, -1.6000000000}, Point2D {+0.4000000000, -2.2000000000},
      Point2D {+1.6000000000, -2.6000000000}, Point2D {+0.0000000000, -3.4000000000},
      Point2D {-2.2000000000, -3.0000000000}, Point2D {-2.8000000000, -1.4000000000},
      Point2D {-1.0000000000, +0.4000000000}, Point2D {+1.8000000000, +1.8000000000},
      Point2D {+3.4000000000, +3.0000000000}, Point2D {+5.0000000000, +3.6000000000}
    },
    axom::Array<double> {
      0.0000000000, 0.0000000000, 0.0000000000, 0.0000000000,
      0.0476190476, 0.0952380952, 0.1428571429, 0.1904761905,
      0.2380952381, 0.2857142857, 0.3333333333, 0.3809523810,
      0.4285714286, 0.4761904762, 0.5238095238, 0.5714285714,
      0.6190476190, 0.6666666667, 0.7142857143, 0.7619047619,
      0.8095238095, 0.8571428571, 0.9047619048, 0.9523809524,
      1.0000000000, 1.0000000000, 1.0000000000, 1.0000000000
    },
    3 /* degree */);
}

NURBSCurve2D make_ellipse_curve()
{
  // Quadratic rational NURBS ellipse (degree 2), 9-control-point 4-arc construction. 
  // Center, semi-axes:
  //   center      = (+0.055509, +0.246636)
  //   semi-axes   = (2.506091, 2.506234)   (a == b -> circle in this case)
  //   rotation    = +0.0000 degrees
  // Corner control-point weights are w = cos(pi/4) = sqrt(2)/2.
  return NURBSCurve2D(
    axom::Array<Point2D> {
      Point2D {+2.5615994286, +0.2466357797}, Point2D {+2.5615994286, +2.7528699841}, Point2D {+0.0555086484, +2.7528699841},
      Point2D {-2.4505821318, +2.7528699841}, Point2D {-2.4505821318, +0.2466357797}, Point2D {-2.4505821318, -2.2595984246},
      Point2D {+0.0555086484, -2.2595984246}, Point2D {+2.5615994286, -2.2595984246}, Point2D {+2.5615994286, +0.2466357797}
    },
    axom::Array<double> {
      1.0000000000, 0.7071067812, 1.0000000000,
      0.7071067812, 1.0000000000, 0.7071067812,
      1.0000000000, 0.7071067812, 1.0000000000
    },
    axom::Array<double> {
      0.0000000000, 0.0000000000, 0.0000000000, 0.2500000000,
      0.2500000000, 0.5000000000, 0.5000000000, 0.7500000000,
      0.7500000000, 1.0000000000, 1.0000000000, 1.0000000000
    },
    2 /* degree */);
}

Per a python script generated by claude, the intersections should be:

  "approximate_curve_curve_intersections": [
    [
      -1.7060766733448747,
      2.0292202966044366
    ],
    [
      2.044376271360025,
      -1.2782097206437852
    ],
    [
      1.884074237582652,
      1.9604430112255964
    ],
    [
      -2.1644784828526533,
      -0.9161966686461217
    ],
    [
      0.5039968961448462,
      -2.2191102081421588
    ]
  ]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks @kennyweiss for this nice example. I added it in a812569.

axom::Array<T> u_local, v_local;

// Intersect Bezier segment i of n1 with Bezier segment j of n2
intersect(beziers1[i], beziers2[j], u_local, v_local, tol);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we need to handle the case where the intersection is at a knot vector, and could be listed multiple times in the output? (e.g. by deduplicating the list). We should mention how this is handled in the doxygen.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The intersect(bezier, bezier) routine assumes a half-open parameter space (since bezier-bezier intersections use recursive subdivision to get all the intersections), so there shouldn't need to be a deduplicating. If an intersection is listed twice, that's almost always going to suggest some numerical issue.

Copy link
Copy Markdown
Member

@kennyweiss kennyweiss left a comment

Choose a reason for hiding this comment

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

Thanks for contribution and the updates @dylan-copeland !

@kennyweiss kennyweiss added the Primal Issues related to Axom's 'primal component label May 20, 2026
Comment thread src/axom/primal/operators/detail/intersect_bezier_impl.hpp
Copy link
Copy Markdown
Contributor

@jcs15c jcs15c left a comment

Choose a reason for hiding this comment

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

Looks great, thanks for clarifying in the documentation about tangencies!

Comment thread src/axom/primal/tests/primal_nurbs_curve.cpp
@kennyweiss kennyweiss merged commit 1147e13 into develop May 21, 2026
15 checks passed
@kennyweiss kennyweiss deleted the feature/copeland/nurbs_intersect branch May 21, 2026 15:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request Primal Issues related to Axom's 'primal component

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants