Arch: Initial Nest command

This commit is contained in:
Yorik van Havre
2017-07-10 19:16:10 -03:00
parent a8faef009b
commit a3d7881746
6 changed files with 1508 additions and 8 deletions

534
src/Mod/Arch/ArchNesting.py Normal file
View File

@@ -0,0 +1,534 @@
# -*- coding: utf-8 -*-
#***************************************************************************
#* *
#* Copyright (c) 2017 Yorik van Havre <yorik@uncreated.net> *
#* *
#* 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()

View File

@@ -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())

View File

@@ -38,6 +38,7 @@ SET(Arch_SRCS
ArchPrecast.py
importSH3D.py
ArchPipe.py
ArchNesting.py
)
SET(Dice3DS_SRCS

View File

@@ -79,6 +79,7 @@
<file>icons/Arch_Pipe_Tree.svg</file>
<file>icons/Arch_PipeConnector.svg</file>
<file>icons/Arch_ToggleSubs.svg</file>
<file>icons/Arch_Nest.svg</file>
<file>ui/ParametersWindowDouble.svg</file>
<file>ui/ParametersWindowSimple.svg</file>
<file>ui/ParametersWindowFixed.svg</file>
@@ -96,6 +97,7 @@
<file>ui/GitTaskPanel.ui</file>
<file>ui/DialogBimServerLogin.ui</file>
<file>ui/DialogDisplayText.ui</file>
<file>ui/ArchNest.ui</file>
<file>translations/Arch_af.qm</file>
<file>translations/Arch_de.qm</file>
<file>translations/Arch_fi.qm</file>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,183 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>266</width>
<height>475</height>
</rect>
</property>
<property name="windowTitle">
<string>Nesting</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Container</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QListWidget" name="Container">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>24</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="ButtonContainer">
<property name="text">
<string>Pick selected</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Shapes</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QListWidget" name="Shapes"/>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="ButtonShapes">
<property name="text">
<string>Add selected</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="ButtonRemove">
<property name="text">
<string>Remove</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Nesting parameters</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Rotations</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Tolerance</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Arcs subdivisions</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QDoubleSpinBox" name="Tolerance">
<property name="toolTip">
<string>Closer than this, two points are considered equal</string>
</property>
<property name="decimals">
<number>8</number>
</property>
<property name="value">
<double>0.000100000000000</double>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="Subdivisions">
<property name="toolTip">
<string>The number of segments to divide non-linear edges into, for calculations. If curved shapes overlap, try raising this value</string>
</property>
<property name="statusTip">
<string/>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="value">
<number>4</number>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="Rotations">
<property name="toolTip">
<string>A comma-separated list of angles to try and rotate the shapes</string>
</property>
<property name="text">
<string>0,90,180,270</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_4">
<property name="title">
<string>Nesting operation</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>1</number>
</property>
<property name="format">
<string>pass %p</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QPushButton" name="ButtonStart">
<property name="text">
<string>Start</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="ButtonStop">
<property name="text">
<string>Stop</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>