Files
OpenNest/docs/superpowers/plans/2026-03-18-pattern-tile-layout.md
2026-03-18 09:31:27 -04:00

30 KiB

Pattern Tile Layout Window 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: Build a tool window where the user selects two drawings, arranges them as a unit cell, and tiles the pattern across a configurable plate with an option to apply the result.

Architecture: A PatternTileForm dialog with a horizontal SplitContainer — left panel is a PlateView for unit cell editing (plate outline hidden via zero-size plate), right panel is a read-only PlateView for tile preview. A PatternTiler helper in Engine handles the tiling math (deliberate addition beyond the spec for separation of concerns — the spec says "no engine changes" but the tiler is pure logic with no side-effects). The form is opened from the Tools menu and returns a result to EditNestForm for placement.

Tech Stack: WinForms (.NET 8), existing PlateView/DrawControl controls, Compactor.Push (angle-based overload), Part.CloneAtOffset

Spec: docs/superpowers/specs/2026-03-18-pattern-tile-layout-design.md


Task 1: PatternTiler — tiling algorithm in Engine

The pure logic component that takes a unit cell (list of parts) and tiles it across a plate work area. No UI dependency.

Files:

  • Create: OpenNest.Engine/PatternTiler.cs

  • Test: OpenNest.Tests/PatternTilerTests.cs

  • Step 1: Write failing tests for PatternTiler

// OpenNest.Tests/PatternTilerTests.cs
using OpenNest;
using OpenNest.Engine;
using OpenNest.Geometry;

namespace OpenNest.Tests;

public class PatternTilerTests
{
    private static Drawing MakeSquareDrawing(double size)
    {
        var pgm = new CNC.Program();
        pgm.Add(new CNC.LinearMove(size, 0));
        pgm.Add(new CNC.LinearMove(size, size));
        pgm.Add(new CNC.LinearMove(0, size));
        pgm.Add(new CNC.LinearMove(0, 0));
        return new Drawing("square", pgm);
    }

    [Fact]
    public void Tile_SinglePart_FillsGrid()
    {
        var drawing = MakeSquareDrawing(10);
        var cell = new List<Part> { Part.CreateAtOrigin(drawing) };
        // Size(width=X, length=Y) — 30 wide, 20 tall
        var plateSize = new Size(30, 20);
        var partSpacing = 0.0;

        var result = PatternTiler.Tile(cell, plateSize, partSpacing);

        // 3 columns (30/10) x 2 rows (20/10) = 6 parts
        Assert.Equal(6, result.Count);

        // Verify all parts are within plate bounds
        foreach (var part in result)
        {
            Assert.True(part.BoundingBox.Right <= plateSize.Width + 0.001);
            Assert.True(part.BoundingBox.Top <= plateSize.Length + 0.001);
            Assert.True(part.BoundingBox.Left >= -0.001);
            Assert.True(part.BoundingBox.Bottom >= -0.001);
        }
    }

    [Fact]
    public void Tile_TwoParts_TilesUnitCell()
    {
        var drawing = MakeSquareDrawing(10);
        var partA = Part.CreateAtOrigin(drawing);
        var partB = Part.CreateAtOrigin(drawing);
        partB.Offset(10, 0); // side by side, cell = 20x10

        var cell = new List<Part> { partA, partB };
        var plateSize = new Size(40, 20);
        var partSpacing = 0.0;

        var result = PatternTiler.Tile(cell, plateSize, partSpacing);

        // 2 columns (40/20) x 2 rows (20/10) = 4 cells x 2 parts = 8
        Assert.Equal(8, result.Count);
    }

    [Fact]
    public void Tile_WithSpacing_ReducesCount()
    {
        var drawing = MakeSquareDrawing(10);
        var cell = new List<Part> { Part.CreateAtOrigin(drawing) };
        var plateSize = new Size(30, 20);
        var partSpacing = 2.0;

        var result = PatternTiler.Tile(cell, plateSize, partSpacing);

        // cell width = 10 + 2 = 12, cols = floor(30/12) = 2
        // cell height = 10 + 2 = 12, rows = floor(20/12) = 1
        // 2 x 1 = 2 parts
        Assert.Equal(2, result.Count);
    }

