You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1857 lines
68 KiB
C#
1857 lines
68 KiB
C#
using ClipperLib;
|
|
using Minkowski;
|
|
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Drawing;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace DeepNestLib
|
|
{
|
|
public class Background
|
|
{
|
|
|
|
public static bool EnableCaches = true;
|
|
object lockobj;
|
|
public Background()
|
|
{
|
|
lockobj = new object();
|
|
}
|
|
public static NFP shiftPolygon(NFP p, PlacementItem shift)
|
|
{
|
|
NFP shifted = new NFP();
|
|
for (var i = 0; i < p.length; i++)
|
|
{
|
|
shifted.AddPoint(new SvgPoint(p[i].x + shift.x, p[i].y + shift.y) { exact = p[i].exact });
|
|
}
|
|
if (p.children != null /*&& p.children.Count*/)
|
|
{
|
|
shifted.children = new List<NFP>();
|
|
for (int i = 0; i < p.children.Count; i++)
|
|
{
|
|
shifted.children.Add(shiftPolygon(p.children[i], shift));
|
|
}
|
|
}
|
|
|
|
return shifted;
|
|
}
|
|
|
|
// returns the square of the length of any merged lines
|
|
// filter out any lines less than minlength long
|
|
public static MergedResult mergedLength(NFP[] parts, NFP p, double minlength, double tolerance)
|
|
{
|
|
// var min2 = minlength * minlength;
|
|
// var totalLength = 0;
|
|
// var segments = [];
|
|
|
|
// for (var i = 0; i < p.length; i++)
|
|
// {
|
|
// var A1 = p[i];
|
|
|
|
// if (i + 1 == p.length)
|
|
// {
|
|
// A2 = p[0];
|
|
// }
|
|
// else
|
|
// {
|
|
// var A2 = p[i + 1];
|
|
// }
|
|
|
|
// if (!A1.exact || !A2.exact)
|
|
// {
|
|
// continue;
|
|
// }
|
|
|
|
// var Ax2 = (A2.x - A1.x) * (A2.x - A1.x);
|
|
// var Ay2 = (A2.y - A1.y) * (A2.y - A1.y);
|
|
|
|
// if (Ax2 + Ay2 < min2)
|
|
// {
|
|
// continue;
|
|
// }
|
|
|
|
// var angle = Math.atan2((A2.y - A1.y), (A2.x - A1.x));
|
|
|
|
// var c = Math.cos(-angle);
|
|
// var s = Math.sin(-angle);
|
|
|
|
// var c2 = Math.cos(angle);
|
|
// var s2 = Math.sin(angle);
|
|
|
|
// var relA2 = { x: A2.x - A1.x, y: A2.y - A1.y};
|
|
// var rotA2x = relA2.x * c - relA2.y * s;
|
|
|
|
// for (var j = 0; j < parts.length; j++)
|
|
// {
|
|
// var B = parts[j];
|
|
// if (B.length > 1)
|
|
// {
|
|
// for (var k = 0; k < B.length; k++)
|
|
// {
|
|
// var B1 = B[k];
|
|
|
|
// if (k + 1 == B.length)
|
|
// {
|
|
// var B2 = B[0];
|
|
// }
|
|
// else
|
|
// {
|
|
// var B2 = B[k + 1];
|
|
// }
|
|
|
|
// if (!B1.exact || !B2.exact)
|
|
// {
|
|
// continue;
|
|
// }
|
|
// var Bx2 = (B2.x - B1.x) * (B2.x - B1.x);
|
|
// var By2 = (B2.y - B1.y) * (B2.y - B1.y);
|
|
|
|
// if (Bx2 + By2 < min2)
|
|
// {
|
|
// continue;
|
|
// }
|
|
|
|
// // B relative to A1 (our point of rotation)
|
|
// var relB1 = { x: B1.x - A1.x, y: B1.y - A1.y};
|
|
// var relB2 = { x: B2.x - A1.x, y: B2.y - A1.y};
|
|
|
|
|
|
// // rotate such that A1 and A2 are horizontal
|
|
// var rotB1 = { x: relB1.x* c -relB1.y * s, y: relB1.x* s +relB1.y * c};
|
|
// var rotB2 = { x: relB2.x* c -relB2.y * s, y: relB2.x* s +relB2.y * c};
|
|
|
|
// if(!GeometryUtil.almostEqual(rotB1.y, 0, tolerance) || !GeometryUtil.almostEqual(rotB2.y, 0, tolerance)){
|
|
// continue;
|
|
// }
|
|
|
|
// var min1 = Math.min(0, rotA2x);
|
|
// var max1 = Math.max(0, rotA2x);
|
|
|
|
// var min2 = Math.min(rotB1.x, rotB2.x);
|
|
// var max2 = Math.max(rotB1.x, rotB2.x);
|
|
|
|
// // not overlapping
|
|
// if(min2 >= max1 || max2 <= min1){
|
|
// continue;
|
|
// }
|
|
|
|
// var len = 0;
|
|
// var relC1x = 0;
|
|
// var relC2x = 0;
|
|
|
|
// // A is B
|
|
// if(GeometryUtil.almostEqual(min1, min2) && GeometryUtil.almostEqual(max1, max2)){
|
|
// len = max1-min1;
|
|
// relC1x = min1;
|
|
// relC2x = max1;
|
|
// }
|
|
// // A inside B
|
|
// else if(min1 > min2 && max1<max2){
|
|
// len = max1-min1;
|
|
// relC1x = min1;
|
|
// relC2x = max1;
|
|
// }
|
|
// // B inside A
|
|
// else if(min2 > min1 && max2<max1){
|
|
// len = max2-min2;
|
|
// relC1x = min2;
|
|
// relC2x = max2;
|
|
// }
|
|
// else{
|
|
// len = Math.max(0, Math.min(max1, max2) - Math.max(min1, min2));
|
|
// relC1x = Math.min(max1, max2);
|
|
// relC2x = Math.max(min1, min2);
|
|
// }
|
|
|
|
// if(len* len > min2){
|
|
// totalLength += len;
|
|
|
|
// var relC1 = { x: relC1x * c2, y: relC1x * s2 };
|
|
//var relC2 = { x: relC2x * c2, y: relC2x * s2 };
|
|
|
|
//var C1 = { x: relC1.x + A1.x, y: relC1.y + A1.y };
|
|
//var C2 = { x: relC2.x + A1.x, y: relC2.y + A1.y };
|
|
|
|
//segments.push([C1, C2]);
|
|
// }
|
|
// }
|
|
// }
|
|
|
|
// if(B.children && B.children.length > 0){
|
|
// var child = mergedLength(B.children, p, minlength, tolerance);
|
|
//totalLength += child.totalLength;
|
|
// segments = segments.concat(child.segments);
|
|
// }
|
|
// }
|
|
// }
|
|
|
|
// return {totalLength: totalLength, segments: segments};
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
public class MergedResult
|
|
{
|
|
public double totalLength;
|
|
public object segments;
|
|
}
|
|
|
|
public static NFP[] cloneNfp(NFP[] nfp, bool inner = false)
|
|
{
|
|
if (!inner)
|
|
{
|
|
return new[] { clone(nfp.First()) };
|
|
}
|
|
|
|
// inner nfp is actually an array of nfps
|
|
int nCount = nfp.Length;
|
|
NFP[] newnfps = new NFP[nCount];
|
|
//List<NFP> newnfp = new List<NFP>();
|
|
for (var i = 0; i < nCount; i++)
|
|
{
|
|
newnfps[i] = clone(nfp[i]);
|
|
//newnfp.Add(clone(nfp[i]));
|
|
}
|
|
|
|
return newnfps;// newnfp.ToArray();
|
|
}
|
|
|
|
public static NFP clone(NFP nfp)
|
|
{
|
|
NFP newnfp = new NFP();
|
|
newnfp.source = nfp.source;
|
|
for (var i = 0; i < nfp.length; i++)
|
|
{
|
|
newnfp.AddPoint(new SvgPoint(nfp[i].x, nfp[i].y));
|
|
}
|
|
|
|
if (nfp.children != null && nfp.children.Count > 0)
|
|
{
|
|
newnfp.children = new List<NFP>();
|
|
for (int i = 0; i < nfp.children.Count; i++)
|
|
{
|
|
var child = nfp.children[i];
|
|
NFP newchild = new NFP();
|
|
for (var j = 0; j < child.length; j++)
|
|
{
|
|
newchild.AddPoint(new SvgPoint(child[j].x, child[j].y));
|
|
}
|
|
newnfp.children.Add(newchild);
|
|
}
|
|
}
|
|
|
|
return newnfp;
|
|
}
|
|
|
|
public int callCounter = 0;
|
|
|
|
public Dictionary<string, NFP[]> cacheProcess = new Dictionary<string, NFP[]>();
|
|
public NFP[] Process2(NFP A, NFP B, int type)
|
|
{
|
|
var key = A.source + ";" + B.source + ";" + A.rotation + ";" + B.rotation;
|
|
bool cacheAllow = type != 1;
|
|
if (cacheProcess.ContainsKey(key) && cacheAllow)
|
|
{
|
|
return cacheProcess[key];
|
|
}
|
|
|
|
Stopwatch swg = Stopwatch.StartNew();
|
|
Dictionary<string, List<PointF>> dic1 = new Dictionary<string, List<PointF>>();
|
|
Dictionary<string, List<double>> dic2 = new Dictionary<string, List<double>>();
|
|
dic2.Add("A", new List<double>());
|
|
foreach (var item in A.Points)
|
|
{
|
|
var target = dic2["A"];
|
|
target.Add(item.x);
|
|
target.Add(item.y);
|
|
}
|
|
dic2.Add("B", new List<double>());
|
|
foreach (var item in B.Points)
|
|
{
|
|
var target = dic2["B"];
|
|
target.Add(item.x);
|
|
target.Add(item.y);
|
|
}
|
|
|
|
|
|
List<double> hdat = new List<double>();
|
|
|
|
foreach (var item in A.children)
|
|
{
|
|
foreach (var pitem in item.Points)
|
|
{
|
|
hdat.Add(pitem.x);
|
|
hdat.Add(pitem.y);
|
|
}
|
|
}
|
|
|
|
var aa = dic2["A"];
|
|
var bb = dic2["B"];
|
|
var arr1 = A.children.Select(z => z.Points.Count() * 2).ToArray();
|
|
|
|
MinkowskiWrapper.setData(aa.Count, aa.ToArray(), A.children.Count, arr1, hdat.ToArray(), bb.Count, bb.ToArray());
|
|
MinkowskiWrapper.calculateNFP();
|
|
|
|
callCounter++;
|
|
|
|
int[] sizes = new int[2];
|
|
MinkowskiWrapper.getSizes1(sizes);
|
|
int[] sizes1 = new int[sizes[0]];
|
|
int[] sizes2 = new int[sizes[1]];
|
|
MinkowskiWrapper.getSizes2(sizes1, sizes2);
|
|
double[] dat1 = new double[sizes1.Sum()];
|
|
double[] hdat1 = new double[sizes2.Sum()];
|
|
|
|
MinkowskiWrapper.getResults(dat1, hdat1);
|
|
|
|
if (sizes1.Count() > 1)
|
|
{
|
|
throw new ArgumentException("sizes1 cnt >1");
|
|
}
|
|
|
|
|
|
//convert back to answer here
|
|
bool isa = true;
|
|
List<PointF> Apts = new List<PointF>();
|
|
|
|
List<List<double>> holesval = new List<List<double>>();
|
|
bool holes = false;
|
|
|
|
for (int i = 0; i < dat1.Length; i += 2)
|
|
{
|
|
var x1 = (float)dat1[i];
|
|
var y1 = (float)dat1[i + 1];
|
|
Apts.Add(new PointF(x1, y1));
|
|
}
|
|
|
|
int index = 0;
|
|
for (int i = 0; i < sizes2.Length; i++)
|
|
{
|
|
holesval.Add(new List<double>());
|
|
for (int j = 0; j < sizes2[i]; j++)
|
|
{
|
|
holesval.Last().Add(hdat1[index]);
|
|
index++;
|
|
}
|
|
}
|
|
|
|
List<List<PointF>> holesout = new List<List<PointF>>();
|
|
foreach (var item in holesval)
|
|
{
|
|
holesout.Add(new List<PointF>());
|
|
for (int i = 0; i < item.Count; i += 2)
|
|
{
|
|
var x = (float)item[i];
|
|
var y = (float)item[i + 1];
|
|
holesout.Last().Add(new PointF(x, y));
|
|
}
|
|
}
|
|
|
|
NFP ret = new NFP();
|
|
ret.Points = new List<SvgPoint>() { };
|
|
foreach (var item in Apts)
|
|
{
|
|
ret.AddPoint(new SvgPoint(item.X, item.Y));
|
|
}
|
|
|
|
|
|
foreach (var item in holesout)
|
|
{
|
|
if (ret.children == null)
|
|
ret.children = new List<NFP>();
|
|
|
|
ret.children.Add(new NFP());
|
|
ret.children.Last().Points = new List<SvgPoint>() { };
|
|
foreach (var hitem in item)
|
|
{
|
|
ret.children.Last().AddPoint(new SvgPoint(hitem.X, hitem.Y));
|
|
}
|
|
}
|
|
|
|
swg.Stop();
|
|
var msg = swg.ElapsedMilliseconds;
|
|
var res = new NFP[] { ret };
|
|
|
|
if (cacheAllow)
|
|
{
|
|
cacheProcess.Add(key, res);
|
|
}
|
|
return res;
|
|
}
|
|
|
|
public static NFP getFrame(NFP A)
|
|
{
|
|
var bounds = GeometryUtil.getPolygonBounds(A);
|
|
|
|
// expand bounds by 10%
|
|
bounds.width *= 1.1;
|
|
bounds.height *= 1.1;
|
|
bounds.x -= 0.5 * (bounds.width - (bounds.width / 1.1));
|
|
bounds.y -= 0.5 * (bounds.height - (bounds.height / 1.1));
|
|
|
|
var frame = new NFP();
|
|
frame.push(new SvgPoint(bounds.x, bounds.y));
|
|
frame.push(new SvgPoint(bounds.x + bounds.width, bounds.y));
|
|
frame.push(new SvgPoint(bounds.x + bounds.width, bounds.y + bounds.height));
|
|
frame.push(new SvgPoint(bounds.x, bounds.y + bounds.height));
|
|
|
|
|
|
frame.children = new List<NFP>() { (NFP)A };
|
|
frame.source = A.source;
|
|
frame.rotation = 0;
|
|
|
|
return frame;
|
|
}
|
|
|
|
public NFP[] getInnerNfp(NFP A, NFP B, int type, SvgNestConfig config)
|
|
{
|
|
if (A.source != null && B.source != null)
|
|
{
|
|
|
|
var key = new DbCacheKey()
|
|
{
|
|
A = A.source.Value,
|
|
B = B.source.Value,
|
|
ARotation = 0,
|
|
BRotation = B.rotation,
|
|
//Inside =true??
|
|
};
|
|
//var doc = window.db.find({ A: A.source, B: B.source, Arotation: 0, Brotation: B.rotation }, true);
|
|
var res = window.db.find(key, true);
|
|
if (res != null)
|
|
{
|
|
return res;
|
|
}
|
|
}
|
|
|
|
var frame = getFrame(A);
|
|
|
|
var nfp = getOuterNfp(frame, B, type, true);
|
|
|
|
if (nfp == null || nfp.children == null || nfp.children.Count == 0)
|
|
{
|
|
return null;
|
|
}
|
|
List<NFP> holes = new List<NFP>();
|
|
if (A.children != null && A.children.Count > 0)
|
|
{
|
|
holes = (A.children ?? Enumerable.Empty<NFP>()) // 根据 A.children 的实际类型替换 YourChildType
|
|
.AsParallel().AsOrdered()
|
|
.WithExecutionMode(ParallelExecutionMode.ForceParallelism)
|
|
.WithDegreeOfParallelism(Math.Min(Environment.ProcessorCount - 1, 8))
|
|
.Select(child => getOuterNfp(child, B, 1))
|
|
.Where(hnfp => hnfp != null)
|
|
.ToList();
|
|
}
|
|
|
|
//if (A.children != null && A.children.Count > 0)
|
|
//{
|
|
// for (var i = 0; i < A.children.Count; i++)
|
|
// {
|
|
// var hnfp = getOuterNfp(A.children[i], B, 1);
|
|
// if (hnfp != null)
|
|
// {
|
|
// holes.Add(hnfp);
|
|
// }
|
|
// }
|
|
//}
|
|
|
|
if (holes.Count == 0)
|
|
{
|
|
return nfp.children.ToArray();
|
|
}
|
|
var clipperNfp = innerNfpToClipperCoordinates(nfp.children.ToArray(), config.clipperScale);
|
|
var clipperHoles = innerNfpToClipperCoordinates(holes.ToArray(), config.clipperScale);
|
|
|
|
List<List<IntPoint>> finalNfp = new List<List<IntPoint>>();
|
|
var clipper = new ClipperLib.Clipper();
|
|
|
|
clipper.AddPaths(clipperHoles.Select(z => z.ToList()).ToList(), ClipperLib.PolyType.ptClip, true);
|
|
clipper.AddPaths(clipperNfp.Select(z => z.ToList()).ToList(), ClipperLib.PolyType.ptSubject, true);
|
|
|
|
if (!clipper.Execute(ClipperLib.ClipType.ctDifference, finalNfp, ClipperLib.PolyFillType.pftNonZero, ClipperLib.PolyFillType.pftNonZero))
|
|
{
|
|
return nfp.children.ToArray();
|
|
}
|
|
|
|
if (finalNfp.Count == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
List<NFP> f = new List<NFP>();
|
|
for (var i = 0; i < finalNfp.Count; i++)
|
|
{
|
|
f.Add(toNestCoordinates(finalNfp[i].ToArray(), config.clipperScale));
|
|
}
|
|
|
|
if (A.source != null && B.source != null)
|
|
{
|
|
// insert into db
|
|
//console.log('inserting inner: ', A.source, B.source, B.rotation, f);
|
|
var doc = new DbCacheKey()
|
|
{
|
|
A = A.source.Value,
|
|
B = B.source.Value,
|
|
ARotation = 0,
|
|
BRotation = B.rotation,
|
|
nfp = f.ToArray()
|
|
};
|
|
window.db.insert(doc, true);
|
|
}
|
|
|
|
return f.ToArray();
|
|
|
|
}
|
|
public static NFP rotatePolygon(NFP polygon, float degrees)
|
|
{
|
|
NFP rotated = new NFP();
|
|
|
|
var angle = degrees * Math.PI / 180;
|
|
List<SvgPoint> pp = new List<SvgPoint>();
|
|
for (var i = 0; i < polygon.length; i++)
|
|
{
|
|
var x = polygon[i].x;
|
|
var y = polygon[i].y;
|
|
var x1 = (x * Math.Cos(angle) - y * Math.Sin(angle));
|
|
var y1 = (x * Math.Sin(angle) + y * Math.Cos(angle));
|
|
|
|
pp.Add(new SvgPoint(x1, y1));
|
|
}
|
|
rotated.Points = pp;
|
|
|
|
if (polygon.children != null && polygon.children.Count > 0)
|
|
{
|
|
rotated.children = new List<NFP>(); ;
|
|
for (var j = 0; j < polygon.children.Count; j++)
|
|
{
|
|
rotated.children.Add(rotatePolygon(polygon.children[j], degrees));
|
|
}
|
|
}
|
|
|
|
return rotated;
|
|
}
|
|
|
|
public SheetPlacement placeParts(NFP[] sheets, NFP[] parts, SvgNestConfig config, int nestindex)
|
|
{
|
|
if (sheets == null || sheets.Count() == 0) return null;
|
|
|
|
NFP[] allParts = new NFP[parts.Length];
|
|
Array.Copy(parts, allParts, parts.Length);
|
|
|
|
int i, j, k, m, n;
|
|
double totalsheetarea = 0;
|
|
|
|
NFP part = null;
|
|
// total length of merged lines
|
|
double totalMerged = 0;
|
|
|
|
// rotate paths by given rotation
|
|
var rotated = new List<NFP>();
|
|
for (i = 0; i < parts.Length; i++)
|
|
{
|
|
var r = rotatePolygon(parts[i], parts[i].rotation);
|
|
r.Rotation = parts[i].rotation;
|
|
r.source = parts[i].source;
|
|
r.id = parts[i].id;
|
|
r.Name = parts[i].Name;
|
|
rotated.Add(r);
|
|
}
|
|
|
|
parts = rotated.ToArray();
|
|
|
|
List<SheetPlacementItem> allplacements = new List<SheetPlacementItem>();
|
|
|
|
double fitness = 0;
|
|
|
|
string key;
|
|
NFP nfp;
|
|
double sheetarea = -1;
|
|
int totalPlaced = 0;
|
|
int totalParts = parts.Count();
|
|
float angleIncrement = 360f / config.rotations;
|
|
int maxAttempts = (int)(360f / config.rotations);
|
|
while (parts.Length > 0)
|
|
{
|
|
List<NFP> placed = new List<NFP>();
|
|
|
|
List<PlacementItem> placements = new List<PlacementItem>();
|
|
|
|
// open a new sheet
|
|
var sheet = sheets.First();
|
|
sheets = sheets.Skip(1).ToArray();
|
|
sheetarea = Math.Abs(GeometryUtil.polygonArea(sheet));
|
|
totalsheetarea += sheetarea;
|
|
|
|
//fitness += sheetarea; // add 1 for each new sheet opened (lower fitness is better)
|
|
|
|
string clipkey = "";
|
|
Dictionary<string, ClipCacheItem> clipCache = new Dictionary<string, ClipCacheItem>();
|
|
var clipper = new ClipperLib.Clipper();
|
|
var combinedNfp = new List<List<ClipperLib.IntPoint>>();
|
|
var error = false;
|
|
IntPoint[][] clipperSheetNfp = null;
|
|
double? minwidth = null;
|
|
PlacementItem position = null;
|
|
double? minarea = null;
|
|
for (i = 0; i < parts.Length; i++)
|
|
{
|
|
float prog = 0.66f + 0.34f * (totalPlaced / (float)totalParts);
|
|
DisplayProgress(prog);
|
|
|
|
part = parts[i];
|
|
// inner NFP
|
|
NFP[] sheetNfp = null;
|
|
//Tuple<int, NFP[]> bestNfp = null;
|
|
//Parallel.For(0, maxAttempts, (index, state) =>
|
|
//{// 检查是否已请求停止
|
|
// if (state.IsStopped)
|
|
// {
|
|
// return;
|
|
// }
|
|
// NFP[] sheetNfpBest = getInnerNfp(sheet, part, 0, config);
|
|
// // 检查是否已请求停止
|
|
// if (state.IsStopped)
|
|
// {
|
|
// return;
|
|
// }
|
|
// if (sheetNfpBest != null && sheetNfpBest.Length > 0)
|
|
// {
|
|
// bestNfp = new Tuple<int, NFP[]>(index, sheetNfpBest);
|
|
// state.Stop();
|
|
// return;
|
|
// }
|
|
//});
|
|
//if(bestNfp != null)
|
|
//{
|
|
// var r = rotatePolygon(part, angleIncrement);
|
|
// r.rotation = part.rotation + angleIncrement;
|
|
// r.source = part.source;
|
|
// r.id = part.id;
|
|
// r.Name = part.Name;
|
|
|
|
// // rotation is not in-place
|
|
// part = r;
|
|
// parts[i] = r;
|
|
|
|
// if (part.rotation > 360f)
|
|
// {
|
|
// part.rotation = part.rotation % 360f;
|
|
// }
|
|
// sheetNfp = bestNfp.Item2;
|
|
//}
|
|
|
|
// try all possible rotations until it fits
|
|
// (only do this for the first part of each sheet, to ensure that all parts that can be placed are, even if we have to to open a lot of sheets)
|
|
for (j = 0; j <= maxAttempts; j++)
|
|
{
|
|
//Stopwatch sw = Stopwatch.StartNew();
|
|
sheetNfp = getInnerNfp(sheet, part, 0, config);
|
|
//sw.Stop();
|
|
//Trace.WriteLine($"1: {sw.ElapsedMilliseconds}");
|
|
if (sheetNfp != null && sheetNfp.Length > 0)
|
|
{
|
|
if (sheetNfp[0].length == 0)
|
|
{
|
|
throw new ArgumentException();
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
//Stopwatch sw2 = Stopwatch.StartNew();
|
|
//sw2.Start();
|
|
var r = rotatePolygon(part, angleIncrement);
|
|
r.rotation = part.rotation + angleIncrement;
|
|
r.source = part.source;
|
|
r.id = part.id;
|
|
r.Name = part.Name;
|
|
|
|
// rotation is not in-place
|
|
part = r;
|
|
parts[i] = r;
|
|
|
|
if (part.rotation > 360f)
|
|
{
|
|
part.rotation = part.rotation % 360f;
|
|
}
|
|
//sw2.Stop();
|
|
//Trace.WriteLine($"2: {sw2.ElapsedMilliseconds}");
|
|
}
|
|
// part unplaceable, skip
|
|
if (sheetNfp == null || sheetNfp.Length == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
position = null;
|
|
|
|
if (placed.Count == 0)
|
|
{
|
|
// first placement, put it on the top left corner
|
|
for (j = 0; j < sheetNfp.Length; j++)
|
|
{
|
|
for (k = 0; k < sheetNfp[j].length; k++)
|
|
{
|
|
if (position == null ||
|
|
((sheetNfp[j][k].x - part[0].x) < position.x) ||
|
|
(GeometryUtil._almostEqual(sheetNfp[j][k].x - part[0].x, position.x)
|
|
&& ((sheetNfp[j][k].y - part[0].y) < position.y))
|
|
)
|
|
{
|
|
position = new PlacementItem()
|
|
{
|
|
x = sheetNfp[j][k].x - part[0].x,
|
|
y = sheetNfp[j][k].y - part[0].y,
|
|
id = part.id,
|
|
rotation = part.rotation,
|
|
source = part.source.Value
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
if (position == null)
|
|
{
|
|
throw new Exception("position null");
|
|
//console.log(sheetNfp);
|
|
}
|
|
placements.Add(position);
|
|
placed.Add(part);
|
|
totalPlaced++;
|
|
|
|
continue;
|
|
}
|
|
|
|
clipperSheetNfp = innerNfpToClipperCoordinates(sheetNfp, config.clipperScale);
|
|
|
|
clipper = new ClipperLib.Clipper();
|
|
combinedNfp = new List<List<ClipperLib.IntPoint>>();
|
|
|
|
error = false;
|
|
|
|
// check if stored in clip cache
|
|
//var startindex = 0;
|
|
clipkey = "s:" + part.source + "r:" + part.rotation;
|
|
var startindex = 0;
|
|
if (EnableCaches && clipCache.ContainsKey(clipkey))
|
|
{
|
|
var prevNfp = clipCache[clipkey].nfpp;
|
|
clipper.AddPaths(prevNfp.Select(z => z.ToList()).ToList(), ClipperLib.PolyType.ptSubject, true);
|
|
startindex = clipCache[clipkey].index;
|
|
}
|
|
|
|
for (j = startindex; j < placed.Count; j++)
|
|
{
|
|
nfp = getOuterNfp(placed[j], part, 0);
|
|
// minkowski difference failed. very rare but could happen
|
|
if (nfp == null)
|
|
{
|
|
error = true;
|
|
break;
|
|
}
|
|
// shift to placed location
|
|
for (m = 0; m < nfp.length; m++)
|
|
{
|
|
nfp[m].x += placements[j].x;
|
|
nfp[m].y += placements[j].y;
|
|
}
|
|
if (nfp.children != null && nfp.children.Count > 0)
|
|
{
|
|
for (n = 0; n < nfp.children.Count; n++)
|
|
{
|
|
for (var o = 0; o < nfp.children[n].length; o++)
|
|
{
|
|
nfp.children[n][o].x += placements[j].x;
|
|
nfp.children[n][o].y += placements[j].y;
|
|
}
|
|
}
|
|
}
|
|
|
|
var clipperNfp = nfpToClipperCoordinates(nfp, config.clipperScale);
|
|
|
|
clipper.AddPaths(clipperNfp.Select(z => z.ToList()).ToList(), ClipperLib.PolyType.ptSubject, true);
|
|
}
|
|
//TODO: a lot here to insert
|
|
|
|
if (error || !clipper.Execute(ClipperLib.ClipType.ctUnion, combinedNfp, ClipperLib.PolyFillType.pftNonZero, ClipperLib.PolyFillType.pftNonZero))
|
|
{
|
|
//console.log('clipper error', error);
|
|
continue;
|
|
}
|
|
|
|
|
|
if (EnableCaches)
|
|
{
|
|
clipCache[clipkey] = new ClipCacheItem()
|
|
{
|
|
index = placed.Count - 1,
|
|
nfpp = combinedNfp.Select(z => z.ToArray()).ToArray()
|
|
};
|
|
}
|
|
|
|
//console.log('save cache', placed.length - 1);
|
|
|
|
// difference with sheet polygon
|
|
List<List<IntPoint>> _finalNfp = new List<List<IntPoint>>();
|
|
clipper = new ClipperLib.Clipper();
|
|
|
|
clipper.AddPaths(combinedNfp, ClipperLib.PolyType.ptClip, true);
|
|
|
|
clipper.AddPaths(clipperSheetNfp.Select(z => z.ToList()).ToList(), ClipperLib.PolyType.ptSubject, true);
|
|
|
|
|
|
if (!clipper.Execute(ClipperLib.ClipType.ctDifference, _finalNfp, ClipperLib.PolyFillType.pftEvenOdd, ClipperLib.PolyFillType.pftNonZero))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (_finalNfp == null || _finalNfp.Count == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
|
|
List<NFP> f = new List<NFP>();
|
|
for (j = 0; j < _finalNfp.Count; j++)
|
|
{
|
|
// back to normal scale
|
|
f.Add(Background.toNestCoordinates(_finalNfp[j].ToArray(), config.clipperScale));
|
|
}
|
|
var finalNfp = f;
|
|
//finalNfp = f;
|
|
|
|
// choose placement that results in the smallest bounding box/hull etc
|
|
// todo: generalize gravity direction
|
|
/*var minwidth = null;
|
|
var minarea = null;
|
|
var minx = null;
|
|
var miny = null;
|
|
var nf, area, shiftvector;*/
|
|
minwidth = null;
|
|
minarea = null;
|
|
double? minx = null;
|
|
double? miny = null;
|
|
NFP nf;
|
|
double area = 0;
|
|
PlacementItem shiftvector = null;
|
|
|
|
|
|
NFP allpoints = new NFP();
|
|
for (m = 0; m < placed.Count; m++)
|
|
{
|
|
for (n = 0; n < placed[m].length; n++)
|
|
{
|
|
allpoints.AddPoint(
|
|
new SvgPoint(
|
|
placed[m][n].x + placements[m].x, placed[m][n].y + placements[m].y));
|
|
}
|
|
}
|
|
|
|
PolygonBounds allbounds = null;
|
|
PolygonBounds partbounds = null;
|
|
if (config.placementType == PlacementTypeEnum.gravity || config.placementType == PlacementTypeEnum.box)
|
|
{
|
|
allbounds = GeometryUtil.getPolygonBounds(allpoints);
|
|
|
|
NFP partpoints = new NFP();
|
|
for (m = 0; m < part.length; m++)
|
|
{
|
|
partpoints.AddPoint(new SvgPoint(part[m].x, part[m].y));
|
|
}
|
|
partbounds = GeometryUtil.getPolygonBounds(partpoints);
|
|
}
|
|
else
|
|
{
|
|
allpoints = getHull(allpoints);
|
|
}
|
|
for (j = 0; j < finalNfp.Count; j++)
|
|
{
|
|
nf = finalNfp[j];
|
|
//console.log('evalnf',nf.length);
|
|
for (k = 0; k < nf.length; k++)
|
|
{
|
|
shiftvector = new PlacementItem()
|
|
{
|
|
id = part.id,
|
|
x = nf[k].x - part[0].x,
|
|
y = nf[k].y - part[0].y,
|
|
source = part.source.Value,
|
|
rotation = part.rotation
|
|
};
|
|
PolygonBounds rectbounds = null;
|
|
if (config.placementType == PlacementTypeEnum.gravity || config.placementType == PlacementTypeEnum.box)
|
|
{
|
|
NFP poly = new NFP();
|
|
poly.AddPoint(new SvgPoint(allbounds.x, allbounds.y));
|
|
poly.AddPoint(new SvgPoint(allbounds.x + allbounds.width, allbounds.y));
|
|
poly.AddPoint(new SvgPoint(allbounds.x + allbounds.width, allbounds.y + allbounds.height));
|
|
poly.AddPoint(new SvgPoint(allbounds.x, allbounds.y + allbounds.height));
|
|
/*
|
|
[
|
|
// allbounds points
|
|
{ x: allbounds.x, y: allbounds.y},
|
|
{ x: allbounds.x + allbounds.width, y: allbounds.y},
|
|
{ x: allbounds.x + allbounds.width, y: allbounds.y + allbounds.height},
|
|
{ x: allbounds.x, y: allbounds.y + allbounds.height},*/
|
|
|
|
poly.AddPoint(new SvgPoint(partbounds.x + shiftvector.x, partbounds.y + shiftvector.y));
|
|
poly.AddPoint(new SvgPoint(partbounds.x + partbounds.width + shiftvector.x, partbounds.y + shiftvector.y));
|
|
poly.AddPoint(new SvgPoint(partbounds.x + partbounds.width + shiftvector.x, partbounds.y + partbounds.height + shiftvector.y));
|
|
poly.AddPoint(new SvgPoint(partbounds.x + shiftvector.x, partbounds.y + partbounds.height + shiftvector.y));
|
|
/*
|
|
[
|
|
|
|
// part points
|
|
{ x: partbounds.x + shiftvector.x, y: partbounds.y + shiftvector.y},
|
|
{ x: partbounds.x + partbounds.width + shiftvector.x, y: partbounds.y + shiftvector.y},
|
|
{ x: partbounds.x + partbounds.width + shiftvector.x, y: partbounds.y + partbounds.height + shiftvector.y},
|
|
{ x: partbounds.x + shiftvector.x, y: partbounds.y + partbounds.height + shiftvector.y}
|
|
]*/
|
|
rectbounds = GeometryUtil.getPolygonBounds(poly);
|
|
|
|
// weigh width more, to help compress in direction of gravity
|
|
if (config.placementType == PlacementTypeEnum.gravity)
|
|
{
|
|
area = rectbounds.width * 2 + rectbounds.height;
|
|
}
|
|
else
|
|
{
|
|
area = rectbounds.width * rectbounds.height;
|
|
//// 修改单侧部井的权重
|
|
//if(part.Name!=null && part.Name == "单侧")
|
|
//{
|
|
// area *= 0.2;
|
|
//}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// must be convex hull
|
|
var localpoints = clone(allpoints);
|
|
|
|
for (m = 0; m < part.length; m++)
|
|
{
|
|
localpoints.AddPoint(new SvgPoint(part[m].x + shiftvector.x, part[m].y + shiftvector.y));
|
|
}
|
|
|
|
area = Math.Abs(GeometryUtil.polygonArea(getHull(localpoints)));
|
|
shiftvector.hull = getHull(localpoints);
|
|
shiftvector.hullsheet = getHull(sheet);
|
|
}
|
|
//console.timeEnd('evalbounds');
|
|
//console.time('evalmerge');
|
|
MergedResult merged = null;
|
|
if (config.mergeLines)
|
|
{
|
|
throw new NotImplementedException();
|
|
// if lines can be merged, subtract savings from area calculation
|
|
var shiftedpart = shiftPolygon(part, shiftvector);
|
|
List<NFP> shiftedplaced = new List<NFP>();
|
|
|
|
for (m = 0; m < placed.Count; m++)
|
|
{
|
|
shiftedplaced.Add(shiftPolygon(placed[m], placements[m]));
|
|
}
|
|
|
|
// don't check small lines, cut off at about 1/2 in
|
|
double minlength = 0.5 * config.scale;
|
|
merged = mergedLength(shiftedplaced.ToArray(), shiftedpart, minlength, 0.1 * config.curveTolerance);
|
|
area -= merged.totalLength * config.timeRatio;
|
|
}
|
|
|
|
//console.timeEnd('evalmerge');
|
|
if (
|
|
minarea == null ||
|
|
area < minarea ||
|
|
(GeometryUtil._almostEqual(minarea, area) && (minx == null || shiftvector.x < minx)) ||
|
|
(GeometryUtil._almostEqual(minarea, area) && (minx != null && GeometryUtil._almostEqual(shiftvector.x, minx) && shiftvector.y < miny))
|
|
)
|
|
{
|
|
minarea = area;
|
|
|
|
minwidth = rectbounds != null ? rectbounds.width : 0;
|
|
position = shiftvector;
|
|
if (minx == null || shiftvector.x < minx)
|
|
{
|
|
minx = shiftvector.x;
|
|
}
|
|
if (miny == null || shiftvector.y < miny)
|
|
{
|
|
miny = shiftvector.y;
|
|
}
|
|
|
|
if (config.mergeLines)
|
|
{
|
|
position.mergedLength = merged.totalLength;
|
|
position.mergedSegments = merged.segments;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (position != null)
|
|
{
|
|
placed.Add(part);
|
|
totalPlaced++;
|
|
placements.Add(position);
|
|
if (position.mergedLength != null)
|
|
{
|
|
totalMerged += position.mergedLength.Value;
|
|
}
|
|
}
|
|
// send placement progress signal
|
|
var placednum = placed.Count;
|
|
for (j = 0; j < allplacements.Count; j++)
|
|
{
|
|
placednum += allplacements[j].sheetplacements.Count;
|
|
}
|
|
//console.log(placednum, totalnum);
|
|
//ipcRenderer.send('background-progress', { index: nestindex, progress: 0.5 + 0.5 * (placednum / totalnum)});
|
|
|
|
//console.timeEnd('placement');
|
|
}
|
|
|
|
//if (!minwidth.HasValue)
|
|
//{
|
|
// fitness = double.NaN;
|
|
//}
|
|
//else
|
|
//{
|
|
// fitness += (minwidth.Value / sheetarea) + minarea.Value;
|
|
//}
|
|
|
|
for (i = 0; i < placed.Count; i++)
|
|
{
|
|
var index = Array.IndexOf(parts, placed[i]);
|
|
if (index >= 0)
|
|
{
|
|
parts = parts.splice(index, 1);
|
|
}
|
|
}
|
|
if (placements != null && placements.Count > 0)
|
|
{
|
|
allplacements.Add(new SheetPlacementItem()
|
|
{
|
|
sheetId = sheet.id,
|
|
sheetSource = sheet.source.Value,
|
|
sheetplacements = placements
|
|
});
|
|
//allplacements.Add({ sheet: sheet.source, sheetid: sheet.id, sheetplacements: placements});
|
|
}
|
|
else
|
|
{
|
|
break; // something went wrong
|
|
}
|
|
|
|
if (sheets.Count() == 0)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
// there were parts that couldn't be placed
|
|
// scale this value high - we really want to get all the parts in, even at the cost of opening new sheets
|
|
// 原适配率
|
|
//for (i = 0; i < parts.Count(); i++)
|
|
//{
|
|
// fitness += 100000000 * (Math.Abs(GeometryUtil.polygonArea(parts[i])) / totalsheetarea);
|
|
//}
|
|
// 自定义的适配率
|
|
double dFitValue = 0;
|
|
foreach(var sheet in allplacements)
|
|
{
|
|
foreach(PlacementItem item in sheet.sheetplacements)
|
|
{
|
|
NFP find = allParts.FirstOrDefault(it => it.id == item.id);
|
|
if(find != null)
|
|
{
|
|
if (find.Name.Equals("双侧"))
|
|
{
|
|
dFitValue += 2.2;
|
|
}
|
|
else
|
|
{
|
|
dFitValue += 1.0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
fitness = (totalParts*3.0 - dFitValue) / (totalParts*3.0);
|
|
// send finish progerss signal
|
|
//ipcRenderer.send('background-progress', { index: nestindex, progress: -1});
|
|
return new SheetPlacement()
|
|
{
|
|
placements = new[] { allplacements.ToList() },
|
|
fitness = fitness,
|
|
// paths = paths,
|
|
area = sheetarea,
|
|
mergedLength = totalMerged
|
|
};
|
|
//return { placements: allplacements, fitness: fitness, area: sheetarea, mergedLength: totalMerged };
|
|
}
|
|
// jsClipper uses X/Y instead of x/y...
|
|
public DataInfo data;
|
|
NFP[] parts;
|
|
|
|
int index;
|
|
// run the placement synchronously
|
|
public windowUnk window = new windowUnk();
|
|
|
|
public Action<SheetPlacement> ResponseAction;
|
|
|
|
public long LastPlacePartTime = 0;
|
|
public void sync()
|
|
{
|
|
//console.log('starting synchronous calculations', Object.keys(window.nfpCache).length);
|
|
//console.log('in sync');
|
|
var c = 0;
|
|
foreach (var key in window.nfpCache)
|
|
{
|
|
c++;
|
|
}
|
|
//console.log('nfp cached:', c);
|
|
Stopwatch sw = Stopwatch.StartNew();
|
|
var placement = placeParts(data.sheets.ToArray(), parts, data.config, index);
|
|
sw.Stop();
|
|
LastPlacePartTime = sw.ElapsedMilliseconds;
|
|
|
|
placement.index = data.index;
|
|
ResponseAction(placement);
|
|
//ipcRenderer.send('background-response', placement);
|
|
}
|
|
public void BackgroundStart(DataInfo data)
|
|
{
|
|
this.data = data;
|
|
var index = data.index;
|
|
var individual = data.individual;
|
|
|
|
var parts = individual.placements;
|
|
var rotations = individual.Rotation;
|
|
var ids = data.ids;
|
|
var sources = data.sources;
|
|
var children = data.children;
|
|
|
|
for (var i = 0; i < parts.Count; i++)
|
|
{
|
|
parts[i].rotation = rotations[i];
|
|
parts[i].id = ids[i];
|
|
parts[i].source = sources[i];
|
|
if (!data.config.simplify)
|
|
{
|
|
parts[i].children = children[i];
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < data.sheets.Count; i++)
|
|
{
|
|
data.sheets[i].id = data.sheetids[i];
|
|
data.sheets[i].source = data.sheetsources[i];
|
|
data.sheets[i].children = data.sheetchildren[i];
|
|
}
|
|
|
|
// preprocess
|
|
List<NfpPair> pairs = new List<NfpPair>();
|
|
|
|
if (Background.UseParallel)
|
|
{
|
|
object lobj = new object();
|
|
Parallel.For(0, parts.Count, i =>
|
|
{
|
|
{
|
|
var B = parts[i];
|
|
for (var j = 0; j < i; j++)
|
|
{
|
|
var A = parts[j];
|
|
var key = new NfpPair()
|
|
{
|
|
A = A,
|
|
B = B,
|
|
ARotation = A.rotation,
|
|
BRotation = B.rotation,
|
|
Asource = A.source.Value,
|
|
Bsource = B.source.Value
|
|
|
|
};
|
|
var doc = new DbCacheKey()
|
|
{
|
|
A = A.source.Value,
|
|
B = B.source.Value,
|
|
|
|
ARotation = A.rotation,
|
|
BRotation = B.rotation
|
|
|
|
};
|
|
lock (lobj)
|
|
{
|
|
if (!inpairs(key, pairs.ToArray()) && !window.db.has(doc))
|
|
{
|
|
pairs.Add(key);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
else
|
|
{
|
|
for (var i = 0; i < parts.Count; i++)
|
|
{
|
|
var B = parts[i];
|
|
for (var j = 0; j < i; j++)
|
|
{
|
|
var A = parts[j];
|
|
var key = new NfpPair()
|
|
{
|
|
A = A,
|
|
B = B,
|
|
ARotation = A.rotation,
|
|
BRotation = B.rotation,
|
|
Asource = A.source.Value,
|
|
Bsource = B.source.Value
|
|
|
|
};
|
|
var doc = new DbCacheKey()
|
|
{
|
|
A = A.source.Value,
|
|
B = B.source.Value,
|
|
|
|
ARotation = A.rotation,
|
|
BRotation = B.rotation
|
|
|
|
};
|
|
if (!inpairs(key, pairs.ToArray()) && !window.db.has(doc))
|
|
{
|
|
pairs.Add(key);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//console.log('pairs: ', pairs.length);
|
|
//console.time('Total');
|
|
|
|
this.parts = parts.ToArray();
|
|
if (pairs.Count > 0)
|
|
{
|
|
|
|
var ret1 = pmapDeepNest(pairs);
|
|
thenDeepNest(ret1, parts);
|
|
}
|
|
else
|
|
{
|
|
sync();
|
|
}
|
|
}
|
|
public NFP getPart(int source, List<NFP> parts)
|
|
{
|
|
for (var k = 0; k < parts.Count; k++)
|
|
{
|
|
if (parts[k].source == source)
|
|
{
|
|
return parts[k];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public void thenIterate(NfpPair processed, List<NFP> parts)
|
|
{
|
|
// returned data only contains outer nfp, we have to account for any holes separately in the synchronous portion
|
|
// this is because the c++ addon which can process interior nfps cannot run in the worker thread
|
|
var A = getPart(processed.Asource, parts);
|
|
var B = getPart(processed.Bsource, parts);
|
|
|
|
List<NFP> Achildren = new List<NFP>();
|
|
|
|
|
|
if (A.children != null)
|
|
{
|
|
for (int j = 0; j < A.children.Count; j++)
|
|
{
|
|
Achildren.Add(rotatePolygon(A.children[j], processed.ARotation));
|
|
}
|
|
}
|
|
|
|
if (Achildren.Count > 0)
|
|
{
|
|
var Brotated = rotatePolygon(B, processed.BRotation);
|
|
var bbounds = GeometryUtil.getPolygonBounds(Brotated);
|
|
List<NFP> cnfp = new List<NFP>();
|
|
|
|
for (int j = 0; j < Achildren.Count; j++)
|
|
{
|
|
var cbounds = GeometryUtil.getPolygonBounds(Achildren[j]);
|
|
if (cbounds.width > bbounds.width && cbounds.height > bbounds.height)
|
|
{
|
|
var n = getInnerNfp(Achildren[j], Brotated, 1, data.config);
|
|
if (n != null && n.Count() > 0)
|
|
{
|
|
cnfp.AddRange(n);
|
|
}
|
|
}
|
|
}
|
|
|
|
processed.nfp.children = cnfp;
|
|
}
|
|
DbCacheKey doc = new DbCacheKey()
|
|
{
|
|
A = processed.Asource,
|
|
B = processed.Bsource,
|
|
ARotation = processed.ARotation,
|
|
BRotation = processed.BRotation,
|
|
nfp = new[] { processed.nfp }
|
|
};
|
|
|
|
/*var doc = {
|
|
A: processed[i].Asource,
|
|
B: processed[i].Bsource,
|
|
Arotation: processed[i].Arotation,
|
|
Brotation: processed[i].Brotation,
|
|
nfp: processed[i].nfp
|
|
|
|
};*/
|
|
window.db.insert(doc);
|
|
}
|
|
|
|
public Action<float> DisplayProgressAction { get; set; }
|
|
public void DisplayProgress(float p)
|
|
{
|
|
DisplayProgressAction?.Invoke(p);
|
|
}
|
|
public void thenDeepNest(NfpPair[] processed, List<NFP> parts)
|
|
{
|
|
int cnt = 0;
|
|
if (UseParallel)
|
|
{
|
|
Parallel.For(0, processed.Count(), (i) =>
|
|
{
|
|
float progress = 0.33f + 0.33f * (cnt / (float)processed.Count());
|
|
cnt++;
|
|
DisplayProgress(progress);
|
|
thenIterate(processed[i], parts);
|
|
});
|
|
|
|
}
|
|
else
|
|
{
|
|
for (var i = 0; i < processed.Count(); i++)
|
|
{
|
|
float progress = 0.33f + 0.33f * (cnt / (float)processed.Count());
|
|
cnt++;
|
|
DisplayProgress(progress);
|
|
thenIterate(processed[i], parts);
|
|
}
|
|
}
|
|
|
|
//console.timeEnd('Total');
|
|
//console.log('before sync');
|
|
sync();
|
|
}
|
|
|
|
public bool inpairs(NfpPair key, NfpPair[] p)
|
|
{
|
|
for (var i = 0; i < p.Length; i++)
|
|
{
|
|
if (p[i].Asource == key.Asource && p[i].Bsource == key.Bsource && p[i].ARotation == key.ARotation && p[i].BRotation == key.BRotation)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public static bool UseParallel = false;
|
|
public NfpPair[] pmapDeepNest(List<NfpPair> pairs)
|
|
{
|
|
NfpPair[] ret = new NfpPair[pairs.Count()];
|
|
int cnt = 0;
|
|
if (UseParallel)
|
|
{
|
|
Parallel.For(0, pairs.Count, (i) =>
|
|
{
|
|
ret[i] = process(pairs[i]);
|
|
float progress = 0.22f+0.11f * (cnt / (float)pairs.Count);
|
|
cnt++;
|
|
DisplayProgressAction?.Invoke(progress);
|
|
// DisplayProgress(progress);
|
|
});
|
|
}
|
|
else
|
|
{
|
|
for (int i = 0; i < pairs.Count; i++)
|
|
{
|
|
var item = pairs[i];
|
|
ret[i] = process(item);
|
|
float progress = 0.22f + 0.11f * (cnt / (float)pairs.Count);
|
|
cnt++;
|
|
DisplayProgress(progress);
|
|
}
|
|
}
|
|
return ret.ToArray();
|
|
}
|
|
public NfpPair process(NfpPair pair)
|
|
{
|
|
var A = rotatePolygon(pair.A, pair.ARotation);
|
|
var B = rotatePolygon(pair.B, pair.BRotation);
|
|
|
|
///////////////////
|
|
var Ac = _Clipper.ScaleUpPaths(A, 10000000);
|
|
|
|
var Bc = _Clipper.ScaleUpPaths(B, 10000000);
|
|
for (var i = 0; i < Bc.Length; i++)
|
|
{
|
|
Bc[i].X *= -1;
|
|
Bc[i].Y *= -1;
|
|
}
|
|
var solution = ClipperLib.Clipper.MinkowskiSum(new List<IntPoint>(Ac), new List<IntPoint>(Bc), true);
|
|
NFP clipperNfp = null;
|
|
|
|
double? largestArea = null;
|
|
for (int i = 0; i < solution.Count(); i++)
|
|
{
|
|
var n = toNestCoordinates(solution[i].ToArray(), 10000000);
|
|
var sarea = -GeometryUtil.polygonArea(n);
|
|
if (largestArea == null || largestArea < sarea)
|
|
{
|
|
clipperNfp = n;
|
|
largestArea = sarea;
|
|
}
|
|
}
|
|
|
|
for (var i = 0; i < clipperNfp.length; i++)
|
|
{
|
|
clipperNfp[i].x += B[0].x;
|
|
clipperNfp[i].y += B[0].y;
|
|
}
|
|
|
|
//return new SvgNestPort.NFP[] { new SvgNestPort.NFP() { Points = clipperNfp.Points } };
|
|
|
|
//////////////
|
|
|
|
pair.A = null;
|
|
pair.B = null;
|
|
pair.nfp = clipperNfp;
|
|
return pair;
|
|
}
|
|
|
|
public static NFP toNestCoordinates(IntPoint[] polygon, double scale)
|
|
{
|
|
//var clone = new List<SvgPoint>();
|
|
NFP nfpReturn = new NFP();
|
|
for (var i = 0; i < polygon.Count(); i++)
|
|
{
|
|
nfpReturn.AddPoint(new SvgPoint(
|
|
polygon[i].X / scale,
|
|
polygon[i].Y / scale
|
|
));
|
|
}
|
|
// return new NFP() { Points = clone.ToArray() };
|
|
return nfpReturn;
|
|
}
|
|
|
|
public static NFP getHull(NFP polygon)
|
|
{
|
|
// convert to hulljs format
|
|
/*var hull = new ConvexHullGrahamScan();
|
|
for(var i=0; i<polygon.length; i++){
|
|
hull.addPoint(polygon[i].x, polygon[i].y);
|
|
}
|
|
|
|
return hull.getHull();*/
|
|
double[][] points = new double[polygon.length][];
|
|
for (var i = 0; i < polygon.length; i++)
|
|
{
|
|
points[i] = (new double[] { polygon[i].x, polygon[i].y });
|
|
}
|
|
|
|
var hullpoints = D3.polygonHull(points);
|
|
|
|
if (hullpoints == null)
|
|
{
|
|
return polygon;
|
|
}
|
|
|
|
NFP hull = new NFP();
|
|
foreach(var pt in hullpoints)
|
|
{
|
|
hull.AddPoint(new SvgPoint(pt[0], pt[1]));
|
|
}
|
|
return hull;
|
|
}
|
|
|
|
|
|
// returns clipper nfp. Remember that clipper nfp are a list of polygons, not a tree!
|
|
public static IntPoint[][] nfpToClipperCoordinates(NFP nfp, double clipperScale = 10000000)
|
|
{
|
|
|
|
List<IntPoint[]> clipperNfp = new List<IntPoint[]>();
|
|
|
|
// children first
|
|
if (nfp.children != null && nfp.children.Count > 0)
|
|
{
|
|
for (var j = 0; j < nfp.children.Count; j++)
|
|
{
|
|
if (GeometryUtil.polygonArea(nfp.children[j]) < 0)
|
|
{
|
|
nfp.children[j].reverse();
|
|
}
|
|
//var childNfp = SvgNest.toClipperCoordinates(nfp.children[j]);
|
|
var childNfp = _Clipper.ScaleUpPaths(nfp.children[j], clipperScale);
|
|
clipperNfp.Add(childNfp);
|
|
}
|
|
}
|
|
|
|
if (GeometryUtil.polygonArea(nfp) > 0)
|
|
{
|
|
nfp.reverse();
|
|
}
|
|
|
|
|
|
//var outerNfp = SvgNest.toClipperCoordinates(nfp);
|
|
|
|
// clipper js defines holes based on orientation
|
|
|
|
var outerNfp = _Clipper.ScaleUpPaths(nfp, clipperScale);
|
|
|
|
//var cleaned = ClipperLib.Clipper.CleanPolygon(outerNfp, 0.00001*config.clipperScale);
|
|
|
|
clipperNfp.Add(outerNfp);
|
|
//var area = Math.abs(ClipperLib.Clipper.Area(cleaned));
|
|
|
|
return clipperNfp.ToArray();
|
|
}
|
|
// inner nfps can be an array of nfps, outer nfps are always singular
|
|
public static IntPoint[][] innerNfpToClipperCoordinates(NFP[] nfp, double clipperScale)
|
|
{
|
|
List<IntPoint[]> clipperNfp = new List<IntPoint[]>();
|
|
foreach (NFP item in nfp)
|
|
{
|
|
var clip = nfpToClipperCoordinates(item, clipperScale);
|
|
clipperNfp.AddRange(clip);
|
|
}
|
|
|
|
return clipperNfp.ToArray();
|
|
}
|
|
|
|
public NFP[] NewMinkowskiSum(NFP pattern, NFP path, int type, bool useChilds = false, bool takeOnlyBiggestArea = true)
|
|
{
|
|
var key = pattern.source + ";" + path.source + ";" + pattern.rotation + ";" + path.rotation;
|
|
bool cacheAllow = type != 1;
|
|
if (cacheProcess.ContainsKey(key) && cacheAllow)
|
|
{
|
|
return cacheProcess[key];
|
|
}
|
|
|
|
var ac = _Clipper.ScaleUpPaths(pattern, 10000000);
|
|
List<List<IntPoint>> solution = null;
|
|
if (useChilds)
|
|
{
|
|
var bc = Background.nfpToClipperCoordinates(path, 10000000);
|
|
for (var i = 0; i < bc.Length; i++)
|
|
{
|
|
for (int j = 0; j < bc[i].Length; j++)
|
|
{
|
|
bc[i][j].X *= -1;
|
|
bc[i][j].Y *= -1;
|
|
}
|
|
}
|
|
|
|
solution = ClipperLib.Clipper.MinkowskiSum(new List<IntPoint>(ac), new List<List<IntPoint>>(bc.Select(z => z.ToList())), true);
|
|
}
|
|
else
|
|
{
|
|
var bc = _Clipper.ScaleUpPaths(path, 10000000);
|
|
for (var i = 0; i < bc.Length; i++)
|
|
{
|
|
bc[i].X *= -1;
|
|
bc[i].Y *= -1;
|
|
}
|
|
solution = Clipper.MinkowskiSum(new List<IntPoint>(ac), new List<IntPoint>(bc), true);
|
|
}
|
|
NFP clipperNfp = null;
|
|
|
|
double? largestArea = null;
|
|
int largestIndex = -1;
|
|
|
|
for (int i = 0; i < solution.Count; i++)
|
|
{
|
|
var n = toNestCoordinates(solution[i].ToArray(), 10000000);
|
|
var sarea = Math.Abs(GeometryUtil.polygonArea(n));
|
|
if (largestArea == null || largestArea < sarea)
|
|
{
|
|
clipperNfp = n;
|
|
largestArea = sarea;
|
|
largestIndex = i;
|
|
}
|
|
}
|
|
if (!takeOnlyBiggestArea)
|
|
{
|
|
for (int j = 0; j < solution.Count; j++)
|
|
{
|
|
if (j == largestIndex) continue;
|
|
var n = toNestCoordinates(solution[j].ToArray(), 10000000);
|
|
if (clipperNfp.children == null)
|
|
clipperNfp.children = new List<NFP>();
|
|
clipperNfp.children.Add(n);
|
|
}
|
|
}
|
|
for (var i = 0; i < clipperNfp.Length; i++)
|
|
{
|
|
clipperNfp[i].x *= -1;
|
|
clipperNfp[i].y *= -1;
|
|
clipperNfp[i].x += pattern[0].x;
|
|
clipperNfp[i].y += pattern[0].y;
|
|
}
|
|
if (clipperNfp.children != null)
|
|
foreach (var nFP in clipperNfp.children)
|
|
{
|
|
for (int j = 0; j < nFP.Length; j++)
|
|
{
|
|
|
|
nFP.Points[j].x *= -1;
|
|
nFP.Points[j].y *= -1;
|
|
nFP.Points[j].x += pattern[0].x;
|
|
nFP.Points[j].y += pattern[0].y;
|
|
}
|
|
}
|
|
var res = new[] { clipperNfp };
|
|
if (cacheAllow)
|
|
{
|
|
cacheProcess.Add(key, res);
|
|
}
|
|
return res;
|
|
}
|
|
public static void ExecuteSTA(Action act)
|
|
{
|
|
if (!Debugger.IsAttached) return;
|
|
Thread thread = new Thread(() => { act(); });
|
|
thread.SetApartmentState(ApartmentState.STA);
|
|
thread.Start();
|
|
thread.Join();
|
|
}
|
|
|
|
public static bool UseExternalDll = false;
|
|
|
|
public NFP getOuterNfp(NFP A, NFP B, int type, bool inside = false)//todo:?inside def?
|
|
{
|
|
NFP[] nfp = null;
|
|
var key = new DbCacheKey()
|
|
{
|
|
A = A.source,
|
|
B = B.source,
|
|
ARotation = A.rotation,
|
|
BRotation = B.rotation,
|
|
//Type = type
|
|
};
|
|
|
|
var doc = window.db.find(key);
|
|
if (doc != null)
|
|
{
|
|
return doc.First();
|
|
}
|
|
|
|
/*
|
|
|
|
// try the file cache if the calculation will take a long time
|
|
var doc = window.db.find({ A: A.source, B: B.source, Arotation: A.rotation, Brotation: B.rotation });
|
|
|
|
if (doc)
|
|
{
|
|
return doc;
|
|
}*/
|
|
// not found in cache
|
|
if (inside || (A.children != null && A.children.Count > 0))
|
|
{
|
|
lock (lockobj)
|
|
{
|
|
if (UseExternalDll)
|
|
{
|
|
nfp = Process2(A, B, type);
|
|
}
|
|
else
|
|
{
|
|
nfp = NewMinkowskiSum(B, A, type, true, takeOnlyBiggestArea: false);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var Ac = _Clipper.ScaleUpPaths(A, 10000000);
|
|
|
|
var Bc = _Clipper.ScaleUpPaths(B, 10000000);
|
|
for (var i = 0; i < Bc.Length; i++)
|
|
{
|
|
Bc[i].X *= -1;
|
|
Bc[i].Y *= -1;
|
|
}
|
|
//Stopwatch sw = new Stopwatch();
|
|
//sw.Start();
|
|
var solution = ClipperLib.Clipper.MinkowskiSum(new List<IntPoint>(Ac), new List<IntPoint>(Bc), true);
|
|
//sw.Stop();
|
|
//Trace.WriteLine($"ClipperLib.Clipper.MinkowskiSum: {sw.ElapsedMilliseconds}");
|
|
NFP clipperNfp = null;
|
|
|
|
double? largestArea = null;
|
|
for (int i = 0; i < solution.Count(); i++)
|
|
{
|
|
var n = Background.toNestCoordinates(solution[i].ToArray(), 10000000);
|
|
var sarea = GeometryUtil.polygonArea(n);
|
|
if (largestArea == null || largestArea > sarea)
|
|
{
|
|
clipperNfp = n;
|
|
largestArea = sarea;
|
|
}
|
|
}
|
|
|
|
for (var i = 0; i < clipperNfp.length; i++)
|
|
{
|
|
clipperNfp[i].x += B[0].x;
|
|
clipperNfp[i].y += B[0].y;
|
|
}
|
|
nfp = new NFP[] { new NFP() { Points = clipperNfp.Points } };
|
|
}
|
|
|
|
if (nfp == null || nfp.Length == 0)
|
|
{
|
|
//console.log('holy shit', nfp, A, B, JSON.stringify(A), JSON.stringify(B));
|
|
return null;
|
|
}
|
|
|
|
NFP nfps = nfp.First();
|
|
/*
|
|
nfp = nfp.pop();
|
|
*/
|
|
if (nfps == null || nfps.Length == 0)
|
|
{
|
|
return null;
|
|
}
|
|
/*
|
|
if (!nfp || nfp.length == 0)
|
|
{
|
|
return null;
|
|
}
|
|
*/
|
|
if (!inside && A.source != null && B.source != null)
|
|
{
|
|
var doc2 = new DbCacheKey()
|
|
{
|
|
A = A.source.Value,
|
|
B = B.source.Value,
|
|
ARotation = A.rotation,
|
|
BRotation = B.rotation,
|
|
nfp = nfp
|
|
};
|
|
window.db.insert(doc2);
|
|
}
|
|
/*
|
|
if (!inside && typeof A.source !== 'undefined' && typeof B.source !== 'undefined')
|
|
{
|
|
// insert into db
|
|
doc = {
|
|
A: A.source,
|
|
B: B.source,
|
|
Arotation: A.rotation,
|
|
Brotation: B.rotation,
|
|
nfp: nfp
|
|
|
|
};
|
|
window.db.insert(doc);
|
|
}
|
|
*/
|
|
return nfps;
|
|
|
|
|
|
|
|
}
|
|
}
|
|
|
|
public class ClipCacheItem
|
|
{
|
|
public int index;
|
|
public IntPoint[][] nfpp;
|
|
}
|
|
|
|
public class dbCache
|
|
{
|
|
public dbCache(windowUnk w)
|
|
{
|
|
window = w;
|
|
}
|
|
public bool has(DbCacheKey obj)
|
|
{
|
|
lock (lockobj)
|
|
{
|
|
var key = getKey(obj);
|
|
//var key = "A" + obj.A + "B" + obj.B + "Arot" + (int)Math.Round(obj.ARotation) + "Brot" + (int)Math.Round(obj.BRotation);
|
|
if (window.nfpCache.ContainsKey(key))
|
|
{
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
}
|
|
|
|
public windowUnk window;
|
|
public object lockobj = new object();
|
|
|
|
string getKey(DbCacheKey obj)
|
|
{
|
|
// var key = "A" + obj.A + "B" + obj.B + "Arot" + (int)Math.Round(obj.ARotation * 10000) + "Brot" + (int)Math.Round((obj.BRotation * 10000)) + ";" + obj.Type;
|
|
return obj.KeyValue;
|
|
}
|
|
internal void insert(DbCacheKey obj, bool inner = false)
|
|
{
|
|
var key = getKey(obj);
|
|
//if (window.performance.memory.totalJSHeapSize < 0.8 * window.performance.memory.jsHeapSizeLimit)
|
|
{
|
|
lock (lockobj)
|
|
{
|
|
if (!window.nfpCache.ContainsKey(key))
|
|
{
|
|
window.nfpCache.Add(key, Background.cloneNfp(obj.nfp, inner).ToList());
|
|
}
|
|
else
|
|
{
|
|
throw new Exception("trouble .cache allready has such key");
|
|
// var lst = Background.cloneNfp(obj.nfp, inner).ToList();
|
|
// window.nfpCache[key] = lst;
|
|
}
|
|
}
|
|
//console.log('cached: ',window.cache[key].poly);
|
|
//console.log('using', window.performance.memory.totalJSHeapSize/window.performance.memory.jsHeapSizeLimit);
|
|
}
|
|
}
|
|
public NFP[] find(DbCacheKey obj, bool inner = false)
|
|
{
|
|
lock (lockobj)
|
|
{
|
|
var key = getKey(obj);
|
|
//var key = "A" + obj.A + "B" + obj.B + "Arot" + (int)Math.Round(obj.ARotation) + "Brot" + (int)Math.Round((obj.BRotation));
|
|
|
|
//console.log('key: ', key);
|
|
if (window.nfpCache.ContainsKey(key))
|
|
{
|
|
return Background.cloneNfp(window.nfpCache[key].ToArray(), inner);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
}
|
|
public class windowUnk
|
|
{
|
|
public windowUnk()
|
|
{
|
|
db = new dbCache(this);
|
|
}
|
|
public Dictionary<string, List<NFP>> nfpCache = new Dictionary<string, List<NFP>>();
|
|
public dbCache db;
|
|
}
|
|
}
|