FreeCAD / src /Mod /CAM /Path /Op /Deburr.py
AbdulElahGwaith's picture
Upload folder using huggingface_hub
985c397 verified
# SPDX-License-Identifier: LGPL-2.1-or-later
# ***************************************************************************
# * Copyright (c) 2018 sliptonic <shopinthewoods@gmail.com> *
# * 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