makearmy-app/public/svgnest/util/placementworker.js

297 lines
7.7 KiB
JavaScript

// jsClipper uses X/Y instead of x/y...
function toClipperCoordinates(polygon){
var clone = [];
for(var i=0; i<polygon.length; i++){
clone.push({
X: polygon[i].x,
Y: polygon[i].y
});
}
return clone;
};
function toNestCoordinates(polygon, scale){
var clone = [];
for(var i=0; i<polygon.length; i++){
clone.push({
x: polygon[i].X/scale,
y: polygon[i].Y/scale
});
}
return clone;
};
function rotatePolygon(polygon, degrees){
var rotated = [];
var angle = degrees * Math.PI / 180;
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);
rotated.push({x:x1, y:y1});
}
if(polygon.children && polygon.children.length > 0){
rotated.children = [];
for(var j=0; j<polygon.children.length; j++){
rotated.children.push(rotatePolygon(polygon.children[j], degrees));
}
}
return rotated;
};
function PlacementWorker(binPolygon, paths, ids, rotations, config, nfpCache){
this.binPolygon = binPolygon;
this.paths = paths;
this.ids = ids;
this.rotations = rotations;
this.config = config;
this.nfpCache = nfpCache || {};
// return a placement for the paths/rotations given
// happens inside a webworker
this.placePaths = function(paths){
var self = global.env.self;
if(!self.binPolygon){
return null;
}
var i, j, k, m, n, path;
// rotate paths by given rotation
var rotated = [];
for(i=0; i<paths.length; i++){
var r = rotatePolygon(paths[i], paths[i].rotation);
r.rotation = paths[i].rotation;
r.source = paths[i].source;
r.id = paths[i].id;
rotated.push(r);
}
paths = rotated;
var allplacements = [];
var fitness = 0;
var binarea = Math.abs(GeometryUtil.polygonArea(self.binPolygon));
var key, nfp;
while(paths.length > 0){
var placed = [];
var placements = [];
fitness += 1; // add 1 for each new bin opened (lower fitness is better)
for(i=0; i<paths.length; i++){
path = paths[i];
// inner NFP
key = JSON.stringify({A:-1,B:path.id,inside:true,Arotation:0,Brotation:path.rotation});
var binNfp = self.nfpCache[key];
// part unplaceable, skip
if(!binNfp || binNfp.length == 0){
continue;
}
// ensure all necessary NFPs exist
var error = false;
for(j=0; j<placed.length; j++){
key = JSON.stringify({A:placed[j].id,B:path.id,inside:false,Arotation:placed[j].rotation,Brotation:path.rotation});
nfp = self.nfpCache[key];
if(!nfp){
error = true;
break;
}
}
// part unplaceable, skip
if(error){
continue;
}
var position = null;
if(placed.length == 0){
// first placement, put it on the left
for(j=0; j<binNfp.length; j++){
for(k=0; k<binNfp[j].length; k++){
if(position === null || binNfp[j][k].x-path[0].x < position.x ){
position = {
x: binNfp[j][k].x-path[0].x,
y: binNfp[j][k].y-path[0].y,
id: path.id,
rotation: path.rotation
}
}
}
}
placements.push(position);
placed.push(path);
continue;
}
var clipperBinNfp = [];
for(j=0; j<binNfp.length; j++){
clipperBinNfp.push(toClipperCoordinates(binNfp[j]));
}
ClipperLib.JS.ScaleUpPaths(clipperBinNfp, self.config.clipperScale);
var clipper = new ClipperLib.Clipper();
var combinedNfp = new ClipperLib.Paths();
for(j=0; j<placed.length; j++){
key = JSON.stringify({A:placed[j].id,B:path.id,inside:false,Arotation:placed[j].rotation,Brotation:path.rotation});
nfp = self.nfpCache[key];
if(!nfp){
continue;
}
for(k=0; k<nfp.length; k++){
var clone = toClipperCoordinates(nfp[k]);
for(m=0; m<clone.length; m++){
clone[m].X += placements[j].x;
clone[m].Y += placements[j].y;
}
ClipperLib.JS.ScaleUpPath(clone, self.config.clipperScale);
clone = ClipperLib.Clipper.CleanPolygon(clone, 0.0001*self.config.clipperScale);
var area = Math.abs(ClipperLib.Clipper.Area(clone));
if(clone.length > 2 && area > 0.1*self.config.clipperScale*self.config.clipperScale){
clipper.AddPath(clone, ClipperLib.PolyType.ptSubject, true);
}
}
}
if(!clipper.Execute(ClipperLib.ClipType.ctUnion, combinedNfp, ClipperLib.PolyFillType.pftNonZero, ClipperLib.PolyFillType.pftNonZero)){
continue;
}
// difference with bin polygon
var finalNfp = new ClipperLib.Paths();
clipper = new ClipperLib.Clipper();
clipper.AddPaths(combinedNfp, ClipperLib.PolyType.ptClip, true);
clipper.AddPaths(clipperBinNfp, ClipperLib.PolyType.ptSubject, true);
if(!clipper.Execute(ClipperLib.ClipType.ctDifference, finalNfp, ClipperLib.PolyFillType.pftNonZero, ClipperLib.PolyFillType.pftNonZero)){
continue;
}
finalNfp = ClipperLib.Clipper.CleanPolygons(finalNfp, 0.0001*self.config.clipperScale);
for(j=0; j<finalNfp.length; j++){
var area = Math.abs(ClipperLib.Clipper.Area(finalNfp[j]));
if(finalNfp[j].length < 3 || area < 0.1*self.config.clipperScale*self.config.clipperScale){
finalNfp.splice(j,1);
j--;
}
}
if(!finalNfp || finalNfp.length == 0){
continue;
}
var f = [];
for(j=0; j<finalNfp.length; j++){
// back to normal scale
f.push(toNestCoordinates(finalNfp[j], self.config.clipperScale));
}
finalNfp = f;
// choose placement that results in the smallest bounding box
// could use convex hull instead, but it can create oddly shaped nests (triangles or long slivers) which are not optimal for real-world use
// todo: generalize gravity direction
var minwidth = null;
var minarea = null;
var minx = null;
var nf, area, shiftvector;
for(j=0; j<finalNfp.length; j++){
nf = finalNfp[j];
if(Math.abs(GeometryUtil.polygonArea(nf)) < 2){
continue;
}
for(k=0; k<nf.length; k++){
var allpoints = [];
for(m=0; m<placed.length; m++){
for(n=0; n<placed[m].length; n++){
allpoints.push({x:placed[m][n].x+placements[m].x, y: placed[m][n].y+placements[m].y});
}
}
shiftvector = {
x: nf[k].x-path[0].x,
y: nf[k].y-path[0].y,
id: path.id,
rotation: path.rotation,
nfp: combinedNfp
};
for(m=0; m<path.length; m++){
allpoints.push({x: path[m].x+shiftvector.x, y:path[m].y+shiftvector.y});
}
var rectbounds = GeometryUtil.getPolygonBounds(allpoints);
// weigh width more, to help compress in direction of gravity
area = rectbounds.width*2 + rectbounds.height;
if(minarea === null || area < minarea || (GeometryUtil.almostEqual(minarea, area) && (minx === null || shiftvector.x < minx))){
minarea = area;
minwidth = rectbounds.width;
position = shiftvector;
minx = shiftvector.x;
}
}
}
if(position){
placed.push(path);
placements.push(position);
}
}
if(minwidth){
fitness += minwidth/binarea;
}
for(i=0; i<placed.length; i++){
var index = paths.indexOf(placed[i]);
if(index >= 0){
paths.splice(index,1);
}
}
if(placements && placements.length > 0){
allplacements.push(placements);
}
else{
break; // something went wrong
}
}
// there were parts that couldn't be placed
fitness += 2*paths.length;
return {placements: allplacements, fitness: fitness, paths: paths, area: binarea };
};
}
(typeof window !== 'undefined' ? window : self).PlacementWorker = PlacementWorker;
// clipperjs uses alerts for warnings
function alert(message) {
console.log('alert: ', message);
}