Files
OpenNest/docs/superpowers/plans/2026-03-12-contour-reindexing.md
2026-03-13 20:30:00 -04:00

8.5 KiB

Contour Re-Indexing Implementation Plan

For agentic workers: REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Add entity-splitting primitives (Line.SplitAt, Arc.SplitAt), a Shape.ReindexAt method, and wire them into ContourCuttingStrategy.Apply() to replace the NotImplementedException stubs.

Architecture: Bottom-up — build splitting primitives first, then the reindexing algorithm on top, then wire into the strategy. Each layer depends only on the one below it.

Tech Stack: C# / .NET 8, OpenNest.Core (Geometry + CNC namespaces)

Spec: docs/superpowers/specs/2026-03-12-contour-reindexing-design.md


File Structure

File Change Responsibility
OpenNest.Core/Geometry/Line.cs Add method SplitAt(Vector) — split a line at a point into two halves
OpenNest.Core/Geometry/Arc.cs Add method SplitAt(Vector) — split an arc at a point into two halves
OpenNest.Core/Geometry/Shape.cs Add method ReindexAt(Vector, Entity) — reorder a closed contour to start at a given point
OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs Add method + modify ConvertShapeToMoves + replace two NotImplementedException blocks

Chunk 1: Splitting Primitives

Task 1: Add Line.SplitAt(Vector)

Files:

  • Modify: OpenNest.Core/Geometry/Line.cs

  • Step 1: Add SplitAt method to Line

Add the following method to the Line class (after the existing ClosestPointTo method):

public (Line first, Line second) SplitAt(Vector point)
{
    var first = point.DistanceTo(StartPoint) < Tolerance.Epsilon
        ? null
        : new Line(StartPoint, point);

    var second = point.DistanceTo(EndPoint) < Tolerance.Epsilon
        ? null
        : new Line(point, EndPoint);

    return (first, second);
}
  • Step 2: Build to verify

Run: dotnet build OpenNest.Core/OpenNest.Core.csproj Expected: Build succeeded, 0 errors

  • Step 3: Commit
git add OpenNest.Core/Geometry/Line.cs
git commit -m "feat: add Line.SplitAt(Vector) splitting primitive"

Task 2: Add Arc.SplitAt(Vector)

Files:

  • Modify: OpenNest.Core/Geometry/Arc.cs

  • Step 1: Add SplitAt method to Arc

Add the following method to the Arc class (after the existing EndPoint method):

public (Arc first, Arc second) SplitAt(Vector point)
{
    if (point.DistanceTo(StartPoint()) < Tolerance.Epsilon)
        return (null, new Arc(Center, Radius, StartAngle, EndAngle, IsReversed));

    if (point.DistanceTo(EndPoint()) < Tolerance.Epsilon)
        return (new Arc(Center, Radius, StartAngle, EndAngle, IsReversed), null);

    var splitAngle = Angle.NormalizeRad(Center.AngleTo(point));

    var firstArc = new Arc(Center, Radius, StartAngle, splitAngle, IsReversed);
    var secondArc = new Arc(Center, Radius, splitAngle, EndAngle, IsReversed);

    return (firstArc, secondArc);
}

Key details from spec:

  • Compare distances to StartPoint()/EndPoint() rather than comparing angles (avoids 0/2π wrap-around issues).

  • splitAngle is computed from Center.AngleTo(point), normalized.

  • Both halves preserve center, radius, and IsReversed direction.

  • Step 2: Build to verify

Run: dotnet build OpenNest.Core/OpenNest.Core.csproj Expected: Build succeeded, 0 errors

  • Step 3: Commit
git add OpenNest.Core/Geometry/Arc.cs
git commit -m "feat: add Arc.SplitAt(Vector) splitting primitive"

Chunk 2: Shape.ReindexAt

Task 3: Add Shape.ReindexAt(Vector, Entity)

Files:

  • Modify: OpenNest.Core/Geometry/Shape.cs

  • Step 1: Add ReindexAt method to Shape

Add the following method to the Shape class (after the existing ClosestPointTo(Vector, out Entity) method around line 201):

