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:
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();
if (
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);
}
}
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
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))
+ (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.