diff --git a/src/Mod/Path/PathScripts/PathAreaOp.py b/src/Mod/Path/PathScripts/PathAreaOp.py index cc3fd35b55..d7d16cd8e3 100644 --- a/src/Mod/Path/PathScripts/PathAreaOp.py +++ b/src/Mod/Path/PathScripts/PathAreaOp.py @@ -43,13 +43,11 @@ __url__ = "http://www.freecadweb.org" __doc__ = "Base class and properties for Path.Area based operations." __contributors__ = "russ4262 (Russell Johnson)" __createdDate__ = "2017" -__scriptVersion__ = "2j testing" -__lastModified__ = "2019-07-12 00:11 CST" +__scriptVersion__ = "2m testing" +__lastModified__ = "2019-07-20 13:29 CST" LOGLEVEL = PathLog.Level.INFO PathLog.setLevel(LOGLEVEL, PathLog.thisModule()) -# PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) - if LOGLEVEL is PathLog.Level.DEBUG: PathLog.trackModule() @@ -328,6 +326,7 @@ class ObjectOp(PathOp.ObjectOp): self.leadIn = 2.0 # pylint: disable=attribute-defined-outside-init self.cloneNames = [] # pylint: disable=attribute-defined-outside-init self.guiMsgs = [] # pylint: disable=attribute-defined-outside-init + self.tempObjectNames = [] # pylint: disable=attribute-defined-outside-init self.stockBB = PathUtils.findParentJob(obj).Stock.Shape.BoundBox # pylint: disable=attribute-defined-outside-init self.useTempJobClones('Delete') # Clear temporary group and recreate for temp job clones @@ -405,7 +404,7 @@ class ObjectOp(PathOp.ObjectOp): for shp in aOS: if len(shp) == 2: (fc, iH) = shp - # fc, iH, sub, angle, axis + # fc, iH, sub, angle, axis, strtDep, finDep tup = fc, iH, 'otherOp', 0.0, 'S', obj.StartDepth.Value, obj.FinalDepth.Value shapes.append(tup) else: @@ -422,17 +421,9 @@ class ObjectOp(PathOp.ObjectOp): shapes = [j['shape'] for j in jobs] - # PathLog.debug("Pre_path depths are Start: {}, and Final: {}".format(obj.StartDepth.Value, obj.FinalDepth.Value)) sims = [] numShapes = len(shapes) - # if numShapes == 1: - # nextAxis = shapes[0][4] - # elif numShapes > 1: - # nextAxis = shapes[1][4] - # else: - # nextAxis = 'L' - for ns in range(0, numShapes): (shape, isHole, sub, angle, axis, strDep, finDep) = shapes[ns] # pylint: disable=unused-variable if ns < numShapes - 1: @@ -497,6 +488,8 @@ class ObjectOp(PathOp.ObjectOp): self.useTempJobClones('Delete') # Delete temp job clone group and contents self.guiMessage('title', None, show=True) # Process GUI messages to user + for ton in self.tempObjectNames: # remove temporary objects by name + FreeCAD.ActiveDocument.removeObject(ton) PathLog.debug("obj.Name: " + str(obj.Name) + "\n\n") return sims diff --git a/src/Mod/Path/PathScripts/PathPocket.py b/src/Mod/Path/PathScripts/PathPocket.py index 835083b2d8..0110435eba 100644 --- a/src/Mod/Path/PathScripts/PathPocket.py +++ b/src/Mod/Path/PathScripts/PathPocket.py @@ -37,8 +37,8 @@ __url__ = "http://www.freecadweb.org" __doc__ = "Class and implementation of the 3D Pocket operation." __contributors__ = "russ4262 (Russell Johnson)" __created__ = "2014" -__scriptVersion__ = "1b testing" -__lastModified__ = "2019-07-01 20:13 CST" +__scriptVersion__ = "2g testing" +__lastModified__ = "2019-07-20 22:02 CST" LOGLEVEL = False @@ -48,6 +48,7 @@ if LOGLEVEL: else: PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) + # Qt translation handling def translate(context, text, disambig=None): return QtCore.QCoreApplication.translate(context, text, disambig) @@ -64,6 +65,12 @@ class ObjectPocket(PathPocketBase.ObjectPocket): if not hasattr(obj, 'HandleMultipleFeatures'): obj.addProperty('App::PropertyEnumeration', 'HandleMultipleFeatures', 'Pocket', QtCore.QT_TRANSLATE_NOOP('PathPocket', 'Choose how to process multiple Base Geometry features.')) obj.HandleMultipleFeatures = ['Collectively', 'Individually'] + if not hasattr(obj, 'AdaptivePocketStart'): + obj.addProperty('App::PropertyBool', 'AdaptivePocketStart', 'Pocket', QtCore.QT_TRANSLATE_NOOP('App::Property', 'Use adaptive algorithm to eliminate excessive air milling above planar pocket top.')) + if not hasattr(obj, 'AdaptivePocketFinish'): + obj.addProperty('App::PropertyBool', 'AdaptivePocketFinish', 'Pocket', QtCore.QT_TRANSLATE_NOOP('App::Property', 'Use adaptive algorithm to eliminate excessive air milling below planar pocket bottom.')) + if not hasattr(obj, 'ProcessStockArea'): + obj.addProperty('App::PropertyBool', 'ProcessStockArea', 'Pocket', QtCore.QT_TRANSLATE_NOOP('App::Property', 'Process the model and stock in an operation with no Base Geometry selected.')) def opOnDocumentRestored(self, obj): '''opOnDocumentRestored(obj) ... adds the properties if they doesn't exist.''' @@ -76,28 +83,58 @@ class ObjectPocket(PathPocketBase.ObjectPocket): '''areaOpShapes(obj) ... return shapes representing the solids to be removed.''' PathLog.track() + subObjTups = [] removalshapes = [] + if obj.Base: - PathLog.debug("base items exist. Processing...") + PathLog.debug("base items exist. Processing... ") for base in obj.Base: - PathLog.debug("Base item: {}".format(base)) + PathLog.debug("obj.Base item: {}".format(base)) # Check if all subs are faces - allFaceSubs = True + allSubsFaceType = True Faces = [] for sub in base[1]: if "Face" in sub: - Faces.append(getattr(base[0].Shape, sub)) + face = getattr(base[0].Shape, sub) + Faces.append(face) + subObjTups.append((sub, face)) else: - allFaceSubs = False + allSubsFaceType = False break - if allFaceSubs is True and obj.HandleMultipleFeatures == 'Collectively': - shape = Part.makeCompound(Faces) - env = PathUtils.getEnvelope(base[0].Shape, subshape=shape, depthparams=self.depthparams) - obj.removalshape = env.cut(base[0].Shape) - obj.removalshape.tessellate(0.1) - removalshapes.append((obj.removalshape, False)) + if len(Faces) == 0: + allSubsFaceType = False + + if allSubsFaceType is True and obj.HandleMultipleFeatures == 'Collectively': + if obj.OpFinalDepth == obj.FinalDepth: + (fzmin, fzmax) = self.getMinMaxOfFaces(Faces) + obj.FinalDepth.Value = fzmin + finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0 + self.depthparams = PathUtils.depth_params( + clearance_height=obj.ClearanceHeight.Value, + safe_height=obj.SafeHeight.Value, + start_depth=obj.StartDepth.Value, + step_down=obj.StepDown.Value, + z_finish_step=finish_step, + final_depth=fzmin, + user_depths=None) + PathLog.info("Updated obj.FinalDepth.Value and self.depthparams to zmin: {}".format(fzmin)) + + if obj.AdaptivePocketStart is True or obj.AdaptivePocketFinish is True: + pocketTup = self.calculateAdaptivePocket(obj, base, subObjTups) + if pocketTup is not False: + removalshapes.append(pocketTup) # (shape, isHole, sub, angle, axis, strDep, finDep) + else: + strDep = obj.StartDepth.Value + finDep = obj.FinalDepth.Value + + shape = Part.makeCompound(Faces) + env = PathUtils.getEnvelope(base[0].Shape, subshape=shape, depthparams=self.depthparams) + obj.removalshape = env.cut(base[0].Shape) + obj.removalshape.tessellate(0.1) + # (shape, isHole, sub, angle, axis, strDep, finDep) + removalshapes.append((obj.removalshape, False, '3DPocket', 0.0, 'X', strDep, finDep)) else: for sub in base[1]: if "Face" in sub: @@ -109,15 +146,62 @@ class ObjectPocket(PathPocketBase.ObjectPocket): env = PathUtils.getEnvelope(base[0].Shape, subshape=shape, depthparams=self.depthparams) obj.removalshape = env.cut(base[0].Shape) obj.removalshape.tessellate(0.1) + removalshapes.append((obj.removalshape, False)) else: # process the job base object as a whole PathLog.debug("processing the whole job base object") + strDep = obj.StartDepth.Value + finDep = obj.FinalDepth.Value + recomputeDepthparams = False for base in self.model: - env = PathUtils.getEnvelope(base.Shape, subshape=None, depthparams=self.depthparams) - obj.removalshape = env.cut(base.Shape) - obj.removalshape.tessellate(0.1) - removalshapes.append((obj.removalshape, False)) + if obj.OpFinalDepth == obj.FinalDepth: + if base.Shape.BoundBox.ZMin < obj.FinalDepth.Value: + obj.FinalDepth.Value = base.Shape.BoundBox.ZMin + finDep = base.Shape.BoundBox.ZMin + recomputeDepthparams = True + PathLog.info("Updated obj.FinalDepth.Value to {}".format(finDep)) + if obj.OpStartDepth == obj.StartDepth: + if base.Shape.BoundBox.ZMax > obj.StartDepth.Value: + obj.StartDepth.Value = base.Shape.BoundBox.ZMax + finDep = base.Shape.BoundBox.ZMax + recomputeDepthparams = True + PathLog.info("Updated obj.StartDepth.Value to {}".format(strDep)) + if recomputeDepthparams is True: + finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0 + self.depthparams = PathUtils.depth_params( + clearance_height=obj.ClearanceHeight.Value, + safe_height=obj.SafeHeight.Value, + start_depth=obj.StartDepth.Value, + step_down=obj.StepDown.Value, + z_finish_step=finish_step, + final_depth=obj.FinalDepth.Value, + user_depths=None) + recomputeDepthparams = False + + if obj.ProcessStockArea is True: + job = PathUtils.findParentJob(obj) + finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0 + + depthparams = PathUtils.depth_params( + clearance_height=obj.ClearanceHeight.Value, + safe_height=obj.SafeHeight.Value, + start_depth=obj.StartDepth.Value, + step_down=obj.StepDown.Value, + z_finish_step=finish_step, + final_depth=base.Shape.BoundBox.ZMin, + user_depths=None) + stockEnvShape = PathUtils.getEnvelope(job.Stock.Shape, subshape=None, depthparams=depthparams) + + obj.removalshape = stockEnvShape.cut(base.Shape) + obj.removalshape.tessellate(0.1) + else: + env = PathUtils.getEnvelope(base.Shape, subshape=None, depthparams=self.depthparams) + obj.removalshape = env.cut(base.Shape) + obj.removalshape.tessellate(0.1) + + removalshapes.append((obj.removalshape, False, '3DPocket', 0.0, 'X', strDep, finDep)) + return removalshapes def areaOpSetDefaultValues(self, obj, job): @@ -125,6 +209,563 @@ class ObjectPocket(PathPocketBase.ObjectPocket): obj.StepOver = 100 obj.ZigZagAngle = 45 obj.HandleMultipleFeatures = 'Collectively' + obj.AdaptivePocketStart = False + obj.AdaptivePocketFinish = False + obj.ProcessStockArea = False + + # methods for eliminating air milling with some pockets: adpative start and finish + def calculateAdaptivePocket(self, obj, base, subObjTups): + '''calculateAdaptivePocket(obj, base, subObjTups) + Orient multiple faces around common facial center of mass. + Identify edges that are connections for adjacent faces. + Attempt to separate unconnected edges into top and bottom loops of the pocket. + Trim the top and bottom of the pocket if available and requested. + return: tuple with pocket shape information''' + low = [] + high = [] + removeList = [] + Faces = [] + allEdges = [] + makeHighFace = 0 + tryNonPlanar = False + isHighFacePlanar = True + isLowFacePlanar = True + faceType = 0 + + for (sub, face) in subObjTups: + Faces.append(face) + + # identify max and min face heights for top loop + (zmin, zmax) = self.getMinMaxOfFaces(Faces) + + # Order faces around common center of mass + subObjTups = self.orderFacesAroundCenterOfMass(subObjTups) + # find connected edges and map to edge names of base + (connectedEdges, touching) = self.findSharedEdges(subObjTups) + (low, high) = self.identifyUnconnectedEdges(subObjTups, touching) + + if len(high) > 0 and obj.AdaptivePocketStart is True: + # attempt planar face with top edges of pocket + allEdges = [] + makeHighFace = 0 + tryNonPlanar = False + for (sub, face, ei) in high: + allEdges.append(face.Edges[ei]) + + (hzmin, hzmax) = self.getMinMaxOfFaces(allEdges) + + try: + highFaceShape = Part.Face(Part.Wire(Part.__sortEdges__(allEdges))) + except Exception as ee: + PathLog.warning(ee) + PathLog.error(translate("Path", "A planar adaptive start is unavailable. The non-planar will be attempted.")) + tryNonPlanar = True + else: + makeHighFace = 1 + + if tryNonPlanar is True: + try: + highFaceShape = Part.makeFilledFace(Part.__sortEdges__(allEdges)) # NON-planar face method + except Exception as eee: + PathLog.warning(eee) + PathLog.error(translate("Path", "The non-planar adaptive start is also unavailable.") + "(1)") + isHighFacePlanar = False + else: + makeHighFace = 2 + + if makeHighFace > 0: + FreeCAD.ActiveDocument.addObject('Part::Feature', 'topEdgeFace') + highFace = FreeCAD.ActiveDocument.ActiveObject + highFace.Shape = highFaceShape + removeList.append(highFace.Name) + + # verify non-planar face is within high edge loop Z-boundaries + if makeHighFace == 2: + mx = hzmax + obj.StepDown.Value + mn = hzmin - obj.StepDown.Value + if highFace.Shape.BoundBox.ZMax > mx or highFace.Shape.BoundBox.ZMin < mn: + PathLog.warning("ZMaxDiff: {}; ZMinDiff: {}".format(highFace.Shape.BoundBox.ZMax - mx, highFace.Shape.BoundBox.ZMin - mn)) + PathLog.error(translate("Path", "The non-planar adaptive start is also unavailable.") + "(2)") + isHighFacePlanar = False + makeHighFace = 0 + else: + isHighFacePlanar = False + + if len(low) > 0 and obj.AdaptivePocketFinish is True: + # attempt planar face with bottom edges of pocket + allEdges = [] + for (sub, face, ei) in low: + allEdges.append(face.Edges[ei]) + + # (lzmin, lzmax) = self.getMinMaxOfFaces(allEdges) + + try: + lowFaceShape = Part.Face(Part.Wire(Part.__sortEdges__(allEdges))) + # lowFaceShape = Part.makeFilledFace(Part.__sortEdges__(allEdges)) # NON-planar face method + except Exception as ee: + PathLog.error(ee) + PathLog.error("An adaptive finish is unavailable.") + isLowFacePlanar = False + else: + FreeCAD.ActiveDocument.addObject('Part::Feature', 'bottomEdgeFace') + lowFace = FreeCAD.ActiveDocument.ActiveObject + lowFace.Shape = lowFaceShape + removeList.append(lowFace.Name) + else: + isLowFacePlanar = False + + # Start with a regular pocket envelope + strDep = obj.StartDepth.Value + finDep = obj.FinalDepth.Value + cuts = [] + starts = [] + finals = [] + starts.append(obj.StartDepth.Value) + finals.append(zmin) + if obj.AdaptivePocketStart is True or len(subObjTups) == 1: + strDep = zmax + obj.StepDown.Value + starts.append(zmax + obj.StepDown.Value) + + finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0 + depthparams = PathUtils.depth_params( + clearance_height=obj.ClearanceHeight.Value, + safe_height=obj.SafeHeight.Value, + start_depth=strDep, + step_down=obj.StepDown.Value, + z_finish_step=finish_step, + final_depth=finDep, + user_depths=None) + shape = Part.makeCompound(Faces) + env = PathUtils.getEnvelope(base[0].Shape, subshape=shape, depthparams=depthparams) + cuts.append(env.cut(base[0].Shape)) + + # Might need to change to .cut(job.Stock.Shape) if pocket has no bottom + # job = PathUtils.findParentJob(obj) + # envBody = env.cut(job.Stock.Shape) + + if isHighFacePlanar is True and len(subObjTups) > 1: + starts.append(hzmax + obj.StepDown.Value) + # make shape to trim top of reg pocket + strDep1 = obj.StartDepth.Value + (hzmax - hzmin) + if makeHighFace == 1: + # Planar face + finDep1 = highFace.Shape.BoundBox.ZMin + obj.StepDown.Value + else: + # Non-Planar face + finDep1 = hzmin + obj.StepDown.Value + depthparams1 = PathUtils.depth_params( + clearance_height=obj.ClearanceHeight.Value, + safe_height=obj.SafeHeight.Value, + start_depth=strDep1, + step_down=obj.StepDown.Value, + z_finish_step=finish_step, + final_depth=finDep1, + user_depths=None) + envTop = PathUtils.getEnvelope(base[0].Shape, subshape=highFace.Shape, depthparams=depthparams1) + cbi = len(cuts) - 1 + cuts.append(cuts[cbi].cut(envTop)) + + if isLowFacePlanar is True and len(subObjTups) > 1: + # make shape to trim top of pocket + if makeHighFace == 1: + # Planar face + strDep2 = lowFace.Shape.BoundBox.ZMax + else: + # Non-Planar face + strDep2 = hzmax + finDep2 = obj.FinalDepth.Value + depthparams2 = PathUtils.depth_params( + clearance_height=obj.ClearanceHeight.Value, + safe_height=obj.SafeHeight.Value, + start_depth=strDep2, + step_down=obj.StepDown.Value, + z_finish_step=finish_step, + final_depth=finDep2, + user_depths=None) + envBottom = PathUtils.getEnvelope(base[0].Shape, subshape=lowFace.Shape, depthparams=depthparams2) + cbi = len(cuts) - 1 + cuts.append(cuts[cbi].cut(envBottom)) + + # package pocket details into tuple + sdi = len(starts) - 1 + fdi = len(finals) - 1 + cbi = len(cuts) - 1 + pocket = (cuts[cbi], False, '3DPocket', 0.0, 'X', starts[sdi], finals[fdi]) + if FreeCAD.GuiUp: + import FreeCADGui + for rn in removeList: + FreeCADGui.ActiveDocument.getObject(rn).Visibility = False + + for rn in removeList: + FreeCAD.ActiveDocument.getObject(rn).purgeTouched() + self.tempObjectNames.append(rn) + return pocket + + def orderFacesAroundCenterOfMass(self, subObjTups): + '''orderFacesAroundCenterOfMass(subObjTups) + Order list of faces by center of mass in angular order around + average center of mass for all faces. Positive X-axis is zero degrees. + return: subObjTups [ordered/sorted]''' + import math + newList = [] + vectList = [] + comList = [] + sortList = [] + subCnt = 0 + sumCom = FreeCAD.Vector(0.0, 0.0, 0.0) + avgCom = FreeCAD.Vector(0.0, 0.0, 0.0) + + def getDrctn(vectItem): + return vectItem[3] + + def getFaceIdx(sub): + return int(sub.replace('Face', '')) - 1 + + # get CenterOfMass for each face and add to sumCenterOfMass for average calculation + for (sub, face) in subObjTups: + # for (bsNm, fIdx, eIdx, vIdx) in bfevList: + # face = FreeCAD.ActiveDocument.getObject(bsNm).Shape.Faces[fIdx] + subCnt += 1 + com = face.CenterOfMass + comList.append((sub, face, com)) + sumCom = sumCom.add(com) # add sub COM to sum + + # Calculate average CenterOfMass for all faces combined + avgCom.x = sumCom.x / subCnt + avgCom.y = sumCom.y / subCnt + avgCom.z = sumCom.z / subCnt + + # calculate vector (mag, direct) for each face from avgCom + for (sub, face, com) in comList: + adjCom = com.sub(avgCom) # effectively treats avgCom as origin for each face. + mag = math.sqrt(adjCom.x**2 + adjCom.y**2) # adjCom.Length without Z values + drctn = 0.0 + # Determine direction of vector + if adjCom.x > 0.0: + if adjCom.y > 0.0: # Q1 + drctn = math.degrees(math.atan(adjCom.y/adjCom.x)) + elif adjCom.y < 0.0: + drctn = -math.degrees(math.atan(adjCom.x/adjCom.y)) + 270.0 + elif adjCom.y == 0.0: + drctn = 0.0 + elif adjCom.x < 0.0: + if adjCom.y < 0.0: + drctn = math.degrees(math.atan(adjCom.y/adjCom.x)) + 180.0 + elif adjCom.y > 0.0: + drctn = -math.degrees(math.atan(adjCom.x/adjCom.y)) + 90.0 + elif adjCom.y == 0.0: + drctn = 180.0 + elif adjCom.x == 0.0: + if adjCom.y < 0.0: + drctn = 270.0 + elif adjCom.y > 0.0: + drctn = 90.0 + vectList.append((sub, face, mag, drctn)) + + # Sort faces by directional component of vector + sortList = sorted(vectList, key=getDrctn) + + # remove magnitute and direction values + for (sub, face, mag, drctn) in sortList: + newList.append((sub, face)) + + # Rotate list items so highest face is first + zmax = newList[0][1].BoundBox.ZMax + idx = 0 + for i in range(0, len(newList)): + (sub, face) = newList[i] + fIdx = getFaceIdx(sub) + # face = FreeCAD.ActiveDocument.getObject(bsNm).Shape.Faces[fIdx] + if face.BoundBox.ZMax > zmax: + zmax = face.BoundBox.ZMax + idx = i + if face.BoundBox.ZMax == zmax: + if fIdx < getFaceIdx(newList[idx][0]): + idx = i + if idx > 0: + for z in range(0, idx): + newList.append(newList.pop(0)) + + return newList + + def findSharedEdges(self, subObjTups): + '''findSharedEdges(self, subObjTups) + Find connected edges given a group of faces''' + checkoutList = [] + searchedList = [] + shared = [] + touching = {} + touchingCleaned = {} + + # Prepare dictionary for edges in shared + for (sub, face) in subObjTups: + touching[sub] = [] + + # prepare list of indexes as proxies for subObjTups items + numFaces = len(subObjTups) + for nf in range(0, numFaces): + checkoutList.append(nf) + + for co in range(0, len(checkoutList)): + if len(checkoutList) < 2: + break + + # Checkout first sub for analysis + checkedOut1 = checkoutList.pop() + searchedList.append(checkedOut1) + (sub1, face1) = subObjTups[checkedOut1] + + # Compare checked out sub to others for shared + for co in range(0, len(checkoutList)): + # Checkout second sub for analysis + (sub2, face2) = subObjTups[co] + + # analyze two subs for common faces + for ei1 in range(0, len(face1.Edges)): + edg1 = face1.Edges[ei1] + for ei2 in range(0, len(face2.Edges)): + edg2 = face2.Edges[ei2] + if edg1.isSame(edg2) is True: + PathLog.debug("{}.Edges[{}] connects at {}.Edges[{}]".format(sub1, ei1, sub2, ei2)) + shared.append((sub1, face1, ei1)) + touching[sub1].append(ei1) + touching[sub2].append(ei2) + # Efor + # Remove duplicates from edge lists + for sub in touching: + touchingCleaned[sub] = [] + for s in touching[sub]: + if s not in touchingCleaned[sub]: + touchingCleaned[sub].append(s) + + return (shared, touchingCleaned) + + def identifyUnconnectedEdges(self, subObjTups, touching): + '''identifyUnconnectedEdges(subObjTups, touching) + Categorize unconnected edges into two groups, if possible: low and high''' + # Identify unconnected edges + # (should be top edge loop if all faces form loop with bottom face(s) included) + high = [] + low = [] + holding = [] + + for (sub, face) in subObjTups: + holding = [] + for ei in range(0, len(face.Edges)): + if ei not in touching[sub]: + holding.append((sub, face, ei)) + # Assign unconnected edges based upon category: high or low + if len(holding) == 1: + high.append(holding.pop()) + elif len(holding) == 2: + edg0 = holding[0][1].Edges[holding[0][2]] + edg1 = holding[1][1].Edges[holding[1][2]] + if self.hasCommonVertex(edg0, edg1, show=False) < 0: + # Edges not connected - probably top and bottom if faces in loop + if edg0.CenterOfMass.z > edg1.CenterOfMass.z: + high.append(holding[0]) + low.append(holding[1]) + else: + high.append(holding[1]) + low.append(holding[0]) + else: + # Edges are connected - all top, or all bottom edges + com = FreeCAD.Vector(0, 0, 0) + com.add(edg0.CenterOfMass) + com.add(edg1.CenterOfMass) + avgCom = FreeCAD.Vector(com.x/2.0, com.y/2.0, com.z/2.0) + if avgCom.z > face.CenterOfMass.z: + high.extend(holding) + else: + low.extend(holding) + elif len(holding) > 2: + # attempt to break edges into two groups of connected edges. + # determine which group has higher center of mass, and assign as high, the other as low + (lw, hgh) = self.groupConnectedEdges(holding) + low.extend(lw) + high.extend(hgh) + # Eif + # Efor + return (low, high) + + def hasCommonVertex(self, edge1, edge2, show=False): + '''findCommonVertexIndexes(edge1, edge2, show=False) + Compare vertexes of two edges to identify a common vertex. + Returns the vertex index of edge1 to which edge2 is connected''' + if show is True: + PathLog.info("New findCommonVertex()... ") + + oIdx = 0 + listOne = edge1.Vertexes + listTwo = edge2.Vertexes + + # Find common vertexes + for o in listOne: + if show is True: + PathLog.info(" one ({}, {}, {})".format(o.X, o.Y, o.Z)) + for t in listTwo: + if show is True: + PathLog.error("two ({}, {}, {})".format(t.X, t.Y, t.Z)) + if o.X == t.X: + if o.Y == t.Y: + if o.Z == t.Z: + if show is True: + PathLog.info("found") + return oIdx + oIdx += 1 + return -1 + + def groupConnectedEdges(self, holding): + '''groupConnectedEdges(self, holding) + Take edges and determine which are connected. + Group connected chains/loops into: low and high''' + holds = [] + grps = [] + searched = [] + stop = False + attachments = [] + loops = 1 + + def updateAttachments(grps): + atchmnts = [] + lenGrps = len(grps) + if lenGrps > 0: + lenG0 = len(grps[0]) + if lenG0 < 2: + atchmnts.append((0, 0)) + else: + atchmnts.append((0, 0)) + atchmnts.append((0, lenG0 - 1)) + if lenGrps == 2: + lenG1 = len(grps[1]) + if lenG1 < 2: + atchmnts.append((1, 0)) + else: + atchmnts.append((1, 0)) + atchmnts.append((1, lenG1 - 1)) + return atchmnts + + def isSameVertex(o, t): + if o.X == t.X: + if o.Y == t.Y: + if o.Z == t.Z: + return True + return False + + for hi in range(0, len(holding)): + holds.append(hi) + + # Place initial edge in first group and update attachments + h0 = holds.pop() + grps.append([h0]) + attachments = updateAttachments(grps) + + while len(holds) > 0: + if loops > 500: + PathLog.error('BREAK --- LOOPS LIMIT of 500 ---') + break + save = False + + h2 = holds.pop() + (sub2, face2, ei2) = holding[h2] + + # Cycle through attachments for connection to existing + for (g, t) in attachments: + h1 = grps[g][t] + (sub1, face1, ei1) = holding[h1] + + edg1 = face1.Edges[ei1] + edg2 = face2.Edges[ei2] + + # CV = self.hasCommonVertex(edg1, edg2, show=False) + + # Check attachment based on attachments order + if t == 0: + # is last vertex of h2 == first vertex of h1 + e2lv = len(edg2.Vertexes) - 1 + one = edg2.Vertexes[e2lv] + two = edg1.Vertexes[0] + if isSameVertex(one, two) is True: + # Connected, insert h1 in front of h2 + grps[g].insert(0, h2) + stop = True + else: + # is last vertex of h1 == first vertex of h2 + e1lv = len(edg1.Vertexes) - 1 + one = edg1.Vertexes[e1lv] + two = edg2.Vertexes[0] + if isSameVertex(one, two) is True: + # Connected, append h1 after h2 + grps[g].append(h2) + stop = True + + if stop is True: + # attachment was found + attachments = updateAttachments(grps) + holds.extend(searched) + stop = False + break + else: + # no attachment found + save = True + # Efor + if save is True: + searched.append(h2) + if len(holds) == 0: + if len(grps) == 1: + h0 = searched.pop(0) + grps.append([h0]) + attachments = updateAttachments(grps) + holds.extend(searched) + # Eif + loops += 1 + # Ewhile + + low = [] + high = [] + if len(grps) == 1: + grps.append([]) + grp0 = [] + grp1 = [] + com0 = FreeCAD.Vector(0, 0, 0) + com1 = FreeCAD.Vector(0, 0, 0) + if len(grps[0]) > 0: + for g in grps[0]: + grp0.append(holding[g]) + (sub, face, ei) = holding[g] + com0 = com0.add(face.Edges[ei].CenterOfMass) + com0z = com0.z / len(grps[0]) + if len(grps[1]) > 0: + for g in grps[1]: + grp1.append(holding[g]) + (sub, face, ei) = holding[g] + com1 = com1.add(face.Edges[ei].CenterOfMass) + com1z = com1.z / len(grps[1]) + + if len(grps[1]) > 0: + if com0z > com1z: + low = grp1 + high = grp0 + else: + low = grp0 + high = grp1 + else: + low = grp0 + high = grp0 + + return (low, high) + + def getMinMaxOfFaces(self, Faces): + '''getMinMaxOfFaces(Faces) + return the zmin and zmax values for given set of faces or edges.''' + zmin = Faces[0].BoundBox.ZMax + zmax = Faces[0].BoundBox.ZMin + for f in Faces: + if f.BoundBox.ZMin < zmin: + zmin = f.BoundBox.ZMin + if f.BoundBox.ZMax > zmax: + zmax = f.BoundBox.ZMax + return (zmin, zmax) def SetupProperties():