    [Fact]
    public void Tile_EmptyCell_ReturnsEmpty()
    {
        var result = PatternTiler.Tile(new List<Part>(), new Size(100, 100), 0);
        Assert.Empty(result);
    }

    [Fact]
    public void Tile_NonSquarePlate_CorrectAxes()
    {
        var drawing = MakeSquareDrawing(10);
        var cell = new List<Part> { Part.CreateAtOrigin(drawing) };
        // Wide plate: 50 in X (Width), 10 in Y (Length) — should fit 5x1
        var plateSize = new Size(50, 10);

        var result = PatternTiler.Tile(cell, plateSize, 0);

        Assert.Equal(5, result.Count);

        // Verify parts span the X axis, not Y
        var maxRight = result.Max(p => p.BoundingBox.Right);
        var maxTop = result.Max(p => p.BoundingBox.Top);
        Assert.True(maxRight <= 50.001);
        Assert.True(maxTop <= 10.001);
    }

    [Fact]
    public void Tile_CellLargerThanPlate_ReturnsSingleCell()
    {
        var drawing = MakeSquareDrawing(50);
        var cell = new List<Part> { Part.CreateAtOrigin(drawing) };
        var plateSize = new Size(30, 30);

        var result = PatternTiler.Tile(cell, plateSize, 0);

        // Cell doesn't fit at all — 0 parts
        Assert.Empty(result);
    }
}
  • Step 2: Run tests to verify they fail

Run: dotnet test OpenNest.Tests --filter "FullyQualifiedName~PatternTilerTests" -v n Expected: Build error — PatternTiler does not exist.

  • Step 3: Implement PatternTiler
// OpenNest.Engine/PatternTiler.cs
using System;
using System.Collections.Generic;
using System.Linq;
using OpenNest.Geometry;

namespace OpenNest.Engine
{
    public static class PatternTiler
    {
        /// <summary>
        /// Tiles a unit cell across a plate, returning cloned parts.
        /// </summary>
        /// <param name="cell">The unit cell parts (positioned relative to each other).</param>
        /// <param name="plateSize">The plate size to tile across.</param>
        /// <param name="partSpacing">Spacing to add around each cell.</param>
        /// <returns>List of cloned parts filling the plate.</returns>
        public static List<Part> Tile(List<Part> cell, Size plateSize, double partSpacing)
        {
            if (cell == null || cell.Count == 0)
                return new List<Part>();

            var cellBox = cell.GetBoundingBox();
            var halfSpacing = partSpacing / 2;

            var cellWidth = cellBox.Width + partSpacing;
            var cellHeight = cellBox.Length + partSpacing;

            if (cellWidth <= 0 || cellHeight <= 0)
                return new List<Part>();

            // Size.Width = X-axis, Size.Length = Y-axis
            var cols = (int)System.Math.Floor(plateSize.Width / cellWidth);
            var rows = (int)System.Math.Floor(plateSize.Length / cellHeight);

            if (cols <= 0 || rows <= 0)
                return new List<Part>();

            // Offset to normalize cell origin to (halfSpacing, halfSpacing)
            var cellOrigin = cellBox.Location;
            var baseOffset = new Vector(halfSpacing - cellOrigin.X, halfSpacing - cellOrigin.Y);

            var result = new List<Part>(cols * rows * cell.Count);

            for (var row = 0; row < rows; row++)
            {
                for (var col = 0; col < cols; col++)
                {
                    var tileOffset = baseOffset + new Vector(col * cellWidth, row * cellHeight);

                    foreach (var part in cell)
                    {
                        result.Add(part.CloneAtOffset(tileOffset));
                    }
                }
            }

            return result;
        }
    }
}
  • Step 4: Run tests to verify they pass

Run: dotnet test OpenNest.Tests --filter "FullyQualifiedName~PatternTilerTests" -v n Expected: All 6 tests PASS.

  • Step 5: Commit
git add OpenNest.Engine/PatternTiler.cs OpenNest.Tests/PatternTilerTests.cs
git commit -m "feat(engine): add PatternTiler for unit cell tiling across plates"

Task 2: PatternTileForm — the dialog window

The WinForms dialog with split layout, drawing pickers, plate size controls, and the two PlateView panels. This task builds the form shell and layout — interaction logic comes in Task 3.

