# -*- coding: utf-8 -*- # *************************************************************************** # * 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 PathScripts.PathEngraveBase as PathEngraveBase import PathScripts.PathGeom as PathGeom import PathScripts.PathLog as PathLog import PathScripts.PathOp as PathOp import PathScripts.PathOpTools as PathOpTools import math from PySide import QtCore # lazily loaded modules from lazy_loader.lazy_loader import LazyLoader Part = LazyLoader('Part', globals(), 'Part') __title__ = "Path Deburr Operation" __author__ = "sliptonic (Brad Collette), Schildkroet" __url__ = "http://www.freecadweb.org" __doc__ = "Deburr operation." PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) # PathLog.trackModule(PathLog.thisModule()) # Qt translation handling def translate(context, text, disambig=None): return QtCore.QCoreApplication.translate(context, text, disambig) 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 PathGeom.isRoughly(angle, 180) or PathGeom.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 PathGeom.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): PathLog.track(obj.Label) obj.addProperty('App::PropertyDistance', 'Width', 'Deburr', QtCore.QT_TRANSLATE_NOOP('PathDeburr', 'The desired width of the chamfer')) obj.addProperty('App::PropertyDistance', 'ExtraDepth', 'Deburr', QtCore.QT_TRANSLATE_NOOP('PathDeburr', 'The additional depth of the tool path')) obj.addProperty('App::PropertyEnumeration', 'Join', 'Deburr', QtCore.QT_TRANSLATE_NOOP('PathDeburr', 'How to join chamfer segments')) obj.Join = ['Round', 'Miter'] obj.setEditorMode('Join', 2) # hide for now obj.addProperty('App::PropertyEnumeration', 'Direction', 'Deburr', QtCore.QT_TRANSLATE_NOOP('PathDeburr', 'Direction of Operation')) obj.Direction = ['CW', 'CCW'] obj.addProperty('App::PropertyEnumeration', 'Side', 'Deburr', QtCore.QT_TRANSLATE_NOOP('PathDeburr', 'Side of Operation')) obj.Side = ['Outside', 'Inside'] obj.setEditorMode('Side', 2) # Hide property, it's calculated by op obj.addProperty('App::PropertyInteger', 'EntryPoint', 'Deburr', QtCore.QT_TRANSLATE_NOOP('PathDeburr', 'Select the segment, there the operations starts')) def opOnDocumentRestored(self, obj): obj.setEditorMode('Join', 2) # hide for now def opExecute(self, obj): PathLog.track(obj.Label) 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 PathLog.track(obj.Label, depth, offset) self.basewires = [] # pylint: disable=attribute-defined-outside-init self.adjusted_basewires = [] # pylint: disable=attribute-defined-outside-init 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 # pylint: disable=attribute-defined-outside-init for edgelist in Part.sortEdges(edges): basewires.append(Part.Wire(edgelist)) self.basewires.extend(basewires) for w in basewires: self.adjusted_basewires.append(w) wire = PathOpTools.offsetWire(w, base.Shape, offset, True) if wire: wires.append(wire) # Set direction of op forward = (obj.Direction == 'CW') 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) PathLog.track(obj.Label, depth, zValues) if obj.EntryPoint < 0: obj.EntryPoint = 0 self.wires = wires # pylint: disable=attribute-defined-outside-init 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): PathLog.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): '''Create(name) ... Creates and returns a Deburr operation.''' if obj is None: obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) obj.Proxy = ObjectDeburr(obj, name) return obj