Path: Add G84/G74 Tapping Operation (#8069)

This commit is contained in:
Dan Henderson
2024-12-06 11:21:49 -06:00
committed by GitHub
parent ad50bb9bef
commit 95ef2d5147
14 changed files with 1616 additions and 5 deletions

View File

@@ -0,0 +1,132 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2021 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 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)

View File

@@ -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

View File

@@ -52,6 +52,7 @@
<file>icons/CAM_SimulatorGL.svg</file>
<file>icons/CAM_Slot.svg</file>
<file>icons/CAM_Stop.svg</file>
<file>icons/CAM_Tapping.svg</file>
<file>icons/CAM_ThreadMilling.svg</file>
<file>icons/CAM_ToolBit.svg</file>
<file>icons/CAM_ToolChange.svg</file>
@@ -108,6 +109,7 @@
<file>panels/PageOpProfileFullEdit.ui</file>
<file>panels/PageOpSlotEdit.ui</file>
<file>panels/PageOpSurfaceEdit.ui</file>
<file>panels/PageOpTappingEdit.ui</file>
<file>panels/PageOpThreadMillingEdit.ui</file>
<file>panels/PageOpWaterlineEdit.ui</file>
<file>panels/PageOpVcarveEdit.ui</file>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -0,0 +1,145 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>532</width>
<height>351</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="1" column="0" colspan="3">
<layout class="QGridLayout" name="gridLayout">
<item row="4" column="4">
<widget class="QLabel" name="dwellTimelabel">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Time</string>
</property>
</widget>
</item>
<item row="3" column="1" rowspan="2" colspan="2">
<widget class="QCheckBox" name="dwellEnabled">
<property name="text">
<string>Dwell</string>
</property>
</widget>
</item>
<item row="5" column="6">
<widget class="QComboBox" name="ExtraOffset">
<item>
<property name="text">
<string>None</string>
</property>
</item>
<item>
<property name="text">
<string>Tap Tip</string>
</property>
</item>
<item>
<property name="text">
<string>2x Tap Tip</string>
</property>
</item>
</widget>
</item>
<item row="4" column="6">
<widget class="Gui::QuantitySpinBox" name="dwellTime" native="true">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="5" column="4">
<widget class="QLabel" name="Offsetlabel">
<property name="text">
<string>Extend Depth</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="0" colspan="3">
<widget class="QFrame" name="frame">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="1" column="1">
<widget class="QComboBox" name="coolantController">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The tool and its settings to be used for this operation.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Coolant Mode</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>ToolController</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="toolController">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The tool and its settings to be used for this operation.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="2" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>Gui::QuantitySpinBox</class>
<extends>QWidget</extends>
<header>Gui/QuantitySpinBox.h</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>toolController</tabstop>
<tabstop>dwellEnabled</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@@ -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

View File

@@ -0,0 +1,112 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2021 sliptonic <shopinthewoods@gmail.com> *
# * 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)]

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,187 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2017 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 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")

View File

@@ -0,0 +1,289 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2014 Yorik van Havre <yorik@uncreated.net> *
# * 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

View File

@@ -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

View File

@@ -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": {}
}

Binary file not shown.