Files
OpenNest/docs/superpowers/plans/2026-03-15-strip-nester.md
AJ Isaacs 1d1cf41ba0 docs: update strip nester plan for abstract engine architecture
StripNester becomes StripNestEngine extending NestEngineBase.
Uses DefaultNestEngine internally via composition.
Registered in NestEngineRegistry.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 21:00:42 -04:00

22 KiB

Strip Nester 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: Implement a strip-based multi-drawing nesting strategy as a NestEngineBase subclass that dedicates a tight strip to the largest-area drawing and fills the remnant with remaining drawings.

Architecture: StripNestEngine extends NestEngineBase, uses DefaultNestEngine internally (composition) for individual fills. Registered in NestEngineRegistry. For single-item fills, delegates to DefaultNestEngine. For multi-drawing nesting, orchestrates the strip+remnant strategy. The MCP autonest_plate tool always runs StripNestEngine as a competitor alongside the current sequential approach, picking the denser result.

Tech Stack: C# / .NET 8, OpenNest.Engine, OpenNest.Mcp

Spec: docs/superpowers/specs/2026-03-15-strip-nester-design.md

Depends on: docs/superpowers/plans/2026-03-15-abstract-nest-engine.md (must be implemented first — provides NestEngineBase, DefaultNestEngine, NestEngineRegistry)


Chunk 1: Core StripNestEngine

Task 1: Create StripDirection enum

Files:

  • Create: OpenNest.Engine/StripDirection.cs

  • Step 1: Create the enum file

namespace OpenNest
{
    public enum StripDirection
    {
        Bottom,
        Left
    }
}
  • Step 2: Build to verify

Run: dotnet build OpenNest.Engine/OpenNest.Engine.csproj Expected: Build succeeded

  • Step 3: Commit
git add OpenNest.Engine/StripDirection.cs
git commit -m "feat: add StripDirection enum"

Task 2: Create StripNestResult internal class

Files:

  • Create: OpenNest.Engine/StripNestResult.cs

  • Step 1: Create the result class

using System.Collections.Generic;
using OpenNest.Geometry;

namespace OpenNest
{
    internal class StripNestResult
    {
        public List<Part> Parts { get; set; } = new();
        public Box StripBox { get; set; }
        public Box RemnantBox { get; set; }
        public FillScore Score { get; set; }
        public StripDirection Direction { get; set; }
    }
}
  • Step 2: Build to verify

Run: dotnet build OpenNest.Engine/OpenNest.Engine.csproj Expected: Build succeeded

  • Step 3: Commit
git add OpenNest.Engine/StripNestResult.cs
git commit -m "feat: add StripNestResult internal class"

Task 3: Create StripNestEngine — class skeleton with selection and estimation helpers

Files:

  • Create: OpenNest.Engine/StripNestEngine.cs

This task creates the class extending NestEngineBase, with Name/Description overrides, the single-item Fill override that delegates to DefaultNestEngine, and the helper methods for strip item selection and dimension estimation. The main Nest method is added in the next task.

  • Step 1: Create StripNestEngine with skeleton and helpers
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using OpenNest.Geometry;
using OpenNest.Math;

namespace OpenNest
{
    public class StripNestEngine : NestEngineBase
    {
        private const int MaxShrinkIterations = 20;

        public StripNestEngine(Plate plate) : base(plate)
        {
        }

        public override string Name => "Strip";

        public override string Description => "Strip-based nesting for mixed-drawing layouts";

        /// <summary>
        /// Single-item fill delegates to DefaultNestEngine.
        /// The strip strategy adds value for multi-drawing nesting, not single-item fills.
        /// </summary>
        public override List<Part> Fill(NestItem item, Box workArea,
            IProgress<NestProgress> progress, CancellationToken token)
        {
            var inner = new DefaultNestEngine(Plate);
            return inner.Fill(item, workArea, progress, token);
        }