Files:

  • Create: OpenNest/Forms/PatternTileForm.cs
  • Create: OpenNest/Forms/PatternTileForm.Designer.cs

Key reference files:

  • OpenNest/Forms/BestFitViewerForm.cs — similar standalone tool form pattern

  • OpenNest/Controls/PlateView.cs — the control used in both panels

  • OpenNest/Forms/EditPlateForm.cs — plate size input pattern

  • Step 1: Create PatternTileForm.Designer.cs

// OpenNest/Forms/PatternTileForm.Designer.cs
namespace OpenNest.Forms
{
    partial class PatternTileForm
    {
        private System.ComponentModel.IContainer components = null;

        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
                components.Dispose();
            base.Dispose(disposing);
        }

        private void InitializeComponent()
        {
            this.topPanel = new System.Windows.Forms.FlowLayoutPanel();
            this.lblDrawingA = new System.Windows.Forms.Label();
            this.cboDrawingA = new System.Windows.Forms.ComboBox();
            this.lblDrawingB = new System.Windows.Forms.Label();
            this.cboDrawingB = new System.Windows.Forms.ComboBox();
            this.lblPlateSize = new System.Windows.Forms.Label();
            this.txtPlateSize = new System.Windows.Forms.TextBox();
            this.lblPartSpacing = new System.Windows.Forms.Label();
            this.nudPartSpacing = new System.Windows.Forms.NumericUpDown();
            this.btnAutoArrange = new System.Windows.Forms.Button();
            this.btnApply = new System.Windows.Forms.Button();
            this.splitContainer = new System.Windows.Forms.SplitContainer();
            this.topPanel.SuspendLayout();
            ((System.ComponentModel.ISupportInitialize)(this.nudPartSpacing)).BeginInit();
            ((System.ComponentModel.ISupportInitialize)(this.splitContainer)).BeginInit();
            this.splitContainer.SuspendLayout();
            this.SuspendLayout();
            //
            // topPanel — FlowLayoutPanel for correct left-to-right ordering
            //
            this.topPanel.Controls.Add(this.lblDrawingA);
            this.topPanel.Controls.Add(this.cboDrawingA);
            this.topPanel.Controls.Add(this.lblDrawingB);
            this.topPanel.Controls.Add(this.cboDrawingB);
            this.topPanel.Controls.Add(this.lblPlateSize);
            this.topPanel.Controls.Add(this.txtPlateSize);
            this.topPanel.Controls.Add(this.lblPartSpacing);
            this.topPanel.Controls.Add(this.nudPartSpacing);
            this.topPanel.Controls.Add(this.btnAutoArrange);
            this.topPanel.Controls.Add(this.btnApply);
            this.topPanel.Dock = System.Windows.Forms.DockStyle.Top;
            this.topPanel.Height = 36;
            this.topPanel.Padding = new System.Windows.Forms.Padding(4, 4, 4, 4);
            this.topPanel.WrapContents = false;
            //
            // lblDrawingA
            //
            this.lblDrawingA.Text = "Drawing A:";
            this.lblDrawingA.AutoSize = true;
            this.lblDrawingA.Margin = new System.Windows.Forms.Padding(3, 5, 0, 0);
            //
            // cboDrawingA
            //
            this.cboDrawingA.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
            this.cboDrawingA.Width = 130;
            //
            // lblDrawingB
            //
            this.lblDrawingB.Text = "Drawing B:";
            this.lblDrawingB.AutoSize = true;
            this.lblDrawingB.Margin = new System.Windows.Forms.Padding(10, 5, 0, 0);
            //
            // cboDrawingB
            //
            this.cboDrawingB.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
            this.cboDrawingB.Width = 130;
            //
            // lblPlateSize
            //
            this.lblPlateSize.Text = "Plate Size:";
            this.lblPlateSize.AutoSize = true;
            this.lblPlateSize.Margin = new System.Windows.Forms.Padding(10, 5, 0, 0);
            //
            // txtPlateSize
            //
            this.txtPlateSize.Width = 80;
            //
            // lblPartSpacing
            //
            this.lblPartSpacing.Text = "Spacing:";
            this.lblPartSpacing.AutoSize = true;
            this.lblPartSpacing.Margin = new System.Windows.Forms.Padding(10, 5, 0, 0);
            //
            // nudPartSpacing
            //
            this.nudPartSpacing.Width = 60;
            this.nudPartSpacing.DecimalPlaces = 2;
            this.nudPartSpacing.Increment = 0.25m;
            this.nudPartSpacing.Maximum = 100;
            this.nudPartSpacing.Minimum = 0;
            //
            // btnAutoArrange
            //
            this.btnAutoArrange.Text = "Auto-Arrange";
            this.btnAutoArrange.Width = 100;
            this.btnAutoArrange.Margin = new System.Windows.Forms.Padding(10, 0, 0, 0);
            //
            // btnApply
            //
            this.btnApply.Text = "Apply...";
            this.btnApply.Width = 80;
            //
            // splitContainer
            //
            this.splitContainer.Dock = System.Windows.Forms.DockStyle.Fill;
            this.splitContainer.SplitterDistance = 350;
            //
            // PatternTileForm
            //
            this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.ClientSize = new System.Drawing.Size(900, 550);
            this.Controls.Add(this.splitContainer);
            this.Controls.Add(this.topPanel);
            this.MinimumSize = new System.Drawing.Size(700, 400);
            this.Name = "PatternTileForm";
            this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
            this.Text = "Pattern Tile Layout";
            this.topPanel.ResumeLayout(false);
            this.topPanel.PerformLayout();
            ((System.ComponentModel.ISupportInitialize)(this.nudPartSpacing)).EndInit();
            ((System.ComponentModel.ISupportInitialize)(this.splitContainer)).EndInit();
            this.splitContainer.ResumeLayout(false);
            this.ResumeLayout(false);
        }

