diff --git a/src/Mod/Arch/ArchNesting.py b/src/Mod/Arch/ArchNesting.py index 5df6f66d74..bef5dd815b 100644 --- a/src/Mod/Arch/ArchNesting.py +++ b/src/Mod/Arch/ArchNesting.py @@ -36,7 +36,6 @@ TOLERANCE = 0.0001 # smaller than this, two points are considered equal DISCRETIZE = 4 # the number of segments in which arcs must be subdivided ROTATIONS = [0,90,180,270] # the possible rotations to try - class Nester: @@ -44,12 +43,83 @@ class Nester: """Nester([container,shapes]): Creates a nester object with a container shape and a list of other shapes to nest into it. Container and - shapes must be Part.Faces.""" + shapes must be Part.Faces. + Typical workflow: + + n = Nester() # creates the nester + n.addContainer(object) # adds a doc object as the container + n.addObjects(objects) # adds a list of doc objects as shapes + n.run() # runs the nesting + n.show() # creates a preview (compound) of the results + n.apply() # applies transformations to the original objects + + Defaults (can be changed): + + Nester.TOLERANCE = 0.0001 + Nester.DISCRETIZE = 4 + Nester.ROTATIONS = [0,90,180,270] + """ + + self.objects = None self.container = container self.shapes = shapes self.results = [] # storage for the different results + self.indexedFaces = None + self.running = True + self.progress = 0 + self.setCounter = None # optionally define a setCounter(value) function where value is a % + def addObjects(self,objects): + + """addObjects(objects): adds FreeCAD DocumentObjects to the nester""" + + if not isinstance(objects,list): + objects = [objects] + if not self.objects: + self.objects = {} + if not self.shapes: + self.shapes = [] + for obj in objects: + if obj.isDerivedFrom("Part::Feature"): + h = obj.Shape.hashCode() + if not h in self.objects: + self.objects[h] = obj + self.shapes.append(obj.Shape) + + def addContainer(self,container): + + """addContainer(object): adds a FreeCAD DocumentObject as the container""" + + if container.isDerivedFrom("Part::Feature"): + self.container = container.Shape + + def clear(self): + + """clear(): Removes all objects and shape from the nester""" + + self.objects = None + self.shapes = None + + def stop(self): + + """stop((): stops the computation""" + + self.running = False + + def update(self): + + """update(): internal function to verify if computation can + go on""" + + if self.setCounter: + self.setCounter(self.progress) + if FreeCAD.GuiUp: + from PySide import QtGui + QtGui.qApp.processEvents() + if not self.running: + return False + return True def run(self): @@ -57,6 +127,10 @@ class Nester: shapes, each primary list being one filled container, or None if the operation failed.""" + # reset abort mechanism and variables + + self.running = True + self.progress = 0 starttime = datetime.now() # general conformity tests @@ -73,6 +147,8 @@ class Nester: return normal = self.container.normalAt(0,0) for s in self.shapes: + if not self.update(): + return if len(s.Faces) != 1: print("One of the shapes does not contain exactly one face. Aborting") return @@ -91,6 +167,9 @@ class Nester: # LONG-TERM TODO # add genetic algo to swap pieces, and check if the result is better + # track progresses + step = 100.0/(len(self.shapes)*len(ROTATIONS)) + # store hashCode together with the face so we can change the order # and still identify the original face, so we can calculate a transform afterwards self.indexedfaces = [[shape.hashCode(),shape] for shape in self.shapes] @@ -107,6 +186,8 @@ class Nester: # discretize non-linear edges and remove holes nfaces = [] for face in faces: + if not self.update(): + return nedges = [] allLines = True for edge in face[1].OuterWire.OrderedEdges: @@ -165,6 +246,11 @@ class Nester: for rotation in ROTATIONS: + if not self.update(): + return + + self.progress += step + print(rotation,", ",end="") hashcode = face[0] rotface = face[1].copy() @@ -195,13 +281,15 @@ class Nester: # check for available space on each existing sheet for sheetnumber,sheet in enumerate(sheets): - # Get the no-fit polygon for each already placed face in # current sheet. That is, a polygon in which basepoint # cannot be, if we want our face to not overlap with the # placed face. # To do this, we "circulate" the face around the placed face + if not self.update(): + return + nofitpol = [] for placed in sheet: pts = [] @@ -209,6 +297,9 @@ class Nester: for placedvert in self.order(placed[1],right=True): fpts = [] for i,rotvert in enumerate(rotverts): + if not self.update(): + return + facecopy = rotface.copy() facecopy.translate(placedvert.sub(rotvert)) @@ -227,22 +318,35 @@ class Nester: # if all vertices are outside, the pieces could still # overlap - # TODO this code is slow and could be otimized... - if outside: for e1 in facecopy.OuterWire.Edges: for e2 in placed[1].OuterWire.Edges: - p = DraftGeomUtils.findIntersection(e1,e2) - if p: - p = p[0] - p1 = e1.Vertexes[0].Point - p2 = e1.Vertexes[1].Point - p3 = e2.Vertexes[0].Point - p4 = e2.Vertexes[1].Point - if (p.sub(p1).Length > TOLERANCE) and (p.sub(p2).Length > TOLERANCE) \ - and (p.sub(p3).Length > TOLERANCE) and (p.sub(p4).Length > TOLERANCE): - outside = False - break + if not self.update(): + return + + if True: + # Draft code (SLOW) + p = DraftGeomUtils.findIntersection(e1,e2) + if p: + p = p[0] + p1 = e1.Vertexes[0].Point + p2 = e1.Vertexes[1].Point + p3 = e2.Vertexes[0].Point + p4 = e2.Vertexes[1].Point + if (p.sub(p1).Length > TOLERANCE) and (p.sub(p2).Length > TOLERANCE) \ + and (p.sub(p3).Length > TOLERANCE) and (p.sub(p4).Length > TOLERANCE): + outside = False + break + else: + # alt code: using distToShape (EVEN SLOWER!) + p = e1.distToShape(e2) + if p: + if p[0] < TOLERANCE: + # allow vertex-to-vertex intersection + if (p[2][0][0] != "Vertex") or (p[2][0][3] != "Vertex"): + outside = False + break + if outside: fpts.append([faceverts[0],i]) @@ -281,6 +385,9 @@ class Nester: while overlap: overlap = False for i in range(len(pol.OuterWire.Edges)-1): + if not self.update(): + return + v1 = DraftGeomUtils.vec(pol.OuterWire.OrderedEdges[i]) v2 = DraftGeomUtils.vec(pol.OuterWire.OrderedEdges[i+1]) if abs(v1.getAngle(v2)-math.pi) <= TOLERANCE: @@ -325,8 +432,10 @@ class Nester: #for i,p in enumerate(faceverts): # Draft.makeText([str(i)],point=p) return - nofitpol.append(pol) - #Part.show(pol) + + if pol.isValid(): + nofitpol.append(pol) + #Part.show(pol) # Union all the no-fit pols into one @@ -335,6 +444,8 @@ class Nester: elif len(nofitpol) > 1: b = nofitpol.pop() for n in nofitpol: + if not self.update(): + return b = b.fuse(n) nofitpol = b @@ -384,6 +495,9 @@ class Nester: # intersect with already placed pieces fitverts = sorted([v.Point for v in fitpol.Vertexes],key=lambda v: v.x) for p in fitverts: + if not self.update(): + return + trface = rotface.copy() trface.translate(p.sub(basepoint)) ok = True @@ -509,18 +623,78 @@ class Nester: if self.results: result = self.results[-1] offset = FreeCAD.Vector(0,0,0) + feats = [] for sheet in result: shapes = [self.container.OuterWire] shapes.extend([face[1] for face in sheet]) comp = Part.makeCompound(shapes) comp.translate(offset) - Part.show(comp) + o = FreeCAD.ActiveDocument.addObject("Part::Feature","Nest") + o.Shape = comp + feats.append(o) offset = offset.add(FreeCAD.Vector(1.1*self.container.BoundBox.XLength,0,0)) + FreeCAD.ActiveDocument.recompute() + return feats + + def getPlacements(self,result=None): + + """getPlacements([result]): returns a dictionary of hashCode:Placement + pairs from the given result or the last computed result if none + is given. The Placement contains a translation vector and a rotation + to be given to the final object.""" + + if not self.indexedfaces: + print("error: shapes were not indexed. Please run() first") + return + if not result: + result = [] + if self.results: + result = self.results[-1] + d = {} + offset = FreeCAD.Vector(0,0,0) + for sheet in result: + for face in sheet: + orig = None + for pair in self.indexedfaces: + if pair[0] == face[0]: + orig = pair[1] + if not orig: + print("error: hashCode mismatch between original and transformed face") + return + shape = face[1] + if offset.Length: + shape.translate(offset) + deltav = shape.Faces[0].CenterOfMass.sub(orig.Faces[0].CenterOfMass) + rot = FreeCAD.Rotation(orig.Vertexes[0].Point.sub(orig.Faces[0].CenterOfMass),shape.Vertexes[0].Point.sub(shape.Faces[0].CenterOfMass)) + pla = FreeCAD.Placement(deltav,rot) + d[face[0]] = pla + offset = offset.add(FreeCAD.Vector(1.1*self.container.BoundBox.XLength,0,0)) + return d + + def apply(self,result=None): + + """apply([result]): Applies the computed placements of the given + result, or the last computed result if none is given, to the + document objects given to the nester via addObjects() before + running.""" + + if not self.objects: + print("objects list is empty") + return + p = self.getPlacements(result) + if p: + for key,pla in p.items(): + if key in self.objects: + sh = self.objects[key].Shape.copy() + sh.translate(pla.Base) + sh.rotate(sh.Faces[0].CenterOfMass,pla.Rotation.Axis,math.degrees(pla.Rotation.Angle)) + self.objects[key].Placement = sh.Placement + else: + print("error: hashCode mismatch with original object") def test(): - "runs a test with selected shapes, container selected last" import FreeCADGui diff --git a/src/Mod/Arch/ArchPanel.py b/src/Mod/Arch/ArchPanel.py index d8effb24f0..7351035473 100644 --- a/src/Mod/Arch/ArchPanel.py +++ b/src/Mod/Arch/ArchPanel.py @@ -970,6 +970,7 @@ class PanelSheet(Draft._DraftObject): obj.addProperty("App::PropertyBool","MakeFace","Arch",QT_TRANSLATE_NOOP("App::Property","If True, the object is rendered as a face, if possible.")) obj.addProperty("App::PropertyAngle","GrainDirection","Arch",QT_TRANSLATE_NOOP("App::Property","Specifies an angle for the wood grain (Clockwise, 0 is North)")) obj.addProperty("App::PropertyFloat","Scale","Arch", QT_TRANSLATE_NOOP("App::Property","Specifies the scale applied to each panel view.")) + obj.addProperty("App::PropertyFloatList","Rotations","Arch", QT_TRANSLATE_NOOP("App::Property","A list of possible rotations for the nester")) obj.Proxy = self self.Type = "PanelSheet" obj.TagSize = 10 @@ -1175,7 +1176,7 @@ class ViewProviderPanelSheet(Draft._ViewProviderDraft): else: vobj.Pattern = "None" Draft._ViewProviderDraft.onChanged(self,vobj,prop) - + def updateData(self,obj,prop): if prop in ["Width","Height"]: @@ -1235,24 +1236,42 @@ class NestTaskPanel: '''The TaskPanel for Arch Nest command''' def __init__(self,obj=None): + import ArchNesting self.form = FreeCADGui.PySideUic.loadUi(":/ui/ArchNest.ui") self.form.progressBar.hide() + self.form.ButtonPreview.setEnabled(False) + self.form.ButtonStop.setEnabled(False) QtCore.QObject.connect(self.form.ButtonContainer,QtCore.SIGNAL("pressed()"),self.getContainer) QtCore.QObject.connect(self.form.ButtonShapes,QtCore.SIGNAL("pressed()"),self.getShapes) QtCore.QObject.connect(self.form.ButtonRemove,QtCore.SIGNAL("pressed()"),self.removeShapes) QtCore.QObject.connect(self.form.ButtonStart,QtCore.SIGNAL("pressed()"),self.start) QtCore.QObject.connect(self.form.ButtonStop,QtCore.SIGNAL("pressed()"),self.stop) + QtCore.QObject.connect(self.form.ButtonPreview,QtCore.SIGNAL("pressed()"),self.preview) self.shapes = [] self.container = None self.nester = None - - def getStandardButtons(self): - return int(QtGui.QDialogButtonBox.Close) + self.temps = [] def reject(self): self.stop() + self.clearTemps() return True - + + def accept(self): + self.stop() + self.clearTemps() + if self.nester: + FreeCAD.ActiveDocument.openTransaction("Nesting") + self.nester.apply() + FreeCAD.ActiveDocument.commitTransaction() + return True + + def clearTemps(self): + for t in self.temps: + if FreeCAD.ActiveDocument.getObject(t.Name): + FreeCAD.ActiveDocument.removeObject(t.Name) + self.temps = [] + def getContainer(self): s = FreeCADGui.Selection.getSelection() if len(s) == 1: @@ -1262,6 +1281,12 @@ class NestTaskPanel: self.form.Container.clear() self.addObject(s[0],self.form.Container) self.container = s[0] + else: + FreeCAD.Console.PrintError(translate("Arch","This object has no face")) + if Draft.getType(s[0]) == "PanelSheet": + if hasattr(s[0],"Rotations"): + if s[0].Rotations: + self.form.Rotations.setText(str(s[0].Rotations)) def getShapes(self): s = FreeCADGui.Selection.getSelection() @@ -1290,8 +1315,13 @@ class NestTaskPanel: self.shapes.remove(o) self.form.Shapes.takeItem(self.form.Shapes.row(i)) + def setCounter(self,value): + self.form.progressBar.setValue(value) + def start(self): - self.form.progressBar.setValue(1) + self.clearTemps() + self.form.progressBar.setFormat("pass 1: %p%") + self.form.progressBar.setValue(0) self.form.progressBar.show() tolerance = self.form.Tolerance.value() discretize = self.form.Subdivisions.value() @@ -1300,14 +1330,35 @@ class NestTaskPanel: ArchNesting.TOLERANCE = tolerance ArchNesting.DISCRETIZE = discretize ArchNesting.ROTATIONS = rotations - n = ArchNesting.Nester(container=self.container.Shape,shapes=[o.Shape for o in self.shapes]) - result = n.run() + self.nester = ArchNesting.Nester() + self.nester.addContainer(self.container) + self.nester.addObjects(self.shapes) + self.nester.setCounter = self.setCounter + self.form.ButtonStop.setEnabled(True) + self.form.ButtonStart.setEnabled(False) + self.form.ButtonPreview.setEnabled(False) + QtGui.qApp.processEvents() + result = self.nester.run() + self.form.progressBar.hide() + self.form.ButtonStart.setEnabled(True) + self.form.ButtonStop.setEnabled(False) if result: - n.show() - - def stop(self): - pass + self.form.ButtonPreview.setEnabled(True) + def stop(self): + if self.nester: + self.nester.stop() + self.form.ButtonStart.setEnabled(True) + self.form.ButtonStop.setEnabled(False) + self.form.ButtonPreview.setEnabled(False) + self.form.progressBar.hide() + + def preview(self): + self.clearTemps() + if self.nester: + t = self.nester.show() + if t: + self.temps.extend(t) if FreeCAD.GuiUp: diff --git a/src/Mod/Arch/Resources/ui/ArchNest.ui b/src/Mod/Arch/Resources/ui/ArchNest.ui index 97d7b29642..98d8a814f4 100644 --- a/src/Mod/Arch/Resources/ui/ArchNest.ui +++ b/src/Mod/Arch/Resources/ui/ArchNest.ui @@ -171,6 +171,13 @@ + + + + Preview + + +