`IntersectionEvent` has wrong side of curve

I have the attached file with two curves:
badIntersect.3dm (56.9 KB)

and the following plugin command code:

using Rhino;
using Rhino.Commands;
using Rhino.DocObjects;
using Rhino.Geometry;
using Rhino.Geometry.Intersect;
using Rhino.Input;
using Rhino.Input.Custom;

namespace MyPlugin.Commands
{
	public class MyCurveOverlap : Command
	{
		public MyCurveOverlap()
		{
			Instance = this;
		}

		public static MyCurveOverlap Instance { get; private set; }

		public override string EnglishName => "MyCurveOverlap";

		protected override Result RunCommand(RhinoDoc doc, RunMode mode)
		{
			Curve c1, c2;
			using (GetObject go = new())
			{
				go.SetCommandPrompt("Choose first curve");
				go.GeometryFilter = ObjectType.Curve;
				if (go.Get() == GetResult.Cancel)
				{
					return Result.Cancel;
				}
				c1 = go.Object(0).Curve();

				go.SetCommandPrompt("Choose second curve");
				go.EnablePreSelect(false, true);
				if (go.Get() == GetResult.Cancel)
				{
					return Result.Cancel;
				}
				c2 = go.Object(0).Curve();
			}

			foreach (IntersectionEvent ie in Intersection.CurveCurve(c1, c2, 0.01, 0.01))
			{
				if (ie.IsOverlap)
				{
					doc.Objects.Add(c1.Trim(ie.OverlapA));
					doc.Objects.Add(c2.Trim(ie.OverlapB));
				}
			}

			return Result.Success;
		}
	}
}

When I run the command on the two curves in the file, one of the added interval-trimmed curves is the overlapping portion of the first input curve, while the other is the non-overlapping portion of the second input curve. This happens when you pick the curves in either order (which rules out my suspicion that it had to do with interval wrapping on closed curves). This seems to be a clear bug, so I’d like to have some idea of its cause so I can handle it appropriately until it is fixed. When does this behavior happen and do you have any recommendations for addressing it?

Thanks,
- Russell

Hi @Russell_Emerine,

Give this a try:

using Rhino;
using Rhino.Commands;
using Rhino.DocObjects;
using Rhino.Geometry;
using Rhino.Geometry.Intersect;
using Rhino.Input.Custom;

namespace MyPlugin.Commands
{
  public class MyCurveOverlap : Command
  {
    public MyCurveOverlap() { Instance = this; }

    public static MyCurveOverlap Instance { get; private set; }

    public override string EnglishName => "MyCurveOverlap";

    protected override Result RunCommand(RhinoDoc doc, RunMode mode)
    {
      Curve c1, c2;
      using (GetObject go = new())
      {
        go.SetCommandPrompt("Choose first curve");
        go.GeometryFilter = ObjectType.Curve;
        if (go.Get() == GetResult.Cancel)
          return Result.Cancel;
        c1 = go.Object(0).Curve();

        go.SetCommandPrompt("Choose second curve");
        go.EnablePreSelect(false, true);
        if (go.Get() == GetResult.Cancel)
          return Result.Cancel;
        c2 = go.Object(0).Curve();
      }

      var tol = doc.ModelAbsoluteTolerance;
      var added = false;

      foreach (IntersectionEvent ie in Intersection.CurveCurve(c1, c2, tol, tol))
      {
        if (!ie.IsOverlap)
          continue;

        // The curves may run in opposite directions through the overlap, in which
        // case OverlapB comes back decreasing. Curve.Trim wants increasing intervals;
        // given a decreasing one it returns the complement (or wraps a closed curve).
        var domainA = ie.OverlapA;
        var domainB = ie.OverlapB;
        domainA.MakeIncreasing();
        domainB.MakeIncreasing();

        var trimA = c1.Trim(domainA);
        var trimB = c2.Trim(domainB);

        if (trimA != null && doc.Objects.Add(trimA) != Guid.Empty)
          added = true;
        if (trimB != null && doc.Objects.Add(trimB) != Guid.Empty)
          added = true;
      }

      if (added)
        doc.Views.Redraw();

      return Result.Success;
    }
  }
}

