From a3d788174657a583e1bdb344205ac1ac1c826ebd Mon Sep 17 00:00:00 2001 From: Yorik van Havre Date: Mon, 10 Jul 2017 19:16:10 -0300 Subject: [PATCH] Arch: Initial Nest command --- src/Mod/Arch/ArchNesting.py | 534 ++++++++++++++++ src/Mod/Arch/ArchPanel.py | 116 +++- src/Mod/Arch/CMakeLists.txt | 1 + src/Mod/Arch/Resources/Arch.qrc | 2 + src/Mod/Arch/Resources/icons/Arch_Nest.svg | 680 +++++++++++++++++++++ src/Mod/Arch/Resources/ui/ArchNest.ui | 183 ++++++ 6 files changed, 1508 insertions(+), 8 deletions(-) create mode 100644 src/Mod/Arch/ArchNesting.py create mode 100644 src/Mod/Arch/Resources/icons/Arch_Nest.svg create mode 100644 src/Mod/Arch/Resources/ui/ArchNest.ui diff --git a/src/Mod/Arch/ArchNesting.py b/src/Mod/Arch/ArchNesting.py new file mode 100644 index 0000000000..5df6f66d74 --- /dev/null +++ b/src/Mod/Arch/ArchNesting.py @@ -0,0 +1,534 @@ +# -*- coding: utf-8 -*- + +#*************************************************************************** +#* * +#* Copyright (c) 2017 Yorik van Havre * +#* * +#* This program is free software; you can redistribute it and/or modify * +#* it under the terms of the GNU Lesser General Public License (LGPL) * +#* as published by the Free Software Foundation; either version 2 of * +#* the License, or (at your option) any later version. * +#* for detail see the LICENCE text file. * +#* * +#* This program is distributed in the hope that it will be useful, * +#* but WITHOUT ANY WARRANTY; without even the implied warranty of * +#* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +#* GNU Library General Public License for more details. * +#* * +#* You should have received a copy of the GNU Library General Public * +#* License along with this program; if not, write to the Free Software * +#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +#* USA * +#* * +#*************************************************************************** + + +from __future__ import print_function + +import FreeCAD, Part, DraftGeomUtils, WorkingPlane, DraftVecUtils, math, Draft +from datetime import datetime + +# This is roughly based on the no-fit polygon algorithm, used in +# SvgNest: https://github.com/Jack000/SVGnest +# Wikihouse plugin: https://github.com/tav/wikihouse-plugin/blob/master/wikihouse.rb + +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: + + + def __init__(self,container=None,shapes=None): + + """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.""" + + self.container = container + self.shapes = shapes + self.results = [] # storage for the different results + + + def run(self): + + """run(): Runs a nesting operation. Returns a list of lists of + shapes, each primary list being one filled container, or None + if the operation failed.""" + + starttime = datetime.now() + + # general conformity tests + + print("Executing conformity tests ... ",end="") + if not self.container: + print("Empty container. Aborting") + return + if not self.shapes: + print("Empty shapes. Aborting") + return + if not isinstance(self.container,Part.Face): + print("Container is not a face. Aborting") + return + normal = self.container.normalAt(0,0) + for s in self.shapes: + if len(s.Faces) != 1: + print("One of the shapes does not contain exactly one face. Aborting") + return + # check if all faces correctly oriented (same normal) + if s.Faces[0].normalAt(0,0).getAngle(normal) > TOLERANCE: + # let pass faces with inverted normal + if s.Faces[0].normalAt(0,0).getAngle(normal)-math.pi > TOLERANCE: + print("One of the face doesn't have the same orientation as the container. Aborting") + return + + # TODO + # allow to use a non-rectangular container + # manage margins/paddings + # allow to prevent or force specific rotations for a piece + + # LONG-TERM TODO + # add genetic algo to swap pieces, and check if the result is better + + # 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] + + # build a clean copy so we don't touch the original + faces = list(self.indexedfaces) + + # replace shapes by their face + faces = [[f[0],f[1].Faces[0]] for f in faces] + + # order by area + faces = sorted(faces,key=lambda face: face[1].Area) + + # discretize non-linear edges and remove holes + nfaces = [] + for face in faces: + nedges = [] + allLines = True + for edge in face[1].OuterWire.OrderedEdges: + if isinstance(edge.Curve,(Part.LineSegment,Part.Line)): + nedges.append(edge) + else: + allLines = False + last = edge.Vertexes[0].Point + for i in range(DISCRETIZE): + s = float(i+1)/DISCRETIZE + par = edge.FirstParameter + (edge.LastParameter-edge.FirstParameter)*s + new = edge.valueAt(par) + nedges.append(Part.LineSegment(last,new).toShape()) + last = new + f = Part.Face(Part.Wire(nedges)) + if not f.isValid(): + if allLines: + print("Invalid face found in set. Aborting") + else: + print("Face distretizing failed. Aborting") + return + nfaces.append([face[0],f]) + faces = nfaces + + # container for sheets with a first, empty sheet + sheets = [[]] + + print("Everything OK (",datetime.now()-starttime,")") + + # main loop + + facenumber = 1 + facesnumber = len(faces) + + #print("Vertices per face:",[len(face[1].Vertexes) for face in faces]) + + while faces: + + print("Placing piece",facenumber,"/",facesnumber,"Area:",FreeCAD.Units.Quantity(faces[-1][1].Area,FreeCAD.Units.Area).getUserPreferred()[0],": ",end="") + + face = faces.pop() + boc = self.container.BoundBox + + # this stores the available solutions for each rotation of a piece + # contains [sheetnumber,face,xlength] lists, + # face being [hascode,transformed face] and xlength + # the X size of all boundboxes of placed pieces + available = [] + + # this stores the possible positions on a blank + # sheet, in case we need to create a new one + initials = [] + + # this checks if the piece don't fit in the container + unfit = True + + for rotation in ROTATIONS: + + print(rotation,", ",end="") + hashcode = face[0] + rotface = face[1].copy() + if rotation: + rotface.rotate(rotface.CenterOfMass,normal,rotation) + bof = rotface.BoundBox + rotverts = self.order(rotface) + #for i,v in enumerate(rotverts): + # Draft.makeText([str(i)],point=v) + basepoint = rotverts[0] # leftmost point of the rotated face + basecorner = boc.getPoint(0) # lower left corner of the container + + # See if the piece fits in the container dimensions + if (bof.XLength < boc.XLength) and (bof.YLength < boc.YLength): + unfit = False + + # Get the fit polygon of the container + # that is, the polygon inside which basepoint can + # circulate, and the face still be fully inside the container + + v1 = basecorner.add(basepoint.sub(bof.getPoint(0))) + v2 = v1.add(FreeCAD.Vector(0,boc.YLength-bof.YLength,0)) + v3 = v2.add(FreeCAD.Vector(boc.XLength-bof.XLength,0,0)) + v4 = v3.add(FreeCAD.Vector(0,-(boc.YLength-bof.YLength),0)) + binpol = Part.Face(Part.makePolygon([v1,v2,v3,v4,v1])) + initials.append([binpol,[hashcode,rotface],basepoint]) + + # 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 + + nofitpol = [] + for placed in sheet: + pts = [] + pi = 0 + for placedvert in self.order(placed[1],right=True): + fpts = [] + for i,rotvert in enumerate(rotverts): + facecopy = rotface.copy() + facecopy.translate(placedvert.sub(rotvert)) + + # test if all the points of the face are outside the + # placed face (except the base point, which is coincident) + + outside = True + faceverts = self.order(facecopy) + for vert in faceverts: + if (vert.sub(placedvert)).Length > TOLERANCE: + if placed[1].isInside(vert,TOLERANCE,True): + outside = False + break + + # also need to test for edge intersection, because even + # 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 outside: + fpts.append([faceverts[0],i]) + #Draft.makeText([str(i)],point=faceverts[0]) + + # reorder available solutions around a same point if needed + # ensure they are in the correct order + + idxs = [p[1] for p in fpts] + if (0 in idxs) and (len(faceverts)-1 in idxs): + slicepoint = len(fpts) + last = len(faceverts) + for p in reversed(fpts): + if p[1] == last-1: + slicepoint -= 1 + last -= 1 + else: + break + fpts = fpts[slicepoint:]+fpts[:slicepoint] + #print(fpts) + pts.extend(fpts) + + # create the polygon + + if len(pts) < 3: + print("Error calculating a no-fit polygon. Aborting") + return + pts = [p[0] for p in pts] + pol = Part.Face(Part.makePolygon(pts+[pts[0]])) + + if not pol.isValid(): + + # fix overlapping edges + + overlap = True + while overlap: + overlap = False + for i in range(len(pol.OuterWire.Edges)-1): + v1 = DraftGeomUtils.vec(pol.OuterWire.OrderedEdges[i]) + v2 = DraftGeomUtils.vec(pol.OuterWire.OrderedEdges[i+1]) + if abs(v1.getAngle(v2)-math.pi) <= TOLERANCE: + overlap = True + ne = Part.LineSegment(pol.OuterWire.OrderedEdges[i].Vertexes[0].Point, + pol.OuterWire.OrderedEdges[i+1].Vertexes[-1].Point).toShape() + pol = Part.Face(Part.Wire(pol.OuterWire.OrderedEdges[:i]+[ne]+pol.OuterWire.OrderedEdges[i+2:])) + break + + if not pol.isValid(): + + # trying basic OCC fix + + pol.fix(0,0,0) + if pol.isValid(): + if pol.ShapeType == "Face": + pol = Part.Face(pol.OuterWire) # discard possible inner holes + elif pol.Faces: + # several faces after the fix, keep the biggest one + a = 0 + ff = None + for f in pol.Faces: + if f.Area > a: + a = f.Area + ff = f + if ff: + pol = ff + else: + print("Unable to fix invalid no-fit polygon. Aborting") + Part.show(pol) + return + + if not pol.isValid(): + + # none of the fixes worked. Epic fail. + + print("Invalid no-fit polygon. Aborting") + Part.show(pol.OuterWire) + for p in sheet: + Part.show(p[1]) + Part.show(facecopy) + #for i,p in enumerate(faceverts): + # Draft.makeText([str(i)],point=p) + return + nofitpol.append(pol) + #Part.show(pol) + + # Union all the no-fit pols into one + + if len(nofitpol) == 1: + nofitpol = nofitpol[0] + elif len(nofitpol) > 1: + b = nofitpol.pop() + for n in nofitpol: + b = b.fuse(n) + nofitpol = b + + # remove internal edges (discard edges shared by 2 faces) + + lut = {} + for f in fitpol.Faces: + for e in f.Edges: + h = e.hashCode() + if h in lut: + lut[h].append(e) + else: + lut[h] = [e] + edges = [e[0] for e in lut.values() if len(e) == 1] + try: + pol = Part.Face(Part.Wire(edges)) + except: + # above method can fail sometimes. Try a slower method + w = DraftGeomUtils.findWires(edges) + if len(w) == 1: + if w[0].isClosed(): + try: + pol = Part.Face(w[0]) + except: + print("Error merging polygons. Aborting") + try: + Part.show(Part.Wire(edges)) + except: + for e in edges: + Part.show(e) + return + + # subtract the no-fit polygon from the container's fit polygon + # we then have the zone where the face can be placed + + if nofitpol: + fitpol = binpol.cut(nofitpol) + else: + fitpol = binpol.copy() + + # check that we have some space on this sheet + + if (fitpol.Area > 0) and fitpol.Vertexes: + + # order the fitpol vertexes by smallest X + # and try to place the piece, making sure it doesn't + # intersect with already placed pieces + fitverts = sorted([v.Point for v in fitpol.Vertexes],key=lambda v: v.x) + for p in fitverts: + trface = rotface.copy() + trface.translate(p.sub(basepoint)) + ok = True + for placed in sheet: + if ok: + for vert in trface.Vertexes: + if placed[1].isInside(vert.Point,TOLERANCE,False): + ok = False + break + if ok: + for e1 in trface.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): + ok = False + break + if not ok: + break + if ok: + rotface = trface + break + else: + print("Couldn't determine location on sheet. Aborting") + return + + # check the X space occupied by this solution + + bb = rotface.BoundBox + for placed in sheet: + bb.add(placed[1].BoundBox) + available.append([sheetnumber,[hashcode,rotface],bb.XMax,fitpol]) + + if unfit: + print("One face doesn't fit in the container. Aborting") + return + + if available: + + # order by smallest X size and take the first one + available = sorted(available,key=lambda sol: sol[2]) + print("Adding piece to sheet",available[0][0]+1) + sheets[available[0][0]].append(available[0][1]) + #Part.show(available[0][3]) + + else: + + # adding to the leftmost vertex of the binpol + + sheet = [] + print("Creating new sheet, adding piece to sheet",len(sheets)) + # order initial positions by smallest X size + initials = sorted(initials,key=lambda sol: sol[1][1].BoundBox.XLength) + hashcode = initials[0][1][0] + face = initials[0][1][1] + # order binpol vertexes by X coord + verts = sorted([v.Point for v in initials[0][0].Vertexes],key=lambda v: v.x) + face.translate(verts[0].sub(initials[0][2])) + sheet.append([hashcode,face]) + sheets.append(sheet) + + facenumber += 1 + + print("Run time:",datetime.now()-starttime) + self.results.append(sheets) + return sheets + + + def order(self,face,right=False): + + """order(face,[right]): returns a list of vertices + ordered clockwise. The first vertex will be the + lefmost one, unless right is True, in which case the + first vertex will be the rightmost one""" + + verts = [v.Point for v in face.OuterWire.OrderedVertexes] + + # flatten the polygon on the XY plane + + wp = WorkingPlane.plane() + wp.alignToPointAndAxis(face.CenterOfMass,face.normalAt(0,0)) + pverts = [] + for v in verts: + vx = DraftVecUtils.project(v,wp.u) + lx = vx.Length + if vx.getAngle(wp.u) > 1: + lx = -lx + vy = DraftVecUtils.project(v,wp.v) + ly = vy.Length + if vy.getAngle(wp.v) > 1: + ly = -ly + pverts.append(FreeCAD.Vector(lx,ly,0)) + pverts.append(pverts[0]) + + # https://stackoverflow.com/questions/1165647/how-to-determine-if-a-list-of-polygon-points-are-in-clockwise-order + + s = 0 + for i in range(len(pverts)-1): + s += (pverts[i+1].x-pverts[i].x)*(pverts[i+1].y+pverts[i].y) + if s < 0: + verts.reverse() + elif s == 0: + print("error computing winding direction") + return + + return verts + + + def show(self,result=None): + + """show([result]): creates shapes in the document, showing + the given result (list of sheets) or the last result if + none is provided""" + + if not result: + result = [] + if self.results: + result = self.results[-1] + offset = FreeCAD.Vector(0,0,0) + 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) + offset = offset.add(FreeCAD.Vector(1.1*self.container.BoundBox.XLength,0,0)) + + +def test(): + + + "runs a test with selected shapes, container selected last" + + import FreeCADGui + sel = FreeCADGui.Selection.getSelection() + if sel: + container = sel.pop().Shape + shapes = [o.Shape for o in sel] + n = Nester(container,shapes) + result = n.run() + if result: + n.show() diff --git a/src/Mod/Arch/ArchPanel.py b/src/Mod/Arch/ArchPanel.py index 61f72d46ed..d8effb24f0 100644 --- a/src/Mod/Arch/ArchPanel.py +++ b/src/Mod/Arch/ArchPanel.py @@ -21,7 +21,7 @@ #* * #*************************************************************************** -import FreeCAD,Draft,ArchComponent,DraftVecUtils,ArchCommands,math, Part +import FreeCAD,Draft,ArchComponent,DraftVecUtils,ArchCommands,math, Part, ArchNesting from FreeCAD import Vector if FreeCAD.GuiUp: import FreeCADGui @@ -52,13 +52,14 @@ __url__ = "http://www.freecadweb.org" # Description l w t Presets = [None, - ["Plywood 12mm, 1220 x 2440",1200,2400,12], - ["Plywood 15mm, 1220 x 2440",1200,2400,15], - ["Plywood 18mm, 1220 x 2440",1200,2400,18], - ["Plywood 25mm, 1220 x 2440",1200,2400,25], + ["Plywood 12mm, 1220 x 2440",1220,2440,12], + ["Plywood 15mm, 1220 x 2440",1220,2440,15], + ["Plywood 18mm, 1220 x 2440",1220,2440,18], + ["Plywood 25mm, 1220 x 2440",1220,2440,25], ["MDF 3mm, 900 x 600", 900, 600, 3], ["MDF 6mm, 900 x 600", 900, 600, 6], - ["OSB 18mm, 1200 x 2400", 1200,2400,18]] + ["OSB 18mm, 1220 x 2440", 1220,2440,18], + ] def makePanel(baseobj=None,length=0,width=0,thickness=0,placement=None,name="Panel"): '''makePanel([obj],[length],[width],[thickness],[placement]): creates a @@ -344,7 +345,6 @@ class CommandPanelSheet: FreeCAD.ActiveDocument.recompute() - class _Panel(ArchComponent.Component): "The Panel object" def __init__(self,obj): @@ -735,6 +735,7 @@ class PanelCut(Draft._DraftObject): obj.addProperty("App::PropertyAngle","TagRotation","Arch",QT_TRANSLATE_NOOP("App::Property","The rotation of the tag text")) obj.addProperty("App::PropertyFile","FontFile","Arch",QT_TRANSLATE_NOOP("App::Property","The font of the tag text")) 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::PropertyFloatList","AllowedAngles","Arch",QT_TRANSLATE_NOOP("App::Property","The allowed angles this object can be rotated to when placed on sheets")) obj.Proxy = self self.Type = "PanelCut" obj.TagText = "%tag%" @@ -1210,12 +1211,110 @@ class SheetTaskPanel(ArchComponent.ComponentTaskPanel): FreeCADGui.runCommand("Draft_Edit") +class CommandNest: + + + "the Arch Panel command definition" + def GetResources(self): + return {'Pixmap' : 'Arch_Nest', + 'MenuText': QT_TRANSLATE_NOOP("Arch_Nest","Nest"), + 'Accel': "N, E", + 'ToolTip': QT_TRANSLATE_NOOP("Arch_Nest","Nests a series of selected shapes in a container")} + + def IsActive(self): + return not FreeCAD.ActiveDocument is None + + def Activated(self): + FreeCADGui.Control.closeDialog() + FreeCADGui.Control.showDialog(NestTaskPanel()) + + +class NestTaskPanel: + + + '''The TaskPanel for Arch Nest command''' + + def __init__(self,obj=None): + self.form = FreeCADGui.PySideUic.loadUi(":/ui/ArchNest.ui") + self.form.progressBar.hide() + 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) + self.shapes = [] + self.container = None + self.nester = None + + def getStandardButtons(self): + return int(QtGui.QDialogButtonBox.Close) + + def reject(self): + self.stop() + return True + + def getContainer(self): + s = FreeCADGui.Selection.getSelection() + if len(s) == 1: + if s[0].isDerivedFrom("Part::Feature"): + if len(s[0].Shape.Faces) == 1: + if not (s[0] in self.shapes): + self.form.Container.clear() + self.addObject(s[0],self.form.Container) + self.container = s[0] + + def getShapes(self): + s = FreeCADGui.Selection.getSelection() + for o in s: + if o.isDerivedFrom("Part::Feature"): + if not o in self.shapes: + if o != self.container: + self.addObject(o,self.form.Shapes) + self.shapes.append(o) + + def addObject(self,obj,form): + i = QtGui.QListWidgetItem() + i.setText(obj.Label) + i.setToolTip(obj.Name) + if hasattr(obj.ViewObject,"Proxy"): + i.setIcon(QtGui.QIcon(obj.ViewObject.Proxy.getIcon())) + else: + i.setIcon(QtGui.QIcon(":/icons/Tree_Part.svg")) + form.addItem(i) + + def removeShapes(self): + for i in self.form.Shapes.selectedItems(): + o = FreeCAD.ActiveDocument.getObject(i.toolTip()) + if o: + if o in self.shapes: + self.shapes.remove(o) + self.form.Shapes.takeItem(self.form.Shapes.row(i)) + + def start(self): + self.form.progressBar.setValue(1) + self.form.progressBar.show() + tolerance = self.form.Tolerance.value() + discretize = self.form.Subdivisions.value() + rotations = [float(x) for x in self.form.Rotations.text().split(",")] + import ArchNesting + 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() + if result: + n.show() + + def stop(self): + pass + + if FreeCAD.GuiUp: class CommandPanelGroup: def GetCommands(self): - return tuple(['Arch_Panel','Arch_Panel_Cut','Arch_Panel_Sheet']) + return tuple(['Arch_Panel','Arch_Panel_Cut','Arch_Panel_Sheet','Arch_Nest']) def GetResources(self): return { 'MenuText': QT_TRANSLATE_NOOP("Arch_PanelTools",'Panel tools'), 'ToolTip': QT_TRANSLATE_NOOP("Arch_PanelTools",'Panel tools') @@ -1226,4 +1325,5 @@ if FreeCAD.GuiUp: FreeCADGui.addCommand('Arch_Panel',CommandPanel()) FreeCADGui.addCommand('Arch_Panel_Cut',CommandPanelCut()) FreeCADGui.addCommand('Arch_Panel_Sheet',CommandPanelSheet()) + FreeCADGui.addCommand('Arch_Nest',CommandNest()) FreeCADGui.addCommand('Arch_PanelTools', CommandPanelGroup()) diff --git a/src/Mod/Arch/CMakeLists.txt b/src/Mod/Arch/CMakeLists.txt index ad2b1e4a09..f0ecc73ef1 100644 --- a/src/Mod/Arch/CMakeLists.txt +++ b/src/Mod/Arch/CMakeLists.txt @@ -38,6 +38,7 @@ SET(Arch_SRCS ArchPrecast.py importSH3D.py ArchPipe.py + ArchNesting.py ) SET(Dice3DS_SRCS diff --git a/src/Mod/Arch/Resources/Arch.qrc b/src/Mod/Arch/Resources/Arch.qrc index 85ec1628c5..c5b5e36eb5 100644 --- a/src/Mod/Arch/Resources/Arch.qrc +++ b/src/Mod/Arch/Resources/Arch.qrc @@ -79,6 +79,7 @@ icons/Arch_Pipe_Tree.svg icons/Arch_PipeConnector.svg icons/Arch_ToggleSubs.svg + icons/Arch_Nest.svg ui/ParametersWindowDouble.svg ui/ParametersWindowSimple.svg ui/ParametersWindowFixed.svg @@ -96,6 +97,7 @@ ui/GitTaskPanel.ui ui/DialogBimServerLogin.ui ui/DialogDisplayText.ui + ui/ArchNest.ui translations/Arch_af.qm translations/Arch_de.qm translations/Arch_fi.qm diff --git a/src/Mod/Arch/Resources/icons/Arch_Nest.svg b/src/Mod/Arch/Resources/icons/Arch_Nest.svg new file mode 100644 index 0000000000..927c49ec24 --- /dev/null +++ b/src/Mod/Arch/Resources/icons/Arch_Nest.svg @@ -0,0 +1,680 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + [Yorik van Havre] + + + Arch_Panel_Sheet + 2016-12-17 + http://www.freecadweb.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/Mod/Arch/Resources/icons/Arch_Panel_Sheet.svg + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html + + + [agryson] Alexander Gryson + + + + + + + + + + + + + + + + + diff --git a/src/Mod/Arch/Resources/ui/ArchNest.ui b/src/Mod/Arch/Resources/ui/ArchNest.ui new file mode 100644 index 0000000000..97d7b29642 --- /dev/null +++ b/src/Mod/Arch/Resources/ui/ArchNest.ui @@ -0,0 +1,183 @@ + + + Form + + + + 0 + 0 + 266 + 475 + + + + Nesting + + + + + + Container + + + + + + + 16777215 + 24 + + + + + + + + Pick selected + + + + + + + + + + Shapes + + + + + + + + + + + Add selected + + + + + + + Remove + + + + + + + + + + + + Nesting parameters + + + + + + Rotations + + + + + + + Tolerance + + + + + + + Arcs subdivisions + + + + + + + Closer than this, two points are considered equal + + + 8 + + + 0.000100000000000 + + + + + + + The number of segments to divide non-linear edges into, for calculations. If curved shapes overlap, try raising this value + + + + + + 1 + + + 4 + + + + + + + A comma-separated list of angles to try and rotate the shapes + + + 0,90,180,270 + + + + + + + + + + Nesting operation + + + + + + 1 + + + pass %p + + + + + + + + + Start + + + + + + + Stop + + + + + + + + + + + + +