From 082c44464b7bab2e0c0a9135c63211ed359e01dd Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Sat, 20 Jul 2019 22:05:01 -0500 Subject: [PATCH] 3D Pocket: upgrade to adaptive start and finish apply algorithms to some pockets to eliminate air milling and adapt start and finish heights and paths to top and bottom of certain pockets. Depth correction & flake8 formatting use list to track successive cuts so variations of settings are tracked and applied correctly Apply faceType specific depth calculations Planar face requires one depth source, non-planar requires a different source. --- src/Mod/Path/PathScripts/PathAreaOp.py | 19 +- src/Mod/Path/PathScripts/PathPocket.py | 675 ++++++++++++++++++++++++- 2 files changed, 664 insertions(+), 30 deletions(-) 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():