public Shape ReindexAt(Vector point, Entity entity)
{
    // Circle case: return a new shape with just the circle
    if (entity is Circle)
    {
        var result = new Shape();
        result.Entities.Add(entity);
        return result;
    }

    var i = Entities.IndexOf(entity);
    if (i < 0)
        throw new ArgumentException("Entity not found in shape", nameof(entity));

    // Split the entity at the point
    Entity firstHalf = null;
    Entity secondHalf = null;

    if (entity is Line line)
    {
        var (f, s) = line.SplitAt(point);
        firstHalf = f;
        secondHalf = s;
    }
    else if (entity is Arc arc)
    {
        var (f, s) = arc.SplitAt(point);
        firstHalf = f;
        secondHalf = s;
    }

    // Build reindexed entity list
    var entities = new List<Entity>();

    // secondHalf of split entity (if not null)
    if (secondHalf != null)
        entities.Add(secondHalf);

    // Entities after the split index (wrapping)
    for (var j = i + 1; j < Entities.Count; j++)
        entities.Add(Entities[j]);

    // Entities before the split index (wrapping)
    for (var j = 0; j < i; j++)
        entities.Add(Entities[j]);

    // firstHalf of split entity (if not null)
    if (firstHalf != null)
        entities.Add(firstHalf);

    var reindexed = new Shape();
    reindexed.Entities.AddRange(entities);
    return reindexed;
}

The Shape class already imports System and System.Collections.Generic, so no new usings needed.

  • Step 2: Build to verify

Run: dotnet build OpenNest.Core/OpenNest.Core.csproj Expected: Build succeeded, 0 errors

  • Step 3: Commit
git add OpenNest.Core/Geometry/Shape.cs
git commit -m "feat: add Shape.ReindexAt(Vector, Entity) for contour reordering"

Chunk 3: Wire into ContourCuttingStrategy

Task 4: Add ConvertShapeToMoves and replace stubs

Files:

  • Modify: OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs

  • Step 1: Add ConvertShapeToMoves private method

Add the following private method to ContourCuttingStrategy (after the existing SelectLeadOut method, before the closing brace of the class):

private List<ICode> ConvertShapeToMoves(Shape shape, Vector startPoint)
{
    var moves = new List<ICode>();

    foreach (var entity in shape.Entities)
    {
        if (entity is Line line)
        {
            moves.Add(new LinearMove(line.EndPoint));
        }
        else if (entity is Arc arc)
        {
            moves.Add(new ArcMove(arc.EndPoint(), arc.Center, arc.IsReversed ? RotationType.CW : RotationType.CCW));
        }
        else if (entity is Circle circle)
        {
            moves.Add(new ArcMove(startPoint, circle.Center, circle.Rotation));
        }
        else
        {
            throw new System.InvalidOperationException($"Unsupported entity type: {entity.Type}");
        }
    }

    return moves;
}

This matches the ConvertGeometry.AddArc/AddCircle/AddLine patterns but without RapidMove between entities (they are contiguous in a reindexed shape).

  • Step 2: Replace cutout NotImplementedException (line 41)

In the Apply method, replace:

                // Contour re-indexing: split shape entities at closestPt so cutting
                // starts there, convert to ICode, and add to result.Codes
                throw new System.NotImplementedException("Contour re-indexing not yet implemented");

With:

                var reindexed = cutout.ReindexAt(closestPt, entity);
                result.Codes.AddRange(ConvertShapeToMoves(reindexed, closestPt));
                // TODO: MicrotabLeadOut — trim last cutting move by GapSize
  • Step 3: Replace perimeter NotImplementedException (line 57)

In the Apply method, replace:

                throw new System.NotImplementedException("Contour re-indexing not yet implemented");

With:

                var reindexed = profile.Perimeter.ReindexAt(perimeterPt, perimeterEntity);
                result.Codes.AddRange(ConvertShapeToMoves(reindexed, perimeterPt));
                // TODO: MicrotabLeadOut — trim last cutting move by GapSize
  • Step 4: Build to verify

Run: dotnet build OpenNest.Core/OpenNest.Core.csproj Expected: Build succeeded, 0 errors

  • Step 5: Build full solution

Run: dotnet build OpenNest.sln Expected: Build succeeded, 0 errors

  • Step 6: Commit
git add OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs
git commit -m "feat: wire contour re-indexing into ContourCuttingStrategy.Apply()"