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 0000000000..087665c9fa Binary files /dev/null and b/src/Mod/CAM/CAMTests/Fixtures/OpHelix_v0-21.FCStd differ diff --git a/src/Mod/CAM/CAMTests/PathTestUtils.py b/src/Mod/CAM/CAMTests/PathTestUtils.py index ffdbf01cd1..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) @@ -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/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/TestPathHelix.py b/src/Mod/CAM/CAMTests/TestPathHelix.py index bd32272eb7..18d0df432f 100644 --- a/src/Mod/CAM/CAMTests/TestPathHelix.py +++ b/src/Mod/CAM/CAMTests/TestPathHelix.py @@ -20,13 +20,18 @@ # * * # *************************************************************************** +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 +FIXTURE_PATH = pathlib.Path(__file__).parent / "Fixtures" + Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) # Path.Log.trackModule(Path.Log.thisModule()) @@ -39,6 +44,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) @@ -48,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""" @@ -62,8 +76,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] @@ -126,3 +138,103 @@ class TestPathHelix(PathTestUtils.PathTestBase): self.assertRoughly( 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() + 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.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.Name} {created_with}", + ) + self.assertEqual( + helix.StartSide, + start_side, + 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.Name} {created_with}" + ) + self.assertEqual( + helix.Direction, + direction, + msg=f"Direction changed after recomputing {helix.Name} {created_with}", + ) + self.assertEqual( + helix.StartSide, + start_side, + 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}", + ) + + # 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/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/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 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..cf07cebfe7 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. + + + + 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 + + + + + + + + + + + + + + + + + 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/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/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/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 7df87e70c4..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.""" @@ -68,13 +98,17 @@ 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"), (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": @@ -103,9 +137,11 @@ 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)", ), ) + 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") 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" 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