        /// <summary>
        /// Selects the item that consumes the most plate area (bounding box area x quantity).
        /// Returns the index into the items list.
        /// </summary>
        private static int SelectStripItemIndex(List<NestItem> items, Box workArea)
        {
            var bestIndex = 0;
            var bestArea = 0.0;

            for (var i = 0; i < items.Count; i++)
            {
                var bbox = items[i].Drawing.Program.BoundingBox();
                var qty = items[i].Quantity > 0
                    ? items[i].Quantity
                    : (int)(workArea.Area() / bbox.Area());
                var totalArea = bbox.Area() * qty;

                if (totalArea > bestArea)
                {
                    bestArea = totalArea;
                    bestIndex = i;
                }
            }

            return bestIndex;
        }

        /// <summary>
        /// Estimates the strip dimension (height for bottom, width for left) needed
        /// to fit the target quantity. Tries 0 deg and 90 deg rotations and picks the shorter.
        /// This is only an estimate for the shrink loop starting point — the actual fill
        /// uses DefaultNestEngine.Fill which tries many rotation angles internally.
        /// </summary>
        private static double EstimateStripDimension(NestItem item, double stripLength, double maxDimension)
        {
            var bbox = item.Drawing.Program.BoundingBox();
            var qty = item.Quantity > 0
                ? item.Quantity
                : System.Math.Max(1, (int)(stripLength * maxDimension / bbox.Area()));

            // At 0 deg: parts per row along strip length, strip dimension is bbox.Length
            var perRow0 = (int)(stripLength / bbox.Width);
            var rows0 = perRow0 > 0 ? (int)System.Math.Ceiling((double)qty / perRow0) : int.MaxValue;
            var dim0 = rows0 * bbox.Length;

            // At 90 deg: rotated bounding box (Width and Length swap)
            var perRow90 = (int)(stripLength / bbox.Length);
            var rows90 = perRow90 > 0 ? (int)System.Math.Ceiling((double)qty / perRow90) : int.MaxValue;
            var dim90 = rows90 * bbox.Width;

            var estimate = System.Math.Min(dim0, dim90);

            // Clamp to available dimension
            return System.Math.Min(estimate, maxDimension);
        }
    }
}
  • Step 2: Build to verify

Run: dotnet build OpenNest.Engine/OpenNest.Engine.csproj Expected: Build succeeded

  • Step 3: Commit
git add OpenNest.Engine/StripNestEngine.cs
git commit -m "feat: add StripNestEngine skeleton with Fill delegate and estimation helpers"

Task 4: Add the Nest method and TryOrientation

Files:

  • Modify: OpenNest.Engine/StripNestEngine.cs

This is the main multi-drawing algorithm: tries both orientations, fills strip + remnant, compares results. Uses DefaultNestEngine internally for all fill operations (composition pattern per the abstract engine spec).

Key detail: The remnant fill shrinks the remnant box after each item fill using ComputeRemainderWithin to prevent overlapping placements.

  • Step 1: Add Nest, TryOrientation, and ComputeRemainderWithin methods

