| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | from PySide.QtCore import QT_TRANSLATE_NOOP |
| | import FreeCAD |
| | import Part |
| | import Path |
| | import Path.Op.Base as PathOp |
| | import Path.Op.PocketBase as PathPocketBase |
| | import PathScripts.PathUtils as PathUtils |
| |
|
| | |
| | from lazy_loader.lazy_loader import LazyLoader |
| |
|
| | __title__ = "CAM 3D Pocket Operation" |
| | __author__ = "Yorik van Havre <yorik@uncreated.net>" |
| | __url__ = "https://www.freecad.org" |
| | __doc__ = "Class and implementation of the 3D Pocket operation." |
| | __created__ = "2014" |
| |
|
| | if False: |
| | Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) |
| | Path.Log.trackModule(Path.Log.thisModule()) |
| | else: |
| | Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) |
| |
|
| |
|
| | translate = FreeCAD.Qt.translate |
| |
|
| |
|
| | class ObjectPocket(PathPocketBase.ObjectPocket): |
| | """Proxy object for Pocket operation.""" |
| |
|
| | def pocketOpFeatures(self, obj): |
| | return PathOp.FeatureNoFinalDepth |
| |
|
| | def initPocketOp(self, obj): |
| | """initPocketOp(obj) ... setup receiver""" |
| | if not hasattr(obj, "HandleMultipleFeatures"): |
| | obj.addProperty( |
| | "App::PropertyEnumeration", |
| | "HandleMultipleFeatures", |
| | "Pocket", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "Choose how to process multiple Base Geometry features.", |
| | ), |
| | ) |
| |
|
| | if not hasattr(obj, "AdaptivePocketStart"): |
| | obj.addProperty( |
| | "App::PropertyBool", |
| | "AdaptivePocketStart", |
| | "Pocket", |
| | 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", |
| | 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", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "Process the model and stock in an operation with no Base Geometry selected.", |
| | ), |
| | ) |
| |
|
| | |
| | for n in self.propertyEnumerations(): |
| | setattr(obj, n[0], n[1]) |
| |
|
| | @classmethod |
| | def propertyEnumerations(self, dataType="data"): |
| | """propertyEnumerations(dataType="data")... return property enumeration lists of specified dataType. |
| | Args: |
| | dataType = 'data', 'raw', 'translated' |
| | Notes: |
| | 'data' is list of internal string literals used in code |
| | 'raw' is list of (translated_text, data_string) tuples |
| | 'translated' is list of translated string literals |
| | """ |
| |
|
| | enums = { |
| | "HandleMultipleFeatures": [ |
| | (translate("CAM_Pocket", "Collectively"), "Collectively"), |
| | (translate("CAM_Pocket", "Individually"), "Individually"), |
| | ], |
| | } |
| |
|
| | if dataType == "raw": |
| | return enums |
| |
|
| | data = list() |
| | idx = 0 if dataType == "translated" else 1 |
| |
|
| | Path.Log.debug(enums) |
| |
|
| | for k, v in enumerate(enums): |
| | data.append((v, [tup[idx] for tup in enums[v]])) |
| | Path.Log.debug(data) |
| |
|
| | return data |
| |
|
| | def opOnDocumentRestored(self, obj): |
| | """opOnDocumentRestored(obj) ... adds the properties if they doesn't exist.""" |
| | super().opOnDocumentRestored(obj) |
| | self.initPocketOp(obj) |
| |
|
| | def pocketInvertExtraOffset(self): |
| | return False |
| |
|
| | def opUpdateDepths(self, obj): |
| | """opUpdateDepths(obj) ... Implement special depths calculation.""" |
| | |
| | if not obj.Base or len(obj.Base) == 0: |
| | if len(self.job.Model.Group) == 1: |
| | finDep = self.job.Model.Group[0].Shape.BoundBox.ZMin |
| | else: |
| | finDep = min([m.Shape.BoundBox.ZMin for m in self.job.Model.Group]) |
| | obj.setExpression("OpFinalDepth", "{} mm".format(finDep)) |
| |
|
| | def areaOpShapes(self, obj): |
| | """areaOpShapes(obj) ... return shapes representing the solids to be removed.""" |
| | Path.Log.track() |
| |
|
| | subObjTups = [] |
| | removalshapes = [] |
| |
|
| | if obj.Base: |
| | Path.Log.debug("base items exist. Processing... ") |
| | for base in obj.Base: |
| | Path.Log.debug("obj.Base item: {}".format(base)) |
| |
|
| | |
| | allSubsFaceType = True |
| | Faces = [] |
| | for sub in base[1]: |
| | if "Face" in sub: |
| | face = getattr(base[0].Shape, sub) |
| | Faces.append(face) |
| | subObjTups.append((sub, face)) |
| | else: |
| | allSubsFaceType = False |
| | break |
| |
|
| | if len(Faces) == 0: |
| | allSubsFaceType = False |
| |
|
| | if allSubsFaceType is True and obj.HandleMultipleFeatures == "Collectively": |
| | (fzmin, fzmax) = self.getMinMaxOfFaces(Faces) |
| | if obj.FinalDepth.Value < fzmin: |
| | Path.Log.warning( |
| | translate( |
| | "CAM", |
| | "Final depth set below ZMin of face(s) selected.", |
| | ) |
| | ) |
| |
|
| | if obj.AdaptivePocketStart is True or obj.AdaptivePocketFinish is True: |
| | pocketTup = self.calculateAdaptivePocket(obj, base, subObjTups) |
| | if pocketTup is not False: |
| | obj.removalshape = pocketTup[0] |
| | removalshapes.append(pocketTup) |
| | else: |
| | shape = Part.makeCompound(Faces) |
| | env = PathUtils.getEnvelope( |
| | base[0].Shape, subshape=shape, depthparams=self.depthparams |
| | ) |
| | rawRemovalShape = env.cut(base[0].Shape) |
| | faceExtrusions = [f.extrude(FreeCAD.Vector(0.0, 0.0, 1.0)) for f in Faces] |
| | obj.removalshape = _identifyRemovalSolids(rawRemovalShape, faceExtrusions) |
| | removalshapes.append( |
| | (obj.removalshape, False, "3DPocket") |
| | ) |
| | else: |
| | for sub in base[1]: |
| | if "Face" in sub: |
| | shape = Part.makeCompound([getattr(base[0].Shape, sub)]) |
| | else: |
| | edges = [getattr(base[0].Shape, sub) for sub in base[1]] |
| | shape = Part.makeFace(edges, "Part::FaceMakerSimple") |
| |
|
| | env = PathUtils.getEnvelope( |
| | base[0].Shape, subshape=shape, depthparams=self.depthparams |
| | ) |
| | rawRemovalShape = env.cut(base[0].Shape) |
| | faceExtrusions = [shape.extrude(FreeCAD.Vector(0.0, 0.0, 1.0))] |
| | obj.removalshape = _identifyRemovalSolids(rawRemovalShape, faceExtrusions) |
| | removalshapes.append((obj.removalshape, False, "3DPocket")) |
| |
|
| | else: |
| | Path.Log.debug("processing the whole job base object") |
| | for base in self.model: |
| | if obj.ProcessStockArea is True: |
| | job = PathUtils.findParentJob(obj) |
| |
|
| | stockEnvShape = PathUtils.getEnvelope( |
| | job.Stock.Shape, subshape=None, depthparams=self.depthparams |
| | ) |
| |
|
| | rawRemovalShape = stockEnvShape.cut(base.Shape) |
| | else: |
| | env = PathUtils.getEnvelope( |
| | base.Shape, subshape=None, depthparams=self.depthparams |
| | ) |
| | rawRemovalShape = env.cut(base.Shape) |
| |
|
| | |
| | removalSolids = [ |
| | s |
| | for s in rawRemovalShape.Solids |
| | if Path.Geom.isRoughly(s.BoundBox.ZMax, rawRemovalShape.BoundBox.ZMax) |
| | ] |
| |
|
| | |
| | if len(removalSolids) > 1: |
| | seed = removalSolids[0] |
| | for tt in removalSolids[1:]: |
| | fusion = seed.fuse(tt) |
| | seed = fusion |
| | removalShape = seed |
| | else: |
| | removalShape = removalSolids[0] |
| |
|
| | obj.removalshape = removalShape |
| | removalshapes.append((obj.removalshape, False, "3DPocket")) |
| |
|
| | return removalshapes |
| |
|
| | def areaOpSetDefaultValues(self, obj, job): |
| | """areaOpSetDefaultValues(obj, job) ... set default values""" |
| | obj.StepOver = 50 |
| | obj.ZigZagAngle = 45 |
| | obj.HandleMultipleFeatures = "Collectively" |
| | obj.AdaptivePocketStart = False |
| | obj.AdaptivePocketFinish = False |
| | obj.ProcessStockArea = False |
| |
|
| | |
| | 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 |
| |
|
| | for sub, face in subObjTups: |
| | Faces.append(face) |
| |
|
| | |
| | (zmin, zmax) = self.getMinMaxOfFaces(Faces) |
| |
|
| | |
| | subObjTups = self.orderFacesAroundCenterOfMass(subObjTups) |
| | |
| | (connectedEdges, touching) = self.findSharedEdges(subObjTups) |
| | (low, high) = self.identifyUnconnectedEdges(subObjTups, touching) |
| |
|
| | if len(high) > 0 and obj.AdaptivePocketStart is True: |
| | |
| | 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: |
| | Path.Log.warning(ee) |
| | Path.Log.error( |
| | translate( |
| | "CAM", |
| | "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) |
| | ) |
| | except Exception as eee: |
| | Path.Log.warning(eee) |
| | Path.Log.error( |
| | translate("CAM", "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) |
| |
|
| | |
| | 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: |
| | Path.Log.warning( |
| | "ZMaxDiff: {}; ZMinDiff: {}".format( |
| | highFace.Shape.BoundBox.ZMax - mx, |
| | highFace.Shape.BoundBox.ZMin - mn, |
| | ) |
| | ) |
| | Path.Log.error( |
| | translate("CAM", "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: |
| | |
| | allEdges = [] |
| | for sub, face, ei in low: |
| | allEdges.append(face.Edges[ei]) |
| |
|
| | |
| |
|
| | try: |
| | lowFaceShape = Part.Face(Part.Wire(Part.__sortEdges__(allEdges))) |
| | |
| | except Exception as ee: |
| | Path.Log.error(ee) |
| | Path.Log.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 |
| |
|
| | |
| | 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)) |
| |
|
| | |
| | |
| | |
| |
|
| | if isHighFacePlanar is True and len(subObjTups) > 1: |
| | starts.append(hzmax + obj.StepDown.Value) |
| | |
| | strDep1 = obj.StartDepth.Value + (hzmax - hzmin) |
| | if makeHighFace == 1: |
| | |
| | finDep1 = highFace.Shape.BoundBox.ZMin + obj.StepDown.Value |
| | else: |
| | |
| | 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: |
| | |
| | if makeHighFace == 1: |
| | |
| | strDep2 = lowFace.Shape.BoundBox.ZMax |
| | else: |
| | |
| | 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)) |
| |
|
| | |
| | cbi = len(cuts) - 1 |
| | pocket = (cuts[cbi], False, "3DPocket") |
| | 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 |
| |
|
| | |
| | for sub, face in subObjTups: |
| | |
| | |
| | subCnt += 1 |
| | com = face.CenterOfMass |
| | comList.append((sub, face, com)) |
| | sumCom = sumCom.add(com) |
| |
|
| | |
| | avgCom.x = sumCom.x / subCnt |
| | avgCom.y = sumCom.y / subCnt |
| | avgCom.z = sumCom.z / subCnt |
| |
|
| | |
| | for sub, face, com in comList: |
| | adjCom = com.sub(avgCom) |
| | mag = math.sqrt(adjCom.x**2 + adjCom.y**2) |
| | drctn = 0.0 |
| | |
| | if adjCom.x > 0.0: |
| | if adjCom.y > 0.0: |
| | 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)) |
| |
|
| | |
| | sortList = sorted(vectList, key=getDrctn) |
| |
|
| | |
| | for sub, face, mag, drctn in sortList: |
| | newList.append((sub, face)) |
| |
|
| | |
| | zmax = newList[0][1].BoundBox.ZMax |
| | idx = 0 |
| | for i in range(0, len(newList)): |
| | (sub, face) = newList[i] |
| | fIdx = getFaceIdx(sub) |
| | |
| | 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 = {} |
| |
|
| | |
| | for sub, face in subObjTups: |
| | touching[sub] = [] |
| |
|
| | |
| | numFaces = len(subObjTups) |
| | for nf in range(0, numFaces): |
| | checkoutList.append(nf) |
| |
|
| | for co in range(0, len(checkoutList)): |
| | if len(checkoutList) < 2: |
| | break |
| |
|
| | |
| | checkedOut1 = checkoutList.pop() |
| | searchedList.append(checkedOut1) |
| | (sub1, face1) = subObjTups[checkedOut1] |
| |
|
| | |
| | for co in range(0, len(checkoutList)): |
| | |
| | (sub2, face2) = subObjTups[co] |
| |
|
| | |
| | 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: |
| | Path.Log.debug( |
| | "{}.Edges[{}] connects at {}.Edges[{}]".format(sub1, ei1, sub2, ei2) |
| | ) |
| | shared.append((sub1, face1, ei1)) |
| | touching[sub1].append(ei1) |
| | touching[sub2].append(ei2) |
| | |
| | |
| | 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""" |
| | |
| | |
| | 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)) |
| | |
| | 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: |
| | |
| | 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: |
| | |
| | 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: |
| | |
| | |
| | (lw, hgh) = self.groupConnectedEdges(holding) |
| | low.extend(lw) |
| | high.extend(hgh) |
| | |
| | |
| | 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: |
| | Path.Log.info("New findCommonVertex()... ") |
| |
|
| | oIdx = 0 |
| | listOne = edge1.Vertexes |
| | listTwo = edge2.Vertexes |
| |
|
| | |
| | for o in listOne: |
| | if show is True: |
| | Path.Log.info(" one ({}, {}, {})".format(o.X, o.Y, o.Z)) |
| | for t in listTwo: |
| | if show is True: |
| | Path.Log.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: |
| | Path.Log.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) |
| |
|
| | |
| | h0 = holds.pop() |
| | grps.append([h0]) |
| | attachments = updateAttachments(grps) |
| |
|
| | while len(holds) > 0: |
| | if loops > 500: |
| | Path.Log.error("BREAK --- LOOPS LIMIT of 500 ---") |
| | break |
| | save = False |
| |
|
| | h2 = holds.pop() |
| | (sub2, face2, ei2) = holding[h2] |
| |
|
| | |
| | for g, t in attachments: |
| | h1 = grps[g][t] |
| | (sub1, face1, ei1) = holding[h1] |
| |
|
| | edg1 = face1.Edges[ei1] |
| | edg2 = face2.Edges[ei2] |
| |
|
| | |
| |
|
| | |
| | if t == 0: |
| | |
| | e2lv = len(edg2.Vertexes) - 1 |
| | one = edg2.Vertexes[e2lv] |
| | two = edg1.Vertexes[0] |
| | if isSameVertex(one, two) is True: |
| | |
| | grps[g].insert(0, h2) |
| | stop = True |
| | else: |
| | |
| | e1lv = len(edg1.Vertexes) - 1 |
| | one = edg1.Vertexes[e1lv] |
| | two = edg2.Vertexes[0] |
| | if isSameVertex(one, two) is True: |
| | |
| | grps[g].append(h2) |
| | stop = True |
| |
|
| | if stop is True: |
| | |
| | attachments = updateAttachments(grps) |
| | holds.extend(searched) |
| | stop = False |
| | break |
| | else: |
| | |
| | save = True |
| | |
| | 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) |
| | |
| | loops += 1 |
| | |
| |
|
| | 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 _identifyRemovalSolids(sourceShape, commonShapes): |
| | """_identifyRemovalSolids(sourceShape, commonShapes) |
| | Loops through solids in sourceShape to identify commonality with solids in commonShapes. |
| | The sourceShape solids with commonality are returned as Part.Compound shape.""" |
| | common = Part.makeCompound(commonShapes) |
| | removalSolids = [s for s in sourceShape.Solids if s.common(common).Volume > 0.0] |
| | return Part.makeCompound(removalSolids) |
| |
|
| |
|
| | def _extrudeBaseDown(base): |
| | """_extrudeBaseDown(base) |
| | Extrudes and fuses all non-vertical faces downward to a level 1.0 mm below base ZMin.""" |
| | allExtrusions = list() |
| | zMin = base.Shape.BoundBox.ZMin |
| | bbFace = Path.Geom.makeBoundBoxFace(base.Shape.BoundBox, offset=5.0) |
| | bbFace.translate(FreeCAD.Vector(0.0, 0.0, float(int(base.Shape.BoundBox.ZMin - 5.0)))) |
| | direction = FreeCAD.Vector(0.0, 0.0, -1.0) |
| |
|
| | |
| | for f in base.Shape.Faces: |
| | fbb = f.BoundBox |
| | if not Path.Geom.isRoughly(f.normalAt(0, 0).z, 0.0): |
| | pp = bbFace.makeParallelProjection(f.Wires[0], direction) |
| | face = Part.Face(Part.Wire(pp.Edges)) |
| | face.translate(FreeCAD.Vector(0.0, 0.0, fbb.ZMin)) |
| | ext = face.extrude(FreeCAD.Vector(0.0, 0.0, zMin - fbb.ZMin - 1.0)) |
| | allExtrusions.append(ext) |
| |
|
| | |
| | seed = allExtrusions.pop() |
| | fusion = seed.fuse(allExtrusions) |
| | fusion.translate(FreeCAD.Vector(0.0, 0.0, zMin - fusion.BoundBox.ZMin - 1.0)) |
| |
|
| | return fusion.cut(base.Shape) |
| |
|
| |
|
| | def SetupProperties(): |
| | return PathPocketBase.SetupProperties() + ["HandleMultipleFeatures"] |
| |
|
| |
|
| | def Create(name, obj=None, parentJob=None): |
| | """Create(name) ... Creates and returns a Pocket operation.""" |
| | if obj is None: |
| | obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) |
| | obj.Proxy = ObjectPocket(obj, name, parentJob) |
| | return obj |
| |
|