Creating a rectangular jigsaw puzzle with specific dimensions inside AutoCAD using .NET

Following on from the last post, where we saw an outline for this series of posts on AutoCAD I/O, today's post adds a command to our jigsaw application that creates the geometry for a jigsaw puzzle of a specified size and with a specified number of pieces.

As jigsaw puzzle pieces are largely quite square, it actually took me some time to get my head around the mathematics needed to calculate the number of pieces we need in each of the X and Y directions to make a puzzle of a certain size. And it's (with hindsight) obviously not possible to make a square puzzle work with an arbitrary number of pieces, which is why the application asks for an approximate number of pieces and then does its best to meet it.

The approach should be fairly obvious from the codeโ€ฆ here's the new JIGG command in action:

Grid-based jigsaw

At the command-line we see that the puzzle is actually smaller that the proposed 13K pieces, because we couldn't can't create a rectangle of that size.

Puzzle will be 147 x 88 (12936 in total).

Here's the C# code:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Geometry;

using Autodesk.AutoCAD.Runtime;

using System;

 

namespace JigsawGenerator

{

  public class Commands

  {

    // The WIGL command asks the user to enter this value (which

    // influences the extent of the "wiggle"). For the JIG, JIGG

    // and JIGL commands we just use this hardcoded value.

    // We could certainly ask the user to enter it or get it

    // from a system variable, of course

 

    const double wigFac = 0.8;

 

    // We'll store a central random number generator,

    // which means we'll get more random results

 

    private Random _rnd = null;

 

    // Constructor

 

    public Commands()

    {

      _rnd = new Random();

    }

 

    [CommandMethod("JIG")]

    public void JigEntity()

    {

      var doc = Application.DocumentManager.MdiActiveDocument;

      if (null == doc)

        return;

      var db = doc.Database;

      var ed = doc.Editor;

 

      // Select our entity to create a tab for

 

      var peo = new PromptEntityOptions("\nSelect entity to jig");

      peo.SetRejectMessage("\nEntity must be a curve.");

      peo.AddAllowedClass(typeof(Curve), false);

 

      var per = ed.GetEntity(peo);

      if (per.Status != PromptStatus.OK)

        return;

 

      // We'll ask the user to select intersecting/delimiting

      // entities: if they choose none we use the whole length

 

      ed.WriteMessage(

        "\nSelect intersecting entities. " +

        "Hit enter to use whole entity."

      );

 

      var pso = new PromptSelectionOptions();

      var psr = ed.GetSelection();

      i
f
(

        psr.Status != PromptStatus.OK &&

        psr.Status != PromptStatus.Error // No selection

      )

        return;

 

      using (var tr = doc.TransactionManager.StartTransaction())

      {

        // Open our main curve

 

        var cur =

          tr.GetObject(per.ObjectId, OpenMode.ForRead) as Curve;

 

        double start = 0, end = 0;

        bool bounded = false;

 

        if (cur != null)

        {

          // We'll collect the intersections, if we have

          // delimiting entities selected

 

          var pts = new Point3dCollection();

 

          if (psr.Value != null)

          {

            // Loop through and collect the intersections

 

            foreach (var id in psr.Value.GetObjectIds())

            {

              var ent = (Entity)tr.GetObject(id, OpenMode.ForRead);

 

              cur.IntersectWith(

                ent,

                Intersect.OnBothOperands,

                pts,

                IntPtr.Zero,

                IntPtr.Zero

              );

            }

          }

 

          ed.WriteMessage(

            "\nFound {0} intersection points.", pts.Count

          );

 

          // If we have no intersections, use the start and end

          // points

 

          if (pts.Count == 0)

          {

            start = cur.StartParam;

            end = cur.EndParam;

            pts.Add(cur.StartPoint);

            pts.Add(cur.EndPoint);

            bounded = true;

          }

          else if (pts.Count == 2)

          {

            start = cur.GetParameterAtPoint(pts[0]);

            end = cur.GetParameterAtPoint(pts[1]);

            bounded = true;

          }

 

          // If we have a bounded length, create our tab in a random

          // direction

 

          if (bounded)

          {

            var left = _rnd.NextDouble() >= 0.5;

 

            var sp = CreateTab(cur, start, end, pts, left);

 

            var btr =

              (BlockTableRecord)tr.GetObject(

                SymbolUtilityServices.GetBlockModelSpaceId(db),

                OpenMode.ForWrite

              );

            btr.AppendEntity(sp);

            tr.AddNewlyCreatedDBObject(sp, true);

          }

        }

 

        tr.Commit();

      }

    }

 

