Download src/Mod/CAM/Path/Op/MillFace.py from AbdulElahGwaith/FreeCAD: direct link, hf CLI and curl.
- Browser
- Download file 15.9 kB
-
https://huggingface.co/AbdulElahGwaith/FreeCAD/resolve/main/src/Mod/CAM/Path/Op/MillFace.py
- Command line
-
hf download hf://AbdulElahGwaith/FreeCAD/src/Mod/CAM/Path/Op/MillFace.py
-
curl -L -o MillFace.py https://huggingface.co/AbdulElahGwaith/FreeCAD/resolve/main/src/Mod/CAM/Path/Op/MillFace.py
15.9 kB
| # SPDX-License-Identifier: LGPL-2.1-or-later | |
| # *************************************************************************** | |
| # * Copyright (c) 2016 sliptonic <shopinthewoods@gmail.com> * | |
| # * * | |
| # * This program is free software; you can redistribute it and/or modify * | |
| # * it under the terms of the GNU Lesser General Public License (LGPL) * | |
| # * as published by the Free Software Foundation; either version 2 of * | |
| # * the License, or (at your option) any later version. * | |
| # * for detail see the LICENCE text file. * | |
| # * * | |
| # * This program is distributed in the hope that it will be useful, * | |
| # * but WITHOUT ANY WARRANTY; without even the implied warranty of * | |
| # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * | |
| # * GNU Library General Public License for more details. * | |
| # * * | |
| # * You should have received a copy of the GNU Library General Public * | |
| # * License along with this program; if not, write to the Free Software * | |
| # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * | |
| # * USA * | |
| # * * | |
| # *************************************************************************** | |
| import FreeCAD | |
| import Path | |
| import Path.Op.PocketBase as PathPocketBase | |
| import PathScripts.PathUtils as PathUtils | |
| from PySide.QtCore import QT_TRANSLATE_NOOP | |
| import numpy | |
| # lazily loaded modules | |
| from lazy_loader.lazy_loader import LazyLoader | |
| Part = LazyLoader("Part", globals(), "Part") | |
| __title__ = "CAM Mill Face Operation" | |
| __author__ = "sliptonic (Brad Collette)" | |
| __url__ = "https://www.freecad.org" | |
| __doc__ = "Class and implementation of Mill Facing operation." | |
| __contributors__ = "russ4262 (Russell Johnson)" | |
| 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 ObjectFace(PathPocketBase.ObjectPocket): | |
| """Proxy object for Mill Facing operation.""" | |
| def propertyEnumerations(self, dataType="data"): | |
| """helixOpPropertyEnumerations(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 = { | |
| "BoundaryShape": [ | |
| (translate("CAM_Pocket", "Boundbox"), "Boundbox"), | |
| (translate("CAM_Pocket", "Face Region"), "Face Region"), | |
| (translate("CAM_Pocket", "Perimeter"), "Perimeter"), | |
| (translate("CAM_Pocket", "Stock"), "Stock"), | |
| ], | |
| } | |
| 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 initPocketOp(self, obj): | |
| Path.Log.track() | |
| """initPocketOp(obj) ... create facing specific properties""" | |
| obj.addProperty( | |
| "App::PropertyEnumeration", | |
| "BoundaryShape", | |
| "Face", | |
| QT_TRANSLATE_NOOP("App::Property", "Shape to use for calculating Boundary"), | |
| ) | |
| obj.addProperty( | |
| "App::PropertyBool", | |
| "ClearEdges", | |
| "Face", | |
| QT_TRANSLATE_NOOP( | |
| "App::Property", "Clear edges of surface (Only applicable to BoundBox)" | |
| ), | |
| ) | |
| if not hasattr(obj, "ExcludeRaisedAreas"): | |
| obj.addProperty( | |
| "App::PropertyBool", | |
| "ExcludeRaisedAreas", | |
| "Face", | |
| QT_TRANSLATE_NOOP("App::Property", "Exclude milling raised areas inside the face."), | |
| ) | |
| for n in self.propertyEnumerations(): | |
| setattr(obj, n[0], n[1]) | |
| def pocketInvertExtraOffset(self): | |
| return True | |
| def areaOpOnChanged(self, obj, prop): | |
| """areaOpOnChanged(obj, prop) ... facing specific depths calculation.""" | |
| Path.Log.track(prop) | |
| if prop == "StepOver" and obj.StepOver == 0: | |
| obj.StepOver = 1 | |
| # default depths calculation not correct for facing | |
| if prop == "Base": | |
| job = PathUtils.findParentJob(obj) | |
| if job: | |
| obj.OpStartDepth = job.Stock.Shape.BoundBox.ZMax | |
| if len(obj.Base) >= 1: | |
| Path.Log.debug("processing") | |
| sublist = [] | |
| for i in obj.Base: | |
| o = i[0] | |
| for s in i[1]: | |
| sublist.append(o.Shape.getElement(s)) | |
| # If the operation has a geometry identified the Finaldepth | |
| # is the top of the bboundbox which includes all features. | |
| # Otherwise, top of part. | |
| obj.OpFinalDepth = Part.makeCompound(sublist).BoundBox.ZMax | |
| elif job: | |
| obj.OpFinalDepth = job.Proxy.modelBoundBox(job).ZMax | |
| def areaOpShapes(self, obj): | |
| """areaOpShapes(obj) ... return top face""" | |
| # Facing is done either against base objects | |
| self.removalshapes = [] | |
| holeShape = None | |
| Path.Log.debug("depthparams: {}".format([i for i in self.depthparams])) | |
| if obj.Base: | |
| Path.Log.debug("obj.Base: {}".format(obj.Base)) | |
| faces = [] | |
| holes = [] | |
| holeEnvs = [] | |
| oneBase = [obj.Base[0][0], True] | |
| sub0 = getattr(obj.Base[0][0].Shape, obj.Base[0][1][0]) | |
| minHeight = sub0.BoundBox.ZMax | |
| for b in obj.Base: | |
| for sub in b[1]: | |
| shape = getattr(b[0].Shape, sub) | |
| if isinstance(shape, Part.Face): | |
| faces.append(shape) | |
| if shape.BoundBox.ZMin < minHeight: | |
| minHeight = shape.BoundBox.ZMin | |
| # Limit to one model base per operation | |
| if oneBase[0] is not b[0]: | |
| oneBase[1] = False | |
| if numpy.isclose(abs(shape.normalAt(0, 0).z), 1): # horizontal face | |
| # Analyze internal closed wires to determine if raised or a recess | |
| for wire in shape.Wires[1:]: | |
| if obj.ExcludeRaisedAreas: | |
| ip = self.isPocket(b[0], shape, wire) | |
| if ip is False: | |
| holes.append((b[0].Shape, wire)) | |
| else: | |
| holes.append((b[0].Shape, wire)) | |
| else: | |
| Path.Log.warning( | |
| 'The base subobject, "{0}," is not a face. Ignoring "{0}."'.format(sub) | |
| ) | |
| if obj.ExcludeRaisedAreas and len(holes) > 0: | |
| for shape, wire in holes: | |
| f = Part.makeFace(wire, "Part::FaceMakerSimple") | |
| env = PathUtils.getEnvelope(shape, subshape=f, depthparams=self.depthparams) | |
| holeEnvs.append(env) | |
| holeShape = Part.makeCompound(holeEnvs) | |
| Path.Log.debug("Working on a collection of faces {}".format(faces)) | |
| planeshape = Part.makeCompound(faces) | |
| # If no base object, do planing of top surface of entire model | |
| else: | |
| planeshape = Part.makeCompound([base.Shape for base in self.model]) | |
| Path.Log.debug("Working on a shape {}".format(obj.Label)) | |
| # Find the correct shape depending on Boundary shape. | |
| Path.Log.debug("Boundary Shape: {}".format(obj.BoundaryShape)) | |
| bb = planeshape.BoundBox | |
| # Apply offset for clearing edges | |
| offset = 0 | |
| if obj.ClearEdges: | |
| offset = self.radius + 0.1 | |
| bb.XMin = bb.XMin - offset | |
| bb.YMin = bb.YMin - offset | |
| bb.XMax = bb.XMax + offset | |
| bb.YMax = bb.YMax + offset | |
| if obj.BoundaryShape == "Boundbox": | |
| bbperim = Part.makeBox( | |
| bb.XLength, | |
| bb.YLength, | |
| 1, | |
| FreeCAD.Vector(bb.XMin, bb.YMin, bb.ZMin), | |
| FreeCAD.Vector(0, 0, 1), | |
| ) | |
| env = PathUtils.getEnvelope(partshape=bbperim, depthparams=self.depthparams) | |
| if obj.ExcludeRaisedAreas and oneBase[1]: | |
| includedFaces = self.getAllIncludedFaces(oneBase[0], env, faceZ=minHeight) | |
| if len(includedFaces) > 0: | |
| includedShape = Part.makeCompound(includedFaces) | |
| includedEnv = PathUtils.getEnvelope( | |
| oneBase[0].Shape, | |
| subshape=includedShape, | |
| depthparams=self.depthparams, | |
| ) | |
| env = env.cut(includedEnv) | |
| elif obj.BoundaryShape == "Stock": | |
| stock = PathUtils.findParentJob(obj).Stock.Shape | |
| env = stock | |
| if obj.ExcludeRaisedAreas and oneBase[1]: | |
| includedFaces = self.getAllIncludedFaces(oneBase[0], stock, faceZ=minHeight) | |
| if len(includedFaces) > 0: | |
| stockEnv = PathUtils.getEnvelope(partshape=stock, depthparams=self.depthparams) | |
| includedShape = Part.makeCompound(includedFaces) | |
| includedEnv = PathUtils.getEnvelope( | |
| oneBase[0].Shape, | |
| subshape=includedShape, | |
| depthparams=self.depthparams, | |
| ) | |
| env = stockEnv.cut(includedEnv) | |
| elif obj.BoundaryShape == "Perimeter": | |
| if obj.ClearEdges: | |
| psZMin = planeshape.BoundBox.ZMin | |
| ofstShape = PathUtils.getOffsetArea( | |
| planeshape, self.radius * 1.25, plane=planeshape | |
| ) | |
| ofstShape.translate(FreeCAD.Vector(0.0, 0.0, psZMin - ofstShape.BoundBox.ZMin)) | |
| env = PathUtils.getEnvelope(partshape=ofstShape, depthparams=self.depthparams) | |
| else: | |
| env = PathUtils.getEnvelope(partshape=planeshape, depthparams=self.depthparams) | |
| elif obj.BoundaryShape == "Face Region": | |
| baseShape = planeshape # oneBase[0].Shape | |
| psZMin = planeshape.BoundBox.ZMin | |
| ofst = 0.0 | |
| if obj.ClearEdges: | |
| ofst = self.tool.Diameter * 0.51 | |
| ofstShape = PathUtils.getOffsetArea(planeshape, ofst, plane=planeshape) | |
| ofstShape.translate(FreeCAD.Vector(0.0, 0.0, psZMin - ofstShape.BoundBox.ZMin)) | |
| # Calculate custom depth params for removal shape envelope, with start and final depth buffers | |
| custDepthparams = self._customDepthParams( | |
| obj, obj.StartDepth.Value + 0.2, obj.FinalDepth.Value - 0.1 | |
| ) # only an envelope | |
| ofstShapeEnv = PathUtils.getEnvelope(partshape=ofstShape, depthparams=custDepthparams) | |
| if obj.ExcludeRaisedAreas: | |
| env = ofstShapeEnv.cut(baseShape) | |
| env.translate( | |
| FreeCAD.Vector(0.0, 0.0, -0.00001) | |
| ) # lower removal shape into buffer zone | |
| else: | |
| env = ofstShapeEnv | |
| if holeShape: | |
| Path.Log.debug("Processing holes and face ...") | |
| holeEnv = PathUtils.getEnvelope(partshape=holeShape, depthparams=self.depthparams) | |
| newEnv = env.cut(holeEnv) | |
| tup = newEnv, False, "pathMillFace" | |
| else: | |
| Path.Log.debug("Processing solid face ...") | |
| tup = env, False, "pathMillFace" | |
| self.removalshapes.append(tup) | |
| obj.removalshape = self.removalshapes[0][0] # save removal shape | |
| return self.removalshapes | |
| def areaOpSetDefaultValues(self, obj, job): | |
| """areaOpSetDefaultValues(obj, job) ... initialize mill facing properties""" | |
| obj.StepOver = 50 | |
| obj.ZigZagAngle = 45.0 | |
| obj.ExcludeRaisedAreas = False | |
| obj.ClearEdges = False | |
| # need to overwrite the default depth calculations for facing | |
| if job and len(job.Model.Group) > 0: | |
| obj.OpStartDepth = job.Stock.Shape.BoundBox.ZMax | |
| obj.OpFinalDepth = job.Proxy.modelBoundBox(job).ZMax | |
| # If the operation has a geometry identified the Finaldepth | |
| # is the top of the boundbox which includes all features. | |
| if len(obj.Base) >= 1: | |
| shapes = [] | |
| for base, subs in obj.Base: | |
| for s in subs: | |
| shapes.append(getattr(base.Shape, s)) | |
| obj.OpFinalDepth = Part.makeCompound(shapes).BoundBox.ZMax | |
| def isPocket(self, b, f, w): | |
| e = w.Edges[0] | |
| for fi in range(0, len(b.Shape.Faces)): | |
| face = b.Shape.Faces[fi] | |
| for ei in range(0, len(face.Edges)): | |
| edge = face.Edges[ei] | |
| if e.isSame(edge): | |
| if f is face: | |
| # Alternative: run loop to see if all edges are same | |
| pass # same source face, look for another | |
| else: | |
| if face.CenterOfMass.z < f.CenterOfMass.z: | |
| return True | |
| return False | |
| def getAllIncludedFaces(self, base, env, faceZ): | |
| """getAllIncludedFaces(base, env, faceZ)... | |
| Return all `base` faces extending above `faceZ` whose boundboxes overlap with the `env` boundbox. | |
| """ | |
| included = [] | |
| eXMin = env.BoundBox.XMin | |
| eXMax = env.BoundBox.XMax | |
| eYMin = env.BoundBox.YMin | |
| eYMax = env.BoundBox.YMax | |
| eZMin = faceZ | |
| def isOverlap(faceMin, faceMax, envMin, envMax): | |
| if faceMax > envMin: | |
| if faceMin <= envMax or faceMin == envMin: | |
| return True | |
| return False | |
| for fi in range(0, len(base.Shape.Faces)): | |
| # Check all faces of `base` shape | |
| incl = False | |
| face = base.Shape.Faces[fi] | |
| fXMin = face.BoundBox.XMin | |
| fXMax = face.BoundBox.XMax | |
| fYMin = face.BoundBox.YMin | |
| fYMax = face.BoundBox.YMax | |
| fZMax = face.BoundBox.ZMax | |
| if fZMax > eZMin: | |
| # Include face if its boundbox overlaps envelope boundbox | |
| if isOverlap(fXMin, fXMax, eXMin, eXMax): # check X values | |
| if isOverlap(fYMin, fYMax, eYMin, eYMax): # check Y values | |
| incl = True | |
| if incl: | |
| included.append(face) | |
| return included | |
| def SetupProperties(): | |
| setup = PathPocketBase.SetupProperties() | |
| setup.append("BoundaryShape") | |
| setup.append("ExcludeRaisedAreas") | |
| setup.append("ClearEdges") | |
| return setup | |
| def Create(name, obj=None, parentJob=None): | |
| """Create(name) ... Creates and returns a Mill Facing operation.""" | |
| if obj is None: | |
| obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) | |
| obj.Proxy = ObjectFace(obj, name, parentJob) | |
| return obj | |