        private System.Windows.Forms.FlowLayoutPanel topPanel;
        private System.Windows.Forms.Label lblDrawingA;
        private System.Windows.Forms.ComboBox cboDrawingA;
        private System.Windows.Forms.Label lblDrawingB;
        private System.Windows.Forms.ComboBox cboDrawingB;
        private System.Windows.Forms.Label lblPlateSize;
        private System.Windows.Forms.TextBox txtPlateSize;
        private System.Windows.Forms.Label lblPartSpacing;
        private System.Windows.Forms.NumericUpDown nudPartSpacing;
        private System.Windows.Forms.Button btnAutoArrange;
        private System.Windows.Forms.Button btnApply;
        private System.Windows.Forms.SplitContainer splitContainer;
    }
}
  • Step 2: Create PatternTileForm.cs — form shell with PlateViews
// OpenNest/Forms/PatternTileForm.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using OpenNest.Controls;
using OpenNest.Geometry;

namespace OpenNest.Forms
{
    public enum PatternTileTarget
    {
        CurrentPlate,
        NewPlate
    }

    public class PatternTileResult
    {
        public List<Part> Parts { get; set; }
        public PatternTileTarget Target { get; set; }
        public Size PlateSize { get; set; }
    }

    public partial class PatternTileForm : Form
    {
        private readonly Nest nest;
        private readonly PlateView cellView;
        private readonly PlateView previewView;

        public PatternTileResult Result { get; private set; }

        public PatternTileForm(Nest nest)
        {
            this.nest = nest;
            InitializeComponent();

            // Unit cell editor — plate outline hidden via zero-size plate
            cellView = new PlateView();
            cellView.Plate.Size = new Size(0, 0);
            cellView.Plate.Quantity = 0; // prevent Drawing.Quantity.Nested side-effects
            cellView.DrawOrigin = false;
            cellView.DrawBounds = false; // hide selection bounding box overlay
            cellView.Dock = DockStyle.Fill;
            splitContainer.Panel1.Controls.Add(cellView);

            // Tile preview — plate visible, read-only
            previewView = new PlateView();
            previewView.Plate.Quantity = 0; // prevent Drawing.Quantity.Nested side-effects
            previewView.AllowSelect = false;
            previewView.AllowDrop = false;
            previewView.DrawBounds = false;
            previewView.Dock = DockStyle.Fill;
            splitContainer.Panel2.Controls.Add(previewView);

            // Populate drawing dropdowns
            var drawings = nest.Drawings.OrderBy(d => d.Name).ToList();
            cboDrawingA.Items.Add("(none)");
            cboDrawingB.Items.Add("(none)");
            foreach (var d in drawings)
            {
                cboDrawingA.Items.Add(d);
                cboDrawingB.Items.Add(d);
            }
            cboDrawingA.SelectedIndex = 0;
            cboDrawingB.SelectedIndex = 0;

            // Default plate size from nest defaults
            var defaults = nest.PlateDefaults;
            txtPlateSize.Text = defaults.Size.ToString();
            nudPartSpacing.Value = (decimal)defaults.PartSpacing;

            // Wire events
            cboDrawingA.SelectedIndexChanged += OnDrawingChanged;
            cboDrawingB.SelectedIndexChanged += OnDrawingChanged;
            txtPlateSize.TextChanged += OnPlateSettingsChanged;
            nudPartSpacing.ValueChanged += OnPlateSettingsChanged;
            btnAutoArrange.Click += OnAutoArrangeClick;
            btnApply.Click += OnApplyClick;
            cellView.MouseUp += OnCellMouseUp;
        }

