/*! * SvgNest * Licensed under the MIT license */ (function(root){ 'use strict'; root.SvgNest = new SvgNest(); function SvgNest(){ var self = this; var svg = null; // keep a reference to any style nodes, to maintain color/fill info this.style = null; var parts = null; var tree = null; var bin = null; var binPolygon = null; var binBounds = null; var nfpCache = {}; var config = { clipperScale: 10000000, curveTolerance: 0.3, spacing: 0, rotations: 4, populationSize: 10, mutationRate: 10, useHoles: false, exploreConcave: false }; this.working = false; var GA = null; var best = null; var workerTimer = null; var progress = 0; this.parsesvg = function(svgstring){ // reset if in progress this.stop(); bin = null; binPolygon = null; tree = null; // parse svg svg = SvgParser.load(svgstring); this.style = SvgParser.getStyle(); svg = SvgParser.clean(); tree = this.getParts(svg.childNodes); //re-order elements such that deeper elements are on top, so they can be moused over function zorder(paths){ // depth-first var length = paths.length; for(var i=0; i 0){ zorder(paths[i].children); } } } return svg; } this.setbin = function(element){ if(!svg){ return; } bin = element; } this.config = function(c){ // clean up inputs if(!c){ return config; } if(c.curveTolerance && !GeometryUtil.almostEqual(parseFloat(c.curveTolerance), 0)){ config.curveTolerance = parseFloat(c.curveTolerance); } if('spacing' in c){ config.spacing = parseFloat(c.spacing); } if(c.rotations && parseInt(c.rotations) > 0){ config.rotations = parseInt(c.rotations); } if(c.populationSize && parseInt(c.populationSize) > 2){ config.populationSize = parseInt(c.populationSize); } if(c.mutationRate && parseInt(c.mutationRate) > 0){ config.mutationRate = parseInt(c.mutationRate); } if('useHoles' in c){ config.useHoles = !!c.useHoles; } if('exploreConcave' in c){ config.exploreConcave = !!c.exploreConcave; } SvgParser.config({ tolerance: config.curveTolerance}); best = null; nfpCache = {}; binPolygon = null; GA = null; return config; } // progressCallback is called when progress is made // displayCallback is called when a new placement has been made this.start = function(progressCallback, displayCallback){ if(!svg || !bin){ return false; } parts = Array.prototype.slice.call(svg.childNodes); var binindex = parts.indexOf(bin); if(binindex >= 0){ // don't process bin as a part of the tree parts.splice(binindex, 1); } // build tree without bin tree = this.getParts(parts.slice(0)); offsetTree(tree, 0.5*config.spacing, this.polygonOffset.bind(this)); // offset tree recursively function offsetTree(t, offset, offsetFunction){ for(var i=0; i 0){ offsetTree(t[i].childNodes, -offset, offsetFunction); } } } binPolygon = SvgParser.polygonify(bin); binPolygon = this.cleanPolygon(binPolygon); if(!binPolygon || binPolygon.length < 3){ return false; } binBounds = GeometryUtil.getPolygonBounds(binPolygon); if(config.spacing > 0){ var offsetBin = this.polygonOffset(binPolygon, -0.5*config.spacing); if(offsetBin.length == 1){ // if the offset contains 0 or more than 1 path, something went wrong. binPolygon = offsetBin.pop(); } } binPolygon.id = -1; // put bin on origin var xbinmax = binPolygon[0].x; var xbinmin = binPolygon[0].x; var ybinmax = binPolygon[0].y; var ybinmin = binPolygon[0].y; for(var i=1; i xbinmax){ xbinmax = binPolygon[i].x; } else if(binPolygon[i].x < xbinmin){ xbinmin = binPolygon[i].x; } if(binPolygon[i].y > ybinmax){ ybinmax = binPolygon[i].y; } else if(binPolygon[i].y < ybinmin){ ybinmin = binPolygon[i].y; } } for(i=0; i 0){ binPolygon.reverse(); } // remove duplicate endpoints, ensure counterclockwise winding direction for(i=0; i 0){ tree[i].reverse(); } } var self = this; this.working = false; workerTimer = setInterval(function(){ if(!self.working){ self.launchWorkers.call(self, tree, binPolygon, config, progressCallback, displayCallback); self.working = true; } progressCallback(progress); }, 100); } this.launchWorkers = function(tree, binPolygon, config, progressCallback, displayCallback){ function shuffle(array) { var currentIndex = array.length, temporaryValue, randomIndex ; // While there remain elements to shuffle... while (0 !== currentIndex) { // Pick a remaining element... randomIndex = Math.floor(Math.random() * currentIndex); currentIndex -= 1; // And swap it with the current element. temporaryValue = array[currentIndex]; array[currentIndex] = array[randomIndex]; array[randomIndex] = temporaryValue; } return array; } var i,j; if(GA === null){ // initiate new GA var adam = tree.slice(0); // seed with decreasing area adam.sort(function(a, b){ return Math.abs(GeometryUtil.polygonArea(b)) - Math.abs(GeometryUtil.polygonArea(a)); }); GA = new GeneticAlgorithm(adam, binPolygon, config); } var individual = null; // evaluate all members of the population for(i=0; i 0){ for(var i=0; i 0){ nfp[i].reverse(); } } } else{ // warning on null inner NFP // this is not an error, as the part may simply be larger than the bin or otherwise unplaceable due to geometry log('NFP Warning: ', pair.key); } } else{ if(searchEdges){ nfp = GeometryUtil.noFitPolygon(A,B,false,searchEdges); } else{ nfp = minkowskiDifference(A,B); } // sanity check if(!nfp || nfp.length == 0){ log('NFP Error: ', pair.key); log('A: ',JSON.stringify(A)); log('B: ',JSON.stringify(B)); return null; } for(var i=0; i 0){ nfp[i].reverse(); } if(i > 0){ if(GeometryUtil.pointInPolygon(nfp[i][0], nfp[0])){ if(GeometryUtil.polygonArea(nfp[i]) < 0){ nfp[i].reverse(); } } } } // generate nfps for children (holes of parts) if any exist if(useHoles && A.childNodes && A.childNodes.length > 0){ var Bbounds = GeometryUtil.getPolygonBounds(B); for(var i=0; i Bbounds.width && Abounds.height > Bbounds.height){ var cnfp = GeometryUtil.noFitPolygon(A.childNodes[i],B,true,searchEdges); // ensure all interior NFPs have the same winding direction if(cnfp && cnfp.length > 0){ for(var j=0; j sarea){ clipperNfp = n; largestArea = sarea; } } for(var i=0; i 2 && Math.abs(GeometryUtil.polygonArea(poly)) > config.curveTolerance*config.curveTolerance){ poly.source = i; polygons.push(poly); } } // turn the list into a tree toTree(polygons); function toTree(list, idstart){ var parents = []; var i,j; // assign a unique id to each leaf var id = idstart || 0; for(i=0; i biggestarea){ biggest = simple[i]; biggestarea = area; } } // clean up singularities, coincident points and edges var clean = ClipperLib.Clipper.CleanPolygon(biggest, config.curveTolerance*config.clipperScale); if(!clean || clean.length == 0){ return null; } return this.clipperToSvg(clean); } // converts a polygon from normal float coordinates to integer coordinates used by clipper, as well as x/y -> X/Y this.svgToClipper = function(polygon){ var clip = []; for(var i=0; i 0){ var flattened = _flattenTree(part.children, true); for(k=0; k 0){ flat = flat.concat(_flattenTree(t[i].children, !hole)); } } return flat; } return svglist; } this.stop = function(){ this.working = false; if(workerTimer){ clearInterval(workerTimer); } }; } function GeneticAlgorithm(adam, bin, config){ this.config = config || { populationSize: 10, mutationRate: 10, rotations: 4 }; this.binBounds = GeometryUtil.getPolygonBounds(bin); // population is an array of individuals. Each individual is a object representing the order of insertion and the angle each part is rotated var angles = []; for(var i=0; i 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var temp = array[i]; array[i] = array[j]; array[j] = temp; } return array; } angleList = shuffleArray(angleList); for(i=0; i= 0){ pop.splice(pop.indexOf(exclude),1); } var rand = Math.random(); var lower = 0; var weight = 1/pop.length; var upper = weight; for(var i=0; i lower && rand < upper){ return pop[i]; } lower = upper; upper += 2*weight * ((pop.length-i)/pop.length); } return pop[0]; } })(window);