From 1917d15bd198d21362d56062f6fa555815f43ff9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20B=C3=A4hr?= Date: Sun, 3 Nov 2024 23:21:40 +0100 Subject: [PATCH 1/4] CAM: Add tests loading a helical path created with v0.21 This demonstrates a regression introduced with PR #14364, where documents containing helix operations cannot be recomputed any more (see #15643). This is in preparation for a fix and to ensure we don't break it again. The fixture has been created with the official mac build of FreeCAD-0.21.1 --- .../CAM/CAMTests/Fixtures/OpHelix_v0-21.FCStd | Bin 0 -> 30107 bytes src/Mod/CAM/CAMTests/PathTestUtils.py | 11 ++++ src/Mod/CAM/CAMTests/TestPathHelix.py | 48 ++++++++++++++++++ src/Mod/CAM/CMakeLists.txt | 12 +++++ 4 files changed, 71 insertions(+) create mode 100644 src/Mod/CAM/CAMTests/Fixtures/OpHelix_v0-21.FCStd diff --git a/src/Mod/CAM/CAMTests/Fixtures/OpHelix_v0-21.FCStd b/src/Mod/CAM/CAMTests/Fixtures/OpHelix_v0-21.FCStd new file mode 100644 index 0000000000000000000000000000000000000000..087665c9faa73db275db6a8bea43ef8110a9e965 GIT binary patch literal 30107 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 QNV%Asi;GH-{b{QIA67z)A^-pY literal 0 HcmV?d00001 diff --git a/src/Mod/CAM/CAMTests/PathTestUtils.py b/src/Mod/CAM/CAMTests/PathTestUtils.py index ffdbf01cd1..b12560f672 100644 --- a/src/Mod/CAM/CAMTests/PathTestUtils.py +++ b/src/Mod/CAM/CAMTests/PathTestUtils.py @@ -185,3 +185,14 @@ class PathTestBase(unittest.TestCase): self.assertEqual(len(pts0), len(pts1)) for i in range(len(pts0)): self.assertCoincide(pts0[i], pts1[i]) + + def assertSuccessfulRecompute(self, doc, *objs, msg=None): + """Asserts that the given objects can be successfully recomputed.""" + if len(objs) == 0: + doc.recompute() + objs = doc.Objects + else: + doc.recompute(objs) + failed_objects = [o.Name for o in objs if "Invalid" in o.State] + if len(failed_objects) > 0: + self.fail(msg or f"Recompute failed for {failed_objects}") diff --git a/src/Mod/CAM/CAMTests/TestPathHelix.py b/src/Mod/CAM/CAMTests/TestPathHelix.py index bd32272eb7..772f1ab231 100644 --- a/src/Mod/CAM/CAMTests/TestPathHelix.py +++ b/src/Mod/CAM/CAMTests/TestPathHelix.py @@ -20,6 +20,8 @@ # * * # *************************************************************************** +import pathlib + import Draft import FreeCAD import Path @@ -27,6 +29,8 @@ import Path.Main.Job as PathJob import Path.Op.Helix as PathHelix import CAMTests.PathTestUtils as PathTestUtils +FIXTURE_PATH = pathlib.Path(__file__).parent / "Fixtures" + Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) # Path.Log.trackModule(Path.Log.thisModule()) @@ -126,3 +130,47 @@ class TestPathHelix(PathTestUtils.PathTestBase): self.assertRoughly( round(pos.Length / 10, 0), proxy.holeDiameter(op, model, sub) ) + + def testRecomputeHelixFromV021(self): + """Verify that we can still open and recompute a Helix created with older FreeCAD""" + self.tearDown() + self.doc = FreeCAD.openDocument(str(FIXTURE_PATH / "OpHelix_v0-21.FCStd")) + created_with = f"created with {self.doc.getProgramVersion()}" + + def check(helix, direction, start_side, cut_mode): + with self.subTest(f"{helix.Name}: {direction}, {start_side}, {cut_mode}"): + # no recompute yet, i.e. check original as precondition + self.assertEqual( + helix.Direction, + direction, + msg=f"Direction does not match fixture for helix {created_with}", + ) + self.assertEqual( + helix.StartSide, + start_side, + msg=f"StartSide does not match fixture for helix {created_with}", + ) + + # now see whether we can recompute the object from the old document + helix.enforceRecompute() + self.assertSuccessfulRecompute( + self.doc, helix, msg=f"Cannot recompute helix {created_with}" + ) + self.assertEqual( + helix.Direction, + direction, + msg=f"Direction changed after recomputing helix {created_with}", + ) + self.assertEqual( + helix.StartSide, + start_side, + msg=f"StartSide changed after recomputing helix {created_with}", + ) + # self.assertEqual(helix.CutMode, cut_mode, + # msg=f"CutMode not correctly derived for helix {created_with}") + + # object names and expected values defined in the fixture + check(self.doc.Helix, "CW", "Inside", "Conventional") + check(self.doc.Helix001, "CW", "Outside", "Climb") + check(self.doc.Helix002, "CCW", "Inside", "Climb") + check(self.doc.Helix003, "CCW", "Outside", "Conventional") diff --git a/src/Mod/CAM/CMakeLists.txt b/src/Mod/CAM/CMakeLists.txt index ad349d54c6..48b6bca114 100644 --- a/src/Mod/CAM/CMakeLists.txt +++ b/src/Mod/CAM/CMakeLists.txt @@ -349,6 +349,10 @@ SET(Tests_SRCS CAMTests/Tools/Shape/test-path-tool-bit-shape-00.fcstd ) +SET(Tests_Fixtures + CAMTests/Fixtures/OpHelix_v0-21.FCStd +) + SET(PathImages_Ops Images/Ops/chamfer.svg ) @@ -411,6 +415,7 @@ ADD_CUSTOM_TARGET(PathScripts ALL SET(test_files ${Path_Scripts} ${Tests_SRCS} + ${Tests_Fixtures} ) ADD_CUSTOM_TARGET(Tests ALL @@ -536,6 +541,13 @@ INSTALL( Mod/CAM/CAMTests ) +INSTALL( + FILES + ${Tests_Fixtures} + DESTINATION + Mod/CAM/CAMTests/Fixtures +) + INSTALL( DIRECTORY CAMTests/Tools From 3106e2615d0b396198c6daeccd2160a0597b8cf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20B=C3=A4hr?= Date: Thu, 7 Nov 2024 22:31:32 +0100 Subject: [PATCH 2/4] CAM: Fix and enable TestPathHelix again Since the fixture in use has holes smaller than the default tool dimeter, executing a helix operation with default parameters (i.e. all holes) used to fail with an exception. Some tests work around this by actively chaning the tool diameter, and this fix is now moved into the global setup where the respective fixture is loaded. The reason why the tests got disabled in the first place is unclear. --- src/Mod/CAM/CAMTests/TestPathHelix.py | 5 +++-- src/Mod/CAM/TestCAMApp.py | 5 ++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Mod/CAM/CAMTests/TestPathHelix.py b/src/Mod/CAM/CAMTests/TestPathHelix.py index 772f1ab231..5efc50aca2 100644 --- a/src/Mod/CAM/CAMTests/TestPathHelix.py +++ b/src/Mod/CAM/CAMTests/TestPathHelix.py @@ -43,6 +43,9 @@ class TestPathHelix(PathTestUtils.PathTestBase): self.doc = FreeCAD.open(FreeCAD.getHomePath() + "Mod/CAM/CAMTests/test_holes00.fcstd") self.job = PathJob.Create("Job", [self.doc.Body]) + # the smallest hole in the fixture is 1mm in diameter, so our tool must be smaller. + self.job.Tools.Group[0].Tool.Diameter = 0.9 + def tearDown(self): FreeCAD.closeDocument(self.doc.Name) @@ -66,8 +69,6 @@ class TestPathHelix(PathTestUtils.PathTestBase): def test02(self): """Verify Helix generates proper holes for rotated model""" - self.job.Tools.Group[0].Tool.Diameter = 0.5 - op = PathHelix.Create("Helix") proxy = op.Proxy model = self.job.Model.Group[0] diff --git a/src/Mod/CAM/TestCAMApp.py b/src/Mod/CAM/TestCAMApp.py index 2d6e6ae860..ee129b4d8a 100644 --- a/src/Mod/CAM/TestCAMApp.py +++ b/src/Mod/CAM/TestCAMApp.py @@ -37,9 +37,8 @@ from CAMTests.TestPathGeneratorDogboneII import TestGeneratorDogboneII from CAMTests.TestPathGeom import TestPathGeom from CAMTests.TestPathLanguage import TestPathLanguage from CAMTests.TestPathOpDeburr import TestPathOpDeburr - -# from CAMTests.TestPathHelix import TestPathHelix from CAMTests.TestPathHelpers import TestPathHelpers +from CAMTests.TestPathHelix import TestPathHelix from CAMTests.TestPathHelixGenerator import TestPathHelixGenerator from CAMTests.TestPathLog import TestPathLog from CAMTests.TestPathOpUtil import TestPathOpUtil @@ -98,7 +97,7 @@ False if TestPathOpDeburr.__name__ else True False if TestPathDrillable.__name__ else True False if TestPathGeom.__name__ else True False if TestPathHelpers.__name__ else True -# False if TestPathHelix.__name__ else True +False if TestPathHelix.__name__ else True False if TestPathLog.__name__ else True False if TestPathOpUtil.__name__ else True # False if TestPathPost.__name__ else True From d436a595e80c9f125854477773c274139fd424ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20B=C3=A4hr?= Date: Fri, 8 Nov 2024 23:02:44 +0100 Subject: [PATCH 3/4] Revert "CAM: Rename CW/CCW to Climb/Conventional for consistency (#14364)" This reverts commit 7e62d07538d9739dce4483bc84bd9516a706d6ce and adapts to the in between commits where necessary. - CAM: apply precommit 7274dac185339a45020f1dc91f2b6c7478b2b0aa - CAM: rename "Tests" to "CAMTests" 2ff4914fd89cddd2e6907d11ab5e88130e54719d Motivations for the revert: - Instead of actually implementing #14364 only a search and replace in the enums values has been performed. But this does not mean the feature is there, becasue "climb"/"conventional" milling depends not only on the path's direction, but also on the spindle direction and whether the milled feature is "inside" or "outside". - In half of the cases of every changed operation or dressup, depending on whether we mill "inside" or "outside", we now get the wrong results. - In the (rarer) case of a reversed spindle direction, the "other half" is wrong. - Files created with previous versions of FreeCAD cannot be recomputed any longer because there were no precautions on document restore. - In files created with previous versions of FreeCAD one cannot change the broken operation as the choices are now invalid. - The original search/replace was incomplete. - Only property values have been changed, the property names are still inconsistent ("Direction" vs "Cutting Mode"). - The original search/replace also changed internal APIs where the "climb"/"conventional" wording is not applicable as there is no notation of "inside"/"outside" or "forward"/"reverse". If only the path's direction is concerned, "CW"/"CCW" fits better. --- src/Mod/CAM/CAMTests/PathTestUtils.py | 4 +- .../CAM/CAMTests/TestPathDressupDogbone.py | 14 +- src/Mod/CAM/CAMTests/TestPathGeom.py | 62 +- .../CAM/CAMTests/TestPathHelixGenerator.py | 2 +- src/Mod/CAM/CAMTests/TestPathProfile.py | 6 +- .../Gui/Resources/panels/PageOpDeburrEdit.ui | 832 +++++++++--------- .../Gui/Resources/panels/PageOpHelixEdit.ui | 334 +++---- src/Mod/CAM/Path/Base/Generator/helix.py | 6 +- src/Mod/CAM/Path/Dressup/Gui/LeadInOut.py | 6 +- src/Mod/CAM/Path/Geom.py | 8 +- src/Mod/CAM/Path/Op/Area.py | 2 +- src/Mod/CAM/Path/Op/Deburr.py | 10 +- src/Mod/CAM/Path/Op/Helix.py | 6 +- src/Mod/CAM/Path/Op/PocketBase.py | 2 +- src/Mod/CAM/Path/Op/Profile.py | 14 +- .../CAM/Path/Post/scripts/heidenhain_post.py | 6 +- 16 files changed, 653 insertions(+), 661 deletions(-) diff --git a/src/Mod/CAM/CAMTests/PathTestUtils.py b/src/Mod/CAM/CAMTests/PathTestUtils.py index b12560f672..3e50ccf1ac 100644 --- a/src/Mod/CAM/CAMTests/PathTestUtils.py +++ b/src/Mod/CAM/CAMTests/PathTestUtils.py @@ -65,14 +65,14 @@ class PathTestBase(unittest.TestCase): for i in range(0, len(edges)): self.assertLine(edges[i], points[i], points[i + 1]) - def assertArc(self, edge, pt1, pt2, direction="Climb"): + def assertArc(self, edge, pt1, pt2, direction="CW"): """Verify that edge is an arc between pt1 and pt2 with the given direction.""" self.assertIs(type(edge.Curve), Part.Circle) self.assertCoincide(edge.valueAt(edge.FirstParameter), pt1) self.assertCoincide(edge.valueAt(edge.LastParameter), pt2) ptm = edge.valueAt((edge.LastParameter + edge.FirstParameter) / 2) side = Path.Geom.Side.of(pt2 - pt1, ptm - pt1) - if "Climb" == direction: + if "CW" == direction: self.assertEqual(side, Path.Geom.Side.Left) else: self.assertEqual(side, Path.Geom.Side.Right) diff --git a/src/Mod/CAM/CAMTests/TestPathDressupDogbone.py b/src/Mod/CAM/CAMTests/TestPathDressupDogbone.py index 7eb66393c5..4b61b8cba1 100644 --- a/src/Mod/CAM/CAMTests/TestPathDressupDogbone.py +++ b/src/Mod/CAM/CAMTests/TestPathDressupDogbone.py @@ -59,7 +59,7 @@ class TestDressupDogbone(PathTestBase): """Verify bones are inserted for simple moves.""" base = TestProfile( "Inside", - "Climb", + "CW", """ G0 X10 Y10 Z10 G1 Z0 @@ -84,7 +84,7 @@ class TestDressupDogbone(PathTestBase): """Verify bones are inserted if hole ends with rapid move out.""" base = TestProfile( "Inside", - "Climb", + "CW", """ G0 X10 Y10 Z10 G1 Z0 @@ -175,7 +175,7 @@ class TestDressupDogbone(PathTestBase): """Verify no bone is inserted for straight move interrupted by plunge.""" base = TestProfile( "Inside", - "Climb", + "CW", """ G0 X10 Y10 Z10 G1 Z0 @@ -197,7 +197,7 @@ class TestDressupDogbone(PathTestBase): """Verify can handle comments between moves""" base = TestProfile( "Inside", - "Climb", + "CW", """ G0 X10 Y10 Z10 G1 Z0 @@ -220,7 +220,7 @@ class TestDressupDogbone(PathTestBase): base = TestProfile( "Inside", - "Climb", + "CW", """ G0 X10 Y10 Z10 G1 Z0 @@ -246,7 +246,7 @@ class TestDressupDogbone(PathTestBase): """Verify can handle noops between moves""" base = TestProfile( "Inside", - "Climb", + "CW", """ G0 X10 Y10 Z10 G1 Z0 @@ -269,7 +269,7 @@ class TestDressupDogbone(PathTestBase): base = TestProfile( "Inside", - "Climb", + "CW", """ G0 X10 Y10 Z10 G1 Z0 diff --git a/src/Mod/CAM/CAMTests/TestPathGeom.py b/src/Mod/CAM/CAMTests/TestPathGeom.py index 67895e5f24..6e7cb20bef 100644 --- a/src/Mod/CAM/CAMTests/TestPathGeom.py +++ b/src/Mod/CAM/CAMTests/TestPathGeom.py @@ -44,64 +44,56 @@ class TestPathGeom(PathTestBase): def test01(self): """Verify diffAngle functionality.""" - self.assertRoughly(Path.Geom.diffAngle(0, +0 * math.pi / 4, "Climb") / math.pi, 0 / 4.0) - self.assertRoughly(Path.Geom.diffAngle(0, +3 * math.pi / 4, "Climb") / math.pi, 5 / 4.0) - self.assertRoughly(Path.Geom.diffAngle(0, -3 * math.pi / 4, "Climb") / math.pi, 3 / 4.0) - self.assertRoughly(Path.Geom.diffAngle(0, +4 * math.pi / 4, "Climb") / math.pi, 4 / 4.0) + self.assertRoughly(Path.Geom.diffAngle(0, +0 * math.pi / 4, "CW") / math.pi, 0 / 4.0) + self.assertRoughly(Path.Geom.diffAngle(0, +3 * math.pi / 4, "CW") / math.pi, 5 / 4.0) + self.assertRoughly(Path.Geom.diffAngle(0, -3 * math.pi / 4, "CW") / math.pi, 3 / 4.0) + self.assertRoughly(Path.Geom.diffAngle(0, +4 * math.pi / 4, "CW") / math.pi, 4 / 4.0) + self.assertRoughly(Path.Geom.diffAngle(0, +0 * math.pi / 4, "CCW") / math.pi, 0 / 4.0) + self.assertRoughly(Path.Geom.diffAngle(0, +3 * math.pi / 4, "CCW") / math.pi, 3 / 4.0) + self.assertRoughly(Path.Geom.diffAngle(0, -3 * math.pi / 4, "CCW") / math.pi, 5 / 4.0) + self.assertRoughly(Path.Geom.diffAngle(0, +4 * math.pi / 4, "CCW") / math.pi, 4 / 4.0) + self.assertRoughly( - Path.Geom.diffAngle(0, +0 * math.pi / 4, "Conventional") / math.pi, 0 / 4.0 + Path.Geom.diffAngle(+math.pi / 4, +0 * math.pi / 4, "CW") / math.pi, 1 / 4.0 ) self.assertRoughly( - Path.Geom.diffAngle(0, +3 * math.pi / 4, "Conventional") / math.pi, 3 / 4.0 + Path.Geom.diffAngle(+math.pi / 4, +3 * math.pi / 4, "CW") / math.pi, 6 / 4.0 ) self.assertRoughly( - Path.Geom.diffAngle(0, -3 * math.pi / 4, "Conventional") / math.pi, 5 / 4.0 + Path.Geom.diffAngle(+math.pi / 4, -1 * math.pi / 4, "CW") / math.pi, 2 / 4.0 ) self.assertRoughly( - Path.Geom.diffAngle(0, +4 * math.pi / 4, "Conventional") / math.pi, 4 / 4.0 + Path.Geom.diffAngle(-math.pi / 4, +0 * math.pi / 4, "CW") / math.pi, 7 / 4.0 + ) + self.assertRoughly( + Path.Geom.diffAngle(-math.pi / 4, +3 * math.pi / 4, "CW") / math.pi, 4 / 4.0 + ) + self.assertRoughly( + Path.Geom.diffAngle(-math.pi / 4, -1 * math.pi / 4, "CW") / math.pi, 0 / 4.0 ) self.assertRoughly( - Path.Geom.diffAngle(+math.pi / 4, +0 * math.pi / 4, "Climb") / math.pi, 1 / 4.0 - ) - self.assertRoughly( - Path.Geom.diffAngle(+math.pi / 4, +3 * math.pi / 4, "Climb") / math.pi, 6 / 4.0 - ) - self.assertRoughly( - Path.Geom.diffAngle(+math.pi / 4, -1 * math.pi / 4, "Climb") / math.pi, 2 / 4.0 - ) - self.assertRoughly( - Path.Geom.diffAngle(-math.pi / 4, +0 * math.pi / 4, "Climb") / math.pi, 7 / 4.0 - ) - self.assertRoughly( - Path.Geom.diffAngle(-math.pi / 4, +3 * math.pi / 4, "Climb") / math.pi, 4 / 4.0 - ) - self.assertRoughly( - Path.Geom.diffAngle(-math.pi / 4, -1 * math.pi / 4, "Climb") / math.pi, 0 / 4.0 - ) - - self.assertRoughly( - Path.Geom.diffAngle(+math.pi / 4, +0 * math.pi / 4, "Conventional") / math.pi, + Path.Geom.diffAngle(+math.pi / 4, +0 * math.pi / 4, "CCW") / math.pi, 7 / 4.0, ) self.assertRoughly( - Path.Geom.diffAngle(+math.pi / 4, +3 * math.pi / 4, "Conventional") / math.pi, + Path.Geom.diffAngle(+math.pi / 4, +3 * math.pi / 4, "CCW") / math.pi, 2 / 4.0, ) self.assertRoughly( - Path.Geom.diffAngle(+math.pi / 4, -1 * math.pi / 4, "Conventional") / math.pi, + Path.Geom.diffAngle(+math.pi / 4, -1 * math.pi / 4, "CCW") / math.pi, 6 / 4.0, ) self.assertRoughly( - Path.Geom.diffAngle(-math.pi / 4, +0 * math.pi / 4, "Conventional") / math.pi, + Path.Geom.diffAngle(-math.pi / 4, +0 * math.pi / 4, "CCW") / math.pi, 1 / 4.0, ) self.assertRoughly( - Path.Geom.diffAngle(-math.pi / 4, +3 * math.pi / 4, "Conventional") / math.pi, + Path.Geom.diffAngle(-math.pi / 4, +3 * math.pi / 4, "CCW") / math.pi, 4 / 4.0, ) self.assertRoughly( - Path.Geom.diffAngle(-math.pi / 4, -1 * math.pi / 4, "Conventional") / math.pi, + Path.Geom.diffAngle(-math.pi / 4, -1 * math.pi / 4, "CCW") / math.pi, 0 / 4.0, ) @@ -431,7 +423,7 @@ class TestPathGeom(PathTestBase): ), p1, p2, - "Climb", + "CW", ) self.assertArc( Path.Geom.edgeForCmd( @@ -440,7 +432,7 @@ class TestPathGeom(PathTestBase): ), p2, p1, - "Conventional", + "CCW", ) def test30(self): diff --git a/src/Mod/CAM/CAMTests/TestPathHelixGenerator.py b/src/Mod/CAM/CAMTests/TestPathHelixGenerator.py index 867b680d52..b46e3d3c43 100644 --- a/src/Mod/CAM/CAMTests/TestPathHelixGenerator.py +++ b/src/Mod/CAM/CAMTests/TestPathHelixGenerator.py @@ -44,7 +44,7 @@ def _resetArgs(): "step_over": 0.5, "tool_diameter": 5.0, "inner_radius": 0.0, - "direction": "Climb", + "direction": "CW", "startAt": "Inside", } diff --git a/src/Mod/CAM/CAMTests/TestPathProfile.py b/src/Mod/CAM/CAMTests/TestPathProfile.py index b233b3e051..c9823ad7b1 100644 --- a/src/Mod/CAM/CAMTests/TestPathProfile.py +++ b/src/Mod/CAM/CAMTests/TestPathProfile.py @@ -123,7 +123,7 @@ class TestPathProfile(PathTestBase): profile.processCircles = True profile.processHoles = True profile.UseComp = True - profile.Direction = "Climb" + profile.Direction = "CW" _addViewProvider(profile) self.doc.recompute() @@ -162,7 +162,7 @@ class TestPathProfile(PathTestBase): profile.processCircles = True profile.processHoles = True profile.UseComp = False - profile.Direction = "Climb" + profile.Direction = "CW" _addViewProvider(profile) self.doc.recompute() @@ -205,7 +205,7 @@ class TestPathProfile(PathTestBase): profile.processCircles = True profile.processHoles = True profile.UseComp = True - profile.Direction = "Climb" + profile.Direction = "CW" profile.OffsetExtra = -profile.OpToolDiameter / 2.0 _addViewProvider(profile) self.doc.recompute() diff --git a/src/Mod/CAM/Gui/Resources/panels/PageOpDeburrEdit.ui b/src/Mod/CAM/Gui/Resources/panels/PageOpDeburrEdit.ui index be4182f4f8..b10d7c6c19 100644 --- a/src/Mod/CAM/Gui/Resources/panels/PageOpDeburrEdit.ui +++ b/src/Mod/CAM/Gui/Resources/panels/PageOpDeburrEdit.ui @@ -1,422 +1,422 @@ - Form - - - - 0 - 0 - 350 - 450 - + Form + + + + 0 + 0 + 350 + 450 + + + + Form + + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 0 + 0 + - - Form + + + 125 + 0 + - - - - - - 0 - 0 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - - - 0 - 0 - - - - - 125 - 0 - - - - - 16777215 - 16777215 - - - - Tool Controller - - - - - - - The tool and its settings to be used for this operation. - - - - - - - - 0 - 0 - - - - - 125 - 0 - - - - - 16777215 - 16777215 - - - - Coolant Mode - - - - - - - The tool and its settings to be used for this operation. - - - - - - - - - - 6 - - - 12 - - - 12 - - - - - - 0 - 0 - - - - - 125 - 0 - - - - - 16777215 - 16777215 - - - - Direction - - - - - - - The direction in which the profile is performed, clockwise or counterclockwise. - - - Climb - - - 0 - - - - Climb - - - - - Conventional - - - - - - - - - - - - - 125 - 0 - - - - - 16777215 - 16777215 - - - - - - - - - - 50 - 0 - - - - W = - - - - - - - Width of chamfer cut. - - - mm - - - - - - - - - - - - 50 - 0 - - - - h = - - - - - - - Extra depth of tool immersion. - - - mm - - - - - - - - - Qt::Vertical - - - QSizePolicy::Fixed - - - - 20 - 13 - - - - - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - 0 - - - 6 - - - 0 - - - 3 - - - 3 - - - - - - - - 50 - 0 - - - - Join: - - - - - - - Round joint - - - - - - true - - - true - - - true - - - - - - - Miter joint - - - - - - true - - - true - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - - Qt::Vertical - - - - 20 - 154 - - - - - - - - - - - - - - - - - 0 - 0 - - - - - 150 - 150 - - - - - 150 - 150 - - - - TextLabel - - - true - - - Qt::AlignCenter - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - - + + + 16777215 + 16777215 + + + + Tool Controller + + + + + + + The tool and its settings to be used for this operation. + + + + + + + + 0 + 0 + + + + + 125 + 0 + + + + + 16777215 + 16777215 + + + + Coolant Mode + + + + + + + The tool and its settings to be used for this operation. + + + + - - - Gui::InputField - QLineEdit -
Gui/InputField.h
-
-
- - +
+ + + + 6 + + + 12 + + + 12 + + + + + + 0 + 0 + + + + + 125 + 0 + + + + + 16777215 + 16777215 + + + + Direction + + + + + + + The direction in which the profile is performed, clockwise or counterclockwise. + + + CW + + + 0 + + + + CW + + + + + CCW + + + + + + + + + + + + + 125 + 0 + + + + + 16777215 + 16777215 + + + + + + + + + + 50 + 0 + + + + W = + + + + + + + Width of chamfer cut. + + + mm + + + + + + + + + + + + 50 + 0 + + + + h = + + + + + + + Extra depth of tool immersion. + + + mm + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 13 + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 6 + + + 0 + + + 3 + + + 3 + + + + + + + + 50 + 0 + + + + Join: + + + + + + + Round joint + + + + + + true + + + true + + + true + + + + + + + Miter joint + + + + + + true + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 154 + + + + + + + + + + + + + + + + + 0 + 0 + + + + + 150 + 150 + + + + + 150 + 150 + + + + TextLabel + + + true + + + Qt::AlignCenter + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + +
+
+ + + Gui::InputField + QLineEdit +
Gui/InputField.h
+
+
+ +
diff --git a/src/Mod/CAM/Gui/Resources/panels/PageOpHelixEdit.ui b/src/Mod/CAM/Gui/Resources/panels/PageOpHelixEdit.ui index 69d4ee8e72..cdc8f842d8 100644 --- a/src/Mod/CAM/Gui/Resources/panels/PageOpHelixEdit.ui +++ b/src/Mod/CAM/Gui/Resources/panels/PageOpHelixEdit.ui @@ -1,175 +1,175 @@ - Form - - - - 0 - 0 - 400 - 365 - - - - Form - - + Form + + + + 0 + 0 + 400 + 365 + + + + Form + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - - Tool Controller - - - - - - - The tool and its settings to be used for this operation. - - - - - - - Coolant - - - - - - - The tool and its settings to be used for this operation. - - - - - + + + Tool Controller + + + + + + + The tool and its settings to be used for this operation. + + - - - - - - Start from - - - - - - - Specify if the helix operation should start at the inside and work its way outwards, or start at the outside and work its way to the center. - - - - Inside - - - - - Outside - - - - - - - - Direction - - - - - - - The direction for the helix, clockwise or counterclockwise. - - - - Climb - - - - - Conventional - - - - - - - - Step over percent - - - - - - - Specify the percent of the tool diameter each helix will be offset to the previous one. A step over of 100% means no overlap of the individual cuts. - - - 1 - - - 100 - - - 10 - - - 100 - - - - - - - Extra Offset - - - - - - - - - - - - + + + Coolant + + - - - - Qt::Vertical - - - - 20 - 40 - - - + + + + The tool and its settings to be used for this operation. + + - - - - - Gui::InputField - QLineEdit -
Gui/InputField.h
-
-
- - +
+
+ + + + + + + + Start from + + + + + + + Specify if the helix operation should start at the inside and work its way outwards, or start at the outside and work its way to the center. + + + + Inside + + + + + Outside + + + + + + + + Direction + + + + + + + The direction for the helix, clockwise or counterclockwise. + + + + CW + + + + + CCW + + + + + + + + Step over percent + + + + + + + Specify the percent of the tool diameter each helix will be offset to the previous one. A step over of 100% means no overlap of the individual cuts. + + + 1 + + + 100 + + + 10 + + + 100 + + + + + + + Extra Offset + + + + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + +
+
+ + + Gui::InputField + QLineEdit +
Gui/InputField.h
+
+
+ +
diff --git a/src/Mod/CAM/Path/Base/Generator/helix.py b/src/Mod/CAM/Path/Base/Generator/helix.py index f81b501739..02560bf9d1 100644 --- a/src/Mod/CAM/Path/Base/Generator/helix.py +++ b/src/Mod/CAM/Path/Base/Generator/helix.py @@ -45,7 +45,7 @@ def generate( step_over, tool_diameter, inner_radius=0.0, - direction="Climb", + direction="CW", startAt="Outside", ): """generate(edge, hole_radius, inner_radius, step_over) ... generate helix commands. @@ -96,7 +96,7 @@ def generate( elif startAt not in ["Inside", "Outside"]: raise ValueError("Invalid value for parameter 'startAt'") - elif direction not in ["Climb", "Conventional"]: + elif direction not in ["CW", "CCW"]: raise ValueError("Invalid value for parameter 'direction'") if type(step_over) not in [float, int]: @@ -145,7 +145,7 @@ def generate( def helix_cut_r(r): commandlist = [] - arc_cmd = "G2" if direction == "Climb" else "G3" + arc_cmd = "G2" if direction == "CW" else "G3" commandlist.append(Path.Command("G0", {"X": startPoint.x + r, "Y": startPoint.y})) commandlist.append(Path.Command("G1", {"Z": startPoint.z})) for i in range(1, turncount + 1): diff --git a/src/Mod/CAM/Path/Dressup/Gui/LeadInOut.py b/src/Mod/CAM/Path/Dressup/Gui/LeadInOut.py index 4bc39ee3e0..3411cc3a2d 100644 --- a/src/Mod/CAM/Path/Dressup/Gui/LeadInOut.py +++ b/src/Mod/CAM/Path/Dressup/Gui/LeadInOut.py @@ -178,12 +178,12 @@ class ObjectDressup: def getDirectionOfPath(self, obj): op = PathDressup.baseOp(obj.Base) side = op.Side if hasattr(op, "Side") else "Inside" - direction = op.Direction if hasattr(op, "Direction") else "Conventional" + direction = op.Direction if hasattr(op, "Direction") else "CCW" if side == "Outside": - return "left" if direction == "Climb" else "right" + return "left" if direction == "CW" else "right" else: - return "right" if direction == "Climb" else "left" + return "right" if direction == "CW" else "left" def getArcDirection(self, obj): direction = self.getDirectionOfPath(obj) diff --git a/src/Mod/CAM/Path/Geom.py b/src/Mod/CAM/Path/Geom.py index ecca7f3e98..356faf1ad9 100644 --- a/src/Mod/CAM/Path/Geom.py +++ b/src/Mod/CAM/Path/Geom.py @@ -147,10 +147,10 @@ def getAngle(vector): return a -def diffAngle(a1, a2, direction="Climb"): - """diffAngle(a1, a2, [direction='Climb']) +def diffAngle(a1, a2, direction="CW"): + """diffAngle(a1, a2, [direction='CW']) Returns the difference between two angles (a1 -> a2) into a given direction.""" - if direction == "Climb": + if direction == "CW": while a1 < a2: a1 += 2 * math.pi a = a1 - a2 @@ -453,7 +453,7 @@ def edgeForCmd(cmd, startPoint): cw = True else: cw = False - angle = diffAngle(getAngle(A), getAngle(B), "Climb" if cw else "CCW") + angle = diffAngle(getAngle(A), getAngle(B), "CW" if cw else "CCW") height = endPoint.z - startPoint.z pitch = height * math.fabs(2 * math.pi / angle) if angle > 0: diff --git a/src/Mod/CAM/Path/Op/Area.py b/src/Mod/CAM/Path/Op/Area.py index 1b4e4aee1b..8a2bbb2a3b 100644 --- a/src/Mod/CAM/Path/Op/Area.py +++ b/src/Mod/CAM/Path/Op/Area.py @@ -346,7 +346,7 @@ class ObjectOp(PathOp.ObjectOp): verts = hWire.Wires[0].Vertexes idx = 0 - if obj.Direction == "Conventional": + if obj.Direction == "CCW": idx = len(verts) - 1 x = verts[idx].X y = verts[idx].Y diff --git a/src/Mod/CAM/Path/Op/Deburr.py b/src/Mod/CAM/Path/Op/Deburr.py index 1123a4ae5b..e0c990c74a 100644 --- a/src/Mod/CAM/Path/Op/Deburr.py +++ b/src/Mod/CAM/Path/Op/Deburr.py @@ -144,7 +144,7 @@ class ObjectDeburr(PathEngraveBase.ObjectOp): "Deburr", QT_TRANSLATE_NOOP("App::Property", "Direction of toolpath"), ) - # obj.Direction = ["Climb", "Conventional"] + # obj.Direction = ["CW", "CCW"] obj.addProperty( "App::PropertyEnumeration", "Side", @@ -178,8 +178,8 @@ class ObjectDeburr(PathEngraveBase.ObjectOp): # Enumeration lists for App::PropertyEnumeration properties enums = { "Direction": [ - (translate("Path", "Climb"), "Climb"), - (translate("Path", "Conventional"), "Conventional"), + (translate("Path", "CW"), "CW"), + (translate("Path", "CCW"), "CCW"), ], # this is the direction that the profile runs "Join": [ (translate("PathDeburr", "Round"), "Round"), @@ -382,7 +382,7 @@ class ObjectDeburr(PathEngraveBase.ObjectOp): wires.append(wire) # Set direction of op - forward = obj.Direction == "Climb" + forward = obj.Direction == "CW" # Set value of side obj.Side = side[0] @@ -417,7 +417,7 @@ class ObjectDeburr(PathEngraveBase.ObjectOp): obj.Join = "Round" obj.setExpression("StepDown", "0 mm") obj.StepDown = "0 mm" - obj.Direction = "Climb" + obj.Direction = "CW" obj.Side = "Outside" obj.EntryPoint = 0 diff --git a/src/Mod/CAM/Path/Op/Helix.py b/src/Mod/CAM/Path/Op/Helix.py index 7df87e70c4..a283124989 100644 --- a/src/Mod/CAM/Path/Op/Helix.py +++ b/src/Mod/CAM/Path/Op/Helix.py @@ -68,8 +68,8 @@ class ObjectHelix(PathCircularHoleBase.ObjectOp): # Enumeration lists for App::PropertyEnumeration properties enums = { "Direction": [ - (translate("CAM_Helix", "Climb"), "Climb"), - (translate("CAM_Helix", "Conventional"), "Conventional"), + (translate("CAM_Helix", "CW"), "CW"), + (translate("CAM_Helix", "CCW"), "CCW"), ], # this is the direction that the profile runs "StartSide": [ (translate("PathProfile", "Outside"), "Outside"), @@ -103,7 +103,7 @@ class ObjectHelix(PathCircularHoleBase.ObjectOp): "Helix Drill", QT_TRANSLATE_NOOP( "App::Property", - "The direction of the circular cuts, ClockWise (Climb), or CounterClockWise (Conventional)", + "The direction of the circular cuts, ClockWise (CW), or CounterClockWise (CCW)", ), ) diff --git a/src/Mod/CAM/Path/Op/PocketBase.py b/src/Mod/CAM/Path/Op/PocketBase.py index 36cad37b6c..eb5558006e 100644 --- a/src/Mod/CAM/Path/Op/PocketBase.py +++ b/src/Mod/CAM/Path/Op/PocketBase.py @@ -125,7 +125,7 @@ class ObjectPocket(PathAreaOp.ObjectOp): "Pocket", QT_TRANSLATE_NOOP( "App::Property", - "The direction that the toolpath should go around the part ClockWise (Climb) or CounterClockWise (Conventional)", + "The direction that the toolpath should go around the part ClockWise (CW) or CounterClockWise (CCW)", ), ) obj.addProperty( diff --git a/src/Mod/CAM/Path/Op/Profile.py b/src/Mod/CAM/Path/Op/Profile.py index 80d356af63..b2037e5494 100644 --- a/src/Mod/CAM/Path/Op/Profile.py +++ b/src/Mod/CAM/Path/Op/Profile.py @@ -102,7 +102,7 @@ class ObjectProfile(PathAreaOp.ObjectOp): "Profile", QT_TRANSLATE_NOOP( "App::Property", - "The direction that the toolpath should go around the part ClockWise (Climb) or CounterClockWise (Conventional)", + "The direction that the toolpath should go around the part ClockWise (CW) or CounterClockWise (CCW)", ), ), ( @@ -188,8 +188,8 @@ class ObjectProfile(PathAreaOp.ObjectOp): # Enumeration lists for App::PropertyEnumeration properties enums = { "Direction": [ - (translate("PathProfile", "Climb"), "Climb"), - (translate("PathProfile", "Conventional"), "Conventional"), + (translate("PathProfile", "CW"), "CW"), + (translate("PathProfile", "CCW"), "CCW"), ], # this is the direction that the profile runs "HandleMultipleFeatures": [ (translate("PathProfile", "Collectively"), "Collectively"), @@ -225,7 +225,7 @@ class ObjectProfile(PathAreaOp.ObjectOp): """areaOpPropertyDefaults(obj, job) ... returns a dictionary of default values for the operation's properties.""" return { - "Direction": "Climb", + "Direction": "CW", "HandleMultipleFeatures": "Collectively", "JoinType": "Round", "MiterLimit": 0.1, @@ -338,11 +338,11 @@ class ObjectProfile(PathAreaOp.ObjectOp): # Reverse the direction for holes if isHole: - direction = "Climb" if obj.Direction == "Conventional" else "Conventional" + direction = "CW" if obj.Direction == "CCW" else "CCW" else: direction = obj.Direction - if direction == "Conventional": + if direction == "CCW": params["orientation"] = 0 else: params["orientation"] = 1 @@ -351,7 +351,7 @@ class ObjectProfile(PathAreaOp.ObjectOp): if obj.UseComp: offset = self.radius + obj.OffsetExtra.Value if offset == 0.0: - if direction == "Conventional": + if direction == "CCW": params["orientation"] = 1 else: params["orientation"] = 0 diff --git a/src/Mod/CAM/Path/Post/scripts/heidenhain_post.py b/src/Mod/CAM/Path/Post/scripts/heidenhain_post.py index e00754e57a..abbe1a2b23 100644 --- a/src/Mod/CAM/Path/Post/scripts/heidenhain_post.py +++ b/src/Mod/CAM/Path/Post/scripts/heidenhain_post.py @@ -370,11 +370,11 @@ def export(objectslist, filename, argstring): STORED_COMPENSATED_OBJ = commands # Find mill compensation if hasattr(obj, "Side") and hasattr(obj, "Direction"): - if obj.Side == "Outside" and obj.Direction == "Climb": + if obj.Side == "Outside" and obj.Direction == "CW": Compensation = "L" - elif obj.Side == "Outside" and obj.Direction == "Conventional": + elif obj.Side == "Outside" and obj.Direction == "CCW": Compensation = "R" - elif obj.Side != "Outside" and obj.Direction == "Climb": + elif obj.Side != "Outside" and obj.Direction == "CW": Compensation = "R" else: Compensation = "L" From 7c1e4ab630becb295c1c20f609a59b524c372b5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20B=C3=A4hr?= Date: Sun, 10 Nov 2024 17:39:36 +0100 Subject: [PATCH 4/4] CAM: Configure Helix via CutMode, not Direction (#14314) To harmonize the various CAM operations, the helical drill shall also be configured using the cut mode (Climb/Conventional) just like Pocket or FaceMill. This means, the path direction (CW/CCW) is now a hidden read-only output property, calculated from the side (Inside/OutSide) and the here newly introduced cut mode. The spindle direction is not yet taken into account and is always assumed to be "Forward". The GUI strings are kept compatible with what RP#14364 introduced becasue of the v1.0 release string freeze. They need revision once back on the main branch. --- src/Mod/CAM/CAMTests/TestPathHelix.py | 79 +++++++++++++++++-- .../Gui/Resources/panels/PageOpHelixEdit.ui | 6 +- .../CAM/Path/Base/SetupSheetOpPrototype.py | 3 + src/Mod/CAM/Path/Op/Gui/Helix.py | 10 +-- src/Mod/CAM/Path/Op/Helix.py | 75 +++++++++++++++++- 5 files changed, 156 insertions(+), 17 deletions(-) diff --git a/src/Mod/CAM/CAMTests/TestPathHelix.py b/src/Mod/CAM/CAMTests/TestPathHelix.py index 5efc50aca2..18d0df432f 100644 --- a/src/Mod/CAM/CAMTests/TestPathHelix.py +++ b/src/Mod/CAM/CAMTests/TestPathHelix.py @@ -25,6 +25,7 @@ import pathlib import Draft import FreeCAD import Path +import Path.Base.SetupSheetOpPrototype as PathSetupSheetOpPrototype import Path.Main.Job as PathJob import Path.Op.Helix as PathHelix import CAMTests.PathTestUtils as PathTestUtils @@ -55,6 +56,12 @@ class TestPathHelix(PathTestUtils.PathTestBase): op = PathHelix.Create("Helix") op.Proxy.execute(op) + def testCreateWithPrototype(self): + """Verify a Helix can be created on a SetupSheet's prototype instead of a real document object""" + + ptt = PathSetupSheetOpPrototype.OpPrototype("Helix") + op = PathHelix.Create("OpPrototype.Helix", ptt) + def test01(self): """Verify Helix generates proper holes from model""" @@ -132,6 +139,33 @@ class TestPathHelix(PathTestUtils.PathTestBase): round(pos.Length / 10, 0), proxy.holeDiameter(op, model, sub) ) + def testPathDirection(self): + """Verify that the generated paths obays the given parameters""" + helix = PathHelix.Create("Helix") + + def check(start_side, cut_mode, expected_direction): + with self.subTest(f"({start_side}, {cut_mode}) => {expected_direction}"): + helix.StartSide = start_side + helix.CutMode = cut_mode + + self.assertSuccessfulRecompute(self.doc, helix) + + self.assertEqual( + helix.Direction, + expected_direction, + msg=f"Direction was not correctly determined", + ) + self.assertPathDirection( + helix.Path, + expected_direction, + msg=f"Path with wrong direction generated", + ) + + check("Inside", "Conventional", "CW") + check("Outside", "Climb", "CW") + check("Inside", "Climb", "CCW") + check("Outside", "Conventional", "CCW") + def testRecomputeHelixFromV021(self): """Verify that we can still open and recompute a Helix created with older FreeCAD""" self.tearDown() @@ -139,39 +173,68 @@ class TestPathHelix(PathTestUtils.PathTestBase): created_with = f"created with {self.doc.getProgramVersion()}" def check(helix, direction, start_side, cut_mode): - with self.subTest(f"{helix.Name}: {direction}, {start_side}, {cut_mode}"): + with self.subTest(f"{helix.Name}: ({direction}, {start_side}) => {cut_mode}"): # no recompute yet, i.e. check original as precondition + self.assertPathDirection( + helix.Path, + direction, + msg=f"Path direction does not match fixture for {helix.Name} {created_with}", + ) self.assertEqual( helix.Direction, direction, - msg=f"Direction does not match fixture for helix {created_with}", + msg=f"Direction does not match fixture for {helix.Name} {created_with}", ) self.assertEqual( helix.StartSide, start_side, - msg=f"StartSide does not match fixture for helix {created_with}", + msg=f"StartSide does not match fixture for {helix.Name} {created_with}", ) # now see whether we can recompute the object from the old document helix.enforceRecompute() self.assertSuccessfulRecompute( - self.doc, helix, msg=f"Cannot recompute helix {created_with}" + self.doc, helix, msg=f"Cannot recompute {helix.Name} {created_with}" ) self.assertEqual( helix.Direction, direction, - msg=f"Direction changed after recomputing helix {created_with}", + msg=f"Direction changed after recomputing {helix.Name} {created_with}", ) self.assertEqual( helix.StartSide, start_side, - msg=f"StartSide changed after recomputing helix {created_with}", + msg=f"StartSide changed after recomputing {helix.Name} {created_with}", + ) + self.assertEqual( + helix.CutMode, + cut_mode, + msg=f"CutMode not correctly derived for {helix.Name} {created_with}", + ) + self.assertPathDirection( + helix.Path, + direction, + msg=f"Path with wrong direction generated for {helix.Name} {created_with}", ) - # self.assertEqual(helix.CutMode, cut_mode, - # msg=f"CutMode not correctly derived for helix {created_with}") # object names and expected values defined in the fixture check(self.doc.Helix, "CW", "Inside", "Conventional") check(self.doc.Helix001, "CW", "Outside", "Climb") check(self.doc.Helix002, "CCW", "Inside", "Climb") check(self.doc.Helix003, "CCW", "Outside", "Conventional") + + def assertPathDirection(self, path, expected_direction, msg=None): + """Asserts that the given path goes into the expected direction. + + For the general case we'd need to check the sign of the second derivative, + but as we know we work on a helix here, we can take a short cut and just + look at the G2/G3 arc commands. + """ + has_g2 = any(filter(lambda cmd: cmd.Name == "G2", path.Commands)) + has_g3 = any(filter(lambda cmd: cmd.Name == "G3", path.Commands)) + if has_g2 and not has_g3: + self.assertEqual("CW", expected_direction, msg) + elif has_g3 and not has_g2: + self.assertEqual("CCW", expected_direction, msg) + else: + raise NotImplementedError("Cannot determine direction for arbitrary paths") diff --git a/src/Mod/CAM/Gui/Resources/panels/PageOpHelixEdit.ui b/src/Mod/CAM/Gui/Resources/panels/PageOpHelixEdit.ui index cdc8f842d8..cf07cebfe7 100644 --- a/src/Mod/CAM/Gui/Resources/panels/PageOpHelixEdit.ui +++ b/src/Mod/CAM/Gui/Resources/panels/PageOpHelixEdit.ui @@ -89,18 +89,18 @@ - + The direction for the helix, clockwise or counterclockwise. - CW + Climb - CCW + Conventional diff --git a/src/Mod/CAM/Path/Base/SetupSheetOpPrototype.py b/src/Mod/CAM/Path/Base/SetupSheetOpPrototype.py index 7052494f5a..c6cfb52958 100644 --- a/src/Mod/CAM/Path/Base/SetupSheetOpPrototype.py +++ b/src/Mod/CAM/Path/Base/SetupSheetOpPrototype.py @@ -224,6 +224,9 @@ class OpPrototype(object): def setEditorMode(self, name, mode): self.properties[name].setEditorMode(mode) + def setPropertyStatus(self, name, status): + pass + def getProperty(self, name): return self.properties[name] diff --git a/src/Mod/CAM/Path/Op/Gui/Helix.py b/src/Mod/CAM/Path/Op/Gui/Helix.py index 99edc2f321..c06166b076 100644 --- a/src/Mod/CAM/Path/Op/Gui/Helix.py +++ b/src/Mod/CAM/Path/Op/Gui/Helix.py @@ -52,7 +52,7 @@ class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage): """getForm() ... return UI""" form = FreeCADGui.PySideUic.loadUi(":/panels/PageOpHelixEdit.ui") - comboToPropertyMap = [("startSide", "StartSide"), ("direction", "Direction")] + comboToPropertyMap = [("startSide", "StartSide"), ("cutMode", "CutMode")] enumTups = PathHelix.ObjectHelix.helixOpPropertyEnumerations(dataType="raw") @@ -62,8 +62,8 @@ class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage): def getFields(self, obj): """getFields(obj) ... transfers values from UI to obj's properties""" Path.Log.track() - if obj.Direction != str(self.form.direction.currentData()): - obj.Direction = str(self.form.direction.currentData()) + if obj.CutMode != str(self.form.cutMode.currentData()): + obj.CutMode = str(self.form.cutMode.currentData()) if obj.StartSide != str(self.form.startSide.currentData()): obj.StartSide = str(self.form.startSide.currentData()) if obj.StepOver != self.form.stepOverPercent.value(): @@ -78,7 +78,7 @@ class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage): Path.Log.track() self.form.stepOverPercent.setValue(obj.StepOver) - self.selectInComboBox(obj.Direction, self.form.direction) + self.selectInComboBox(obj.CutMode, self.form.cutMode) self.selectInComboBox(obj.StartSide, self.form.startSide) self.setupToolController(obj, self.form.toolController) @@ -94,7 +94,7 @@ class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage): signals.append(self.form.stepOverPercent.editingFinished) signals.append(self.form.extraOffset.editingFinished) - signals.append(self.form.direction.currentIndexChanged) + signals.append(self.form.cutMode.currentIndexChanged) signals.append(self.form.startSide.currentIndexChanged) signals.append(self.form.toolController.currentIndexChanged) signals.append(self.form.coolantController.currentIndexChanged) diff --git a/src/Mod/CAM/Path/Op/Helix.py b/src/Mod/CAM/Path/Op/Helix.py index a283124989..3d0e3378dd 100644 --- a/src/Mod/CAM/Path/Op/Helix.py +++ b/src/Mod/CAM/Path/Op/Helix.py @@ -51,6 +51,36 @@ else: translate = FreeCAD.Qt.translate +def _caclulatePathDirection(mode, side): + """Calculates the path direction from cut mode and cut side""" + # NB: at the time of writing, we need py3.8 compat, thus not using py3.10 pattern machting + if mode == "Conventional" and side == "Inside": + return "CW" + elif mode == "Conventional" and side == "Outside": + return "CCW" + elif mode == "Climb" and side == "Inside": + return "CCW" + elif mode == "Climb" and side == "Outside": + return "CW" + else: + raise ValueError(f"No mapping for '{mode}'/'{side}'") + + +def _caclulateCutMode(direction, side): + """Calculates the cut mode from path direction and cut side""" + # NB: at the time of writing, we need py3.8 compat, thus not using py3.10 pattern machting + if direction == "CW" and side == "Inside": + return "Conventional" + elif direction == "CW" and side == "Outside": + return "Climb" + elif direction == "CCW" and side == "Inside": + return "Climb" + elif direction == "CCW" and side == "Outside": + return "Conventional" + else: + raise ValueError(f"No mapping for '{direction}'/'{side}'") + + class ObjectHelix(PathCircularHoleBase.ObjectOp): """Proxy class for Helix operations.""" @@ -75,6 +105,10 @@ class ObjectHelix(PathCircularHoleBase.ObjectOp): (translate("PathProfile", "Outside"), "Outside"), (translate("PathProfile", "Inside"), "Inside"), ], # side of profile that cutter is on in relation to direction of profile + "CutMode": [ + (translate("CAM_Helix", "Climb"), "Climb"), + (translate("CAM_Helix", "Conventional"), "Conventional"), + ], # whether the tool "rolls" with or against the feed direction along the profile } if dataType == "raw": @@ -106,6 +140,8 @@ class ObjectHelix(PathCircularHoleBase.ObjectOp): "The direction of the circular cuts, ClockWise (CW), or CounterClockWise (CCW)", ), ) + obj.setEditorMode("Direction", ["ReadOnly", "Hidden"]) + obj.setPropertyStatus("Direction", ["ReadOnly", "Output"]) obj.addProperty( "App::PropertyEnumeration", @@ -114,6 +150,17 @@ class ObjectHelix(PathCircularHoleBase.ObjectOp): QT_TRANSLATE_NOOP("App::Property", "Start cutting from the inside or outside"), ) + # TODO: revise property description once v1.0 release string freeze is lifted + obj.addProperty( + "App::PropertyEnumeration", + "CutMode", + "Helix Drill", + QT_TRANSLATE_NOOP( + "App::Property", + "The direction of the circular cuts, ClockWise (Climb), or CounterClockWise (Conventional)", + ), + ) + obj.addProperty( "App::PropertyPercent", "StepOver", @@ -163,9 +210,34 @@ class ObjectHelix(PathCircularHoleBase.ObjectOp): ), ) + if not hasattr(obj, "CutMode"): + # TODO: consolidate the duplicate definitions from opOnDocumentRestored and + # initCircularHoleOperation once back on the main line + obj.addProperty( + "App::PropertyEnumeration", + "CutMode", + "Helix Drill", + QT_TRANSLATE_NOOP( + "App::Property", + "The direction of the circular cuts, ClockWise (Climb), or CounterClockWise (Conventional)", + ), + ) + obj.CutMode = ["Climb", "Conventional"] + if obj.Direction in ["Climb", "Conventional"]: + # For some month, late in the v1.0 release cycle, we had the cut mode assigned + # to the direction (see PR#14364). Let's fix files created in this time as well. + new_dir = "CW" if obj.Direction == "Climb" else "CCW" + obj.Direction = ["CW", "CCW"] + obj.Direction = new_dir + obj.CutMode = _caclulateCutMode(obj.Direction, obj.StartSide) + obj.setEditorMode("Direction", ["ReadOnly", "Hidden"]) + obj.setPropertyStatus("Direction", ["ReadOnly", "Output"]) + def circularHoleExecute(self, obj, holes): """circularHoleExecute(obj, holes) ... generate helix commands for each hole in holes""" Path.Log.track() + obj.Direction = _caclulatePathDirection(obj.CutMode, obj.StartSide) + self.commandlist.append(Path.Command("(helix cut operation)")) self.commandlist.append(Path.Command("G0", {"Z": obj.ClearanceHeight.Value})) @@ -217,8 +289,9 @@ class ObjectHelix(PathCircularHoleBase.ObjectOp): def SetupProperties(): + """Returns property names for which the "Setup Sheet" should provide defaults.""" setup = [] - setup.append("Direction") + setup.append("CutMode") setup.append("StartSide") setup.append("StepOver") setup.append("StartRadius")