Add these methods to the StripNestEngine class, after the EstimateStripDimension method:

        /// <summary>
        /// Multi-drawing strip nesting strategy.
        /// Picks the largest-area drawing for strip treatment, finds the tightest strip
        /// in both bottom and left orientations, fills remnants with remaining drawings,
        /// and returns the denser result.
        /// </summary>
        public List<Part> Nest(List<NestItem> items,
            IProgress<NestProgress> progress, CancellationToken token)
        {
            if (items == null || items.Count == 0)
                return new List<Part>();

            var workArea = Plate.WorkArea();

            // Select which item gets the strip treatment.
            var stripIndex = SelectStripItemIndex(items, workArea);
            var stripItem = items[stripIndex];
            var remainderItems = items.Where((_, i) => i != stripIndex).ToList();

            // Try both orientations.
            var bottomResult = TryOrientation(StripDirection.Bottom, stripItem, remainderItems, workArea, token);
            var leftResult = TryOrientation(StripDirection.Left, stripItem, remainderItems, workArea, token);

            // Pick the better result.
            if (bottomResult.Score >= leftResult.Score)
                return bottomResult.Parts;

            return leftResult.Parts;
        }

        private StripNestResult TryOrientation(StripDirection direction, NestItem stripItem,
            List<NestItem> remainderItems, Box workArea, CancellationToken token)
        {
            var result = new StripNestResult { Direction = direction };

            if (token.IsCancellationRequested)
                return result;

            // Estimate initial strip dimension.
            var stripLength = direction == StripDirection.Bottom ? workArea.Width : workArea.Length;
            var maxDimension = direction == StripDirection.Bottom ? workArea.Length : workArea.Width;
            var estimatedDim = EstimateStripDimension(stripItem, stripLength, maxDimension);

            // Create the initial strip box.
            var stripBox = direction == StripDirection.Bottom
                ? new Box(workArea.X, workArea.Y, workArea.Width, estimatedDim)
                : new Box(workArea.X, workArea.Y, estimatedDim, workArea.Length);

            // Initial fill using DefaultNestEngine (composition, not inheritance).
            var inner = new DefaultNestEngine(Plate);
            var stripParts = inner.Fill(
                new NestItem { Drawing = stripItem.Drawing, Quantity = stripItem.Quantity },
                stripBox, null, token);

            if (stripParts == null || stripParts.Count == 0)
                return result;

            // Measure actual strip dimension from placed parts.
            var placedBox = stripParts.Cast<IBoundable>().GetBoundingBox();
            var actualDim = direction == StripDirection.Bottom
                ? placedBox.Top - workArea.Y
                : placedBox.Right - workArea.X;

            var bestParts = stripParts;
            var bestDim = actualDim;
            var targetCount = stripParts.Count;

            // Shrink loop: reduce strip dimension by PartSpacing until count drops.
            for (var i = 0; i < MaxShrinkIterations; i++)
            {
                if (token.IsCancellationRequested)
                    break;

                var trialDim = bestDim - Plate.PartSpacing;
                if (trialDim <= 0)
                    break;

                var trialBox = direction == StripDirection.Bottom
                    ? new Box(workArea.X, workArea.Y, workArea.Width, trialDim)
                    : new Box(workArea.X, workArea.Y, trialDim, workArea.Length);

                var trialInner = new DefaultNestEngine(Plate);
                var trialParts = trialInner.Fill(
                    new NestItem { Drawing = stripItem.Drawing, Quantity = stripItem.Quantity },
                    trialBox, null, token);

                if (trialParts == null || trialParts.Count < targetCount)
                    break;

                // Same count in a tighter strip — keep going.
                bestParts = trialParts;
                var trialPlacedBox = trialParts.Cast<IBoundable>().GetBoundingBox();
                bestDim = direction == StripDirection.Bottom
                    ? trialPlacedBox.Top - workArea.Y
                    : trialPlacedBox.Right - workArea.X;
            }

            // Build remnant box with spacing gap.
            var spacing = Plate.PartSpacing;
            var remnantBox = direction == StripDirection.Bottom
                ? new Box(workArea.X, workArea.Y + bestDim + spacing,
                    workArea.Width, workArea.Length - bestDim - spacing)
                : new Box(workArea.X + bestDim + spacing, workArea.Y,
                    workArea.Width - bestDim - spacing, workArea.Length);

            // Collect all parts.
            var allParts = new List<Part>(bestParts);

            // If strip item was only partially placed, add leftovers to remainder.
            var placed = bestParts.Count;
            var leftover = stripItem.Quantity > 0 ? stripItem.Quantity - placed : 0;
            var effectiveRemainder = new List<NestItem>(remainderItems);

            if (leftover > 0)
            {
                effectiveRemainder.Add(new NestItem
                {
                    Drawing = stripItem.Drawing,
                    Quantity = leftover
                });
            }

            // Sort remainder by descending bounding box area x quantity.
            effectiveRemainder = effectiveRemainder
                .OrderByDescending(i =>
                {
                    var bb = i.Drawing.Program.BoundingBox();
                    return bb.Area() * (i.Quantity > 0 ? i.Quantity : 1);
                })
                .ToList();

            // Fill remnant with remainder items, shrinking the available area after each.
            if (remnantBox.Width > 0 && remnantBox.Length > 0)
            {
                var currentRemnant = remnantBox;

                foreach (var item in effectiveRemainder)
                {
                    if (token.IsCancellationRequested)
                        break;

                    if (currentRemnant.Width <= 0 || currentRemnant.Length <= 0)
                        break;

                    var remnantInner = new DefaultNestEngine(Plate);
                    var remnantParts = remnantInner.Fill(
                        new NestItem { Drawing = item.Drawing, Quantity = item.Quantity },
                        currentRemnant, null, token);

                    if (remnantParts != null && remnantParts.Count > 0)
                    {
                        allParts.AddRange(remnantParts);

                        // Shrink remnant to avoid overlap with next item.
                        var usedBox = remnantParts.Cast<IBoundable>().GetBoundingBox();
                        currentRemnant = ComputeRemainderWithin(currentRemnant, usedBox, spacing);
                    }
                }
            }

            result.Parts = allParts;
            result.StripBox = direction == StripDirection.Bottom
                ? new Box(workArea.X, workArea.Y, workArea.Width, bestDim)
                : new Box(workArea.X, workArea.Y, bestDim, workArea.Length);
            result.RemnantBox = remnantBox;
            result.Score = FillScore.Compute(allParts, workArea);

            return result;
        }

        /// <summary>
        /// Computes the largest usable remainder within a work area after a portion has been used.
        /// Picks whichever is larger: the horizontal strip to the right, or the vertical strip above.
        /// </summary>
        private static Box ComputeRemainderWithin(Box workArea, Box usedBox, double spacing)
        {
            var hWidth = workArea.Right - usedBox.Right - spacing;
            var hStrip = hWidth > 0
                ? new Box(usedBox.Right + spacing, workArea.Y, hWidth, workArea.Length)
                : Box.Empty;

            var vHeight = workArea.Top - usedBox.Top - spacing;
            var vStrip = vHeight > 0
                ? new Box(workArea.X, usedBox.Top + spacing, workArea.Width, vHeight)
                : Box.Empty;

            return hStrip.Area() >= vStrip.Area() ? hStrip : vStrip;
        }
  • Step 2: Build to verify