    [CommandMethod("JIGL")]

    public void JigLines()

    {

      var doc = Application.DocumentManager.MdiActiveDocument;

      if (null == doc)

        return;

      var db = doc.Database;

      var ed = doc.Editor;

 

      // Here we're going to get a selection set, but only care

      // about lines

 

      var psr = ed.GetSelection();

      if (psr.Status != PromptStatus.OK)

        return;

 

      using (var tr = doc.TransactionManager.StartTransaction())

      {

        var btr =

          (BlockTableRecord)tr.GetObject(

            SymbolUtilityServices.GetBlockModelSpaceId(db),

            OpenMode.ForWrite

          );

 

        // We'll be generating random numbers to decide direction

        // for each tab

 

        foreach (var id in psr.Value.GetObjectIds())

        {

          // We only care about lines

 

          var ln = tr.GetObject(id, OpenMode.ForRead) as Line;

          if (ln != null)

          {

            // Get the start and end points in a collection

 

            var pts =

              new Point3dCollection(

                new Point3d[] {

                  ln.StartPoint,

                  ln.EndPoint

                }

              );

 

            // Decide the direction (randomly) then create the tab

 

            var left = _rnd.NextDouble() >= 0.5;

            var sp =

              CreateTab(ln, ln.StartParam, ln.EndParam, pts, left);

 

            btr.AppendEntity(sp);

            tr.AddNewlyCreatedDBObject(sp, true);< /p>

          }

        }

        tr.Commit();

      }

    }

 

    [CommandMethod("JIGG")]

    public void JigGrid()

