# SPDX-License-Identifier: LGPL-2.1-or-later # *************************************************************************** # * Copyright (c) 2018 sliptonic * # * Copyright (c) 2020-2021 Schildkroet * # * * # * 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.Base as PathOp import Path.Op.EngraveBase as PathEngraveBase import Path.Op.Util as PathOpUtil import math from PySide.QtCore import QT_TRANSLATE_NOOP # lazily loaded modules from lazy_loader.lazy_loader import LazyLoader Part = LazyLoader("Part", globals(), "Part") __title__ = "CAM Deburr Operation" __author__ = "sliptonic (Brad Collette), Schildkroet" __url__ = "https://www.freecad.org" __doc__ = "Deburr operation." 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 def toolDepthAndOffset(width, extraDepth, tool, printInfo): """toolDepthAndOffset(width, extraDepth, tool) ... return tuple for given\n parameters.""" if not hasattr(tool, "Diameter"): raise ValueError("Deburr requires tool with diameter\n") suppressInfo = False if hasattr(tool, "CuttingEdgeAngle"): angle = float(tool.CuttingEdgeAngle) if Path.Geom.isRoughly(angle, 180) or Path.Geom.isRoughly(angle, 0): angle = 180 toolOffset = float(tool.Diameter) / 2 else: if hasattr(tool, "TipDiameter"): toolOffset = float(tool.TipDiameter) / 2 elif hasattr(tool, "FlatRadius"): toolOffset = float(tool.FlatRadius) else: toolOffset = 0.0 if printInfo and not suppressInfo: FreeCAD.Console.PrintMessage( translate( "PathDeburr", "The selected tool has no FlatRadius and no TipDiameter property. Assuming {}\n".format( "Endmill" if angle == 180 else "V-Bit" ), ) ) suppressInfo = True else: angle = 180 toolOffset = float(tool.Diameter) / 2 if printInfo: FreeCAD.Console.PrintMessage( translate( "PathDeburr", "The selected tool has no CuttingEdgeAngle property. Assuming Endmill\n", ) ) suppressInfo = True tan = math.tan(math.radians(angle / 2)) toolDepth = 0 if Path.Geom.isRoughly(tan, 0) else width / tan depth = toolDepth + extraDepth extraOffset = -width if angle == 180 else (extraDepth * tan) offset = toolOffset + extraOffset return (depth, offset, extraOffset, suppressInfo) class ObjectDeburr(PathEngraveBase.ObjectOp): """Proxy class for Deburr operation.""" def opFeatures(self, obj): return ( PathOp.FeatureTool | PathOp.FeatureHeights | PathOp.FeatureStepDown | PathOp.FeatureBaseEdges | PathOp.FeatureBaseFaces | PathOp.FeatureCoolant | PathOp.FeatureBaseGeometry ) def initOperation(self, obj): Path.Log.track(obj.Label) obj.addProperty( "App::PropertyDistance", "Width", "Deburr", QT_TRANSLATE_NOOP("App::Property", "The desired width of the chamfer"), ) obj.addProperty( "App::PropertyDistance", "ExtraDepth", "Deburr", QT_TRANSLATE_NOOP("App::Property", "The additional depth of the toolpath"), ) obj.addProperty( "App::PropertyEnumeration", "Join", "Deburr", QT_TRANSLATE_NOOP("App::Property", "How to join chamfer segments"), ) # obj.Join = ["Round", "Miter"] obj.setEditorMode("Join", 2) # hide for now obj.addProperty( "App::PropertyEnumeration", "Direction", "Deburr", QT_TRANSLATE_NOOP("App::Property", "Direction of toolpath"), ) # obj.Direction = ["CW", "CCW"] obj.addProperty( "App::PropertyEnumeration", "Side", "Deburr", QT_TRANSLATE_NOOP("App::Property", "Side of base object"), ) obj.Side = ["Outside", "Inside"] obj.setEditorMode("Side", 2) # Hide property, it's calculated by op obj.addProperty( "App::PropertyInteger", "EntryPoint", "Deburr", QT_TRANSLATE_NOOP("App::Property", "The segment where the toolpath starts"), ) ENUMS = self.propertyEnumerations() for n in ENUMS: setattr(obj, n[0], n[1]) @classmethod def propertyEnumerations(self, dataType="data"): """opPropertyEnumerations(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 """ # Enumeration lists for App::PropertyEnumeration properties enums = { "Direction": [ (translate("Path", "CW"), "CW"), (translate("Path", "CCW"), "CCW"), ], # this is the direction that the profile runs "Join": [ (translate("PathDeburr", "Round"), "Round"), (translate("PathDeburr", "Miter"), "Miter"), ], # this is the direction that the profile runs } 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[k] = [tup[idx] for tup in v] data.append((v, [tup[idx] for tup in enums[v]])) Path.Log.debug(data) return data def opOnDocumentRestored(self, obj): obj.setEditorMode("Join", 2) # hide for now def opExecute(self, obj): Path.Log.track(obj.Label) if not obj.Base: return if not hasattr(self, "printInfo"): self.printInfo = True try: (depth, offset, extraOffset, suppressInfo) = toolDepthAndOffset( obj.Width.Value, obj.ExtraDepth.Value, self.tool, self.printInfo ) self.printInfo = not suppressInfo except ValueError as e: msg = "{} \n No path will be generated".format(e) raise ValueError(msg) # QtGui.QMessageBox.information(None, "Tool Error", msg) # return Path.Log.track(obj.Label, depth, offset) self.basewires = [] self.adjusted_basewires = [] wires = [] for base, subs in obj.Base: edges = [] basewires = [] max_h = -99999 radius_top = 0 radius_bottom = 0 for f in subs: sub = base.Shape.getElement(f) if type(sub) == Part.Edge: # Edge edges.append(sub) elif type(sub) == Part.Face and sub.normalAt(0, 0) != FreeCAD.Vector( 0, 0, 1 ): # Angled face # If an angled face is selected, the lower edge is projected to the height of the upper edge, # to simulate an edge # Find z value of upper edge for edge in sub.Edges: for p0 in edge.Vertexes: if p0.Point.z > max_h: max_h = p0.Point.z # Find biggest radius for top/bottom for edge in sub.Edges: if Part.Circle == type(edge.Curve): if edge.Vertexes[0].Point.z == max_h: if edge.Curve.Radius > radius_top: radius_top = edge.Curve.Radius else: if edge.Curve.Radius > radius_bottom: radius_bottom = edge.Curve.Radius # Search for lower edge and raise it to height of upper edge for edge in sub.Edges: if Part.Circle == type(edge.Curve): # Edge is a circle if edge.Vertexes[0].Point.z < max_h: if edge.Closed: # Circle # New center center = FreeCAD.Vector( edge.Curve.Center.x, edge.Curve.Center.y, max_h ) new_edge = Part.makeCircle( edge.Curve.Radius, center, FreeCAD.Vector(0, 0, 1), ) edges.append(new_edge) # Modify offset for inner angled faces if radius_bottom < radius_top: offset -= 2 * extraOffset break else: # Arc if edge.Vertexes[0].Point.z == edge.Vertexes[1].Point.z: # Arc vertexes are on same layer l1 = math.sqrt( (edge.Vertexes[0].Point.x - edge.Curve.Center.x) ** 2 + (edge.Vertexes[0].Point.y - edge.Curve.Center.y) ** 2 ) l2 = math.sqrt( (edge.Vertexes[1].Point.x - edge.Curve.Center.x) ** 2 + (edge.Vertexes[1].Point.y - edge.Curve.Center.y) ** 2 ) # New center center = FreeCAD.Vector( edge.Curve.Center.x, edge.Curve.Center.y, max_h, ) # Calculate angles based on x-axis (0 - PI/2) start_angle = math.acos( (edge.Vertexes[0].Point.x - edge.Curve.Center.x) / l1 ) end_angle = math.acos( (edge.Vertexes[1].Point.x - edge.Curve.Center.x) / l2 ) # Angles are based on x-axis (Mirrored on x-axis) -> negative y value means negative angle if edge.Vertexes[0].Point.y < edge.Curve.Center.y: start_angle *= -1 if edge.Vertexes[1].Point.y < edge.Curve.Center.y: end_angle *= -1 # Create new arc new_edge = Part.ArcOfCircle( Part.Circle( center, FreeCAD.Vector(0, 0, 1), edge.Curve.Radius, ), start_angle, end_angle, ).toShape() edges.append(new_edge) # Modify offset for inner angled faces if radius_bottom < radius_top: offset -= 2 * extraOffset break else: # Line if ( edge.Vertexes[0].Point.z == edge.Vertexes[1].Point.z and edge.Vertexes[0].Point.z < max_h ): new_edge = Part.Edge( Part.LineSegment( FreeCAD.Vector( edge.Vertexes[0].Point.x, edge.Vertexes[0].Point.y, max_h, ), FreeCAD.Vector( edge.Vertexes[1].Point.x, edge.Vertexes[1].Point.y, max_h, ), ) ) edges.append(new_edge) elif sub.Wires: basewires.extend(sub.Wires) else: # Flat face basewires.append(Part.Wire(sub.Edges)) self.edges = edges for edgelist in Part.sortEdges(edges): basewires.append(Part.Wire(edgelist)) self.basewires.extend(basewires) # Set default side side = ["Outside"] for w in basewires: self.adjusted_basewires.append(w) wire = PathOpUtil.offsetWire(w, base.Shape, offset, True, side) if wire: wires.append(wire) # Set direction of op forward = obj.Direction == "CW" # Set value of side obj.Side = side[0] # Check side extra for angled faces if radius_top > radius_bottom: obj.Side = "Inside" zValues = [] z = 0 if obj.StepDown.Value != 0: while z + obj.StepDown.Value < depth: z = z + obj.StepDown.Value zValues.append(z) zValues.append(depth) Path.Log.track(obj.Label, depth, zValues) if obj.EntryPoint < 0: obj.EntryPoint = 0 self.wires = wires self.buildpathocc(obj, wires, zValues, True, forward, obj.EntryPoint) def opRejectAddBase(self, obj, base, sub): """The chamfer op can only deal with features of the base model, all others are rejected.""" return base not in self.model def opSetDefaultValues(self, obj, job): Path.Log.track(obj.Label, job.Label) obj.Width = "1 mm" obj.ExtraDepth = "0.5 mm" obj.Join = "Round" obj.setExpression("StepDown", "0 mm") obj.StepDown = "0 mm" obj.Direction = "CW" obj.Side = "Outside" obj.EntryPoint = 0 def SetupProperties(): setup = [] setup.append("Width") setup.append("ExtraDepth") return setup def Create(name, obj=None, parentJob=None): """Create(name) ... Creates and returns a Deburr operation.""" if obj is None: obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) obj.Proxy = ObjectDeburr(obj, name, parentJob) return obj