diff --git a/src/Mod/CAM/CAMTests/TestPathTapGenerator.py b/src/Mod/CAM/CAMTests/TestPathTapGenerator.py new file mode 100644 index 0000000000..527d23f97b --- /dev/null +++ b/src/Mod/CAM/CAMTests/TestPathTapGenerator.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2021 sliptonic * +# * * +# * 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 LICENSE 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 Part +import Path +import Path.Base.Generator.tapping as generator +import CAMTests.PathTestUtils as PathTestUtils + +Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) +Path.Log.trackModule(Path.Log.thisModule()) + + +class TestPathTapGenerator(PathTestUtils.PathTestBase): + def test00(self): + """Test Basic Tap Generator Return""" + v1 = FreeCAD.Vector(0, 0, 10) + v2 = FreeCAD.Vector(0, 0, 0) + + e = Part.makeLine(v1, v2) + + result = generator.generate(e) + + self.assertTrue(type(result) is list) + self.assertTrue(type(result[0]) is Path.Command) + + command = result[0] + + self.assertTrue(command.Name == "G84") + self.assertTrue(command.Parameters["R"] == 10) + self.assertTrue(command.Parameters["X"] == 0) + self.assertTrue(command.Parameters["Y"] == 0) + self.assertTrue(command.Parameters["Z"] == 0) + + # repeat must be > 0 + args = {"edge": e, "repeat": 0} + self.assertRaises(ValueError, generator.generate, **args) + + # repeat must be integer + args = {"edge": e, "repeat": 1.5} + self.assertRaises(ValueError, generator.generate, **args) + + def test10(self): + """Test edge alignment check""" + v1 = FreeCAD.Vector(0, 10, 10) + v2 = FreeCAD.Vector(0, 0, 0) + e = Part.makeLine(v1, v2) + self.assertRaises(ValueError, generator.generate, e) + + v1 = FreeCAD.Vector(0, 0, 0) + v2 = FreeCAD.Vector(0, 0, 10) + e = Part.makeLine(v1, v2) + + self.assertRaises(ValueError, generator.generate, e) + + def test30(self): + """Test Basic Dwell Tap Generator Return""" + v1 = FreeCAD.Vector(0, 0, 10) + v2 = FreeCAD.Vector(0, 0, 0) + + e = Part.makeLine(v1, v2) + + result = generator.generate(e, dwelltime=0.5) + + self.assertTrue(type(result) is list) + self.assertTrue(type(result[0]) is Path.Command) + + command = result[0] + + self.assertTrue(command.Name == "G84") + self.assertTrue(command.Parameters["P"] == 0.5) + + # dwelltime should be a float + args = {"edge": e, "dwelltime": 1} + self.assertRaises(ValueError, generator.generate, **args) + + def test40(self): + """Specifying retract height should set R parameter to specified value""" + v1 = FreeCAD.Vector(0, 0, 10) + v2 = FreeCAD.Vector(0, 0, 0) + + e = Part.makeLine(v1, v2) + + result = generator.generate(e, retractheight=20.0) + + command = result[0] + + self.assertTrue(command.Parameters["R"] == 20.0) + + def test41(self): + """Not specifying retract height should set R parameter to Z position of start point""" + v1 = FreeCAD.Vector(0, 0, 10) + v2 = FreeCAD.Vector(0, 0, 0) + + e = Part.makeLine(v1, v2) + + result = generator.generate(e) + + command = result[0] + + self.assertTrue(command.Parameters["R"] == 10.0) + + def test44(self): + """Non-float retract height should raise ValueError""" + v1 = FreeCAD.Vector(0, 0, 10) + v2 = FreeCAD.Vector(0, 0, 0) + + e = Part.makeLine(v1, v2) + + args = {"edge": e, "retractheight": 1} + self.assertRaises(ValueError, generator.generate, **args) + args = {"edge": e, "retractheight": "1"} + self.assertRaises(ValueError, generator.generate, **args) diff --git a/src/Mod/CAM/CMakeLists.txt b/src/Mod/CAM/CMakeLists.txt index 48b6bca114..578ca2e551 100644 --- a/src/Mod/CAM/CMakeLists.txt +++ b/src/Mod/CAM/CMakeLists.txt @@ -142,6 +142,7 @@ SET(PathPythonPostScripts_SRCS Path/Post/scripts/comparams_post.py Path/Post/scripts/dxf_post.py Path/Post/scripts/dynapath_post.py + Path/Post/scripts/dynapath_4060_post.py Path/Post/scripts/estlcam_post.py Path/Post/scripts/example_pre.py Path/Post/scripts/fablin_post.py @@ -195,6 +196,7 @@ SET(PathPythonOp_SRCS Path/Op/Slot.py Path/Op/Surface.py Path/Op/SurfaceSupport.py + Path/Op/Tapping.py Path/Op/ThreadMilling.py Path/Op/Util.py Path/Op/Vcarve.py @@ -226,6 +228,7 @@ SET(PathPythonOpGui_SRCS Path/Op/Gui/Slot.py Path/Op/Gui/Stop.py Path/Op/Gui/Surface.py + Path/Op/Gui/Tapping.py Path/Op/Gui/ThreadMilling.py Path/Op/Gui/Vcarve.py Path/Op/Gui/Waterline.py @@ -244,6 +247,7 @@ SET(PathPythonBaseGenerator_SRCS Path/Base/Generator/drill.py Path/Base/Generator/helix.py Path/Base/Generator/rotation.py + Path/Base/Generator/tapping.py Path/Base/Generator/threadmilling.py Path/Base/Generator/toolchange.py ) @@ -258,6 +262,7 @@ SET(Tools_SRCS ) SET(Tools_Bit_SRCS + Tools/Bit/375-16_Tap.fctb Tools/Bit/45degree_chamfer.fctb Tools/Bit/5mm-thread-cutter.fctb Tools/Bit/5mm_Drill.fctb @@ -282,6 +287,7 @@ SET(Tools_Shape_SRCS Tools/Shape/endmill.fcstd Tools/Shape/probe.fcstd Tools/Shape/slittingsaw.fcstd + Tools/Shape/tap.fcstd Tools/Shape/thread-mill.fcstd Tools/Shape/v-bit.fcstd ) @@ -329,6 +335,7 @@ SET(Tests_SRCS CAMTests/TestPathRotationGenerator.py CAMTests/TestPathSetupSheet.py CAMTests/TestPathStock.py + CAMTests/TestPathTapGenerator.py CAMTests/TestPathToolChangeGenerator.py CAMTests/TestPathThreadMilling.py CAMTests/TestPathThreadMillingGenerator.py diff --git a/src/Mod/CAM/Gui/Resources/Path.qrc b/src/Mod/CAM/Gui/Resources/Path.qrc index 07cf8cedf0..b8462b1868 100644 --- a/src/Mod/CAM/Gui/Resources/Path.qrc +++ b/src/Mod/CAM/Gui/Resources/Path.qrc @@ -52,6 +52,7 @@ icons/CAM_SimulatorGL.svg icons/CAM_Slot.svg icons/CAM_Stop.svg + icons/CAM_Tapping.svg icons/CAM_ThreadMilling.svg icons/CAM_ToolBit.svg icons/CAM_ToolChange.svg @@ -108,6 +109,7 @@ panels/PageOpProfileFullEdit.ui panels/PageOpSlotEdit.ui panels/PageOpSurfaceEdit.ui + panels/PageOpTappingEdit.ui panels/PageOpThreadMillingEdit.ui panels/PageOpWaterlineEdit.ui panels/PageOpVcarveEdit.ui diff --git a/src/Mod/CAM/Gui/Resources/icons/CAM_Tapping.svg b/src/Mod/CAM/Gui/Resources/icons/CAM_Tapping.svg new file mode 100644 index 0000000000..aeda170b5e --- /dev/null +++ b/src/Mod/CAM/Gui/Resources/icons/CAM_Tapping.svg @@ -0,0 +1,688 @@ + + + + + CAM_Tapping.svg + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + CAM_Tapping.svg + Path_Drilling + 2015-07-04 + http://www.freecadweb.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/Mod/Path/Gui/Resources/icons/CAM_Tapping.svg + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html + + + [agryson] Alexander Gryson + + + + + + [luvtofish] Dan Henderson + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Mod/CAM/Gui/Resources/panels/PageOpTappingEdit.ui b/src/Mod/CAM/Gui/Resources/panels/PageOpTappingEdit.ui new file mode 100644 index 0000000000..dadc2288d4 --- /dev/null +++ b/src/Mod/CAM/Gui/Resources/panels/PageOpTappingEdit.ui @@ -0,0 +1,145 @@ + + + Form + + + + 0 + 0 + 532 + 351 + + + + Form + + + + + + + + false + + + Time + + + + + + + Dwell + + + + + + + + None + + + + + Tap Tip + + + + + 2x Tap Tip + + + + + + + + false + + + + + + + Extend Depth + + + + + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + <html><head/><body><p>The tool and its settings to be used for this operation.</p></body></html> + + + + + + + Coolant Mode + + + + + + + ToolController + + + + + + + <html><head/><body><p>The tool and its settings to be used for this operation.</p></body></html> + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Gui::QuantitySpinBox + QWidget +
Gui/QuantitySpinBox.h
+
+
+ + toolController + dwellEnabled + + + +
diff --git a/src/Mod/CAM/InitGui.py b/src/Mod/CAM/InitGui.py index 6bc0ed8ffe..aef883fef9 100644 --- a/src/Mod/CAM/InitGui.py +++ b/src/Mod/CAM/InitGui.py @@ -113,13 +113,13 @@ class CAMWorkbench(Workbench): twodopcmdlist = [ "CAM_Profile", "CAM_Pocket_Shape", - "CAM_Drilling", "CAM_MillFace", "CAM_Helix", "CAM_Adaptive", ] threedopcmdlist = ["CAM_Pocket3D"] engravecmdlist = ["CAM_Engrave", "CAM_Deburr", "CAM_Vcarve"] + drillingcmdlist = ["CAM_Drilling", "CAM_Tapping"] modcmdlist = ["CAM_OperationCopy", "CAM_Array", "CAM_SimpleCopy"] dressupcmdlist = [ "CAM_DressupAxisMap", @@ -145,7 +145,14 @@ class CAMWorkbench(Workbench): QT_TRANSLATE_NOOP("CAM_EngraveTools", "Engraving Operations"), ), ) - + drillingcmdgroup = ["CAM_DrillingTools"] + FreeCADGui.addCommand( + "CAM_DrillingTools", + PathCommandGroup( + drillingcmdlist, + QT_TRANSLATE_NOOP("CAM_DrillingTools", "Drilling Operations"), + ), + ) threedcmdgroup = threedopcmdlist if Path.Preferences.experimentalFeaturesEnabled(): prepcmdlist.append("CAM_Shape") @@ -196,7 +203,7 @@ class CAMWorkbench(Workbench): self.appendToolbar(QT_TRANSLATE_NOOP("Workbench", "Tool Commands"), toolcmdlist) self.appendToolbar( QT_TRANSLATE_NOOP("Workbench", "New Operations"), - twodopcmdlist + engravecmdgroup + threedcmdgroup, + twodopcmdlist + drillingcmdgroup + engravecmdgroup + threedcmdgroup, ) self.appendToolbar(QT_TRANSLATE_NOOP("Workbench", "Path Modification"), modcmdlist) if extracmdlist: @@ -210,6 +217,7 @@ class CAMWorkbench(Workbench): + toolbitcmdlist + ["Separator"] + twodopcmdlist + + drillingcmdlist + engravecmdlist + ["Separator"] + threedopcmdlist diff --git a/src/Mod/CAM/Path/Base/Generator/tapping.py b/src/Mod/CAM/Path/Base/Generator/tapping.py new file mode 100644 index 0000000000..08a0216b00 --- /dev/null +++ b/src/Mod/CAM/Path/Base/Generator/tapping.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2021 sliptonic * +# * Copyright (c) 2023 luvtofish * +# * * +# * 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 LICENSE 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 Path +import numpy + +__title__ = "Tapping Path Generator" +__author__ = "sliptonic (Brad Collette)" +__url__ = "https://www.freecadweb.org" +__doc__ = "Generates the Tapping toolpath for a single spotshape" +__contributors__ = "luvtofish (Dan Henderson)" + + +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()) + + +def generate(edge, dwelltime=0.0, repeat=1, retractheight=None, righthand=True): + """ + Generates Gcode for tapping a single hole. + + Takes as input an edge. It assumes the edge is trivial with just two vectors. + The edge must be aligned with the Z axes (Vector(0,0,1)) or it is an error. + + The first vertex of the edge will be the startpoint + The second vertex of the edge will be the endpoint. + All other vertices are ignored. + + additionally, you can pass in a dwelltime,and repeat value. + + These will result in appropriate G74 and G84 codes. + + """ + startPoint = edge.Vertexes[0].Point + endPoint = edge.Vertexes[1].Point + + Path.Log.debug(startPoint) + Path.Log.debug(endPoint) + + Path.Log.debug(numpy.isclose(startPoint.sub(endPoint).x, 0, rtol=1e-05, atol=1e-06)) + Path.Log.debug(numpy.isclose(startPoint.sub(endPoint).y, 0, rtol=1e-05, atol=1e-06)) + Path.Log.debug(endPoint) + + if repeat < 1: + raise ValueError("repeat must be 1 or greater") + + if not type(repeat) is int: + raise ValueError("repeat value must be an integer") + + if not type(dwelltime) is float: + raise ValueError("dwelltime must be a float") + + if retractheight is not None and not type(retractheight) is float: + raise ValueError("retractheight must be a float") + + if not ( + numpy.isclose(startPoint.sub(endPoint).x, 0, rtol=1e-05, atol=1e-06) + and (numpy.isclose(startPoint.sub(endPoint).y, 0, rtol=1e-05, atol=1e-06)) + ): + raise ValueError("edge is not aligned with Z axis") + + if startPoint.z < endPoint.z: + raise ValueError("start point is below end point") + + cmdParams = {} + cmdParams["X"] = startPoint.x + cmdParams["Y"] = startPoint.y + cmdParams["Z"] = endPoint.z + cmdParams["R"] = retractheight if retractheight is not None else startPoint.z + + if repeat < 1: + raise ValueError("repeat must be 1 or greater") + + if not type(repeat) is int: + raise ValueError("repeat value must be an integer") + + if repeat > 1: + cmdParams["L"] = repeat + + if dwelltime > 0.0: + cmdParams["P"] = dwelltime + + # Check if tool is lefthand or righthand, set appropriate G-code + if not (righthand): + cmd = "G74" + else: + cmd = "G84" + + return [Path.Command(cmd, cmdParams)] diff --git a/src/Mod/CAM/Path/GuiInit.py b/src/Mod/CAM/Path/GuiInit.py index 533a0da7cd..65a7b798d1 100644 --- a/src/Mod/CAM/Path/GuiInit.py +++ b/src/Mod/CAM/Path/GuiInit.py @@ -73,6 +73,7 @@ def Startup(): from Path.Op.Gui import SimpleCopy from Path.Op.Gui import Slot from Path.Op.Gui import Stop + from Path.Op.Gui import Tapping from Path.Op.Gui import ThreadMilling from Path.Op.Gui import Vcarve from Path.Post import Command diff --git a/src/Mod/CAM/Path/Op/Gui/Selection.py b/src/Mod/CAM/Path/Op/Gui/Selection.py index e36e0b387d..547a6521e4 100644 --- a/src/Mod/CAM/Path/Op/Gui/Selection.py +++ b/src/Mod/CAM/Path/Op/Gui/Selection.py @@ -7,7 +7,7 @@ # * 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. * +# * for detail see the LICENTE text file. * # * * # * This program is distributed in the hope that it will be useful, * # * but WITHOUT ANY WARRANTY; without even the implied warranty of * @@ -29,7 +29,7 @@ import Path import Path.Base.Drillable as Drillable import math -Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) +Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) Path.Log.trackModule(Path.Log.thisModule()) @@ -132,6 +132,18 @@ class DRILLGate(PathBaseGate): return Drillable.isDrillable(shape, subobj, vector=None, allowPartial=True) +class TAPGate(PathBaseGate): + def allow(self, doc, obj, sub): + Path.Log.debug("obj: {} sub: {}".format(obj, sub)) + if not hasattr(obj, "Shape"): + return False + shape = obj.Shape + subobj = shape.getElement(sub) + if subobj.ShapeType not in ["Edge", "Face"]: + return False + return Drillable.isDrillable(shape, subobj, vector=None) + + class FACEGate(PathBaseGate): def allow(self, doc, obj, sub): isFace = False @@ -270,6 +282,12 @@ def drillselect(): FreeCAD.Console.PrintWarning("Drilling Select Mode\n") +def tapselect(): + FreeCADGui.Selection.addSelectionGate(TAPGate()) + if not Path.Preferences.suppressSelectionModeWarning(): + FreeCAD.Console.PrintWarning("Tapping Select Mode\n") + + def engraveselect(): FreeCADGui.Selection.addSelectionGate(ENGRAVEGate()) if not Path.Preferences.suppressSelectionModeWarning(): @@ -349,6 +367,7 @@ def select(op): opsel["Contour"] = contourselect # deprecated opsel["Deburr"] = chamferselect opsel["Drilling"] = drillselect + opsel["Tapping"] = tapselect opsel["Engrave"] = engraveselect opsel["Helix"] = drillselect opsel["MillFace"] = pocketselect diff --git a/src/Mod/CAM/Path/Op/Gui/Tapping.py b/src/Mod/CAM/Path/Op/Gui/Tapping.py new file mode 100644 index 0000000000..c0f51bc0fb --- /dev/null +++ b/src/Mod/CAM/Path/Op/Gui/Tapping.py @@ -0,0 +1,187 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2017 sliptonic * +# * * +# * 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 FreeCADGui +import Path +import Path.Base.Gui.Util as PathGuiUtil +import Path.Op.Tapping as PathTapping +import Path.Op.Gui.Base as PathOpGui +import Path.Op.Gui.CircularHoleBase as PathCircularHoleBaseGui +import PathGui + +from PySide import QtCore + +__title__ = "Path Tapping Operation UI." +__author__ = "sliptonic (Brad Collette)" +__url__ = "https://www.freecadweb.org" +__doc__ = "UI and Command for Path Tapping Operation." +__contributors__ = "luvtofish" + +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()) + + +class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage): + """Controller for the tapping operation's page""" + + def initPage(self, obj): + # self.peckDepthSpinBox = PathGuiUtil.QuantitySpinBox( + # self.form.peckDepth, obj, "PeckDepth" + # ) + # self.peckRetractSpinBox = PathGuiUtil.QuantitySpinBox( + # self.form.peckRetractHeight, obj, "RetractHeight" + # ) + self.dwellTimeSpinBox = PathGuiUtil.QuantitySpinBox(self.form.dwellTime, obj, "DwellTime") + + # self.form.chipBreakEnabled.setEnabled(False) + + def registerSignalHandlers(self, obj): + # self.form.peckEnabled.toggled.connect(self.form.peckDepth.setEnabled) + # self.form.peckEnabled.toggled.connect(self.form.dwellEnabled.setDisabled) + # self.form.peckEnabled.toggled.connect(self.setChipBreakControl) + + self.form.dwellEnabled.toggled.connect(self.form.dwellTime.setEnabled) + self.form.dwellEnabled.toggled.connect(self.form.dwellTimelabel.setEnabled) + # self.form.dwellEnabled.toggled.connect(self.form.peckEnabled.setDisabled) + # self.form.dwellEnabled.toggled.connect(self.setChipBreakControl) + + # self.form.peckRetractHeight.setEnabled(True) + # self.form.retractLabel.setEnabled(True) + + # if self.form.peckEnabled.isChecked(): + # self.form.dwellEnabled.setEnabled(False) + # self.form.peckDepth.setEnabled(True) + # self.form.peckDepthLabel.setEnabled(True) + # self.form.chipBreakEnabled.setEnabled(True) + # elif self.form.dwellEnabled.isChecked(): + if self.form.dwellEnabled.isChecked(): + # self.form.peckEnabled.setEnabled(False) + self.form.dwellTime.setEnabled(True) + self.form.dwellTimelabel.setEnabled(True) + + # self.form.chipBreakEnabled.setEnabled(False) + # else: + # self.form.chipBreakEnabled.setEnabled(False) + + # def setChipBreakControl(self): + # self.form.chipBreakEnabled.setEnabled(self.form.peckEnabled.isChecked()) + + def getForm(self): + """getForm() ... return UI""" + form = FreeCADGui.PySideUic.loadUi(":/panels/PageOpTappingEdit.ui") + + comboToPropertyMap = [("ExtraOffset", "ExtraOffset")] + enumTups = PathTapping.ObjectTapping.propertyEnumerations(dataType="raw") + self.populateCombobox(form, enumTups, comboToPropertyMap) + + return form + + def updateQuantitySpinBoxes(self, index=None): + # self.peckDepthSpinBox.updateSpinBox() + # self.peckRetractSpinBox.updateSpinBox() + self.dwellTimeSpinBox.updateSpinBox() + + def getFields(self, obj): + """setFields(obj) ... update obj's properties with values from the UI""" + Path.Log.track() + # self.peckDepthSpinBox.updateProperty() + # self.peckRetractSpinBox.updateProperty() + self.dwellTimeSpinBox.updateProperty() + + if obj.DwellEnabled != self.form.dwellEnabled.isChecked(): + obj.DwellEnabled = self.form.dwellEnabled.isChecked() + # if obj.PeckEnabled != self.form.peckEnabled.isChecked(): + # obj.PeckEnabled = self.form.peckEnabled.isChecked() + # if obj.chipBreakEnabled != self.form.chipBreakEnabled.isChecked(): + # obj.chipBreakEnabled = self.form.chipBreakEnabled.isChecked() + if obj.ExtraOffset != str(self.form.ExtraOffset.currentData()): + obj.ExtraOffset = str(self.form.ExtraOffset.currentData()) + + self.updateToolController(obj, self.form.toolController) + self.updateCoolant(obj, self.form.coolantController) + + def setFields(self, obj): + """setFields(obj) ... update UI with obj properties' values""" + Path.Log.track() + self.updateQuantitySpinBoxes() + + if obj.DwellEnabled: + self.form.dwellEnabled.setCheckState(QtCore.Qt.Checked) + else: + self.form.dwellEnabled.setCheckState(QtCore.Qt.Unchecked) + + # if obj.PeckEnabled: + # self.form.peckEnabled.setCheckState(QtCore.Qt.Checked) + # else: + # self.form.peckEnabled.setCheckState(QtCore.Qt.Unchecked) + # self.form.chipBreakEnabled.setEnabled(False) + + # if obj.chipBreakEnabled: + # self.form.chipBreakEnabled.setCheckState(QtCore.Qt.Checked) + # else: + # self.form.chipBreakEnabled.setCheckState(QtCore.Qt.Unchecked) + + self.selectInComboBox(obj.ExtraOffset, self.form.ExtraOffset) + + self.setupToolController(obj, self.form.toolController) + self.setupCoolant(obj, self.form.coolantController) + + def getSignalsForUpdate(self, obj): + """getSignalsForUpdate(obj) ... return list of signals which cause the receiver to update the model""" + signals = [] + + # signals.append(self.form.peckRetractHeight.editingFinished) + # signals.append(self.form.peckDepth.editingFinished) + signals.append(self.form.dwellTime.editingFinished) + signals.append(self.form.dwellEnabled.stateChanged) + # signals.append(self.form.peckEnabled.stateChanged) + # signals.append(self.form.chipBreakEnabled.stateChanged) + signals.append(self.form.toolController.currentIndexChanged) + signals.append(self.form.coolantController.currentIndexChanged) + signals.append(self.form.ExtraOffset.currentIndexChanged) + + return signals + + +# def updateData(self, obj, prop): +# if prop in ["PeckDepth", "RetractHeight"] and not prop in ["Base", "Disabled"]: +# self.updateQuantitySpinBoxes() + + +Command = PathOpGui.SetupOperation( + "Tapping", + PathTapping.Create, + TaskPanelOpPage, + "CAM_Tapping", + QtCore.QT_TRANSLATE_NOOP("CAM_Tapping", "Tapping"), + QtCore.QT_TRANSLATE_NOOP( + "CAM_Tapping", + "Creates a Tapping toolpath from the features of a base object", + ), + PathTapping.SetupProperties, +) + +FreeCAD.Console.PrintLog("Loading PathTappingGui... done\n") diff --git a/src/Mod/CAM/Path/Op/Tapping.py b/src/Mod/CAM/Path/Op/Tapping.py new file mode 100644 index 0000000000..33bf0e40d3 --- /dev/null +++ b/src/Mod/CAM/Path/Op/Tapping.py @@ -0,0 +1,289 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2014 Yorik van Havre * +# * Copyright (c) 2020 Schildkroet * +# * Copyright (c) 2023 luvtofish * +# * * +# * 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 LICENSE 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 Part +import Path +import Path.Base.FeedRate as PathFeedRate +import Path.Base.Generator.tapping as tapping +import Path.Base.MachineState as PathMachineState +import Path.Op.Base as PathOp +import Path.Op.CircularHoleBase as PathCircularHoleBase +import PathScripts.PathUtils as PathUtils +from PySide.QtCore import QT_TRANSLATE_NOOP + +__title__ = "Path Tapping Operation" +__author__ = "sliptonic (Brad Collette)" +__url__ = "https://www.freecadweb.org" +__doc__ = "Path Tapping operation." +__contributors__ = "luvtofish (Dan Henderson)" + +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 ObjectTapping(PathCircularHoleBase.ObjectOp): + """Proxy object for Tapping operation.""" + + @classmethod + 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 + """ + + # Enumeration lists for App::PropertyEnumeration properties + enums = { + "ReturnLevel": [ + (translate("CAM_Tapping", "G98"), "G98"), + (translate("CAM_Tapping", "G99"), "G99"), + ], # How high to retract after a tapping move + "ExtraOffset": [ + (translate("CAM_Tapping", "None"), "None"), + (translate("CAM_Tapping", "Drill Tip"), "Drill Tip"), + (translate("CAM_Tapping", "2x Drill Tip"), "2x Drill Tip"), + ], # extra drilling depth to clear tap taper + } + + 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 circularHoleFeatures(self, obj): + """circularHoleFeatures(obj) ... tapping works on anything, turn on all Base geometries and Locations.""" + return PathOp.FeatureBaseGeometry | PathOp.FeatureLocations | PathOp.FeatureCoolant + + def initCircularHoleOperation(self, obj): + """initCircularHoleOperation(obj) ... add tapping specific properties to obj.""" + obj.addProperty( + "App::PropertyFloat", + "DwellTime", + "Tap", + QT_TRANSLATE_NOOP("App::Property", "The time to dwell at bottom of tapping cycle"), + ) + obj.addProperty( + "App::PropertyBool", + "DwellEnabled", + "Tap", + QT_TRANSLATE_NOOP("App::Property", "Enable dwell"), + ) + obj.addProperty( + "App::PropertyBool", + "AddTipLength", + "Tap", + QT_TRANSLATE_NOOP( + "App::Property", + "Calculate the tip length and subtract from final depth", + ), + ) + obj.addProperty( + "App::PropertyEnumeration", + "ReturnLevel", + "Tap", + QT_TRANSLATE_NOOP("App::Property", "Controls how tool retracts Default=G98"), + ) + obj.addProperty( + "App::PropertyDistance", + "RetractHeight", + "Tap", + QT_TRANSLATE_NOOP( + "App::Property", + "The height where feed starts and height during retract tool when path is finished while in a peck operation", + ), + ) + obj.addProperty( + "App::PropertyEnumeration", + "ExtraOffset", + "Tap", + QT_TRANSLATE_NOOP("App::Property", "How far the tap depth is extended"), + ) + + for n in self.propertyEnumerations(): + setattr(obj, n[0], n[1]) + + def circularHoleExecute(self, obj, holes): + """circularHoleExecute(obj, holes) ... generate tapping operation for each hole in holes.""" + Path.Log.track() + machine = PathMachineState.MachineState() + + if not hasattr(obj.ToolController.Tool, "Pitch") or not hasattr( + obj.ToolController.Tool, "TPI" + ): + Path.Log.error( + translate( + "Path_Tapping", + "Tapping Operation requires a Tap tool with Pitch or TPI", + ) + ) + return + + self.commandlist.append(Path.Command("(Begin Tapping)")) + + # rapid to clearance height + command = Path.Command("G0", {"Z": obj.ClearanceHeight.Value}) + machine.addCommand(command) + self.commandlist.append(command) + + self.commandlist.append(Path.Command("G90")) # Absolute distance mode + + # Calculate offsets to add to target edge + endoffset = 0.0 + if obj.ExtraOffset == "Drill Tip": + endoffset = PathUtils.drillTipLength(self.tool) + elif obj.ExtraOffset == "2x Drill Tip": + endoffset = PathUtils.drillTipLength(self.tool) * 2 + + # http://linuxcnc.org/docs/html/gcode/g-code.html#gcode:g98-g99 + self.commandlist.append(Path.Command(obj.ReturnLevel)) + + holes = PathUtils.sort_locations(holes, ["x", "y"]) + + # This section is technical debt. The computation of the + # target shapes should be factored out for re-use. + # This will likely mean refactoring upstream CircularHoleBase to pass + # spot shapes instead of holes. + + startHeight = obj.StartDepth.Value + self.job.SetupSheet.SafeHeightOffset.Value + + edgelist = [] + for hole in holes: + v1 = FreeCAD.Vector(hole["x"], hole["y"], obj.StartDepth.Value) + v2 = FreeCAD.Vector(hole["x"], hole["y"], obj.FinalDepth.Value - endoffset) + edgelist.append(Part.makeLine(v1, v2)) + + # iterate the edgelist and generate gcode + for edge in edgelist: + + Path.Log.debug(edge) + + # move to hole location + + startPoint = edge.Vertexes[0].Point + + command = Path.Command( + "G0", {"X": startPoint.x, "Y": startPoint.y, "Z": startHeight} + ) # DLH added Z offset to fix issue with Dynapath Control. + self.commandlist.append(command) + machine.addCommand(command) + + # command = Path.Command("G0", {"Z": startHeight}) + # self.commandlist.append(command) + # machine.addCommand(command) + + # command = Path.Command("G1", {"Z": obj.StartDepth.Value}) + # self.commandlist.append(command) + # machine.addCommand(command) + + # Technical Debt: We are assuming the edges are aligned. + # This assumption should be corrected and the necessary rotations + # performed to align the edge with the Z axis for drilling + + # Perform tapping + dwelltime = obj.DwellTime if obj.DwellEnabled else 0.0 + repeat = 1 # technical debt: Add a repeat property for user control + + # Get attribute from obj.tool, assign default and set to bool for passing to generate + isRightHand = getattr(obj.ToolController.Tool, "Rotation", "Right Hand") == "Right Hand" + + try: + tappingcommands = tapping.generate( + edge, dwelltime, repeat, obj.RetractHeight.Value, isRightHand + ) + + except ValueError as e: # any targets that fail the generator are ignored + Path.Log.info(e) + continue + + for command in tappingcommands: + self.commandlist.append(command) + machine.addCommand(command) + + # Cancel canned tapping cycle + self.commandlist.append(Path.Command("G80")) + # command = Path.Command("G0", {"Z": obj.SafeHeight.Value}) DLH- Not needed, adds unnecessary move to Z SafeHeight. + # self.commandlist.append(command) + # machine.addCommand(command) DLH - Not needed. + + # Apply feed rates to commands + PathFeedRate.setFeedRate(self.commandlist, obj.ToolController) + + def opSetDefaultValues(self, obj, job): + """opSetDefaultValues(obj, job) ... set default value for RetractHeight""" + obj.ExtraOffset = "None" + + if hasattr(job.SetupSheet, "RetractHeight"): + obj.RetractHeight = job.SetupSheet.RetractHeight + elif self.applyExpression(obj, "RetractHeight", "StartDepth+SetupSheet.SafeHeightOffset"): + if not job: + obj.RetractHeight = 10 + else: + obj.RetractHeight.Value = obj.StartDepth.Value + 1.0 + + if hasattr(job.SetupSheet, "DwellTime"): + obj.DwellTime = job.SetupSheet.DwellTime + else: + obj.DwellTime = 1 + + +def SetupProperties(): + setup = [] + setup.append("DwellTime") + setup.append("DwellEnabled") + setup.append("AddTipLength") + setup.append("ReturnLevel") + setup.append("ExtraOffset") + setup.append("RetractHeight") + return setup + + +def Create(name, obj=None, parentJob=None): + """Create(name) ... Creates and returns a Tapping operation.""" + if obj is None: + obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) + + obj.Proxy = ObjectTapping(obj, name, parentJob) + if obj.Proxy: + obj.Proxy.findAllHoles(obj) + + return obj diff --git a/src/Mod/CAM/TestCAMApp.py b/src/Mod/CAM/TestCAMApp.py index ee129b4d8a..7b7ed471e7 100644 --- a/src/Mod/CAM/TestCAMApp.py +++ b/src/Mod/CAM/TestCAMApp.py @@ -58,6 +58,7 @@ from CAMTests.TestPathPropertyBag import TestPathPropertyBag from CAMTests.TestPathRotationGenerator import TestPathRotationGenerator from CAMTests.TestPathSetupSheet import TestPathSetupSheet from CAMTests.TestPathStock import TestPathStock +from CAMTests.TestPathTapGenerator import TestPathTapGenerator from CAMTests.TestPathThreadMilling import TestPathThreadMilling from CAMTests.TestPathThreadMillingGenerator import TestPathThreadMillingGenerator from CAMTests.TestPathToolBit import TestPathToolBit @@ -110,6 +111,7 @@ False if TestPathPropertyBag.__name__ else True False if TestPathRotationGenerator.__name__ else True False if TestPathSetupSheet.__name__ else True False if TestPathStock.__name__ else True +False if TestPathTapGenerator.__name__ else True False if TestPathThreadMilling.__name__ else True False if TestPathThreadMillingGenerator.__name__ else True False if TestPathToolBit.__name__ else True diff --git a/src/Mod/CAM/Tools/Bit/375-16_Tap.fctb b/src/Mod/CAM/Tools/Bit/375-16_Tap.fctb new file mode 100644 index 0000000000..e4032a36c8 --- /dev/null +++ b/src/Mod/CAM/Tools/Bit/375-16_Tap.fctb @@ -0,0 +1,19 @@ +{ + "version": 2, + "name": "375-16_Tap", + "shape": "tap.fcstd", + "parameter": { + "Coating": "None", + "CuttingEdgeLength": "1.063 \"", + "Diameter": "0.375 \"", + "Flutes": "3", + "Length": "2.500 \"", + "Pitch": "0.000 in", + "Rotation": "Right Hand", + "ShankDiameter": "0.250 \"", + "TPI": "16", + "TipAngle": "90.000 \u00b0", + "Type": "HSS" + }, + "attribute": {} +} diff --git a/src/Mod/CAM/Tools/Shape/tap.fcstd b/src/Mod/CAM/Tools/Shape/tap.fcstd new file mode 100644 index 0000000000..32634aeb58 Binary files /dev/null and b/src/Mod/CAM/Tools/Shape/tap.fcstd differ