        private Drawing SelectedDrawingA =>
            cboDrawingA.SelectedItem as Drawing;

        private Drawing SelectedDrawingB =>
            cboDrawingB.SelectedItem as Drawing;

        private double PartSpacing =>
            (double)nudPartSpacing.Value;

        private bool TryGetPlateSize(out Size size)
        {
            return Size.TryParse(txtPlateSize.Text, out size);
        }

        private void OnDrawingChanged(object sender, EventArgs e)
        {
            RebuildCell();
            RebuildPreview();
        }

        private void OnPlateSettingsChanged(object sender, EventArgs e)
        {
            UpdatePreviewPlateSize();
            RebuildPreview();
        }

        private void OnCellMouseUp(object sender, MouseEventArgs e)
        {
            if (e.Button == MouseButtons.Left && cellView.Plate.Parts.Count == 2)
            {
                CompactCellParts();
            }

            RebuildPreview();
        }

        private void RebuildCell()
        {
            cellView.Plate.Parts.Clear();

            var drawingA = SelectedDrawingA;
            var drawingB = SelectedDrawingB;

            if (drawingA == null && drawingB == null)
                return;

            if (drawingA != null)
            {
                var partA = Part.CreateAtOrigin(drawingA);
                cellView.Plate.Parts.Add(partA);
            }

            if (drawingB != null)
            {
                var partB = Part.CreateAtOrigin(drawingB);

                // Place B to the right of A (or at origin if A is null)
                if (drawingA != null && cellView.Plate.Parts.Count > 0)
                {
                    var aBox = cellView.Plate.Parts[0].BoundingBox;
                    partB.Offset(aBox.Right + PartSpacing, 0);
                }

                cellView.Plate.Parts.Add(partB);
            }

            cellView.ZoomToFit();
        }

        private void CompactCellParts()
        {
            var parts = cellView.Plate.Parts.ToList();
            if (parts.Count < 2)
                return;

            var combinedBox = parts.GetBoundingBox();
            var centroid = combinedBox.Center;
            var syntheticWorkArea = new Box(-10000, -10000, 20000, 20000);

            for (var iteration = 0; iteration < 10; iteration++)
            {
                var totalMoved = 0.0;

                foreach (var part in parts)
                {
                    var partCenter = part.BoundingBox.Center;
                    var dx = centroid.X - partCenter.X;
                    var dy = centroid.Y - partCenter.Y;
                    var dist = System.Math.Sqrt(dx * dx + dy * dy);

                    if (dist < 0.01)
                        continue;

                    var angle = System.Math.Atan2(dy, dx);
                    var single = new List<Part> { part };
                    var obstacles = parts.Where(p => p != part).ToList();

                    totalMoved += Compactor.Push(single, obstacles,
                        syntheticWorkArea, PartSpacing, angle);
                }

                if (totalMoved < 0.01)
                    break;
            }

            cellView.Refresh();
        }

        private void UpdatePreviewPlateSize()
        {
            if (TryGetPlateSize(out var size))
                previewView.Plate.Size = size;
        }

        private void RebuildPreview()
        {
            previewView.Plate.Parts.Clear();

            if (!TryGetPlateSize(out var plateSize))
                return;

            previewView.Plate.Size = plateSize;
            previewView.Plate.PartSpacing = PartSpacing;

            var cellParts = cellView.Plate.Parts.ToList();
            if (cellParts.Count == 0)
                return;

            var tiled = Engine.PatternTiler.Tile(cellParts, plateSize, PartSpacing);

            foreach (var part in tiled)
                previewView.Plate.Parts.Add(part);

            previewView.ZoomToFit();
        }