Run: dotnet build OpenNest.Engine/OpenNest.Engine.csproj Expected: Build succeeded

  • Step 3: Commit
git add OpenNest.Engine/StripNestEngine.cs
git commit -m "feat: add StripNestEngine.Nest with strip fill, shrink loop, and remnant fill"

Task 5: Register StripNestEngine in NestEngineRegistry

Files:

  • Modify: OpenNest.Engine/NestEngineRegistry.cs

  • Step 1: Add Strip registration

In NestEngineRegistry.cs, add the strip engine registration in the static constructor, after the Default registration:

            Register("Strip",
                "Strip-based nesting for mixed-drawing layouts",
                plate => new StripNestEngine(plate));
  • Step 2: Build to verify

Run: dotnet build OpenNest.Engine/OpenNest.Engine.csproj Expected: Build succeeded

  • Step 3: Commit
git add OpenNest.Engine/NestEngineRegistry.cs
git commit -m "feat: register StripNestEngine in NestEngineRegistry"

Chunk 2: MCP Integration

Task 6: Integrate StripNestEngine into autonest_plate MCP tool

Files:

  • Modify: OpenNest.Mcp/Tools/NestingTools.cs

Run the strip nester alongside the existing sequential approach. Both use side-effect-free fills (4-arg Fill returning List<Part>), then the winner's parts are added to the plate.