– Dale

This looks like a good starting point, but I think there might be a couple issues.

The documentation for Curve.Trim says “If curve is open, then trimming interval must be an increasing interval.” I’d then expect the decreasing interval on an open curve to return null instead of the complement. Is the Curve.Trim documentation/behavior wrong?

This also seems to have the limitation that it won’t identify when an overlap with a closed curve correctly returns a decreasing interval. Perhaps this can be resolved by inspecting the tangent direction at one end of the interval. Does that sound reasonable, or would there be problems at curve kinks?

Also, can you confirm that OverlapA is always the right interval for c1? And that c1.PointAt(ie.OverlapA.T0) is the same as c2.PointAt(ie.OverlapB.T0)?

I think it’d be good to specify the nuances of the intervals in the IntersectionEvent documentation.

Thanks,
- Russell

Hi @Russell_Emerine,

Right on all counts.

Curve.Trim — the docs are correct, the behavior has a hole. A decreasing interval on an open curve returns null as documented, except when an endpoint lands exactly on the domain boundary, where it wraps as if the curve were closed and returns the wrong piece. Your file hits it because OverlapB.T0 is exactly curve 2’s domain maximum. My “returns the complement” comment was too loose — that only happens at the domain ends.

Seam crossings — no tangent inspection needed. A seam-crossing overlap comes back as two separate events, both increasing, never as one wrapping interval. So sorting an interval can’t destroy wrap information.

OverlapA — always increasing, in every case I tried (either argument order, reversed curves, closed against closed with opposing orientation). OverlapB is the one carrying direction: decreasing just means the curves run opposite ways through the overlap.

Point correspondence — confirmed. T0T0 and T1T1 match point-for-point, including the decreasing cases. That pairing is why B arrives decreasing.

One more to watch: two closed curves overlapping along their whole length report OverlapA as the full domain but OverlapB as a singleton, and Trim returns null for it.

Replace the loop with this:

foreach (IntersectionEvent ie in Intersection.CurveCurve(c1, c2, tol, tol))
{
  if (!ie.IsOverlap)
    continue;

  // OverlapB arrives decreasing when the curves run in opposite directions.
  // T0/T1 still correspond point-for-point, so sorting loses only the direction.
  if (!TryGetOverlapDomain(ie.OverlapA, c1, out var domainA))
    continue;
  if (!TryGetOverlapDomain(ie.OverlapB, c2, out var domainB))
    continue;

  var trimA = c1.Trim(domainA);
  var trimB = c2.Trim(domainB);

  if (trimA != null && doc.Objects.Add(trimA) != Guid.Empty)
    added = true;
  if (trimB != null && doc.Objects.Add(trimB) != Guid.Empty)
    added = true;
}

/// <summary>
/// Normalizes an overlap interval into one Curve.Trim will accept.
/// </summary>
private static bool TryGetOverlapDomain(Interval overlap, Curve curve, out Interval domain)
{
  domain = new Interval(overlap.Min, overlap.Max);

  // A closed curve overlapping along its entire length reports a singleton.
  if (domain.IsSingleton)
  {
    if (!curve.IsClosed)
      return false;

    domain = curve.Domain;
  }

  return true;
}

If you need the relative direction, read ie.OverlapB.IsDecreasing before sorting.

— Dale

Great, that clears things up. I’d really appreciate patches to the documentation to make these details explicit in the specification.

(If I’m reading this right, you’ll need to fix the code for Curve.Trim in a future release. If the comment you put under the Curve.Trim item is backwards — i.e. the behavior is correct, and the docs have a hole — then I must admit I don’t think that behavior is desirable.)