diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt
index 7046290c7b..b25189233a 100644
--- a/src/Mod/Path/CMakeLists.txt
+++ b/src/Mod/Path/CMakeLists.txt
@@ -111,6 +111,8 @@ SET(PathScripts_SRCS
PathScripts/PathSurface.py
PathScripts/PathSurfaceGui.py
PathScripts/PathSurfaceSupport.py
+ PathScripts/PathThreadMilling.py
+ PathScripts/PathThreadMillingGui.py
PathScripts/PathToolBit.py
PathScripts/PathToolBitCmd.py
PathScripts/PathToolBitEdit.py
@@ -176,6 +178,7 @@ SET(Tools_Shape_SRCS
Tools/Shape/endmill.fcstd
Tools/Shape/probe.fcstd
Tools/Shape/slittingsaw.fcstd
+ Tools/Shape/thread-mill.fcstd
Tools/Shape/v-bit.fcstd
)
@@ -195,6 +198,7 @@ SET(PathTests_SRCS
PathTests/TestPathPreferences.py
PathTests/TestPathSetupSheet.py
PathTests/TestPathStock.py
+ PathTests/TestPathThreadMilling.py
PathTests/TestPathTool.py
PathTests/TestPathToolBit.py
PathTests/TestPathToolController.py
@@ -223,6 +227,15 @@ SET(Path_Images
${PathImages_Tools}
)
+SET(PathData_Threads
+ Data/Threads/metric-internal.csv
+ Data/Threads/imperial-internal.csv
+)
+
+SET(Path_Data
+ ${PathData_Threads}
+)
+
SET(all_files
${PathScripts_SRCS}
${PathScripts_post_SRCS}
@@ -230,6 +243,7 @@ SET(all_files
${Tools_Library_SRCS}
${Tools_Shape_SRCS}
${Path_Images}
+ ${Path_Data}
)
ADD_CUSTOM_TARGET(PathScripts ALL
@@ -304,3 +318,10 @@ INSTALL(
Mod/Path/Images/Tools
)
+INSTALL(
+ FILES
+ ${PathData_Threads}
+ DESTINATION
+ Mod/Path/Data/Threads
+)
+
diff --git a/src/Mod/Path/Data/Threads/imperial-internal.csv b/src/Mod/Path/Data/Threads/imperial-internal.csv
new file mode 100644
index 0000000000..9a171490d9
--- /dev/null
+++ b/src/Mod/Path/Data/Threads/imperial-internal.csv
@@ -0,0 +1,81 @@
+name,tpi,class,dMinorMin,dMinorMax,dPitchMin,dPitchMax,dMajorMin,dMajorMax
+0-80,80,2B,0.0465,0.0514,0.0519,0.0542,0.06,0.0633
+0-80,80,3B,0.0465,0.0514,0.0519,0.0536,0.06,0.0626
+1-64,64,2B,0.0561,0.0623,0.0629,0.0655,0.073,0.0768
+1-64,64,3B,0.0561,0.0623,0.0629,0.0648,0.073,0.076
+1-72,72,2B,0.058,0.0635,0.064,0.0665,0.073,0.0766
+1-72,72,3B,0.058,0.0635,0.064,0.0659,0.073,0.0759
+2-56,56,2B,0.0667,0.0737,0.0744,0.0772,0.086,0.0901
+2-56,56,3B,0.0667,0.0737,0.0744,0.0765,0.086,0.0893
+2-64,64,2B,0.0691,0.0753,0.0759,0.0786,0.086,0.0899
+2-64,64,3B,0.0691,0.0753,0.0759,0.0779,0.086,0.0891
+3-48,48,2B,0.0764,0.0845,0.0855,0.0885,0.099,0.1035
+3-48,48,3B,0.0764,0.0845,0.0855,0.0877,0.099,0.1026
+3-56,56,2B,0.0797,0.0865,0.0874,0.0902,0.099,0.1032
+3-56,56,3B,0.0797,0.0865,0.0874,0.0895,0.099,0.1024
+4-40,40,2B,0.0849,0.0939,0.0958,0.0991,0.112,0.1170
+4-40,40,3B,0.0849,0.0939,0.0958,0.0982,0.112,0.116
+4-48,48,2B,0.0894,0.0968,0.0985,0.1016,0.112,0.1167
+4-48,48,3B,0.0894,0.0968,0.0985,0.1008,0.112,0.1158
+5-40,40,2B,0.0979,0.1062,0.1088,0.1121,0.125,0.1301
+5-40,40,3B,0.0979,0.1062,0.1088,0.1113,0.125,0.1292
+5-44,44,2B,0.1004,0.1079,0.1102,0.1134,0.125,0.1299
+5-44,44,3B,0.1004,0.1079,0.1102,0.1126,0.125,0.129
+6-32,32,2B,0.104,0.114,0.1177,0.1214,0.138,0.1438
+6-32,32,3B,0.104,0.114,0.1177,0.1204,0.138,0.1426
+6-40,40,2B,0.111,0.119,0.1218,0.1252,0.138,0.1433
+6-40,40,3B,0.111,0.1186,0.1218,0.1243,0.138,0.1422
+8-32,32,2B,0.13,0.139,0.1437,0.1475,0.164,0.1700
+8-32,32,3B,0.13,0.1389,0.1437,0.1465,0.164,0.1689
+8-36,36,2B,0.134,0.142,0.146,0.1496,0.164,0.1697
+8-36,36,3B,0.134,0.1416,0.146,0.1487,0.164,0.1687
+10-24,24,2B,0.145,0.156,0.1629,0.1672,0.19,0.197
+10-24,24,3B,0.145,0.1555,0.1629,0.1661,0.19,0.1957
+10-32,32,2B,0.156,0.164,0.1697,0.1736,0.19,0.1963
+10-32,32,3B,0.156,0.1641,0.1697,0.1726,0.19,0.1952
+1/4-20,20,2B,0.196,0.207,0.2175,0.2248,0.25,0.261
+1/4-20,20,3B,0.196,0.207,0.2175,0.22,0.25,0.2554
+1/4-28,28,2B,0.211,0.22,0.2268,0.2311,0.25,0.2573
+1/4-28,28,3B,0.211,0.219,0.2268,0.23,0.25,0.2561
+5/16-18,18,2B,0.252,0.265,0.2764,0.2817,0.3125,0.3217
+5/16-18,18,3B,0.252,0.263,0.2764,0.2803,0.3125,0.3201
+5/16-24,24,2B,0.267,0.277,0.2854,0.2902,0.3125,0.3209
+5/16-24,24,3B,0.267,0.2754,0.2854,0.289,0.3125,0.3196
+3/8-16,16,2B,0.307,0.321,0.3344,0.3401,0.375,0.3852
+3/8-16,16,3B,0.307,0.3182,0.3344,0.3387,0.375,0.3836
+3/8-24,24,2B,0.33,0.34,0.3479,0.3528,0.375,0.3841
+3/8-24,24,3B,0.33,0.3372,0.3479,0.3516,0.375,0.3828
+7/16-14,14,2B,0.36,0.376,0.3911,0.3972,0.4375,0.4488
+7/16-14,14,3B,0.36,0.3717,0.3911,0.3957,0.4375,0.4471
+7/16-20,20,2B,0.383,0.395,0.405,0.4104,0.4375,0.4478
+7/16-20,20,3B,0.383,0.3916,0.405,0.4091,0.4375,0.4463
+1/2-13,13,2B,0.417,0.434,0.45,0.4565,0.5,0.5123
+1/2-13,13,3B,0.417,0.4284,0.45,0.4548,0.5,0.5104
+1/2-20,20,2B,0.446,0.457,0.4675,0.4731,0.5,0.5110
+1/2-20,20,3B,0.446,0.4537,0.4675,0.4717,0.5,0.5095
+5/8-11,11,2B,0.527,0.546,0.566,0.5732,0.625,0.6393
+5/8-11,11,3B,0.527,0.5391,0.566,0.5714,0.625,0.6373
+5/8-18,18,2B,0.565,0.578,0.5889,0.5949,0.625,0.6377
+5/8-18,18,3B,0.565,0.573,0.5889,0.5934,0.625,0.6361
+3/4-10,10,2B,0.642,0.663,0.685,0.6927,0.75,0.7660
+3/4-10,10,3B,0.642,0.6545,0.685,0.6907,0.75,0.7638
+3/4-16,16,2B,0.682,0.696,0.7094,0.7159,0.75,0.7644
+3/4-16,16,3B,0.682,0.6908,0.7094,0.7143,0.75,0.7627
+7/8-9,9,2B,0.755,0.778,0.8028,0.811,0.875,0.8928
+7/8-9,9,3B,0.755,0.7681,0.8028,0.8089,0.875,0.8905
+7/8-14,14,2B,0.798,0.814,0.8286,0.8356,0.875,0.8912
+7/8-14,14,3B,0.798,0.8068,0.8286,0.8339,0.875,0.8894
+1-8,8,2B,0.865,0.89,0.9188,0.9276,1,1.0197
+1-8,8,3B,0.865,0.8797,0.9188,0.9254,1,1.0173
+1-12,12,2B,0.91,0.928,0.9459,0.9535,1,1.0181
+1-12,12,3B,0.91,0.9198,0.9459,0.9516,1,1.0161
+11/8-7,7,2B,0.97,0.998,1.0322,1.0416,1.125,1.1466
+11/8-12,12,2B,1.035,1.053,1.0709,1.0787,1.125,1.1445
+11/4-7,7,2B,1.095,1.123,1.1572,1.1668,1.25,1.273
+11/4-12,12,2B,1.16,1.178,1.1959,1.2039,1.25,1.2709
+13/8-6,6,2B,1.195,1.225,1.2667,1.2771,1.375,1.4002
+13/8-12,12,2B,1.285,1.303,1.3209,1.3291,1.375,1.3974
+11/2-6,6,2B,1.32,1.35,1.3917,1.4022,1.5,1.5264
+11/2-12,12,2B,1.41,1.428,1.4459,1.4542,1.5,1.5237
+13/4-5,5,2B,1.534,1.568,1.6201,1.6317,1.75,1.7802
+2-6,6,2B,1.82,1.85,1.8917,1.9028,2,2.0319
diff --git a/src/Mod/Path/Data/Threads/metric-internal.csv b/src/Mod/Path/Data/Threads/metric-internal.csv
new file mode 100644
index 0000000000..6a821334d5
--- /dev/null
+++ b/src/Mod/Path/Data/Threads/metric-internal.csv
@@ -0,0 +1,82 @@
+name,pitch,tol,dMinorMin,dMinorMax,dPitchMin,dPitchMax,dMajorMin,dMajorMax
+M1.6 x 0.35,0.35,6H,1.221,1.321,1.373,1.458,1.6,1.736
+M2 x 0.4,0.4,6H,1.567,1.679,1.74,1.83,2,2.148
+M2.5 x 0.45,0.45,6H,2.013,2.138,2.208,2.303,2.5,2.66
+M3 x 0.5,0.5,6H,2.459,2.599,2.675,2.775,3,3.172
+M3.5 x 0.6,0.6,6H,2.85,3.01,3.11,3.222,3.5,3.698
+M4 x 0.7,0.7,6H,3.242,3.422,3.545,3.663,4,4.219
+M5 x 0.8,0.8,6H,4.134,4.334,4.48,4.605,5,5.24
+M6 x 1,1,6H,4.917,5.153,5.35,5.5,6,6.294
+M8 x 1,1,6H,6.917,7.153,7.35,7.5,8,8.294
+M8 x 1.25,1.25,6H,6.647,6.912,7.188,7.348,8,8.34
+M10 x 0.75,0.75,6H,9.188,9.378,9.513,9.645,10,10.24
+M10 x 1,1,6H,8.917,9.153,9.35,9.5,10,10.294
+M10 x 1.25,1.25,6H,8.647,8.912,9.188,9.348,10,10.34
+M10 x 1.5,1.5,6H,8.376,8.676,9.026,9.206,10,10.397
+M12 x 1,1,6H,10.917,11.153,11.35,11.51,12,12.304
+M12 x 1.25,1.25,6H,10.647,10.912,11.188,11.368,12,12.36
+M12 x 1.5,1.5,6H,10.376,10.676,11.026,11.216,12,12.407
+M12 x 1.75,1.75,6H,10.106,10.441,10.863,11.063,12,12.452
+M14 x 1.5,1.5,6H,12.376,12.676,13.026,13.216,14,14.407
+M14 x 2,2,6H,11.835,12.21,12.701,12.913,14,14.501
+M15 x 1,1,6H,13.917,14.153,14.35,14.51,15,15.304
+M16 x 1.5,1.5,6H,14.376,14.676,15.026,15.216,16,16.407
+M16 x 2,2,6H,13.835,14.21,14.701,14.913,16,16.501
+M17 x 1,1,6H,15.917,16.153,16.35,16.51,17,17.304
+M18 x 1.5 ,1.5,6H,16.376,16.676,17.026,17.216,18,18.407
+M20 x 1,1,6H,18.917,19.153,19.35,19.51,20,20.304
+M20 x 1.5,1.5,6H,18.376,18.676,19.026,19.216,20,20.407
+M20 x 2.5,2.5,6H,17.294,17.744,18.376,18.6,20,20.585
+M22 x 1.5,1.5,6H,20.376,20.676,21.026,21.216,22,22.407
+M22 x 2.5,2.5,6H,19.294,19.744,20.376,20.6,22,22.585
+M24 x 2,2,6H,21.835,22.21,22.701,22.925,24,24.513
+M24 x 3,3,6H,20.752,21.252,22.051,22.316,24,24.698
+M25 x 1.5,1.5,6H,23.376,23.676,24.026,24.226,25,25.417
+M27 x 2,2,6H,24.835,25.21,25.701,25.925,27,27.513
+M27 x 3,3,6H,23.752,24.252,25.051,25.316,27,27.698
+M30 x 1.5,1.5,6H,28.376,28.676,29.026,29.226,30,30.417
+M30 x 2,2,6H,27.835,28.21,28.701,28.925,30,30.513
+M30 x 3.5,3.5,6H,26.211,26.771,27.727,28.007,30,30.786
+M33 x 2,2,6H,30.835,31.21,31.701,31.925,33,33.513
+M35 x 1.5,1.5,6H,33.376,33.676,34.026,34.226,35,35.417
+M36 x 2,2,6H,33.835,34.21,34.701,34.925,36,36.513
+M36 x 4,4,6H,31.67,32.27,33.402,33.702,36,36.877
+M39 x 2,2,6H,36.835,37.21,37.701,37.925,39,39.513
+M40 x 1.5,1.5,6H,38.376,38.676,39.026,39.226,40,40.417
+M42 x 2,2,6H,39.835,40.21,40.701,40.925,42,42.513
+M42 x 4.5,4.5,6H,37.129,37.799,39.077,39.392,42,42.964
+M45 x 1.5,1.5,6H,43.376,43.676,44.026,44.226,45,45.417
+M48 x 2,2,6H,45.835,46.21,46.701,46.937,48,48.525
+M48 x 5,5,6H,42.587,43.297,44.752,45.087,48,49.056
+M50 x 1.5,1.5,6H,48.376,48.676,49.026,49.238,50,50.429
+M55 x 1.5,1.5,6H,53.376,53.676,54.026,54.238,55,55.429
+M56 x 2,2,6H,53.835,54.21,54.701,54.937,56,56.525
+M56 x 5.5,5.5,6H,50.046,50.796,52.428,52.783,56,57.149
+M60 x 1.5,1.5,6H,58.376,58.676,59.026,59.238,60,60.429
+M64 x 2,2,6H,61.835,62.21,62.701,62.937,64,64.525
+M64 x 6,6,6H,57.505,58.305,60.103,60.478,64,65.241
+M65 x 1.5,1.5,6H,63.376,63.676,64.026,64.238,65,65.429
+M70 x 1.5,1.5,6H,68.376,68.676,69.026,69.238,70,70.429
+M72 x 2,2,6H,69.835,70.21,70.701,70.937,72,72.525
+M72 x 6,6,6H,65.505,66.305,68.103,68.478,72,73.241
+M75 x 1.5,1.5,6H,73.376,73.676,74.026,74.238,75,75.429
+M80 x 1.5,1.5,6H,78.376,78.676,79.026,79.238,80,80.429
+M80 x 2,2,6H,77.835,78.21,78.701,78.937,80,80.525
+M80 x 6,6,6H,73.505,74.305,76.103,76.478,80,81.241
+M85 x 2,2,6H,82.835,83.21,83.701,83.937,85,85.525
+M90 x 2,2,6H,87.835,88.21,88.701,88.937,90,90.525
+M90 x 6,6,6H,83.505,84.305,86.103,86.478,90,91.241
+M95 x 2,2,6H,92.835,93.21,93.701,93.951,95,95.539
+M100 x 2,2,6H,97.835,98.21,98.701,98.951,100,100.539
+M100 x 6,6,6H,93.505,94.305,96.103,96.503,100,101.266
+M105 x 2,2,6H,102.835,103.21,103.701,103.951,105,105.539
+M110 x 2,2,6H,107.835,108.21,108.701,108.951,110,110.539
+M120 x 2,2,6H,117.835,118.21,118.701,118.951,120,120.539
+M130 x 2,2,6H,127.835,128.21,128.701,128.951,130,130.539
+M140 x 2,2,6H,137.835,138.21,138.701,138.951,140,140.539
+M150 x 2,2,6H,147.835,148.21,148.701,148.951,150,150.539
+M160 x 3,3,6H,156.752,157.252,158.051,158.351,160,160.733
+M170 x 3,3,6H,166.752,167.252,168.051,168.351,170,170.733
+M180 x 3,3,6H,176.752,177.252,178.051,178.351,180,180.733
+M190 x 3,3,6H,186.752,187.252,188.051,188.386,190,190.768
+M200 x 3,3,6H,196.752,197.252,198.051,198.386,200,200.768
diff --git a/src/Mod/Path/Data/Threads/sources.txt b/src/Mod/Path/Data/Threads/sources.txt
new file mode 100644
index 0000000000..20fb5e92fd
--- /dev/null
+++ b/src/Mod/Path/Data/Threads/sources.txt
@@ -0,0 +1,4 @@
+https://www.amesweb.info/Screws/Internal-Metric-Thread-Dimensions-Chart.aspx
+https://www.engineersedge.com/thread_strength/internal_screw_threads_chart.htm
+ dMajorMax = dMajorMin * 1.01 * (dMinorMax - dMinorMin)
+ formula empirically derived from metric tolerances
diff --git a/src/Mod/Path/Gui/Resources/Path.qrc b/src/Mod/Path/Gui/Resources/Path.qrc
index f121787924..c1383eb7a5 100644
--- a/src/Mod/Path/Gui/Resources/Path.qrc
+++ b/src/Mod/Path/Gui/Resources/Path.qrc
@@ -60,6 +60,7 @@
icons/Path_Speed.svg
icons/Path_Stock.svg
icons/Path_Stop.svg
+ icons/Path_ThreadMilling.svg
icons/Path_ToolBit.svg
icons/Path_ToolChange.svg
icons/Path_ToolController.svg
@@ -114,6 +115,7 @@
panels/PageOpProfileFullEdit.ui
panels/PageOpSlotEdit.ui
panels/PageOpSurfaceEdit.ui
+ panels/PageOpThreadMillingEdit.ui
panels/PageOpWaterlineEdit.ui
panels/PageOpVcarveEdit.ui
panels/PathEdit.ui
diff --git a/src/Mod/Path/Gui/Resources/icons/Path_ThreadMilling.svg b/src/Mod/Path/Gui/Resources/icons/Path_ThreadMilling.svg
new file mode 100644
index 0000000000..6cc8d4653a
--- /dev/null
+++ b/src/Mod/Path/Gui/Resources/icons/Path_ThreadMilling.svg
@@ -0,0 +1,1616 @@
+
+
+
+
diff --git a/src/Mod/Path/Gui/Resources/panels/PageOpThreadMillingEdit.ui b/src/Mod/Path/Gui/Resources/panels/PageOpThreadMillingEdit.ui
new file mode 100644
index 0000000000..8de86b656a
--- /dev/null
+++ b/src/Mod/Path/Gui/Resources/panels/PageOpThreadMillingEdit.ui
@@ -0,0 +1,245 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 318
+ 756
+
+
+
+ Form
+
+
+ -
+
+
+ Thread
+
+
+
-
+
+
+ Orientation
+
+
+
+ -
+
+
+ 1
+
+
-
+
+ Left Hand
+
+
+ -
+
+ Right Hand
+
+
+
+
+ -
+
+
+ Type
+
+
+
+ -
+
+
-
+
+ Custom
+
+
+ -
+
+ Metric - internal
+
+
+ -
+
+ SAE - internal
+
+
+
+
+ -
+
+
+ -
+
+
+ Fit
+
+
+
+ -
+
+
+ 100
+
+
+ 50
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+ Major Diameter
+
+
+
+ -
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
+ Minor Diameter
+
+
+
+ -
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
+ Pitch
+
+
+
+ -
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
+ TPI
+
+
+
+
+
+
+ -
+
+
+ Tool Controller
+
+
+
-
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+ -
+
+
+ Operation
+
+
+
-
+
+
+ Passes
+
+
+
+ -
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+ 1
+
+
+
+ -
+
+
+ Direction
+
+
+
+ -
+
+
-
+
+ Climb
+
+
+ -
+
+ Conventional
+
+
+
+
+ -
+
+
+ Lead In/Out
+
+
+
+
+
+
+
+
+
+
+ Gui::QuantitySpinBox
+ QDoubleSpinBox
+
+
+
+
+
+
diff --git a/src/Mod/Path/InitGui.py b/src/Mod/Path/InitGui.py
index 7580f38862..d1feaff6a5 100644
--- a/src/Mod/Path/InitGui.py
+++ b/src/Mod/Path/InitGui.py
@@ -99,6 +99,7 @@ class PathWorkbench (Workbench):
extracmdlist = []
# modcmdmore = ["Path_Hop",]
# remotecmdlist = ["Path_Remote"]
+ specialcmdlist = []
if PathPreferences.toolsReallyUseLegacyTools():
@@ -118,6 +119,7 @@ class PathWorkbench (Workbench):
projcmdlist.append("Path_Sanity")
prepcmdlist.append("Path_Shape")
extracmdlist.extend(["Path_Area", "Path_Area_Workplane"])
+ specialcmdlist.append('Path_Thread_Milling')
try:
import ocl # pylint: disable=unused-variable
@@ -145,6 +147,9 @@ class PathWorkbench (Workbench):
"Path", "Supplemental Commands")], prepcmdlist)
self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path"), QtCore.QT_TRANSLATE_NOOP(
"Path", "Path Modification")], modcmdlist)
+ if specialcmdlist:
+ self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path"), QtCore.QT_TRANSLATE_NOOP(
+ "Path", "Specialty Operations")], specialcmdlist)
if extracmdlist:
self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path")], extracmdlist)
diff --git a/src/Mod/Path/PathScripts/PathCircularHoleBase.py b/src/Mod/Path/PathScripts/PathCircularHoleBase.py
index c88cdf90d4..c12d6c1075 100644
--- a/src/Mod/Path/PathScripts/PathCircularHoleBase.py
+++ b/src/Mod/Path/PathScripts/PathCircularHoleBase.py
@@ -765,6 +765,23 @@ class ObjectOp(PathOp.ObjectOp):
PathLog.debug(translate("Path", "Rotated to inverse angle."))
return (clnBase, clnStock, angle)
+ def calculateStartFinalDepths(self, obj, shape, stock):
+ '''calculateStartFinalDepths(obj, shape, stock)
+ Calculate correct start and final depths for the shape(face) object provided.'''
+ finDep = max(obj.FinalDepth.Value, shape.BoundBox.ZMin)
+ stockTop = stock.Shape.BoundBox.ZMax
+ if obj.EnableRotation == 'Off':
+ strDep = obj.StartDepth.Value
+ if strDep <= finDep:
+ strDep = stockTop
+ else:
+ strDep = min(obj.StartDepth.Value, stockTop)
+ if strDep <= finDep:
+ strDep = stockTop
+ msg = translate('Path', "Start depth <= face depth.\nIncreased to stock top.")
+ PathLog.error(msg)
+ return (strDep, finDep)
+
def sortTuplesByIndex(self, TupleList, tagIdx):
'''sortTuplesByIndex(TupleList, tagIdx)
sort list of tuples based on tag index provided
diff --git a/src/Mod/Path/PathScripts/PathGuiInit.py b/src/Mod/Path/PathScripts/PathGuiInit.py
index 6123ddce36..cd48d41309 100644
--- a/src/Mod/Path/PathScripts/PathGuiInit.py
+++ b/src/Mod/Path/PathScripts/PathGuiInit.py
@@ -73,6 +73,7 @@ def Startup():
from PathScripts import PathSlotGui
from PathScripts import PathStop
# from PathScripts import PathSurfaceGui # Added in initGui.py due to OCL dependency
+ from PathScripts import PathThreadMillingGui
from PathScripts import PathToolController
from PathScripts import PathToolControllerGui
from PathScripts import PathToolLibraryManager
diff --git a/src/Mod/Path/PathScripts/PathJobCmd.py b/src/Mod/Path/PathScripts/PathJobCmd.py
index ef92826533..0a9efcb765 100644
--- a/src/Mod/Path/PathScripts/PathJobCmd.py
+++ b/src/Mod/Path/PathScripts/PathJobCmd.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2017 sliptonic *
# * *
diff --git a/src/Mod/Path/PathScripts/PathOp.py b/src/Mod/Path/PathScripts/PathOp.py
index 24497f1a58..0a16a1361c 100644
--- a/src/Mod/Path/PathScripts/PathOp.py
+++ b/src/Mod/Path/PathScripts/PathOp.py
@@ -324,7 +324,7 @@ class ObjectOp(object):
if 1 < len(job.Operations.Group):
obj.ToolController = PathUtil.toolControllerForOp(job.Operations.Group[-2])
else:
- obj.ToolController = PathUtils.findToolController(obj)
+ obj.ToolController = PathUtils.findToolController(obj, self)
if not obj.ToolController:
return None
obj.OpToolDiameter = obj.ToolController.Tool.Diameter
@@ -585,3 +585,11 @@ class ObjectOp(object):
obj.Base = baselist
else:
PathLog.notice((translate("Path", "Base object %s.%s rejected by operation") + "\n") % (base.Label, sub))
+
+ def isToolSupported(self, obj, tool):
+ '''toolSupported(obj, tool) ... Returns true if the op supports the given tool.
+ This function can safely be overwritten by subclasses.'''
+
+ return True
+
+
diff --git a/src/Mod/Path/PathScripts/PathOpGui.py b/src/Mod/Path/PathScripts/PathOpGui.py
index b1643ed119..d0d5c22d83 100644
--- a/src/Mod/Path/PathScripts/PathOpGui.py
+++ b/src/Mod/Path/PathScripts/PathOpGui.py
@@ -383,7 +383,7 @@ class TaskPanelPage(object):
combo.blockSignals(False)
if obj.ToolController is None:
- obj.ToolController = PathUtils.findToolController(obj)
+ obj.ToolController = PathUtils.findToolController(obj, obj.Proxy)
if obj.ToolController is not None:
self.selectInComboBox(obj.ToolController.Label, combo)
@@ -391,7 +391,7 @@ class TaskPanelPage(object):
'''updateToolController(obj, combo) ...
helper function to update obj's ToolController property if a different
one has been selected in the combo box.'''
- tc = PathUtils.findToolController(obj, combo.currentText())
+ tc = PathUtils.findToolController(obj, obj.Proxy, combo.currentText())
if obj.ToolController != tc:
obj.ToolController = tc
diff --git a/src/Mod/Path/PathScripts/PathPreferences.py b/src/Mod/Path/PathScripts/PathPreferences.py
index 6fe58570ef..6954c4b977 100644
--- a/src/Mod/Path/PathScripts/PathPreferences.py
+++ b/src/Mod/Path/PathScripts/PathPreferences.py
@@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
-
# ***************************************************************************
# * *
# * Copyright (c) 2014 Yorik van Havre *
diff --git a/src/Mod/Path/PathScripts/PathSanity.py b/src/Mod/Path/PathScripts/PathSanity.py
index f69465c4e1..09664be146 100644
--- a/src/Mod/Path/PathScripts/PathSanity.py
+++ b/src/Mod/Path/PathScripts/PathSanity.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2016 sliptonic *
# * *
diff --git a/src/Mod/Path/PathScripts/PathSelection.py b/src/Mod/Path/PathScripts/PathSelection.py
index d3b6624214..11a75b31a1 100644
--- a/src/Mod/Path/PathScripts/PathSelection.py
+++ b/src/Mod/Path/PathScripts/PathSelection.py
@@ -98,19 +98,16 @@ class ENGRAVEGate(PathBaseGate):
class CHAMFERGate(PathBaseGate):
- def allow(self, doc, obj, sub): # pylint: disable=unused-argument
+ def allow(self, doc, obj, sub): # pylint: disable=unused-argument
try:
shape = obj.Shape
- except Exception: # pylint: disable=broad-except
+ except Exception: # pylint: disable=broad-except
return False
if math.fabs(shape.Volume) < 1e-9 and len(shape.Wires) > 0:
return True
- if shape.ShapeType == 'Edge':
- return True
-
- if (shape.ShapeType == 'Face' and shape.normalAt(0, 0) == FreeCAD.Vector(0, 0, 1)):
+ if 'Edge' == shape.ShapeType or 'Face' == shape.ShapeType:
return True
if sub:
@@ -385,6 +382,7 @@ def select(op):
opsel['Vcarve'] = vcarveselect
opsel['Probe'] = probeselect
opsel['Custom'] = customselect
+ opsel['Thread Milling'] = drillselect
opsel['TurnFace'] = turnselect
opsel['TurnProfile'] = turnselect
opsel['TurnPartoff'] = turnselect
diff --git a/src/Mod/Path/PathScripts/PathThreadMilling.py b/src/Mod/Path/PathScripts/PathThreadMilling.py
new file mode 100644
index 0000000000..7b15afbc36
--- /dev/null
+++ b/src/Mod/Path/PathScripts/PathThreadMilling.py
@@ -0,0 +1,344 @@
+# -*- coding: utf-8 -*-
+# ***************************************************************************
+# * *
+# * Copyright (c) 2019 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 *
+# * *
+# ***************************************************************************
+
+from __future__ import print_function
+
+import FreeCAD
+import Path
+import PathScripts.PathCircularHoleBase as PathCircularHoleBase
+import PathScripts.PathGeom as PathGeom
+import PathScripts.PathLog as PathLog
+import PathScripts.PathOp as PathOp
+import PathScripts.PathUtils as PathUtils
+import math
+
+from PySide import QtCore
+
+__title__ = "Path Thread Milling Operation"
+__author__ = "sliptonic (Brad Collette)"
+__url__ = "http://www.freecadweb.org"
+__doc__ = "Path thread milling 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 radiiInternal(majorDia, minorDia, toolDia, toolCrest = None):
+ '''internlThreadRadius(majorDia, minorDia, toolDia, toolCrest) ... returns the maximum radius for thread.'''
+ PathLog.track(majorDia, minorDia, toolDia, toolCrest)
+ if toolCrest is None:
+ toolCrest = 0.0
+ # As it turns out metric and imperial standard threads follow the same rules.
+ # The determining factor is the height of the full 60 degree triangle H.
+ # - The minor diameter is 1/4 * H smaller than the pitch diameter.
+ # - The major diameter is 3/8 * H bigger than the pitch diameter
+ # Since we already have the outer diameter it's simpler to just add 1/8 * H
+ # to get the outer tip of the thread.
+ H = ((majorDia - minorDia) / 2.0 ) * 1.6 # (D - d)/2 = 5/8 * H
+ outerTip = majorDia / 2.0 + H / 8.0
+ # Compensate for the crest of the tool
+ toolTip = outerTip - toolCrest * 0.8660254037844386 # math.sqrt(3)/2 ... 60deg triangle height
+ return ((minorDia - toolDia) / 2.0, toolTip - toolDia / 2.0)
+
+def threadPasses(count, radii, majorDia, minorDia, toolDia, toolCrest = None):
+ PathLog.track(count, radii, majorDia, minorDia, toolDia, toolCrest)
+ minor, major = radii(majorDia, minorDia, toolDia, toolCrest)
+ dr = float(major - minor) / count
+ return [major - dr * (count - (i + 1)) for i in range(count)]
+
+class _InternalThread(object):
+ '''Helper class for dealing with different thread types'''
+ def __init__(self, cmd, zStart, zFinal, pitch):
+ self.cmd = cmd
+ if zStart < zFinal:
+ self.pitch = pitch
+ else:
+ self.pitch = -pitch
+ self.hPitch = self.pitch / 2
+ self.zStart = zStart
+ self.zFinal = zFinal
+
+ def overshoots(self, z):
+ '''overshoots(z) ... returns true if adding another half helix goes beyond the thread bounds'''
+ if self.pitch < 0:
+ return z + self.hPitch < self.zFinal
+ return z + self.hPitch > self.zFinal
+
+ def adjustX(self, x, dx):
+ '''adjustX(x, dx) ... move x by dx, the direction depends on the thread settings'''
+ if self.isG3() == (self.pitch > 0):
+ return x + dx
+ return x - dx
+
+ def adjustY(self, y, dy):
+ '''adjustY(y, dy) ... move y by dy, the direction depends on the thread settings'''
+ if self.isG3():
+ return y - dy
+ return y - dy
+
+ def isG3(self):
+ '''isG3() ... returns True if this is a G3 command'''
+ return self.cmd in ['G3', 'G03', 'g3', 'g03']
+
+ def isUp(self):
+ '''isUp() ... returns True if the thread goes from the bottom up'''
+ return self.pitch > 0
+
+def internalThreadCommands(loc, cmd, zStart, zFinal, pitch, radius, leadInOut):
+ '''internalThreadCommands(loc, cmd, zStart, zFinal, pitch, radius) ... returns the g-code to mill the given internal thread'''
+ thread = _InternalThread(cmd, zStart, zFinal, pitch)
+
+ yMin = loc.y - radius
+ yMax = loc.y + radius
+
+ path = []
+ # at this point the tool is at a safe height (depending on the previous thread), so we can move
+ # into position first, and then drop to the start height. If there is any material in the way this
+ # op hasn't been setup properly.
+ path.append(Path.Command('G0', {'X': loc.x, 'Y': loc.y}))
+ path.append(Path.Command('G0', {'Z': thread.zStart}))
+ if leadInOut:
+ path.append(Path.Command(thread.cmd, {'Y': yMax, 'J': (yMax - loc.y) / 2}))
+ else:
+ path.append(Path.Command('G1', {'Y': yMax}))
+
+ z = thread.zStart
+ r = -radius
+ i = 0
+ while True:
+ z = thread.zStart + i*thread.hPitch
+ if thread.overshoots(z):
+ break
+ if 0 == (i & 0x01):
+ y = yMin
+ else:
+ y = yMax
+ path.append(Path.Command(thread.cmd, {'Y': y, 'Z': z + thread.hPitch, 'J': r}))
+ r = -r
+ i = i + 1
+
+
+ z = thread.zStart + i*thread.hPitch
+ if PathGeom.isRoughly(z, thread.zFinal):
+ x = loc.x
+ else:
+ n = math.fabs(thread.zFinal - thread.zStart) / thread.hPitch
+ k = n - int(n)
+ dy = math.cos(k * math.pi)
+ dx = math.sin(k * math.pi)
+ y = thread.adjustY(loc.y, r * dy)
+ x = thread.adjustX(loc.x, r * dx)
+ path.append(Path.Command(thread.cmd, {'X': x, 'Y': y, 'Z': thread.zFinal, 'J': r}))
+
+ if leadInOut:
+ path.append(Path.Command(thread.cmd, {'X': loc.x, 'Y': loc.y, 'I': (loc.x - x) / 2, 'J': (loc.y - y) / 2}))
+ else:
+ path.append(Path.Command('G1', {'X': loc.x, 'Y': loc.y}))
+ return path
+
+class ObjectThreadMilling(PathCircularHoleBase.ObjectOp):
+ '''Proxy object for thread milling operation.'''
+
+ LeftHand = 'LeftHand'
+ RightHand = 'RightHand'
+ ThreadTypeCustom = 'Custom'
+ ThreadTypeMetricInternal = 'Metric - internal'
+ ThreadTypeImperialInternal = 'Imperial - internal'
+ DirectionClimb = 'Climb'
+ DirectionConventional = 'Conventional'
+
+ ThreadOrientations = [LeftHand, RightHand]
+ ThreadTypes = [ThreadTypeCustom, ThreadTypeMetricInternal, ThreadTypeImperialInternal]
+ Directions = [DirectionClimb, DirectionConventional]
+
+ def circularHoleFeatures(self, obj):
+ return PathOp.FeatureBaseGeometry
+
+ def initCircularHoleOperation(self, obj):
+ obj.addProperty("App::PropertyEnumeration", "ThreadOrientation", "Thread", QtCore.QT_TRANSLATE_NOOP("PathThreadMilling", "Set thread orientation"))
+ obj.ThreadOrientation = self.ThreadOrientations
+ obj.addProperty("App::PropertyEnumeration", "ThreadType", "Thread", QtCore.QT_TRANSLATE_NOOP("PathThreadMilling", "Currently only internal"))
+ obj.ThreadType = self.ThreadTypes
+ obj.addProperty("App::PropertyString", "ThreadName", "Thread", QtCore.QT_TRANSLATE_NOOP("PathThreadMilling", "Devfines which standard thread was chosen"))
+ obj.addProperty("App::PropertyLength", "MajorDiameter", "Thread", QtCore.QT_TRANSLATE_NOOP("PathThreadMilling", "Set thread's major diameter"))
+ obj.addProperty("App::PropertyLength", "MinorDiameter", "Thread", QtCore.QT_TRANSLATE_NOOP("PathThreadMilling", "Set thread's minor diameter"))
+ obj.addProperty("App::PropertyLength", "Pitch", "Thread", QtCore.QT_TRANSLATE_NOOP("PathThreadMilling", "Set thread's pitch - used for metric threads"))
+ obj.addProperty("App::PropertyInteger", "TPI", "Thread", QtCore.QT_TRANSLATE_NOOP("PathThreadMilling", "Set thread's tpi - used for imperial threads"))
+ obj.addProperty("App::PropertyInteger", "ThreadFit", "Thread", QtCore.QT_TRANSLATE_NOOP("PathThreadMilling", "Set how many passes are used to cut the thread"))
+ obj.addProperty("App::PropertyInteger", "Passes", "Operation", QtCore.QT_TRANSLATE_NOOP("PathThreadMilling", "Set how many passes are used to cut the thread"))
+ obj.addProperty("App::PropertyEnumeration", "Direction", "Operation", QtCore.QT_TRANSLATE_NOOP("PathThreadMilling", "Direction of thread cutting operation"))
+ obj.addProperty("App::PropertyBool", "LeadInOut", "Operation", QtCore.QT_TRANSLATE_NOOP("PathThreadMilling", "Set to True to get lead in and lead out arcs at the start and end of the thread cut"))
+ obj.addProperty("App::PropertyLink", "ClearanceOp", "Operation", QtCore.QT_TRANSLATE_NOOP("PathThreadMilling", "Operation to clear the inside of the thread"))
+ obj.Direction = self.Directions
+
+ # Rotation related properties
+ if not hasattr(obj, 'EnableRotation'):
+ obj.addProperty("App::PropertyEnumeration", "EnableRotation", "Rotation", QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable rotation to gain access to pockets/areas not normal to Z axis."))
+ obj.EnableRotation = ['Off', 'A(x)', 'B(y)', 'A & B']
+
+
+ def threadStartDepth(self, obj):
+ if obj.ThreadOrientation == self.RightHand:
+ if obj.Direction == self.DirectionClimb:
+ PathLog.track(obj.Label, obj.FinalDepth)
+ return obj.FinalDepth
+ PathLog.track(obj.Label, obj.StartDepth)
+ return obj.StartDepth
+ if obj.Direction == self.DirectionClimb:
+ PathLog.track(obj.Label, obj.StartDepth)
+ return obj.StartDepth
+ PathLog.track(obj.Label, obj.FinalDepth)
+ return obj.FinalDepth
+
+ def threadFinalDepth(self, obj):
+ PathLog.track(obj.Label)
+ if obj.ThreadOrientation == self.RightHand:
+ if obj.Direction == self.DirectionClimb:
+ PathLog.track(obj.Label, obj.StartDepth)
+ return obj.StartDepth
+ PathLog.track(obj.Label, obj.FinalDepth)
+ return obj.FinalDepth
+ if obj.Direction == self.DirectionClimb:
+ PathLog.track(obj.Label, obj.FinalDepth)
+ return obj.FinalDepth
+ PathLog.track(obj.Label, obj.StartDepth)
+ return obj.StartDepth
+
+ def threadDirectionCmd(self, obj):
+ PathLog.track(obj.Label)
+ if obj.ThreadOrientation == self.RightHand:
+ if obj.Direction == self.DirectionClimb:
+ PathLog.track(obj.Label, 'G2')
+ return 'G2'
+ PathLog.track(obj.Label, 'G3')
+ return 'G3'
+ if obj.Direction == self.DirectionClimb:
+ PathLog.track(obj.Label, 'G3')
+ return 'G3'
+ PathLog.track(obj.Label, 'G2')
+ return 'G2'
+
+ def threadSetup(self, obj):
+ # the thing to remember is that Climb, for an internal thread must always be G3
+ if obj.Direction == self.DirectionClimb:
+ if obj.ThreadOrientation == self.RightHand:
+ return ('G3', obj.FinalDepth.Value, obj.StartDepth.Value)
+ return ('G3', obj.StartDepth.Value, obj.FinalDepth.Value)
+ if obj.ThreadOrientation == self.RightHand:
+ return ('G2', obj.StartDepth.Value, obj.FinalDepth.Value)
+ return ('G2', obj.FinalDepth.Value, obj.StartDepth.Value)
+
+ def threadPassRadii(self, obj):
+ PathLog.track(obj.Label)
+ rMajor = (obj.MajorDiameter.Value - self.tool.Diameter) / 2.0
+ rMinor = (obj.MinorDiameter.Value - self.tool.Diameter) / 2.0
+ if obj.Passes < 1:
+ obj.Passes = 1
+ rPass = (rMajor - rMinor) / obj.Passes
+ passes = [rMajor]
+ for i in range(1, obj.Passes):
+ passes.append(rMajor - rPass * i)
+ return list(reversed(passes))
+
+ def executeThreadMill(self, obj, loc, gcode, zStart, zFinal, pitch):
+ PathLog.track(obj.Label, loc, gcode, zStart, zFinal, pitch)
+
+ self.commandlist.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid}))
+
+ for radius in threadPasses(obj.Passes, radiiInternal, obj.MajorDiameter.Value, obj.MinorDiameter.Value, float(self.tool.Diameter), float(self.tool.Crest)):
+ commands = internalThreadCommands(loc, gcode, zStart, zFinal, pitch, radius, obj.LeadInOut)
+ for cmd in commands:
+ p = cmd.Parameters
+ if cmd.Name in ['G0']:
+ p.update({'F': self.vertRapid})
+ if cmd.Name in ['G1', 'G2', 'G3']:
+ p.update({'F': self.horizFeed})
+ cmd.Parameters = p
+ self.commandlist.extend(commands)
+
+ self.commandlist.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid}))
+
+ def circularHoleExecute(self, obj, holes):
+ PathLog.track()
+ if self.isToolSupported(obj, self.tool):
+ self.commandlist.append(Path.Command("(Begin Thread Milling)"))
+
+ (cmd, zStart, zFinal) = self.threadSetup(obj)
+ pitch = obj.Pitch.Value
+ if obj.TPI > 0:
+ pitch = 25.4 / obj.TPI
+ if pitch <= 0:
+ PathLog.error("Cannot create thread with pitch {}".format(pitch))
+ return
+
+ # rapid to clearance height
+ for loc in holes:
+ self.executeThreadMill(obj, FreeCAD.Vector(loc['x'], loc['y'], 0), cmd, zStart, zFinal, pitch)
+ else:
+ PathLog.error("No suitable Tool found for thread milling operation")
+
+
+ def opSetDefaultValues(self, obj, job):
+ obj.ThreadOrientation = self.RightHand
+ obj.ThreadType = self.ThreadTypeMetricInternal
+ obj.ThreadFit = 50
+ obj.Pitch = 1
+ obj.TPI = 0
+ obj.Passes = 1
+ obj.Direction = self.DirectionClimb
+ obj.LeadInOut = True
+
+ def isToolSupported(self, obj, tool):
+ '''Thread milling only supports thread milling cutters.'''
+ return hasattr(tool, 'Diameter') and hasattr(tool, 'Crest')
+
+
+def SetupProperties():
+ setup = []
+ setup.append("ThreadOrientation")
+ setup.append("ThreadType")
+ setup.append("ThreadName")
+ setup.append("ThreadFit")
+ setup.append("MajorDiameter")
+ setup.append("MinorDiameter")
+ setup.append("Pitch")
+ setup.append("TPI")
+ setup.append("Passes")
+ setup.append("Direction")
+ setup.append("LeadInOut")
+ return setup
+
+
+def Create(name, obj=None):
+ '''Create(name) ... Creates and returns a thread milling operation.'''
+ if obj is None:
+ obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
+ obj.Proxy = ObjectThreadMilling(obj, name)
+ if obj.Proxy:
+ obj.Proxy.findAllHoles(obj)
+ return obj
+
diff --git a/src/Mod/Path/PathScripts/PathThreadMillingGui.py b/src/Mod/Path/PathScripts/PathThreadMillingGui.py
new file mode 100644
index 0000000000..66acac9387
--- /dev/null
+++ b/src/Mod/Path/PathScripts/PathThreadMillingGui.py
@@ -0,0 +1,211 @@
+# -*- coding: utf-8 -*-
+# ***************************************************************************
+# * *
+# * Copyright (c) 2019 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 PathScripts.PathCircularHoleBaseGui as PathCircularHoleBaseGui
+import PathScripts.PathThreadMilling as PathThreadMilling
+import PathScripts.PathGui as PathGui
+import PathScripts.PathLog as PathLog
+import PathScripts.PathOpGui as PathOpGui
+import csv
+
+from PySide import QtCore
+
+__title__ = "Path Thread Milling Operation UI."
+__author__ = "sliptonic (Brad Collette)"
+__url__ = "http://www.freecadweb.org"
+__doc__ = "UI and Command for Path Thread Milling Operation."
+
+PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
+#PathLog.trackModule(PathLog.thisModule())
+
+def setupCombo(combo, selections):
+ combo.clear()
+ for item in selections:
+ combo.addItem(item)
+
+def fillThreads(combo, dataFile):
+ combo.blockSignals(True)
+ combo.clear()
+ with open("{}Mod/Path/Data/Threads/{}.csv".format(FreeCAD.getHomePath(), dataFile)) as fp:
+ reader = csv.DictReader(fp)
+ for row in reader:
+ combo.addItem(row['name'], row)
+ combo.setEnabled(True)
+ combo.blockSignals(False)
+
+class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage):
+ '''Controller for the thread milling operation's page'''
+
+ def initPage(self, obj):
+ self.majorDia = PathGui.QuantitySpinBox(self.form.threadMajor, obj, 'MajorDiameter') # pylint: disable=attribute-defined-outside-init
+ self.minorDia = PathGui.QuantitySpinBox(self.form.threadMinor, obj, 'MinorDiameter') # pylint: disable=attribute-defined-outside-init
+ self.pitch = PathGui.QuantitySpinBox(self.form.threadPitch, obj, 'Pitch') # pylint: disable=attribute-defined-outside-init
+
+ setupCombo(self.form.threadOrientation, obj.Proxy.ThreadOrientations)
+ setupCombo(self.form.threadType, obj.Proxy.ThreadTypes)
+ setupCombo(self.form.opDirection, obj.Proxy.Directions)
+
+ def getForm(self):
+ '''getForm() ... return UI'''
+ return FreeCADGui.PySideUic.loadUi(":/panels/PageOpThreadMillingEdit.ui")
+
+ def getFields(self, obj):
+ '''getFields(obj) ... update obj's properties with values from the UI'''
+ PathLog.track()
+
+ self.majorDia.updateProperty()
+ self.minorDia.updateProperty()
+ self.pitch.updateProperty()
+
+ obj.ThreadOrientation = self.form.threadOrientation.currentText()
+ obj.ThreadType = self.form.threadType.currentText()
+ obj.ThreadName = self.form.threadName.currentText()
+ obj.Direction = self.form.opDirection.currentText()
+ obj.Passes = self.form.opPasses.value()
+ obj.LeadInOut = self.form.leadInOut.checkState() == QtCore.Qt.Checked
+ obj.TPI = self.form.threadTPI.value()
+
+ self.updateToolController(obj, self.form.toolController)
+
+ def setFields(self, obj):
+ '''setFields(obj) ... update UI with obj properties' values'''
+ PathLog.track()
+
+ self.form.threadOrientation.setCurrentText(obj.ThreadOrientation)
+
+ self.form.threadType.blockSignals(True)
+ self.form.threadName.blockSignals(True)
+ self.form.threadType.setCurrentText(obj.ThreadType)
+ self._updateFromThreadType()
+ self.form.threadName.setCurrentText(obj.ThreadName)
+ self.form.threadType.blockSignals(False)
+ self.form.threadName.blockSignals(False)
+ self.form.threadTPI.setValue(obj.TPI)
+
+ self.form.opPasses.setValue(obj.Passes)
+ self.form.opDirection.setCurrentText(obj.Direction)
+ self.form.leadInOut.setCheckState(QtCore.Qt.Checked if obj.LeadInOut else QtCore.Qt.Unchecked)
+
+ self.majorDia.updateSpinBox()
+ self.minorDia.updateSpinBox()
+ self.pitch.updateSpinBox()
+
+ self.setupToolController(obj, self.form.toolController)
+
+
+ def _isThreadMetric(self):
+ return self.form.threadType.currentText() == PathThreadMilling.ObjectThreadMilling.ThreadTypeMetricInternal
+
+ def _isThreadImperial(self):
+ return self.form.threadType.currentText() == PathThreadMilling.ObjectThreadMilling.ThreadTypeImperialInternal
+
+ def _updateFromThreadType(self):
+
+ if self.form.threadType.currentText() == PathThreadMilling.ObjectThreadMilling.ThreadTypeCustom:
+ self.form.threadName.setEnabled(False)
+ self.form.threadFit.setEnabled(False)
+ self.form.threadFitLabel.setEnabled(False)
+ self.form.threadPitch.setEnabled(True)
+ self.form.threadPitchLabel.setEnabled(True)
+ self.form.threadTPI.setEnabled(True)
+ self.form.threadTPILabel.setEnabled(True)
+
+ if self._isThreadMetric():
+ self.form.threadFit.setEnabled(True)
+ self.form.threadFitLabel.setEnabled(True)
+ self.form.threadPitch.setEnabled(True)
+ self.form.threadPitchLabel.setEnabled(True)
+ self.form.threadTPI.setEnabled(False)
+ self.form.threadTPILabel.setEnabled(False)
+ self.form.threadTPI.setValue(0)
+ fillThreads(self.form.threadName, 'metric-internal')
+
+ if self._isThreadImperial():
+ self.form.threadFit.setEnabled(True)
+ self.form.threadFitLabel.setEnabled(True)
+ self.form.threadPitch.setEnabled(False)
+ self.form.threadPitchLabel.setEnabled(False)
+ self.form.threadTPI.setEnabled(True)
+ self.form.threadTPILabel.setEnabled(True)
+ self.pitch.updateSpinBox(0)
+ fillThreads(self.form.threadName, 'imperial-internal')
+
+ def _updateFromThreadName(self):
+ thread = self.form.threadName.currentData()
+ fit = float(self.form.threadFit.value()) / 100
+ mamin = float(thread['dMajorMin'])
+ mamax = float(thread['dMajorMax'])
+ major = mamin + (mamax - mamin) * fit
+ mimin = float(thread['dMinorMin'])
+ mimax = float(thread['dMinorMax'])
+ minor = mimin + (mimax - mimin) * fit
+
+ if self._isThreadMetric():
+ pitch = float(thread['pitch'])
+ self.pitch.updateSpinBox(pitch)
+
+ if self._isThreadImperial():
+ tpi = int(thread['tpi'])
+ self.form.threadTPI.setValue(tpi)
+ minor = minor * 25.4
+ major = major * 25.4
+
+ self.majorDia.updateSpinBox(major)
+ self.minorDia.updateSpinBox(minor)
+
+ self.setDirty()
+
+ def getSignalsForUpdate(self, obj):
+ '''getSignalsForUpdate(obj) ... return list of signals which cause the receiver to update the model'''
+ signals = []
+
+ signals.append(self.form.threadMajor.editingFinished)
+ signals.append(self.form.threadMinor.editingFinished)
+ signals.append(self.form.threadPitch.editingFinished)
+ signals.append(self.form.threadOrientation.currentIndexChanged)
+ signals.append(self.form.threadTPI.editingFinished)
+ signals.append(self.form.opDirection.currentIndexChanged)
+ signals.append(self.form.opPasses.editingFinished)
+ signals.append(self.form.leadInOut.stateChanged)
+
+ signals.append(self.form.toolController.currentIndexChanged)
+
+ return signals
+
+ def registerSignalHandlers(self, obj):
+ self.form.threadType.currentIndexChanged.connect(self._updateFromThreadType)
+ self.form.threadName.currentIndexChanged.connect(self._updateFromThreadName)
+ self.form.threadFit.valueChanged.connect(self._updateFromThreadName)
+
+
+Command = PathOpGui.SetupOperation('Thread Milling',
+ PathThreadMilling.Create,
+ TaskPanelOpPage,
+ 'Path-ThreadMilling',
+ QtCore.QT_TRANSLATE_NOOP("PathThreadMilling", "Thread Milling"),
+ QtCore.QT_TRANSLATE_NOOP("PathThreadMilling", "Creates a Path Thread Milling operation from features of a base object"),
+ PathThreadMilling.SetupProperties)
+
+FreeCAD.Console.PrintLog("Loading PathThreadMillingGui ... done\n")
diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py
index a76150b597..9cd6f488de 100644
--- a/src/Mod/Path/PathScripts/PathToolBit.py
+++ b/src/Mod/Path/PathScripts/PathToolBit.py
@@ -322,11 +322,14 @@ class ToolBit(object):
path = findShape(obj.BitShape)
if path:
with open(path, 'rb') as fd:
- zf = zipfile.ZipFile(fd)
- pf = zf.open('thumbnails/Thumbnail.png', 'r')
- data = pf.read()
- pf.close()
- return data
+ try:
+ zf = zipfile.ZipFile(fd)
+ pf = zf.open('thumbnails/Thumbnail.png', 'r')
+ data = pf.read()
+ pf.close()
+ return data
+ except KeyError:
+ pass
return None
def saveToFile(self, obj, path, setFile=True):
diff --git a/src/Mod/Path/PathScripts/PathUtils.py b/src/Mod/Path/PathScripts/PathUtils.py
index e78b1cfb2d..c53b7a2ab6 100644
--- a/src/Mod/Path/PathScripts/PathUtils.py
+++ b/src/Mod/Path/PathScripts/PathUtils.py
@@ -392,19 +392,22 @@ def reverseEdge(e):
return newedge
-def getToolControllers(obj):
+def getToolControllers(obj, proxy=None):
'''returns all the tool controllers'''
+ if proxy is None:
+ proxy = obj.Proxy
try:
job = findParentJob(obj)
except Exception: # pylint: disable=broad-except
job = None
+ print("op={} ({})".format(obj.Label, type(obj)))
if job:
- return job.ToolController
+ return [c for c in job.ToolController if proxy.isToolSupported(obj, c.Tool)]
return []
-def findToolController(obj, name=None):
+def findToolController(obj, proxy, name=None):
'''returns a tool controller with a given name.
If no name is specified, returns the first controller.
if no controller is found, returns None'''
@@ -416,7 +419,7 @@ def findToolController(obj, name=None):
if c is not None:
return c
- controllers = getToolControllers(obj)
+ controllers = getToolControllers(obj, proxy)
if len(controllers) == 0:
return None
diff --git a/src/Mod/Path/PathScripts/PostUtils.py b/src/Mod/Path/PathScripts/PostUtils.py
index 920894603b..06bd1914e0 100644
--- a/src/Mod/Path/PathScripts/PostUtils.py
+++ b/src/Mod/Path/PathScripts/PostUtils.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
#***************************************************************************
#* Copyright (c) 2014 Yorik van Havre *
#* *
diff --git a/src/Mod/Path/PathTests/TestPathThreadMilling.py b/src/Mod/Path/PathTests/TestPathThreadMilling.py
new file mode 100644
index 0000000000..e8ac9c8a65
--- /dev/null
+++ b/src/Mod/Path/PathTests/TestPathThreadMilling.py
@@ -0,0 +1,61 @@
+# -*- coding: utf-8 -*-
+
+# ***************************************************************************
+# * *
+# * Copyright (c) 2019 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 PathScripts.PathGeom as PathGeom
+import PathScripts.PathThreadMilling as PathThreadMilling
+import math
+
+from PathTests.PathTestUtils import PathTestBase
+
+def radii(major, minor, toolDia, toolCrest):
+ '''test radii function for simple testing'''
+ return (minor, major)
+
+class TestPathThreadMilling(PathTestBase):
+ '''Test thread milling basics.'''
+
+ def assertRadii(self, have, want):
+ self.assertRoughly(have[0], want[0])
+ self.assertRoughly(have[1], want[1])
+
+ def assertList(self, have, want):
+ self.assertEqual(len(have), len(want));
+ for i in range(len(have)):
+ self.assertRoughly(have[i], want[i])
+
+ def test00(self):
+ '''Verify internal radii.'''
+ self.assertRadii(PathThreadMilling.radiiInternal(20, 18, 2, 0), (8, 9.2))
+ self.assertRadii(PathThreadMilling.radiiInternal(20, 19, 2, 0), (8.5, 9.1))
+
+ def test01(self):
+ '''Verify internal radii with tool crest.'''
+ self.assertRadii(PathThreadMilling.radiiInternal(20, 18, 2, 0.1), (8, 9.113397))
+
+ def test10(self):
+ '''Verify thread passes.'''
+ self.assertList(PathThreadMilling.threadPasses(1, radii, 10, 9, 0, 0), [10])
+ self.assertList(PathThreadMilling.threadPasses(2, radii, 10, 9, 0, 0), [9.5, 10])
+ self.assertList(PathThreadMilling.threadPasses(5, radii, 10, 9, 0, 0), [9.2, 9.4, 9.6, 9.8, 10])
+
diff --git a/src/Mod/Path/TestPathApp.py b/src/Mod/Path/TestPathApp.py
index 1f4c0247fb..623bf180ee 100644
--- a/src/Mod/Path/TestPathApp.py
+++ b/src/Mod/Path/TestPathApp.py
@@ -41,6 +41,7 @@ from PathTests.TestPathSetupSheet import TestPathSetupSheet
from PathTests.TestPathDeburr import TestPathDeburr
from PathTests.TestPathHelix import TestPathHelix
from PathTests.TestPathVoronoi import TestPathVoronoi
+from PathTests.TestPathThreadMilling import TestPathThreadMilling
# dummy usage to get flake8 and lgtm quiet
False if TestApp.__name__ else True
@@ -62,4 +63,5 @@ False if TestPathHelix.__name__ else True
False if TestPathPreferences.__name__ else True
False if TestPathToolBit.__name__ else True
False if TestPathVoronoi.__name__ else True
+False if TestPathThreadMilling.__name__ else True
diff --git a/src/Mod/Path/Tools/Bit/5mm-thread-cutter.fctb b/src/Mod/Path/Tools/Bit/5mm-thread-cutter.fctb
new file mode 100644
index 0000000000..265978053b
--- /dev/null
+++ b/src/Mod/Path/Tools/Bit/5mm-thread-cutter.fctb
@@ -0,0 +1,14 @@
+{
+ "version": 2,
+ "name": "3mm-thread-cutter",
+ "shape": "thread-mill.fcstd",
+ "parameter": {
+ "Crest": "0.10 mm",
+ "Diameter": "5.00 mm",
+ "Length": "50.00 mm",
+ "NeckDiameter": "3.00 mm",
+ "NeckHeight": "20.00 mm",
+ "ShankDiameter": "5.00 mm"
+ },
+ "attribute": {}
+}
\ No newline at end of file
diff --git a/src/Mod/Path/Tools/Library/Default.fctl b/src/Mod/Path/Tools/Library/Default.fctl
index b3b8ea23c4..60de98e08e 100644
--- a/src/Mod/Path/Tools/Library/Default.fctl
+++ b/src/Mod/Path/Tools/Library/Default.fctl
@@ -31,6 +31,10 @@
{
"nr": 8,
"path": "probe.fctb"
+ },
+ {
+ "nr": 9,
+ "path": "5mm-thread-cutter.fctb"
}
],
"version": 1
diff --git a/src/Mod/Path/Tools/Shape/thread-mill.fcstd b/src/Mod/Path/Tools/Shape/thread-mill.fcstd
new file mode 100644
index 0000000000..ba9294fe6f
Binary files /dev/null and b/src/Mod/Path/Tools/Shape/thread-mill.fcstd differ