    {

      var doc = Application.DocumentManager.MdiActiveDocument;

      if (null == doc)

        return;

      var db = doc.Database;

      var ed = doc.Editor;

 

      // Get overall dimensions of the puzzle

 

      var pdo = new PromptDoubleOptions("\nEnter puzzle width");

      pdo.AllowNegative = false;

      pdo.AllowNone = false;

      pdo.AllowZero = false;

 

      var pdr = ed.GetDouble(pdo);

      if (pdr.Status != PromptStatus.OK)

        return;

 

      var width = pdr.Value;

 

      pdo.Message = "\nEnter puzzle height";

      pdr = ed.GetDouble(pdo);

      if (pdr.Status != PromptStatus.OK)

        return;

 

      var height = pdr.Value;

 

      // Get the (approximate) number of pieces

 

      var pio =

        new PromptIntegerOptions("\nApproximate number of pieces");

      pio.AllowNegative = false;

      pio.AllowNone = false;

      pio.AllowZero = false;

 

      var pir = ed.GetInteger(pio);

      if (pir.Status != PromptStatus.OK)

        return;

 

      var pieces = pir.Value;

 

      var aspect = height / width;

      var piecesY = Math.Floor(Math.Sqrt(aspect * pieces));

      var piecesX = Math.Floor(pieces / piecesY);

 

      ed.WriteMessage(

        "\nPuzzle will be {0} x {1} ({2} in total).",

        piecesX, piecesY, piecesX * piecesY

      );

 

      using (var tr = doc.TransactionManager.StartTransaction())

      {

        var btr =

          (BlockTableRecord)tr.GetObject(

            SymbolUtilityServices.GetBlockModelSpaceId(db),

            OpenMode.ForWrite

          );

 

        var incX = width / piecesX;

        var incY = height / piecesY;

        var tol = Tolerance.Global.EqualPoint;

 

        for (double x = 0; x < width - tol; x += incX)

        {

          for (double y = 0; y < height - tol; y += incY)

          {

       
     var nextX = x + incX;

            var nextY = y + incY;

 

            // At each point in the grid - apart from when along

            // the axes - we're going to create two lines, one

            // in the X direction and one in the Y (along the axes

            // we'll usually be creating one or the other, unless

            // at the origin ๐Ÿ™‚

 

            if (y > 0)

            {

              var sp =

                CreateTabFromPoints(

                  new Point3d(x, y, 0),

                  new Point3d(nextX, y, 0)

                );

              btr.AppendEntity(sp);

              tr.AddNewlyCreatedDBObject(sp, true);

            }

 

            if (x > 0)

            {

              var sp =

                CreateTabFromPoints(

                  new Point3d(x, y, 0),

                  new Point3d(x, nextY, 0)

                );

              btr.AppendEntity(sp);

              tr.AddNewlyCreatedDBObject(sp, true);

            }

          }

        }

 

        // Create the puzzle border as a closed polyline

 

        var pl = new Polyline(4);

        pl.AddVertexAt(0, Point2d.Origin, 0, 0, 0);

        pl.AddVertexAt(1, new Point2d(width, 0), 0, 0, 0);

        pl.AddVertexAt(2, new Point2d(width, height), 0, 0, 0);

        pl.AddVertexAt(3, new Point2d(0, height), 0, 0, 0);

        pl.Closed = true;

 

        btr.AppendEntity(pl);

        tr.AddNewlyCreatedDBObject(pl, true);

 

        tr.Commit();

      }

    }

 

    private Curve CreateTabFromPoints(Point3d start, Point3d end)

    {

      using (var ln = new Line(start, end))

      {

        // Get the start and end points in a collection

 

        var pts =

          new Point3dCollection(new Point3d[] { start, end });

 

        // Decide the direction (randomly) then create the tab

 

        var left = _rnd.NextDouble() >= 0.5;

 

        return CreateTab(ln, ln.StartParam, ln.EndParam, pts, left);

      }

    }

 

    [CommandMethod("WIGL")]

    public void AdjustTabs()

    {

      var doc = Application.DocumentManager.MdiActiveDocument;

      if (null == doc)

        return;

      var db = doc.Database;

      var ed = doc.Editor;

 

      // Here we're going to get a selection set, but only care

      // about splines

 

      var pso = new PromptSelectionOptions();

      var psr = ed.GetSelection();

      if (psr.Status != PromptStatus.OK)

        return;

 

      var pdo = new PromptDoubleOptions("\nEnter wiggle factor");

      pdo.DefaultValue = 0.8;

      pdo.UseDefaultValue = true;

      pdo.AllowNegative = false;

      pdo.AllowZero = false;

 

      var pdr = ed.GetDouble(pdo);

      if (pdr.Status != PromptStatus.OK)

        return;

 

      using (var tr = doc.TransactionManager.StartTransaction())

      {

        foreach (var id in psr.Value.GetObjectIds())

        {

          // We only care about splines

 

          var sp = tr.GetObject(id, OpenMode.ForRead) as Spline;

          if (sp != null && sp.NumFitPoints == 6)

          {

            // Collect the fit points

 

            var pts = sp.FitData.GetFitPoints();

 

            // Adjust them

 

            AddWiggle(pts, pdr.Value);

 

            // Set back the top points to the spline

            // (we know these are the ones that have changed)

 

            sp.UpgradeOpen();

 

            sp.SetFitPointAt(2, pts[2]);

            sp.SetFitPointAt(3, pts[3]);

          }

        }

        tr.Commit();

      }

    }

 

    private Curve CreateTab(

      Curve cur, double start, double end, Point3dCollection pts,

      bool left = true

    )

