def mergeShapes(w1,w2): "returns a Shape built on two walls that share same properties and have a coincident endpoint" if not areSameWallTypes([w1,w2]): return None if (not hasattr(w1.Base,"Shape")) or (not hasattr(w2.Base,"Shape")): return None if w1.Base.Shape.Faces or w2.Base.Shape.Faces: return None # TODO fix this return None eds = w1.Base.Shape.Edges + w2.Base.Shape.Edges import DraftGeomUtils w = DraftGeomUtils.findWires(eds) if len(w) == 1: #print("found common wire") normal,length,width,height = w1.Proxy.getDefaultValues(w1) print(w[0].Edges) sh = w1.Proxy.getBase(w1,w[0],normal,width,height) print(sh) return sh return None
def mergeShapes(w1,w2): "returns a Shape built on two walls that share same properties and have a coincident endpoint" if not areSameWallTypes([w1,w2]): return None if (not hasattr(w1.Base,"Shape")) or (not hasattr(w2.Base,"Shape")): return None if w1.Base.Shape.Faces or w2.Base.Shape.Faces: return None # TODO fix this return None eds = w1.Base.Shape.Edges + w2.Base.Shape.Edges import DraftGeomUtils w = DraftGeomUtils.findWires(eds) if len(w) == 1: #print("found common wire") normal,length,width,height = w1.Proxy.getDefaultValues(w1) print(w[0].Edges) sh = w1.Proxy.getBase(w1,w[0],normal,width,height) print(sh) return sh return None
def Activated(self): import FreeCADGui import Part import DraftGeomUtils objs = FreeCADGui.Selection.getSelection() names = [] edges = [] for obj in objs: if hasattr(obj, "Shape") and hasattr(obj.Shape, "Edges") and obj.Shape.Edges: edges.extend(obj.Shape.Edges) names.append(obj.Name) wires = DraftGeomUtils.findWires(edges) FreeCAD.ActiveDocument.openTransaction("Rewire") selectlist = [] for wire in wires: if DraftGeomUtils.hasCurves(wire): nobj = FreeCAD.ActiveDocument.addObject( "Part::Feature", "Wire") nobj.shape = wire selectlist.append(nobj) else: selectlist.append( Draft.makeWire([v.Point for v in wire.OrderedVertexes])) for name in names: FreeCAD.ActiveDocument.removeObject(name) FreeCAD.ActiveDocument.commitTransaction() FreeCADGui.Selection.clearSelection() for obj in selectlist: FreeCADGui.Selection.addSelection(obj) FreeCAD.ActiveDocument.recompute()
def getCutShapes(objs, section, showHidden): import Part, DraftGeomUtils shapes = [] hshapes = [] sshapes = [] for o in objs: if o.isDerivedFrom("Part::Feature"): if o.Shape.isNull(): pass elif section.OnlySolids: if o.Shape.isValid(): shapes.extend(o.Shape.Solids) else: print(section.Label, ": Skipping invalid object:", o.Label) else: shapes.append(o.Shape) cutface, cutvolume, invcutvolume = ArchCommands.getCutVolume( section.Shape.copy(), shapes) if cutvolume: nsh = [] for sh in shapes: for sol in sh.Solids: if sol.Volume < 0: sol.reverse() c = sol.cut(cutvolume) s = sol.section(cutface) try: wires = DraftGeomUtils.findWires(s.Edges) for w in wires: f = Part.Face(w) sshapes.append(f) #s = Part.Wire(s.Edges) #s = Part.Face(s) except Part.OCCError: #print "ArchDrawingView: unable to get a face" sshapes.append(s) nsh.extend(c.Solids) #sshapes.append(s) if showHidden: c = sol.cut(invcutvolume) hshapes.append(c) shapes = nsh return shapes, hshapes, sshapes, cutface, cutvolume, invcutvolume
def getCutShapes(objs,section,showHidden): import Part,DraftGeomUtils shapes = [] hshapes = [] sshapes = [] for o in objs: if o.isDerivedFrom("Part::Feature"): if o.Shape.isNull(): pass elif section.OnlySolids: if o.Shape.isValid(): shapes.extend(o.Shape.Solids) else: print(section.Label,": Skipping invalid object:",o.Label) else: shapes.append(o.Shape) cutface,cutvolume,invcutvolume = ArchCommands.getCutVolume(section.Shape.copy(),shapes) if cutvolume: nsh = [] for sh in shapes: for sol in sh.Solids: if sol.Volume < 0: sol.reverse() c = sol.cut(cutvolume) s = sol.section(cutface) try: wires = DraftGeomUtils.findWires(s.Edges) for w in wires: f = Part.Face(w) sshapes.append(f) #s = Part.Wire(s.Edges) #s = Part.Face(s) except Part.OCCError: #print "ArchDrawingView: unable to get a face" sshapes.append(s) nsh.extend(c.Solids) #sshapes.append(s) if showHidden: c = sol.cut(invcutvolume) hshapes.append(c) shapes = nsh return shapes,hshapes,sshapes,cutface,cutvolume,invcutvolume
#* 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 * #* * #*************************************************************************** ''' This macro finds the outside profile of a 3D shape. Right now it just creates wires that represent the shape,but it could be used for finding the outside profile of an object for a toolpath ''' import FreeCADGui import DraftGeomUtils from FreeCAD import Vector from PathScripts import find_outer_profile as fop sel = FreeCADGui.Selection.getSelection()[0] obj = sel el = fop.edgelist(obj) hl = fop.horizontal(el) connected = DraftGeomUtils.findWires(hl) goodwires = fop.openFilter(connected) outerwires ,innerwires, same = fop.findOutsideWire(goodwires) #get distance from outerwires Z to bottom of part zdiff = obj.Shape.BoundBox.ZMin- outerwires.BoundBox.ZMax outerwires.Placement.move(Vector(0,0,zdiff)) Part.show(outerwires) zupperouter = outerwires zupperouter.Placement.move(Vector(0,0,obj.Shape.BoundBox.ZMax)) Part.show(zupperouter)
def onChanged(self, obj, prop): if prop in ["Source","RenderingMode","ShowCut"]: import Part, DraftGeomUtils if hasattr(obj,"Source"): if obj.Source: if obj.Source.Objects: objs = Draft.getGroupContents(obj.Source.Objects,walls=True) objs = Draft.removeHidden(objs) # separate spaces self.spaces = [] os = [] for o in objs: if Draft.getType(o) == "Space": self.spaces.append(o) else: os.append(o) objs = os self.svg = '' fillpattern = '<pattern id="sectionfill" patternUnits="userSpaceOnUse" patternTransform="matrix(5,0,0,5,0,0)"' fillpattern += ' x="0" y="0" width="10" height="10">' fillpattern += '<g>' fillpattern += '<rect width="10" height="10" style="stroke:none; fill:#ffffff" /><path style="stroke:#000000; stroke-width:1" d="M0,0 l10,10" /></g></pattern>' # generating SVG if obj.RenderingMode == "Solid": # render using the Arch Vector Renderer import ArchVRM, WorkingPlane wp = WorkingPlane.plane() wp.setFromPlacement(obj.Source.Placement) wp.inverse() render = ArchVRM.Renderer() render.setWorkingPlane(wp) render.addObjects(objs) if hasattr(obj,"ShowCut"): render.cut(obj.Source.Shape,obj.ShowCut) else: render.cut(obj.Source.Shape) self.svg += render.getViewSVG(linewidth="LWPlaceholder") self.svg += fillpattern self.svg += render.getSectionSVG(linewidth="SWPlaceholder",fillpattern="sectionfill") if hasattr(obj,"ShowCut"): if obj.ShowCut: self.svg += render.getHiddenSVG(linewidth="LWPlaceholder") # print render.info() else: # render using the Drawing module import Drawing, Part shapes = [] hshapes = [] sshapes = [] p = FreeCAD.Placement(obj.Source.Placement) self.direction = p.Rotation.multVec(FreeCAD.Vector(0,0,1)) for o in objs: if o.isDerivedFrom("Part::Feature"): if o.Shape.isNull(): pass #FreeCAD.Console.PrintWarning(translate("Arch","Skipping empty object: ")+o.Name) elif o.Shape.isValid(): if hasattr(obj.Source,"OnlySolids"): if obj.Source.OnlySolids: shapes.extend(o.Shape.Solids) else: shapes.append(o.Shape) else: shapes.extend(o.Shape.Solids) else: FreeCAD.Console.PrintWarning(translate("Arch","Skipping invalid object: ")+o.Name) cutface,cutvolume,invcutvolume = ArchCommands.getCutVolume(obj.Source.Shape.copy(),shapes) if cutvolume: nsh = [] for sh in shapes: for sol in sh.Solids: if sol.Volume < 0: sol.reverse() c = sol.cut(cutvolume) s = sol.section(cutface) try: wires = DraftGeomUtils.findWires(s.Edges) for w in wires: f = Part.Face(w) sshapes.append(f) #s = Part.Wire(s.Edges) #s = Part.Face(s) except Part.OCCError: #print "ArchDrawingView: unable to get a face" sshapes.append(s) nsh.extend(c.Solids) #sshapes.append(s) if hasattr(obj,"ShowCut"): if obj.ShowCut: c = sol.cut(invcutvolume) hshapes.append(c) shapes = nsh if shapes: self.shapes = shapes self.baseshape = Part.makeCompound(shapes) svgf = Drawing.projectToSVG(self.baseshape,self.direction) if svgf: svgf = svgf.replace('stroke-width="0.35"','stroke-width="LWPlaceholder"') svgf = svgf.replace('stroke-width="1"','stroke-width="LWPlaceholder"') svgf = svgf.replace('stroke-width:0.01','stroke-width:LWPlaceholder') self.svg += svgf if hshapes: hshapes = Part.makeCompound(hshapes) self.hiddenshape = hshapes svgh = Drawing.projectToSVG(hshapes,self.direction) if svgh: svgh = svgh.replace('stroke-width="0.35"','stroke-width="LWPlaceholder"') svgh = svgh.replace('stroke-width="1"','stroke-width="LWPlaceholder"') svgh = svgh.replace('stroke-width:0.01','stroke-width:LWPlaceholder') svgh = svgh.replace('fill="none"','fill="none"\nstroke-dasharray="DAPlaceholder"') self.svg += svgh if sshapes: svgs = "" if hasattr(obj,"ShowFill"): if obj.ShowFill: svgs += fillpattern svgs += '<g transform="rotate(180)">\n' for s in sshapes: if s.Edges: f = Draft.getSVG(s,direction=self.direction.negative(),linewidth=0,fillstyle="sectionfill",color=(0,0,0)) svgs += f svgs += "</g>\n" sshapes = Part.makeCompound(sshapes) self.sectionshape = sshapes svgs += Drawing.projectToSVG(sshapes,self.direction) if svgs: svgs = svgs.replace('stroke-width="0.35"','stroke-width="SWPlaceholder"') svgs = svgs.replace('stroke-width="1"','stroke-width="SWPlaceholder"') svgs = svgs.replace('stroke-width:0.01','stroke-width:SWPlaceholder') svgs = svgs.replace('stroke-width="0.35 px"','stroke-width="SWPlaceholder"') svgs = svgs.replace('stroke-width:0.35','stroke-width:SWPlaceholder') self.svg += svgs
def buildSVG(self, obj,join=False): "creates a svg representation" import Part, DraftGeomUtils if hasattr(obj,"Source"): if obj.Source: if obj.Source.Objects: objs = Draft.getGroupContents(obj.Source.Objects,walls=True) objs = Draft.removeHidden(objs) self.svg = '' # generating SVG if obj.RenderingMode == "Solid": # render using the Arch Vector Renderer import ArchVRM render = ArchVRM.Renderer() render.setWorkingPlane(obj.Source.Placement) render.addObjects(objs) if hasattr(obj,"ShowCut"): render.cut(obj.Source.Shape,obj.ShowCut) else: render.cut(obj.Source.Shape) self.svg += render.getViewSVG(linewidth="LWPlaceholder") self.svg += render.getSectionSVG(linewidth="SWPLaceholder") if hasattr(obj,"ShowCut"): if obj.ShowCut: self.svg += render.getHiddenSVG(linewidth="LWPlaceholder") # print render.info() else: # render using the Drawing module import Drawing, Part shapes = [] hshapes = [] sshapes = [] p = FreeCAD.Placement(obj.Source.Placement) self.direction = p.Rotation.multVec(FreeCAD.Vector(0,0,1)) for o in objs: if o.isDerivedFrom("Part::Feature"): if o.Shape.isNull(): pass #FreeCAD.Console.PrintWarning(translate("Arch","Skipping empty object: ")+o.Name) elif o.Shape.isValid(): shapes.extend(o.Shape.Solids) else: FreeCAD.Console.PrintWarning(translate("Arch","Skipping invalid object: ")+o.Name) cutface,cutvolume,invcutvolume = ArchCommands.getCutVolume(obj.Source.Shape.copy(),shapes) if cutvolume: nsh = [] for sh in shapes: for sol in sh.Solids: if sol.Volume < 0: sol.reverse() c = sol.cut(cutvolume) s = sol.section(cutface) nsh.extend(c.Solids) sshapes.append(s) if hasattr(obj,"ShowCut"): if obj.ShowCut: c = sol.cut(invcutvolume) hshapes.append(c) shapes = nsh if shapes: self.shapes = shapes self.baseshape = Part.makeCompound(shapes) svgf = Drawing.projectToSVG(self.baseshape,self.direction) if svgf: svgf = svgf.replace('stroke-width="0.35"','stroke-width="LWPlaceholder"') svgf = svgf.replace('stroke-width="1"','stroke-width="LWPlaceholder"') svgf = svgf.replace('stroke-width:0.01','stroke-width:LWPlaceholder') self.svg += svgf if hshapes: hshapes = Part.makeCompound(hshapes) svgh = Drawing.projectToSVG(hshapes,self.direction) if svgh: svgh = svgh.replace('stroke-width="0.35"','stroke-width="LWPlaceholder"') svgh = svgh.replace('stroke-width="1"','stroke-width="LWPlaceholder"') svgh = svgh.replace('stroke-width:0.01','stroke-width:LWPlaceholder') svgh = svgh.replace('fill="none"','fill="none"\nstroke-dasharray="0.09,0.05"') self.svg += svgh if sshapes: edges = [] for s in sshapes: edges.extend(s.Edges) wires = DraftGeomUtils.findWires(edges) faces = [] for w in wires: if (w.ShapeType == "Wire") and w.isClosed(): faces.append(Part.Face(w)) sshapes = Part.makeCompound(faces) svgs = Drawing.projectToSVG(sshapes,self.direction) if svgs: svgs = svgs.replace('stroke-width="0.35"','stroke-width="SWPlaceholder"') svgs = svgs.replace('stroke-width="1"','stroke-width="SWPlaceholder"') svgs = svgs.replace('stroke-width:0.01','stroke-width:SWPlaceholder') self.svg += svgs
def cut(self,cutplane): "Cuts through the shapes with a given cut plane and builds section faces" if self.iscut: return if not self.shapes: if DEBUG: print "No objects to make sections" else: fill = (1.0,1.0,1.0,1.0) placement = FreeCAD.Placement(cutplane.Placement) # building boundbox bb = self.shapes[0][0].BoundBox for sh in self.shapes[1:]: bb.add(sh[0].BoundBox) bb.enlarge(1) um = vm = wm = 0 if not bb.isCutPlane(placement.Base,self.wp.axis): if DEBUG: print "No objects are cut by the plane" else: corners = [FreeCAD.Vector(bb.XMin,bb.YMin,bb.ZMin), FreeCAD.Vector(bb.XMin,bb.YMax,bb.ZMin), FreeCAD.Vector(bb.XMax,bb.YMin,bb.ZMin), FreeCAD.Vector(bb.XMax,bb.YMax,bb.ZMin), FreeCAD.Vector(bb.XMin,bb.YMin,bb.ZMax), FreeCAD.Vector(bb.XMin,bb.YMax,bb.ZMax), FreeCAD.Vector(bb.XMax,bb.YMin,bb.ZMax), FreeCAD.Vector(bb.XMax,bb.YMax,bb.ZMax)] for c in corners: dv = c.sub(placement.Base) um1 = DraftVecUtils.project(dv,self.wp.u).Length um = max(um,um1) vm1 = DraftVecUtils.project(dv,self.wp.v).Length vm = max(vm,vm1) wm1 = DraftVecUtils.project(dv,self.wp.axis).Length wm = max(wm,wm1) p1 = FreeCAD.Vector(-um,vm,0) p2 = FreeCAD.Vector(um,vm,0) p3 = FreeCAD.Vector(um,-vm,0) p4 = FreeCAD.Vector(-um,-vm,0) cutface = Part.makePolygon([p1,p2,p3,p4,p1]) cutface = Part.Face(cutface) cutface.Placement = placement cutnormal = DraftVecUtils.scaleTo(self.wp.axis,wm) cutvolume = cutface.extrude(cutnormal) shapes = [] faces = [] sections = [] for sh in self.shapes: for sol in sh[0].Solids: c = sol.cut(cutvolume) shapes.append([c]+sh[1:]) for f in c.Faces: faces.append([f]+sh[1:]) sec = sol.section(cutface) if sec.Edges: wires = DraftGeomUtils.findWires(sec.Edges) for w in wires: sec = Part.Face(w) sections.append([sec,fill]) self.shapes = shapes self.faces = faces self.sections = sections if DEBUG: print "Built ",len(self.sections)," sections, ", len(self.faces), " faces retained" self.iscut = True self.oriented = False self.trimmed = False self.sorted = False self.joined = False
def getCutShapes(objs,section,showHidden,groupSshapesByObject=False): import Part,DraftGeomUtils shapes = [] hshapes = [] sshapes = [] objectShapes = [] objectSshapes = [] for o in objs: if o.isDerivedFrom("Part::Feature"): if o.Shape.isNull(): pass elif section.OnlySolids: if o.Shape.isValid(): solids = [] solids.extend(o.Shape.Solids) shapes.extend(solids) objectShapes.append((o, solids)) else: print(section.Label,": Skipping invalid object:",o.Label) else: shapes.append(o.Shape) objectShapes.append((o, [o.Shape])) clip = False if hasattr(section, "Clip"): clip = section.Clip cutface,cutvolume,invcutvolume = ArchCommands.getCutVolume(section.Shape.copy(),shapes,clip) shapes =[] if cutvolume: for o, shapeList in objectShapes: tmpSshapes = [] for sh in shapeList: for sol in sh.Solids: if sol.Volume < 0: sol.reverse() c = sol.cut(cutvolume) s = sol.section(cutface) try: wires = DraftGeomUtils.findWires(s.Edges) for w in wires: f = Part.Face(w) tmpSshapes.append(f) #s = Part.Wire(s.Edges) #s = Part.Face(s) except Part.OCCError: #print "ArchDrawingView: unable to get a face" tmpSshapes.append(s) shapes.extend(c.Solids) #sshapes.append(s) if showHidden: c = sol.cut(invcutvolume) hshapes.append(c) if len(tmpSshapes) > 0: sshapes.extend(tmpSshapes) if groupSshapesByObject: objectSshapes.append((o, tmpSshapes)) if groupSshapesByObject: return shapes,hshapes,sshapes,cutface,cutvolume,invcutvolume,objectSshapes else: return shapes,hshapes,sshapes,cutface,cutvolume,invcutvolume
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.""" # reset abort mechanism and variables self.running = True self.progress = 0 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 not self.update(): return 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 # 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] # 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: if not self.update(): return 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: if not self.update(): return self.progress += step 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 if not self.update(): return nofitpol = [] for placed in sheet: pts = [] pi = 0 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)) # 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 if outside: for e1 in facecopy.OuterWire.Edges: for e2 in placed[1].OuterWire.Edges: 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]) #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): 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: 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 if pol.isValid(): 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: if not self.update(): return 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: if not self.update(): return 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 getSVG(section,allOn=False,renderMode="Wireframe",showHidden=False,showFill=False,scale=1,linewidth=1,fontsize=1): """getSVG(section,[allOn,renderMode,showHidden,showFill,scale,linewidth,fontsize]) : returns an SVG fragment from an Arch section plane. If allOn is True, all cut objects are shown, regardless if they are visible or not. renderMode can be Wireframe (default) or Solid to use the Arch solid renderer. If showHidden is True, the hidden geometry above the section plane is shown in dashed line. If showFill is True, the cut areas get filled with a pattern""" if not section.Objects: return import DraftGeomUtils p = FreeCAD.Placement(section.Placement) direction = p.Rotation.multVec(FreeCAD.Vector(0,0,1)) objs = Draft.getGroupContents(section.Objects,walls=True,addgroups=True) if not allOn: objs = Draft.removeHidden(objs) # separate spaces spaces = [] nonspaces = [] for o in objs: if Draft.getType(o) == "Space": spaces.append(o) else: nonspaces.append(o) objs = nonspaces svg = '' fillpattern = '<pattern id="sectionfill" patternUnits="userSpaceOnUse" patternTransform="matrix(5,0,0,5,0,0)"' fillpattern += ' x="0" y="0" width="10" height="10">' fillpattern += '<g>' fillpattern += '<rect width="10" height="10" style="stroke:none; fill:#ffffff" /><path style="stroke:#000000; stroke-width:1" d="M0,0 l10,10" /></g></pattern>' # generating SVG if renderMode == "Solid": # render using the Arch Vector Renderer import ArchVRM, WorkingPlane wp = WorkingPlane.plane() wp.setFromPlacement(section.Placement) #wp.inverse() render = ArchVRM.Renderer() render.setWorkingPlane(wp) render.addObjects(objs) if showHidden: render.cut(section.Shape,showHidden) else: render.cut(section.Shape) svg += '<g transform="scale(1,-1)">\n' svg += render.getViewSVG(linewidth="LWPlaceholder") svg += fillpattern svg += render.getSectionSVG(linewidth="SWPlaceholder",fillpattern="sectionfill") if showHidden: svg += render.getHiddenSVG(linewidth="LWPlaceholder") svg += '</g>\n' # print render.info() else: # render using the Drawing module import Drawing, Part shapes = [] hshapes = [] sshapes = [] for o in objs: if o.isDerivedFrom("Part::Feature"): if o.Shape.isNull(): pass elif o.Shape.isValid(): if section.OnlySolids: shapes.extend(o.Shape.Solids) else: shapes.append(o.Shape) else: print section.Label,": Skipping invalid object:",o.Label cutface,cutvolume,invcutvolume = ArchCommands.getCutVolume(section.Shape.copy(),shapes) if cutvolume: nsh = [] for sh in shapes: for sol in sh.Solids: if sol.Volume < 0: sol.reverse() c = sol.cut(cutvolume) s = sol.section(cutface) try: wires = DraftGeomUtils.findWires(s.Edges) for w in wires: f = Part.Face(w) sshapes.append(f) #s = Part.Wire(s.Edges) #s = Part.Face(s) except Part.OCCError: #print "ArchDrawingView: unable to get a face" sshapes.append(s) nsh.extend(c.Solids) #sshapes.append(s) if showHidden: c = sol.cut(invcutvolume) hshapes.append(c) shapes = nsh if shapes: baseshape = Part.makeCompound(shapes) svgf = Drawing.projectToSVG(baseshape,direction) if svgf: svgf = svgf.replace('stroke-width="0.35"','stroke-width="LWPlaceholder"') svgf = svgf.replace('stroke-width="1"','stroke-width="LWPlaceholder"') svgf = svgf.replace('stroke-width:0.01','stroke-width:LWPlaceholder') svg += svgf if hshapes: hshapes = Part.makeCompound(hshapes) svgh = Drawing.projectToSVG(hshapes,direction) if svgh: svgh = svgh.replace('stroke-width="0.35"','stroke-width="LWPlaceholder"') svgh = svgh.replace('stroke-width="1"','stroke-width="LWPlaceholder"') svgh = svgh.replace('stroke-width:0.01','stroke-width:LWPlaceholder') svgh = svgh.replace('fill="none"','fill="none"\nstroke-dasharray="DAPlaceholder"') svg += svgh if sshapes: svgs = "" if showFill: svgs += fillpattern svgs += '<g transform="rotate(180)">\n' for s in sshapes: if s.Edges: f = Draft.getSVG(s,direction=direction.negative(),linewidth=0,fillstyle="sectionfill",color=(0,0,0)) svgs += f svgs += "</g>\n" sshapes = Part.makeCompound(sshapes) svgs += Drawing.projectToSVG(sshapes,direction) if svgs: svgs = svgs.replace('stroke-width="0.35"','stroke-width="SWPlaceholder"') svgs = svgs.replace('stroke-width="1"','stroke-width="SWPlaceholder"') svgs = svgs.replace('stroke-width:0.01','stroke-width:SWPlaceholder') svgs = svgs.replace('stroke-width="0.35 px"','stroke-width="SWPlaceholder"') svgs = svgs.replace('stroke-width:0.35','stroke-width:SWPlaceholder') svg += svgs linewidth = linewidth/scale st = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Arch").GetFloat("CutLineThickness",2) da = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Arch").GetString("archHiddenPattern","30,10") da = da.replace(" ","") svg = svg.replace('LWPlaceholder', str(linewidth) + 'px') svg = svg.replace('SWPlaceholder', str(linewidth*st) + 'px') svg = svg.replace('DAPlaceholder', str(da)) if spaces and round(direction.getAngle(FreeCAD.Vector(0,0,1)),Draft.precision()) in [0,round(math.pi,Draft.precision())]: svg += '<g transform="scale(1,-1)">' for s in spaces: svg += Draft.getSVG(s,scale=scale,fontsize=fontsize,direction=direction) svg += '</g>' # print "complete node:",svg return svg
def onChanged(self, obj, prop): if prop in ["Source", "RenderingMode", "ShowCut"]: import Part, DraftGeomUtils if hasattr(obj, "Source"): if obj.Source: if obj.Source.Objects: objs = Draft.getGroupContents(obj.Source.Objects, walls=True, addgroups=True) objs = Draft.removeHidden(objs) # separate spaces self.spaces = [] os = [] for o in objs: if Draft.getType(o) == "Space": self.spaces.append(o) else: os.append(o) objs = os self.svg = '' fillpattern = '<pattern id="sectionfill" patternUnits="userSpaceOnUse" patternTransform="matrix(5,0,0,5,0,0)"' fillpattern += ' x="0" y="0" width="10" height="10">' fillpattern += '<g>' fillpattern += '<rect width="10" height="10" style="stroke:none; fill:#ffffff" /><path style="stroke:#000000; stroke-width:1" d="M0,0 l10,10" /></g></pattern>' # generating SVG if obj.RenderingMode == "Solid": # render using the Arch Vector Renderer import ArchVRM, WorkingPlane wp = WorkingPlane.plane() wp.setFromPlacement(obj.Source.Placement) #wp.inverse() render = ArchVRM.Renderer() render.setWorkingPlane(wp) render.addObjects(objs) if hasattr(obj, "ShowCut"): render.cut(obj.Source.Shape, obj.ShowCut) else: render.cut(obj.Source.Shape) self.svg += '<g transform="scale(1,-1)">\n' self.svg += render.getViewSVG( linewidth="LWPlaceholder") self.svg += fillpattern self.svg += render.getSectionSVG( linewidth="SWPlaceholder", fillpattern="sectionfill") if hasattr(obj, "ShowCut"): if obj.ShowCut: self.svg += render.getHiddenSVG( linewidth="LWPlaceholder") self.svg += '</g>\n' # print render.info() else: # render using the Drawing module import Drawing, Part shapes = [] hshapes = [] sshapes = [] p = FreeCAD.Placement(obj.Source.Placement) self.direction = p.Rotation.multVec( FreeCAD.Vector(0, 0, 1)) for o in objs: if o.isDerivedFrom("Part::Feature"): if o.Shape.isNull(): pass #FreeCAD.Console.PrintWarning(translate("Arch","Skipping empty object: ")+o.Name) elif o.Shape.isValid(): if hasattr(obj.Source, "OnlySolids"): if obj.Source.OnlySolids: shapes.extend(o.Shape.Solids) else: shapes.append(o.Shape) else: shapes.extend(o.Shape.Solids) else: FreeCAD.Console.PrintWarning( translate( "Arch", "Skipping invalid object: ") + o.Name) cutface, cutvolume, invcutvolume = ArchCommands.getCutVolume( obj.Source.Shape.copy(), shapes) if cutvolume: nsh = [] for sh in shapes: for sol in sh.Solids: if sol.Volume < 0: sol.reverse() c = sol.cut(cutvolume) s = sol.section(cutface) try: wires = DraftGeomUtils.findWires( s.Edges) for w in wires: f = Part.Face(w) sshapes.append(f) #s = Part.Wire(s.Edges) #s = Part.Face(s) except Part.OCCError: #print "ArchDrawingView: unable to get a face" sshapes.append(s) nsh.extend(c.Solids) #sshapes.append(s) if hasattr(obj, "ShowCut"): if obj.ShowCut: c = sol.cut(invcutvolume) hshapes.append(c) shapes = nsh if shapes: self.shapes = shapes self.baseshape = Part.makeCompound(shapes) svgf = Drawing.projectToSVG( self.baseshape, self.direction) if svgf: svgf = svgf.replace( 'stroke-width="0.35"', 'stroke-width="LWPlaceholder"') svgf = svgf.replace( 'stroke-width="1"', 'stroke-width="LWPlaceholder"') svgf = svgf.replace( 'stroke-width:0.01', 'stroke-width:LWPlaceholder') self.svg += svgf if hshapes: hshapes = Part.makeCompound(hshapes) self.hiddenshape = hshapes svgh = Drawing.projectToSVG( hshapes, self.direction) if svgh: svgh = svgh.replace( 'stroke-width="0.35"', 'stroke-width="LWPlaceholder"') svgh = svgh.replace( 'stroke-width="1"', 'stroke-width="LWPlaceholder"') svgh = svgh.replace( 'stroke-width:0.01', 'stroke-width:LWPlaceholder') svgh = svgh.replace( 'fill="none"', 'fill="none"\nstroke-dasharray="DAPlaceholder"' ) self.svg += svgh if sshapes: svgs = "" if hasattr(obj, "ShowFill"): if obj.ShowFill: svgs += fillpattern svgs += '<g transform="rotate(180)">\n' for s in sshapes: if s.Edges: f = Draft.getSVG( s, direction=self.direction. negative(), linewidth=0, fillstyle="sectionfill", color=(0, 0, 0)) svgs += f svgs += "</g>\n" sshapes = Part.makeCompound(sshapes) self.sectionshape = sshapes svgs += Drawing.projectToSVG( sshapes, self.direction) if svgs: svgs = svgs.replace( 'stroke-width="0.35"', 'stroke-width="SWPlaceholder"') svgs = svgs.replace( 'stroke-width="1"', 'stroke-width="SWPlaceholder"') svgs = svgs.replace( 'stroke-width:0.01', 'stroke-width:SWPlaceholder') svgs = svgs.replace( 'stroke-width="0.35 px"', 'stroke-width="SWPlaceholder"') svgs = svgs.replace( 'stroke-width:0.35', 'stroke-width:SWPlaceholder') self.svg += svgs
def execute(self,obj): if not getattr(obj,"AutoUpdate", True): return True import Part, DraftGeomUtils obj.positionBySupport() pl = obj.Placement if obj.Base: if utils.get_type(obj.Base) in ["BuildingPart","SectionPlane"]: objs = [] if utils.get_type(obj.Base) == "SectionPlane": objs = self.excludeNames(obj,obj.Base.Objects) cutplane = obj.Base.Shape else: objs = self.excludeNames(obj,obj.Base.Group) cutplane = Part.makePlane(1000, 1000, App.Vector(-500, -500, 0)) m = 1 if obj.Base.ViewObject and hasattr(obj.Base.ViewObject,"CutMargin"): m = obj.Base.ViewObject.CutMargin.Value cutplane.translate(App.Vector(0,0,m)) cutplane.Placement = cutplane.Placement.multiply(obj.Base.Placement) if objs: onlysolids = True if hasattr(obj.Base,"OnlySolids"): onlysolids = obj.Base.OnlySolids if hasattr(obj,"OnlySolids"): # override base object onlysolids = obj.OnlySolids import Arch objs = groups.get_group_contents(objs, walls=True) if getattr(obj,"VisibleOnly",True): objs = gui_utils.remove_hidden(objs) shapes = [] if getattr(obj,"FuseArch", False): shtypes = {} for o in objs: if utils.get_type(o) in ["Wall","Structure"]: if onlysolids: shtypes.setdefault(o.Material.Name if (hasattr(o,"Material") and o.Material) else "None",[]).extend(o.Shape.Solids) else: shtypes.setdefault(o.Material.Name if (hasattr(o,"Material") and o.Material) else "None",[]).append(o.Shape.copy()) elif hasattr(o,'Shape'): if onlysolids: shapes.extend(o.Shape.Solids) else: shapes.append(o.Shape.copy()) for k, v in shtypes.items(): v1 = v.pop() if v: v1 = v1.multiFuse(v) v1 = v1.removeSplitter() if v1.Solids: shapes.extend(v1.Solids) else: print("Shape2DView: Fusing Arch objects produced non-solid results") shapes.append(v1) else: for o in objs: if hasattr(o,'Shape'): if onlysolids: shapes.extend(o.Shape.Solids) else: shapes.append(o.Shape.copy()) clip = False if hasattr(obj.Base,"Clip"): clip = obj.Base.Clip if hasattr(obj,"Clip"): #override base object clip = obj.Clip depth = None if hasattr(obj.Base,"Depth"): depth = obj.Base.Depth.Value cutp, cutv, iv = Arch.getCutVolume(cutplane, shapes, clip, depth) cuts = [] opl = App.Placement(obj.Base.Placement) proj = opl.Rotation.multVec(App.Vector(0, 0, 1)) if obj.ProjectionMode in ["Solid","Solid faces"]: shapes_to_cut = shapes if obj.ProjectionMode == "Solid faces": shapes_to_cut = [] for s in shapes: shapes_to_cut.extend(s.Faces) for sh in shapes_to_cut: if cutv: if sh.Volume < 0: sh.reverse() #if cutv.BoundBox.intersect(sh.BoundBox): # c = sh.cut(cutv) #else: # c = sh.copy() c = sh.cut(cutv) if onlysolids: cuts.extend(c.Solids) else: cuts.append(c) else: if onlysolids: cuts.extend(sh.Solids) else: cuts.append(sh.copy()) comp = Part.makeCompound(cuts) obj.Shape = self.getProjected(obj,comp,proj) elif obj.ProjectionMode in ["Cutlines", "Cutfaces"]: for sh in shapes: if sh.Volume < 0: sh.reverse() c = sh.section(cutp) if hasattr(obj,"InPlace"): if not obj.InPlace: c = self.getProjected(obj, c, proj) faces = [] if (obj.ProjectionMode == "Cutfaces") and (sh.ShapeType == "Solid"): wires = DraftGeomUtils.findWires(c.Edges) for w in wires: if w.isClosed(): faces.append(Part.Face(w)) if faces: cuts.extend(faces) else: cuts.append(c) comp = Part.makeCompound(cuts) opl = App.Placement(obj.Base.Placement) comp.Placement = opl.inverse() if comp: obj.Shape = comp elif obj.Base.isDerivedFrom("App::DocumentObjectGroup"): shapes = [] objs = self.excludeNames(obj,groups.get_group_contents(obj.Base)) for o in objs: if hasattr(o,'Shape'): if o.Shape: if not o.Shape.isNull(): shapes.append(o.Shape) if shapes: import Part comp = Part.makeCompound(shapes) obj.Shape = self.getProjected(obj,comp,obj.Projection) elif hasattr(obj.Base,'Shape'): if not DraftVecUtils.isNull(obj.Projection): if obj.ProjectionMode == "Solid": obj.Shape = self.getProjected(obj,obj.Base.Shape,obj.Projection) elif obj.ProjectionMode == "Individual Faces": import Part if obj.FaceNumbers: faces = [] for i in obj.FaceNumbers: if len(obj.Base.Shape.Faces) > i: faces.append(obj.Base.Shape.Faces[i]) views = [] for f in faces: views.append(self.getProjected(obj,f,obj.Projection)) if views: obj.Shape = Part.makeCompound(views) else: App.Console.PrintWarning(obj.ProjectionMode+" mode not implemented\n") if not DraftGeomUtils.isNull(pl): obj.Placement = pl
def getSVG(section, allOn=False, renderMode="Wireframe", showHidden=False, showFill=False, scale=1, linewidth=1, fontsize=1): """getSVG(section,[allOn,renderMode,showHidden,showFill,scale,linewidth,fontsize]) : returns an SVG fragment from an Arch section plane. If allOn is True, all cut objects are shown, regardless if they are visible or not. renderMode can be Wireframe (default) or Solid to use the Arch solid renderer. If showHidden is True, the hidden geometry above the section plane is shown in dashed line. If showFill is True, the cut areas get filled with a pattern""" if not section.Objects: return import DraftGeomUtils p = FreeCAD.Placement(section.Placement) direction = p.Rotation.multVec(FreeCAD.Vector(0, 0, 1)) objs = Draft.getGroupContents(section.Objects, walls=True, addgroups=True) if not allOn: objs = Draft.removeHidden(objs) # separate spaces spaces = [] nonspaces = [] for o in objs: if Draft.getType(o) == "Space": spaces.append(o) else: nonspaces.append(o) objs = nonspaces svg = '' fillpattern = '<pattern id="sectionfill" patternUnits="userSpaceOnUse" patternTransform="matrix(5,0,0,5,0,0)"' fillpattern += ' x="0" y="0" width="10" height="10">' fillpattern += '<g>' fillpattern += '<rect width="10" height="10" style="stroke:none; fill:#ffffff" /><path style="stroke:#000000; stroke-width:1" d="M0,0 l10,10" /></g></pattern>' # generating SVG if renderMode == "Solid": # render using the Arch Vector Renderer import ArchVRM, WorkingPlane wp = WorkingPlane.plane() wp.setFromPlacement(section.Placement) #wp.inverse() render = ArchVRM.Renderer() render.setWorkingPlane(wp) render.addObjects(objs) if showHidden: render.cut(section.Shape, showHidden) else: render.cut(section.Shape) svg += '<g transform="scale(1,-1)">\n' svg += render.getViewSVG(linewidth="LWPlaceholder") svg += fillpattern svg += render.getSectionSVG(linewidth="SWPlaceholder", fillpattern="sectionfill") if showHidden: svg += render.getHiddenSVG(linewidth="LWPlaceholder") svg += '</g>\n' # print render.info() else: # render using the Drawing module import Drawing, Part shapes = [] hshapes = [] sshapes = [] for o in objs: if o.isDerivedFrom("Part::Feature"): if o.Shape.isNull(): pass elif o.Shape.isValid(): if section.OnlySolids: shapes.extend(o.Shape.Solids) else: shapes.append(o.Shape) else: print section.Label, ": Skipping invalid object:", o.Label cutface, cutvolume, invcutvolume = ArchCommands.getCutVolume( section.Shape.copy(), shapes) if cutvolume: nsh = [] for sh in shapes: for sol in sh.Solids: if sol.Volume < 0: sol.reverse() c = sol.cut(cutvolume) s = sol.section(cutface) try: wires = DraftGeomUtils.findWires(s.Edges) for w in wires: f = Part.Face(w) sshapes.append(f) #s = Part.Wire(s.Edges) #s = Part.Face(s) except Part.OCCError: #print "ArchDrawingView: unable to get a face" sshapes.append(s) nsh.extend(c.Solids) #sshapes.append(s) if showHidden: c = sol.cut(invcutvolume) hshapes.append(c) shapes = nsh if shapes: baseshape = Part.makeCompound(shapes) svgf = Drawing.projectToSVG(baseshape, direction) if svgf: svgf = svgf.replace('stroke-width="0.35"', 'stroke-width="LWPlaceholder"') svgf = svgf.replace('stroke-width="1"', 'stroke-width="LWPlaceholder"') svgf = svgf.replace('stroke-width:0.01', 'stroke-width:LWPlaceholder') svg += svgf if hshapes: hshapes = Part.makeCompound(hshapes) svgh = Drawing.projectToSVG(hshapes, direction) if svgh: svgh = svgh.replace('stroke-width="0.35"', 'stroke-width="LWPlaceholder"') svgh = svgh.replace('stroke-width="1"', 'stroke-width="LWPlaceholder"') svgh = svgh.replace('stroke-width:0.01', 'stroke-width:LWPlaceholder') svgh = svgh.replace( 'fill="none"', 'fill="none"\nstroke-dasharray="DAPlaceholder"') svg += svgh if sshapes: svgs = "" if showFill: svgs += fillpattern svgs += '<g transform="rotate(180)">\n' for s in sshapes: if s.Edges: f = Draft.getSVG(s, direction=direction.negative(), linewidth=0, fillstyle="sectionfill", color=(0, 0, 0)) svgs += f svgs += "</g>\n" sshapes = Part.makeCompound(sshapes) svgs += Drawing.projectToSVG(sshapes, direction) if svgs: svgs = svgs.replace('stroke-width="0.35"', 'stroke-width="SWPlaceholder"') svgs = svgs.replace('stroke-width="1"', 'stroke-width="SWPlaceholder"') svgs = svgs.replace('stroke-width:0.01', 'stroke-width:SWPlaceholder') svgs = svgs.replace('stroke-width="0.35 px"', 'stroke-width="SWPlaceholder"') svgs = svgs.replace('stroke-width:0.35', 'stroke-width:SWPlaceholder') svg += svgs linewidth = linewidth / scale st = FreeCAD.ParamGet( "User parameter:BaseApp/Preferences/Mod/Arch").GetFloat( "CutLineThickness", 2) da = FreeCAD.ParamGet( "User parameter:BaseApp/Preferences/Mod/Arch").GetString( "archHiddenPattern", "30,10") da = da.replace(" ", "") svg = svg.replace('LWPlaceholder', str(linewidth) + 'px') svg = svg.replace('SWPlaceholder', str(linewidth * st) + 'px') svg = svg.replace('DAPlaceholder', str(da)) if spaces and round( direction.getAngle(FreeCAD.Vector(0, 0, 1)), Draft.precision()) in [0, round(math.pi, Draft.precision())]: svg += '<g transform="scale(1,-1)">' for s in spaces: svg += Draft.getSVG(s, scale=scale, fontsize=fontsize, direction=direction) svg += '</g>' # print "complete node:",svg return svg
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.""" # reset abort mechanism and variables self.running = True self.progress = 0 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 not self.update(): return 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 # 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] # 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: if not self.update(): return 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: if not self.update(): return self.progress += step 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 if not self.update(): return nofitpol = [] for placed in sheet: pts = [] pi = 0 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)) # 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 if outside: for e1 in facecopy.OuterWire.Edges: for e2 in placed[1].OuterWire.Edges: 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]) #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): 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: 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 if pol.isValid(): 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: if not self.update(): return 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: if not self.update(): return 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 cut(self, cutplane): "Cuts through the shapes with a given cut plane and builds section faces" if self.iscut: return if not self.shapes: if DEBUG: print "No objects to make sections" else: fill = (1.0, 1.0, 1.0, 1.0) placement = FreeCAD.Placement(cutplane.Placement) # building boundbox bb = self.shapes[0][0].BoundBox for sh in self.shapes[1:]: bb.add(sh[0].BoundBox) bb.enlarge(1) um = vm = wm = 0 if not bb.isCutPlane(placement.Base, self.wp.axis): if DEBUG: print "No objects are cut by the plane" else: corners = [ FreeCAD.Vector(bb.XMin, bb.YMin, bb.ZMin), FreeCAD.Vector(bb.XMin, bb.YMax, bb.ZMin), FreeCAD.Vector(bb.XMax, bb.YMin, bb.ZMin), FreeCAD.Vector(bb.XMax, bb.YMax, bb.ZMin), FreeCAD.Vector(bb.XMin, bb.YMin, bb.ZMax), FreeCAD.Vector(bb.XMin, bb.YMax, bb.ZMax), FreeCAD.Vector(bb.XMax, bb.YMin, bb.ZMax), FreeCAD.Vector(bb.XMax, bb.YMax, bb.ZMax) ] for c in corners: dv = c.sub(placement.Base) um1 = DraftVecUtils.project(dv, self.wp.u).Length um = max(um, um1) vm1 = DraftVecUtils.project(dv, self.wp.v).Length vm = max(vm, vm1) wm1 = DraftVecUtils.project(dv, self.wp.axis).Length wm = max(wm, wm1) p1 = FreeCAD.Vector(-um, vm, 0) p2 = FreeCAD.Vector(um, vm, 0) p3 = FreeCAD.Vector(um, -vm, 0) p4 = FreeCAD.Vector(-um, -vm, 0) cutface = Part.makePolygon([p1, p2, p3, p4, p1]) cutface = Part.Face(cutface) cutface.Placement = placement cutnormal = DraftVecUtils.scaleTo(self.wp.axis, wm) cutvolume = cutface.extrude(cutnormal) shapes = [] faces = [] sections = [] for sh in self.shapes: for sol in sh[0].Solids: c = sol.cut(cutvolume) shapes.append([c] + sh[1:]) for f in c.Faces: faces.append([f] + sh[1:]) sec = sol.section(cutface) if sec.Edges: wires = DraftGeomUtils.findWires(sec.Edges) for w in wires: sec = Part.Face(w) sections.append([sec, fill]) self.shapes = shapes self.faces = faces self.sections = sections if DEBUG: print "Built ", len(self.sections), " sections, ", len( self.faces), " faces retained" self.iscut = True self.oriented = False self.trimmed = False self.sorted = False self.joined = False
def areaOpShapes(self, obj): '''areaOpShapes(obj) ... returns envelope for all wires formed by the base edges.''' PathLog.track() self.tmpGrp = FreeCAD.ActiveDocument.addObject( 'App::DocumentObjectGroup', 'tmpDebugGrp') tmpGrpNm = self.tmpGrp.Name self.JOB = PathUtils.findParentJob(obj) self.offsetExtra = abs(obj.OffsetExtra.Value) if obj.UseComp: self.useComp = True self.ofstRadius = self.radius + self.offsetExtra self.commandlist.append( Path.Command("(Compensated Tool Path. Diameter: " + str(self.radius * 2) + ")")) else: self.useComp = False self.ofstRadius = self.offsetExtra self.commandlist.append(Path.Command("(Uncompensated Tool Path)")) shapes = [] if obj.Base: basewires = [] zMin = None for b in obj.Base: edgelist = [] for sub in b[1]: edgelist.append(getattr(b[0].Shape, sub)) basewires.append((b[0], DraftGeomUtils.findWires(edgelist))) if zMin is None or b[0].Shape.BoundBox.ZMin < zMin: zMin = b[0].Shape.BoundBox.ZMin PathLog.debug( 'PathProfileEdges areaOpShapes():: len(basewires) is {}'. format(len(basewires))) for base, wires in basewires: for wire in wires: if wire.isClosed() is True: # f = Part.makeFace(wire, 'Part::FaceMakerSimple') # if planar error, Comment out previous line, uncomment the next two (origWire, flatWire) = self._flattenWire(obj, wire, obj.FinalDepth.Value) f = origWire.Shape.Wires[0] if f is not False: # shift the compound to the bottom of the base object for proper sectioning zShift = zMin - f.BoundBox.ZMin newPlace = FreeCAD.Placement( FreeCAD.Vector(0, 0, zShift), f.Placement.Rotation) f.Placement = newPlace env = PathUtils.getEnvelope( base.Shape, subshape=f, depthparams=self.depthparams) shapes.append((env, False)) else: PathLog.error( translate( 'PathProfileEdges', 'The selected edge(s) are inaccessible.')) else: if self.JOB.GeometryTolerance.Value == 0.0: msg = self.JOB.Label + '.GeometryTolerance = 0.0.' msg += translate( 'PathProfileEdges', 'Please set to an acceptable value greater than zero.' ) PathLog.error(msg) else: cutWireObjs = False (origWire, flatWire) = self._flattenWire( obj, wire, obj.FinalDepth.Value) cutShp = self._getCutAreaCrossSection( obj, base, origWire, flatWire) if cutShp is not False: cutWireObjs = self._extractPathWire( obj, base, flatWire, cutShp) if cutWireObjs is not False: for cW in cutWireObjs: shapes.append((cW, False)) self.profileEdgesIsOpen = True else: PathLog.error( translate( 'PathProfileEdges', 'The selected edge(s) are inaccessible.' )) # Delete the temporary objects if PathLog.getLevel(PathLog.thisModule()) != 4: for to in self.tmpGrp.Group: FreeCAD.ActiveDocument.removeObject(to.Name) FreeCAD.ActiveDocument.removeObject(tmpGrpNm) else: if FreeCAD.GuiUp: import FreeCADGui FreeCADGui.ActiveDocument.getObject( tmpGrpNm).Visibility = False return shapes
def getCutShapes(objs, section, showHidden, groupSshapesByObject=False): import Part, DraftGeomUtils shapes = [] hshapes = [] sshapes = [] objectShapes = [] objectSshapes = [] for o in objs: if o.isDerivedFrom("Part::Feature"): if o.Shape.isNull(): pass elif section.OnlySolids: if o.Shape.isValid(): solids = [] solids.extend(o.Shape.Solids) shapes.extend(solids) objectShapes.append((o, solids)) else: print(section.Label, ": Skipping invalid object:", o.Label) else: shapes.append(o.Shape) objectShapes.append((o, [o.Shape])) clip = False if hasattr(section, "Clip"): clip = section.Clip cutface, cutvolume, invcutvolume = ArchCommands.getCutVolume( section.Shape.copy(), shapes, clip) shapes = [] if cutvolume: for o, shapeList in objectShapes: tmpSshapes = [] for sh in shapeList: for sol in sh.Solids: if sol.Volume < 0: sol.reverse() c = sol.cut(cutvolume) s = sol.section(cutface) try: wires = DraftGeomUtils.findWires(s.Edges) for w in wires: f = Part.Face(w) tmpSshapes.append(f) #s = Part.Wire(s.Edges) #s = Part.Face(s) except Part.OCCError: #print "ArchDrawingView: unable to get a face" tmpSshapes.append(s) shapes.extend(c.Solids) #sshapes.append(s) if showHidden: c = sol.cut(invcutvolume) hshapes.append(c) if len(tmpSshapes) > 0: sshapes.extend(tmpSshapes) if groupSshapesByObject: objectSshapes.append((o, tmpSshapes)) if groupSshapesByObject: return shapes, hshapes, sshapes, cutface, cutvolume, invcutvolume, objectSshapes else: return shapes, hshapes, sshapes, cutface, cutvolume, invcutvolume
#* 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 * #* * #*************************************************************************** ''' This macro finds the outside profile of a 3D shape. Right now it just creates wires that represent the shape,but it could be used for finding the outside profile of an object for a toolpath ''' import FreeCADGui import DraftGeomUtils from FreeCAD import Vector from PathScripts import find_outer_profile as fop sel = FreeCADGui.Selection.getSelection()[0] obj = sel el = fop.edgelist(obj) hl = fop.horizontal(el) connected = DraftGeomUtils.findWires(hl) goodwires = fop.openFilter(connected) outerwires, innerwires, same = fop.findOutsideWire(goodwires) #get distance from outerwires Z to bottom of part zdiff = obj.Shape.BoundBox.ZMin - outerwires.BoundBox.ZMax outerwires.Placement.move(Vector(0, 0, zdiff)) Part.show(outerwires) zupperouter = outerwires zupperouter.Placement.move(Vector(0, 0, obj.Shape.BoundBox.ZMax)) Part.show(zupperouter)