Note: After the abstract engine migration, callsites already use NestEngineRegistry.Create(plate). The autonest_plate tool creates a StripNestEngine directly for the strip strategy competition (it's always tried, regardless of active engine selection).

  • Step 1: Refactor AutoNestPlate to run both strategies

In NestingTools.cs, replace the fill/pack logic in AutoNestPlate (the section after the items list is built) with a strategy competition.

Replace the fill/pack logic with:

            // Strategy 1: Strip nesting
            var stripEngine = new StripNestEngine(plate);
            var stripResult = stripEngine.Nest(items, null, CancellationToken.None);
            var stripScore = FillScore.Compute(stripResult, plate.WorkArea());

            // Strategy 2: Current sequential fill
            var seqResult = SequentialFill(plate, items);
            var seqScore = FillScore.Compute(seqResult, plate.WorkArea());

            // Pick winner and apply to plate.
            var winner = stripScore >= seqScore ? stripResult : seqResult;
            var winnerName = stripScore >= seqScore ? "strip" : "sequential";
            plate.Parts.AddRange(winner);
            var totalPlaced = winner.Count;

Update the output section:

            var sb = new StringBuilder();
            sb.AppendLine($"AutoNest plate {plateIndex} ({winnerName} strategy): {(totalPlaced > 0 ? "success" : "no parts placed")}");
            sb.AppendLine($"  Parts placed: {totalPlaced}");
            sb.AppendLine($"  Total parts: {plate.Parts.Count}");
            sb.AppendLine($"  Utilization: {plate.Utilization():P1}");
            sb.AppendLine($"  Strip score: {stripScore.Count} parts, density {stripScore.Density:P1}");
            sb.AppendLine($"  Sequential score: {seqScore.Count} parts, density {seqScore.Density:P1}");

            var groups = plate.Parts.GroupBy(p => p.BaseDrawing.Name);
            foreach (var group in groups)
                sb.AppendLine($"  {group.Key}: {group.Count()}");

            return sb.ToString();
  • Step 2: Add the SequentialFill helper method

Add this private method to NestingTools. It mirrors the existing sequential fill phase using side-effect-free fills.

        private static List<Part> SequentialFill(Plate plate, List<NestItem> items)
        {
            var fillItems = items
                .Where(i => i.Quantity != 1)
                .OrderBy(i => i.Priority)
                .ThenByDescending(i => i.Drawing.Area)
                .ToList();

            var workArea = plate.WorkArea();
            var allParts = new List<Part>();

            foreach (var item in fillItems)
            {
                if (item.Quantity == 0 || workArea.Width <= 0 || workArea.Length <= 0)
                    continue;

                var engine = new DefaultNestEngine(plate);
                var parts = engine.Fill(
                    new NestItem { Drawing = item.Drawing, Quantity = item.Quantity },
                    workArea, null, CancellationToken.None);

                if (parts.Count > 0)
                {
                    allParts.AddRange(parts);
                    var placedBox = parts.Cast<IBoundable>().GetBoundingBox();
                    workArea = ComputeRemainderWithin(workArea, placedBox, plate.PartSpacing);
                }
            }

            return allParts;
        }
  • Step 3: Add required using statement

Add using System.Threading; to the top of NestingTools.cs if not already present.

  • Step 4: Build the full solution

Run: dotnet build OpenNest.sln Expected: Build succeeded

  • Step 5: Commit
git add OpenNest.Mcp/Tools/NestingTools.cs
git commit -m "feat: integrate StripNestEngine into autonest_plate MCP tool"

Chunk 3: Publish and Test

Task 7: Publish MCP server and test with real parts

Files:

  • No code changes — publish and manual testing

  • Step 1: Publish OpenNest.Mcp

Run: dotnet publish OpenNest.Mcp/OpenNest.Mcp.csproj -c Release -o "$USERPROFILE/.claude/mcp/OpenNest.Mcp" Expected: Build and publish succeeded

  • Step 2: Test with SULLYS parts

Using the MCP tools, test the strip nester with the SULLYS-001 and SULLYS-002 parts:

  1. Load the test nest file or import the DXF files
  2. Create a 60x120 plate
  3. Run autonest_plate with both drawings at qty 10
  4. Verify the output reports which strategy won (strip vs sequential)
  5. Verify the output shows scores for both strategies
  6. Check plate info for part placement and utilization
  • Step 3: Compare with current results

Verify the strip nester produces a result matching or improving on the target layout from screenshot 190519 (all 20 parts on one 60x120 plate with organized strip arrangement).

  • Step 4: Commit any fixes

If issues are found during testing, fix and commit with descriptive messages.