Arch: Initial Nest command
This commit is contained in:
534
src/Mod/Arch/ArchNesting.py
Normal file
534
src/Mod/Arch/ArchNesting.py
Normal 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()
|
||||
@@ -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())
|
||||
|
||||
@@ -38,6 +38,7 @@ SET(Arch_SRCS
|
||||
ArchPrecast.py
|
||||
importSH3D.py
|
||||
ArchPipe.py
|
||||
ArchNesting.py
|
||||
)
|
||||
|
||||
SET(Dice3DS_SRCS
|
||||
|
||||
@@ -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>
|
||||
|
||||
680
src/Mod/Arch/Resources/icons/Arch_Nest.svg
Normal file
680
src/Mod/Arch/Resources/icons/Arch_Nest.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 25 KiB |
183
src/Mod/Arch/Resources/ui/ArchNest.ui
Normal file
183
src/Mod/Arch/Resources/ui/ArchNest.ui
Normal 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>
|
||||
Reference in New Issue
Block a user