        private void OnAutoArrangeClick(object sender, EventArgs e)
        {
            var drawingA = SelectedDrawingA;
            var drawingB = SelectedDrawingB;

            if (drawingA == null || drawingB == null)
                return;

            if (!TryGetPlateSize(out var plateSize))
                return;

            Cursor = Cursors.WaitCursor;
            try
            {
                var angles = new[] { 0.0, Math.Angle.ToRadians(90), Math.Angle.ToRadians(180), Math.Angle.ToRadians(270) };
                var bestCell = (List<Part>)null;
                var bestArea = double.MaxValue;

                foreach (var angleA in angles)
                {
                    foreach (var angleB in angles)
                    {
                        var partA = Part.CreateAtOrigin(drawingA, angleA);
                        var partB = Part.CreateAtOrigin(drawingB, angleB);
                        partB.Offset(partA.BoundingBox.Right + PartSpacing, 0);

                        var cell = new List<Part> { partA, partB };

                        // Compact toward center
                        var box = cell.GetBoundingBox();
                        var centroid = box.Center;
                        var syntheticWorkArea = new Box(-10000, -10000, 20000, 20000);

                        for (var i = 0; i < 10; i++)
                        {
                            var moved = 0.0;
                            foreach (var part in cell)
                            {
                                var pc = part.BoundingBox.Center;
                                var dx = centroid.X - pc.X;
                                var dy = centroid.Y - pc.Y;
                                if (System.Math.Sqrt(dx * dx + dy * dy) < 0.01)
                                    continue;

                                var angle = System.Math.Atan2(dy, dx);
                                var single = new List<Part> { part };
                                var obstacles = cell.Where(p => p != part).ToList();
                                moved += Compactor.Push(single, obstacles, syntheticWorkArea, PartSpacing, angle);
                            }
                            if (moved < 0.01) break;
                        }

                        var finalBox = cell.GetBoundingBox();
                        var area = finalBox.Width * finalBox.Length;

                        if (area < bestArea)
                        {
                            bestArea = area;
                            bestCell = cell;
                        }
                    }
                }

                if (bestCell != null)
                {
                    cellView.Plate.Parts.Clear();
                    foreach (var part in bestCell)
                        cellView.Plate.Parts.Add(part);
                    cellView.ZoomToFit();
                    RebuildPreview();
                }
            }
            finally
            {
                Cursor = Cursors.Default;
            }
        }

        private void OnApplyClick(object sender, EventArgs e)
        {
            if (previewView.Plate.Parts.Count == 0)
                return;

            if (!TryGetPlateSize(out var plateSize))
                return;

            var choice = MessageBox.Show(
                "Apply pattern to current plate?\n\nYes = Current plate (clears existing parts)\nNo = New plate",
                "Apply Pattern",
                MessageBoxButtons.YesNoCancel,
                MessageBoxIcon.Question);

            if (choice == DialogResult.Cancel)
                return;

            // Rebuild a fresh set of tiled parts for the caller
            var cellParts = cellView.Plate.Parts.ToList();
            var tiledParts = Engine.PatternTiler.Tile(cellParts, plateSize, PartSpacing);

            Result = new PatternTileResult
            {
                Parts = tiledParts,
                Target = choice == DialogResult.Yes
                    ? PatternTileTarget.CurrentPlate
                    : PatternTileTarget.NewPlate,
                PlateSize = plateSize
            };

            DialogResult = DialogResult.OK;
            Close();
        }
    }
}
  • Step 3: Build to verify compilation

Run: dotnet build OpenNest.sln Expected: Build succeeds with no errors.

  • Step 4: Commit
git add OpenNest/Forms/PatternTileForm.cs OpenNest/Forms/PatternTileForm.Designer.cs
git commit -m "feat(ui): add PatternTileForm dialog with unit cell editor and tile preview"

Task 3: Wire up menu entry and apply logic in MainForm

Add a "Pattern Tile" menu item under Tools, wire it to open PatternTileForm, and handle the result by placing parts on the target plate.