    {

      // Calculate the length of this curve (or section)

 

      var len =

        Math.Abs(

          cur.GetDistanceAtParameter(end) -

          cur.GetDistanceAtParameter(start)

        );

 

      // We're calculating a random delta to adjust the location

      // of the tab along the length

 

 
0;    double delta = 0.01 * len * (_rnd.NextDouble() - 0.5);

 

      // We're going to offset to the side of the core curve for

      // the tab points. This is currently a fixed tab size

      // (could also make this proportional to the curve)

 

      double off = 0.2 * len; // was 0.5

      double fac = 0.5 * (len - 0.5 * off) / len;

      if (left) off = -off;

 

      // Get the next parameter along the length of the curve

      // and add the point associated with it into our fit points

 

      var nxtParam = start + (end - start) * (fac + delta);

      var nxt = cur.GetPointAtParameter(nxtParam);

      pts.Insert(1, nxt);

 

      // Get the direction vector of the curve

 

      var vec = pts[1] - pts[0];

 

      // Rotate it by 90 degrees in the direction we chose,

      // then normalise it and use it to calculate the location

      // of the next point

 

      vec = vec.RotateBy(Math.PI * 0.5, Vector3d.ZAxis);

      vec = off * vec / vec.Length;

      pts.Insert(2, nxt + vec);

 

      // Now we calculate the mirror points to complete the

      // splines definition

 

      nxtParam = end - (end - start) * (fac - delta);

      nxt = cur.GetPointAtParameter(nxtParam);

      pts.Insert(3, nxt + vec);

      pts.Insert(4, nxt);

 

      AddWiggle(pts, wigFac);

 

      // Finally we create our spline

 

      return new Spline(pts, 1, 0);

    }

 

    private void AddWiggle(Point3dCollection pts, double fac)

    {

      const double rebase = 0.3;

 

      // Works on sets of six points only

      //

      //             2--------3

      //             |        |

      //             |        |

      // 0-----------1        4-----------5

 

      if (pts.Count != 6)

        return;

 

      // Our spline's direction, tab width and perpendicular vector

 

      var dir = pts[5] - pts[0];

      dir = dir / dir.Length;

      var tab = (pts[4] - pts[1]).Length;

      var cross = dir.RotateBy(Math.PI * 0.5, Vector3d.ZAxis);

      cross = cross / cross.Length;

 

      // Adjust the "top left" and "top right" points outwards,

      // multiplying by fac1 and the random factor (0-1) brought

      // back towards -0.5 to 0.5 by fac2

 

      pts[2] =

        pts[2]

        - (dir * tab * fac * (_rnd.NextDouble() - rebase))

        + (cross * tab * fac * (_rnd.NextDouble() - rebase));

      pts[3] =

        pts[3]

        + (dir * tab * fac * (_rnd.NextDouble() - rebase))

 &#
160;      + (cross * tab * fac * (_rnd.NextDouble() - rebase));

    }

  }

}

At some point this application will need to take some additional input โ€“ we're going to want to create an engraving layer, displaying a simplified version of a picture or photo โ€“ but that's for a future post.

3 responses to “Creating a rectangular jigsaw puzzle with specific dimensions inside AutoCAD using .NET”

  1. Thank you for this. Unfortunately I'm running acad LT which doesn't have the netload command. Jigsawify.com worked well to create a dwg file for me, but I found myself wanting the Wigl command to achieve larger puzzle nodes. Is there an easy way to incorporate this into the website interface? Much appreciated.

    1. You could build your own site based on the source code that has this functionality, but it's not something I plan on doing myself (the site is basically complete for my purposes).

      It's be easier to find some way to get access to full AutoCAD, if you're just wanting to generate a few puzzles, though...

      Kean

  2. How can i add this code in autocad?

Leave a Reply to Shawn Pettit Cancel reply

Your email address will not be published. Required fields are marked *