Files:

  • Modify: OpenNest/Forms/MainForm.Designer.cs — add menu item

  • Modify: OpenNest/Forms/MainForm.cs — add click handler and apply logic

  • Step 1: Add menu item to MainForm.Designer.cs

In the InitializeComponent method:

  1. Add field declaration at end of class (near the other mnuTools* fields):
private System.Windows.Forms.ToolStripMenuItem mnuToolsPatternTile;
  1. In InitializeComponent, add initialization (near the other mnuTools* instantiations):
this.mnuToolsPatternTile = new System.Windows.Forms.ToolStripMenuItem();
  1. Add to the mnuTools.DropDownItems array — insert mnuToolsPatternTile after mnuToolsBestFitViewer:
mnuTools.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
    mnuToolsMeasureArea, mnuToolsBestFitViewer, mnuToolsPatternTile, mnuToolsAlign,
    toolStripMenuItem14, mnuSetOffsetIncrement, mnuSetRotationIncrement,
    toolStripMenuItem15, mnuToolsOptions });
  1. Add configuration block:
// mnuToolsPatternTile
this.mnuToolsPatternTile.Name = "mnuToolsPatternTile";
this.mnuToolsPatternTile.Size = new System.Drawing.Size(214, 22);
this.mnuToolsPatternTile.Text = "Pattern Tile";
this.mnuToolsPatternTile.Click += PatternTile_Click;
  • Step 2: Add click handler and apply logic to MainForm.cs

Add in the #region Tools Menu Events section, after BestFitViewer_Click:

private void PatternTile_Click(object sender, EventArgs e)
{
    if (activeForm == null)
        return;

    if (activeForm.Nest.Drawings.Count == 0)
    {
        MessageBox.Show("No drawings available.", "Pattern Tile",
            MessageBoxButtons.OK, MessageBoxIcon.Information);
        return;
    }

    using (var form = new PatternTileForm(activeForm.Nest))
    {
        if (form.ShowDialog(this) != DialogResult.OK || form.Result == null)
            return;

        var result = form.Result;

        if (result.Target == PatternTileTarget.CurrentPlate)
        {
            activeForm.PlateView.Plate.Parts.Clear();

            foreach (var part in result.Parts)
                activeForm.PlateView.Plate.Parts.Add(part);

            activeForm.PlateView.ZoomToFit();
        }
        else
        {
            var plate = activeForm.Nest.CreatePlate();
            plate.Size = result.PlateSize;

            foreach (var part in result.Parts)
                plate.Parts.Add(part);

            activeForm.LoadLastPlate();
        }

        activeForm.Nest.UpdateDrawingQuantities();
    }
}
  • Step 3: Build and manually test

Run: dotnet build OpenNest.sln Expected: Build succeeds. Launch the app, create a nest, import some DXFs, then Tools > Pattern Tile opens the form.

  • Step 4: Commit
git add OpenNest/Forms/MainForm.cs OpenNest/Forms/MainForm.Designer.cs
git commit -m "feat(ui): wire Pattern Tile menu item and apply logic in MainForm"

Task 4: Manual testing and polish

Final integration testing and any adjustments.

Files:

  • Possibly modify: OpenNest/Forms/PatternTileForm.cs, OpenNest/Forms/PatternTileForm.Designer.cs

  • Step 1: End-to-end test workflow

  1. Launch the app, create a new nest
  2. Import 2+ DXF drawings
  3. Open Tools > Pattern Tile
  4. Select Drawing A and Drawing B
  5. Verify parts appear in left panel, can be dragged
  6. Verify compaction on mouse release closes gaps
  7. Verify tile preview updates on the right
  8. Change plate size — verify preview updates
  9. Change spacing — verify preview updates
  10. Click Auto-Arrange — verify it picks a tight arrangement
  11. Click Apply > Yes (current plate) — verify parts placed
  12. Reopen, Apply > No (new plate) — verify new plate created with parts
  13. Test single drawing only (one dropdown set, other on "(none)")
  14. Test same drawing in both dropdowns
  • Step 2: Fix any issues found during testing

Address any layout, interaction, or rendering issues discovered.

  • Step 3: Final commit
git add -A
git commit -m "fix(ui): polish PatternTileForm after manual testing"