Download src/Mod/CAM/CAMTests/TestPathAdaptive.py from AbdulElahGwaith/FreeCAD: direct link, hf CLI and curl.
- Browser
- Download file 34.9 kB
-
https://huggingface.co/AbdulElahGwaith/FreeCAD/resolve/main/src/Mod/CAM/CAMTests/TestPathAdaptive.py
- Command line
-
hf download hf://AbdulElahGwaith/FreeCAD/src/Mod/CAM/CAMTests/TestPathAdaptive.py
-
curl -L -o TestPathAdaptive.py https://huggingface.co/AbdulElahGwaith/FreeCAD/resolve/main/src/Mod/CAM/CAMTests/TestPathAdaptive.py
34.9 kB
| # SPDX-License-Identifier: LGPL-2.1-or-later | |
| # *************************************************************************** | |
| # * Copyright (c) 2021 Russell Johnson (russ4262) <russ4262@gmail.com> * | |
| # * * | |
| # * This file is part of the FreeCAD CAx development system. * | |
| # * * | |
| # * 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 Part | |
| import Path.Op.Adaptive as PathAdaptive | |
| import Path.Main.Job as PathJob | |
| from CAMTests.PathTestUtils import PathTestBase | |
| if FreeCAD.GuiUp: | |
| import Path.Main.Gui.Job as PathJobGui | |
| import Path.Op.Gui.Adaptive as PathAdaptiveGui | |
| class TestPathAdaptive(PathTestBase): | |
| """Unit tests for the Adaptive operation.""" | |
| def setUpClass(cls): | |
| """setUpClass()... | |
| This method is called upon instantiation of this test class. Add code and objects here | |
| that are needed for the duration of the test() methods in this class. In other words, | |
| set up the 'global' test environment here; use the `setUp()` method to set up a 'local' | |
| test environment. | |
| This method does not have access to the class `self` reference, but it | |
| is able to call static methods within this same class. | |
| """ | |
| cls.needsInit = True | |
| def initClass(cls): | |
| # Open existing FreeCAD document with test geometry | |
| cls.needsInit = False | |
| FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "True") | |
| cls.doc = FreeCAD.open(FreeCAD.getHomePath() + "Mod/CAM/CAMTests/test_adaptive.fcstd") | |
| FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "") | |
| # Create Job object, adding geometry objects from file opened above | |
| cls.job = PathJob.Create("Job", [cls.doc.Fusion], None) | |
| cls.job.GeometryTolerance.Value = 0.001 | |
| if FreeCAD.GuiUp: | |
| cls.job.ViewObject.Proxy = PathJobGui.ViewProvider(cls.job.ViewObject) | |
| # Instantiate an Adaptive operation for querying available properties | |
| cls.prototype = PathAdaptive.Create("Adaptive") | |
| cls.prototype.Base = [(cls.doc.Fusion, ["Face3"])] | |
| cls.prototype.Label = "Prototype" | |
| _addViewProvider(cls.prototype) | |
| cls.doc.recompute() | |
| def tearDownClass(cls): | |
| """tearDownClass()... | |
| This method is called prior to destruction of this test class. Add code and objects here | |
| that cleanup the test environment after the test() methods in this class have been executed. | |
| This method does not have access to the class `self` reference. This method | |
| is able to call static methods within this same class. | |
| """ | |
| # FreeCAD.Console.PrintMessage("TestPathAdaptive.tearDownClass()\n") | |
| # Close geometry document without saving | |
| if not cls.needsInit: | |
| FreeCAD.closeDocument(cls.doc.Name) | |
| # Setup and tear down methods called before and after each unit test | |
| def setUp(self): | |
| """setUp()... | |
| This method is called prior to each `test()` method. Add code and objects here | |
| that are needed for multiple `test()` methods. | |
| """ | |
| if self.needsInit: | |
| self.initClass() | |
| def tearDown(self): | |
| """tearDown()... | |
| This method is called after each test() method. Add cleanup instructions here. | |
| Such cleanup instructions will likely undo those in the setUp() method. | |
| """ | |
| pass | |
| # Unit tests | |
| def testFaceSingleSimple(self): | |
| """testFaceSingleSimple() Verify path generated on Face3.""" | |
| # Instantiate a Adaptive operation and set Base Geometry | |
| adaptive = PathAdaptive.Create("Adaptive") | |
| adaptive.Base = [(self.doc.Fusion, ["Face3"])] # (base, subs_list) | |
| adaptive.Label = "testFaceSingleSimple+" | |
| adaptive.Comment = "testFaceSingleSimple() Verify path generated on Face3." | |
| # Set additional operation properties | |
| # setDepthsAndHeights(adaptive) | |
| adaptive.FinishingProfile = False | |
| adaptive.HelixAngle = 75.0 | |
| adaptive.LiftDistance.Value = 1.0 | |
| adaptive.StepOver = 75 | |
| adaptive.UseOutline = False | |
| adaptive.setExpression("StepDown", None) | |
| adaptive.StepDown.Value = ( | |
| 20.0 # Have to set expression to None before numerical value assignment | |
| ) | |
| _addViewProvider(adaptive) | |
| self.doc.recompute() | |
| # moves = getGcodeMoves(adaptive.Path.Commands, includeRapids=False) | |
| # operationMoves = "; ".join(moves) | |
| # FreeCAD.Console.PrintMessage("test00_moves: " + operationMoves + "\n") | |
| # self.assertTrue(expected_moves_test01 == operationMoves, | |
| # "expected_moves_test01: {}\noperationMoves: {}".format(expected_moves_test01, operationMoves)) | |
| self.assertTrue(len(adaptive.Path.Commands) > 100, "Command count not greater than 100.") | |
| def testFacesMergedDifferentZ(self): | |
| """testFacesMergedDifferentZ() Verify path generated on adjacent, combined | |
| Face3 and Face10. The Z heights are different.""" | |
| # Instantiate a Adaptive operation and set Base Geometry | |
| adaptive = PathAdaptive.Create("Adaptive") | |
| adaptive.Base = [(self.doc.Fusion, ["Face3", "Face10"])] # (base, subs_list) | |
| adaptive.Label = "testFacesMergedDifferentZ+" | |
| adaptive.Comment = "testFacesMergedDifferentZ() Verify path generated on adjacent, combined Face3 and Face10. The Z heights are different. UseOutline = False" | |
| # Set additional operation properties | |
| # setDepthsAndHeights(adaptive) | |
| adaptive.FinishingProfile = False | |
| adaptive.HelixAngle = 75.0 | |
| adaptive.LiftDistance.Value = 1.0 | |
| adaptive.StepOver = 75 | |
| adaptive.UseOutline = False | |
| adaptive.setExpression("StepDown", None) | |
| adaptive.StepDown.Value = ( | |
| 20.0 # Have to set expression to None before numerical value assignment | |
| ) | |
| _addViewProvider(adaptive) | |
| self.doc.recompute() | |
| self.assertTrue(len(adaptive.Path.Commands) > 100, "Command count not greater than 100.") | |
| def testFacesMergedDifferentZUseOutline(self): | |
| """testFacesMergedDifferentZUseOutline() Verify path generated on adjacent, combined Face3 and Face10. | |
| The Z heights are different.""" | |
| # Instantiate a Adaptive operation and set Base Geometry | |
| adaptive = PathAdaptive.Create("Adaptive") | |
| adaptive.Base = [(self.doc.Fusion, ["Face3", "Face10"])] # (base, subs_list) | |
| adaptive.Label = "testFacesMergedDifferentZUseOutline+" | |
| adaptive.Comment = "testFacesMergedDifferentZUseOutline() Verify path generated on adjacent, combined Face3 and Face10. The Z heights are different. UseOutline = True." | |
| # Set additional operation properties | |
| # setDepthsAndHeights(adaptive) | |
| adaptive.FinishingProfile = False | |
| adaptive.HelixAngle = 75.0 | |
| adaptive.LiftDistance.Value = 1.0 | |
| adaptive.StepOver = 75 | |
| adaptive.UseOutline = True | |
| adaptive.setExpression("StepDown", None) | |
| adaptive.StepDown.Value = ( | |
| 20.0 # Have to set expression to None before numerical value assignment | |
| ) | |
| _addViewProvider(adaptive) | |
| self.doc.recompute() | |
| self.assertTrue(len(adaptive.Path.Commands) > 100, "Command count not greater than 100.") | |
| def testOutlineDifferentZDiscontinuousEdges(self): | |
| """testOutlineDifferentZDiscontinuous() Verify path generated non-closed edges with differing Z-heights that are closed with Z=1 projection: "Edge9", "Edge2", "Edge8", "Edge15", "Edge30", "Edge31", "Edge29", "Edge19".""" | |
| # Instantiate a Adaptive operation and set Base Geometry | |
| adaptive = PathAdaptive.Create("Adaptive") | |
| adaptive.Base = [ | |
| ( | |
| self.doc.Fusion, | |
| [ | |
| "Edge9", | |
| "Edge2", | |
| "Edge8", | |
| "Edge15", | |
| "Edge30", | |
| "Edge31", | |
| "Edge29", | |
| "Edge19", | |
| ], | |
| ) | |
| ] # (base, subs_list) | |
| adaptive.Label = "testOutlineDifferentZDiscontinuous+" | |
| adaptive.Comment = 'testOutlineDifferentZDiscontinuous() Verify path generated non-closed edges with differing Z-heights that are closed with Z=1 projection: "Edge9", "Edge2", "Edge8", "Edge15", "Edge30", "Edge31", "Edge29", "Edge19".' | |
| # Set additional operation properties | |
| # setDepthsAndHeights(adaptive) | |
| adaptive.FinishingProfile = False | |
| adaptive.HelixAngle = 75.0 | |
| adaptive.LiftDistance.Value = 1.0 | |
| adaptive.StepOver = 75 | |
| adaptive.UseOutline = False | |
| adaptive.setExpression("StepDown", None) | |
| adaptive.StepDown.Value = ( | |
| 20.0 # Have to set expression to None before numerical value assignment | |
| ) | |
| _addViewProvider(adaptive) | |
| self.doc.recompute() | |
| self.assertTrue(len(adaptive.Path.Commands) > 100, "Command count not greater than 100.") | |
| def testOutlineDifferentZContinuousEdges(self): | |
| """test05() Verify path generated closed wire with differing Z-heights: "Edge13", "Edge7", "Edge9", "Edge2", "Edge8", "Edge15", "Edge30", "Edge31", "Edge29", "Edge19".""" | |
| # Instantiate a Adaptive operation and set Base Geometry | |
| adaptive = PathAdaptive.Create("Adaptive") | |
| adaptive.Base = [ | |
| ( | |
| self.doc.Fusion, | |
| [ | |
| "Edge13", | |
| "Edge7", | |
| "Edge9", | |
| "Edge2", | |
| "Edge8", | |
| "Edge15", | |
| "Edge30", | |
| "Edge31", | |
| "Edge29", | |
| "Edge19", | |
| ], | |
| ) | |
| ] # (base, subs_list) | |
| adaptive.Label = "testOutlineDifferentZContinuous+" | |
| adaptive.Comment = 'testOutlineDifferentZContinuous() Verify path generated closed wire with differing Z-heights: "Edge13", "Edge7", "Edge9", "Edge2", "Edge8", "Edge15", "Edge30", "Edge31", "Edge29", "Edge19".' | |
| # Set additional operation properties | |
| # setDepthsAndHeights(adaptive) | |
| adaptive.FinishingProfile = False | |
| adaptive.HelixAngle = 75.0 | |
| adaptive.LiftDistance.Value = 1.0 | |
| adaptive.StepOver = 75 | |
| adaptive.UseOutline = False | |
| adaptive.setExpression("StepDown", None) | |
| adaptive.StepDown.Value = ( | |
| 20.0 # Have to set expression to None before numerical value assignment | |
| ) | |
| _addViewProvider(adaptive) | |
| self.doc.recompute() | |
| self.assertTrue(len(adaptive.Path.Commands) > 100, "Command count not greater than 100.") | |
| def testOutlineWithCutout(self): | |
| """testOutlineWithCutout() Verify path generated with outer and inner edge loops at same Z height: "Edge15", "Edge30", "Edge31", "Edge29", "Edge19", "Edge18", "Edge35", "Edge32", "Edge34", "Edge33".""" | |
| # Instantiate a Adaptive operation and set Base Geometry | |
| adaptive = PathAdaptive.Create("Adaptive") | |
| adaptive.Base = [ | |
| ( | |
| self.doc.Fusion, | |
| [ | |
| "Edge15", | |
| "Edge30", | |
| "Edge31", | |
| "Edge29", | |
| "Edge19", | |
| "Edge18", | |
| "Edge35", | |
| "Edge32", | |
| "Edge34", | |
| "Edge33", | |
| ], | |
| ) | |
| ] # (base, subs_list) | |
| adaptive.Label = "testOutlineWithCutout+" | |
| adaptive.Comment = 'testOutlineWithCutout() Verify path generated with outer and inner edge loops at same Z height: "Edge15", "Edge30", "Edge31", "Edge29", "Edge19", "Edge18", "Edge35", "Edge32", "Edge34", "Edge33".' | |
| # Set additional operation properties | |
| # setDepthsAndHeights(adaptive) | |
| adaptive.FinishingProfile = False | |
| adaptive.HelixAngle = 75.0 | |
| adaptive.LiftDistance.Value = 1.0 | |
| adaptive.StepOver = 75 | |
| adaptive.UseOutline = False | |
| adaptive.setExpression("StepDown", None) | |
| adaptive.StepDown.Value = ( | |
| 20.0 # Have to set expression to None before numerical value assignment | |
| ) | |
| _addViewProvider(adaptive) | |
| self.doc.recompute() | |
| # Check command count | |
| self.assertTrue(len(adaptive.Path.Commands) > 100, "Command count not greater than 100.") | |
| # Check if any paths originate inside inner hole of donut. They should not. | |
| isInBox = False | |
| edges = [ | |
| self.doc.Fusion.Shape.getElement(e) for e in ["Edge35", "Edge32", "Edge33", "Edge34"] | |
| ] | |
| square = Part.Wire(edges) | |
| sqrBB = square.BoundBox | |
| minPoint = FreeCAD.Vector(sqrBB.XMin, sqrBB.YMin, 0.0) | |
| maxPoint = FreeCAD.Vector(sqrBB.XMax, sqrBB.YMax, 0.0) | |
| for c in adaptive.Path.Commands: | |
| if pathOriginatesInBox(c, minPoint, maxPoint): | |
| isInBox = True | |
| break | |
| self.assertFalse(isInBox, "Paths originating within the inner hole.") | |
| def testFaceWithCutout(self): | |
| """testFaceWithCutout() Verify path generated on donut-shaped Face10.""" | |
| # Instantiate a Adaptive operation and set Base Geometry | |
| adaptive = PathAdaptive.Create("Adaptive") | |
| adaptive.Base = [(self.doc.Fusion, ["Face10"])] # (base, subs_list) | |
| adaptive.Label = "testFaceWithCutout+" | |
| adaptive.Comment = "testFaceWithCutout() Verify path generated on donut-shaped Face10." | |
| # Set additional operation properties | |
| # setDepthsAndHeights(adaptive) | |
| adaptive.FinishingProfile = False | |
| adaptive.HelixAngle = 75.0 | |
| adaptive.LiftDistance.Value = 1.0 | |
| adaptive.StepOver = 75 | |
| adaptive.UseOutline = False | |
| adaptive.setExpression("StepDown", None) | |
| adaptive.StepDown.Value = ( | |
| 20.0 # Have to set expression to None before numerical value assignment | |
| ) | |
| _addViewProvider(adaptive) | |
| self.doc.recompute() | |
| self.assertTrue(len(adaptive.Path.Commands) > 100, "Command count not greater than 100.") | |
| # Check if any paths originate inside inner hole of donut. They should not. | |
| isInBox = False | |
| edges = [ | |
| self.doc.Fusion.Shape.getElement(e) for e in ["Edge35", "Edge32", "Edge33", "Edge34"] | |
| ] | |
| square = Part.Wire(edges) | |
| sqrBB = square.BoundBox | |
| minPoint = FreeCAD.Vector(sqrBB.XMin, sqrBB.YMin, 0.0) | |
| maxPoint = FreeCAD.Vector(sqrBB.XMax, sqrBB.YMax, 0.0) | |
| for c in adaptive.Path.Commands: | |
| if pathOriginatesInBox(c, minPoint, maxPoint): | |
| isInBox = True | |
| break | |
| self.assertFalse(isInBox, "Paths originating within the inner hole.") | |
| # Set Adaptive op to only use the outline of the face. | |
| adaptive.UseOutline = True | |
| self.doc.recompute() | |
| # Check if any paths originate inside inner hole of donut. They should not. | |
| isInBox = False | |
| edges = [ | |
| self.doc.Fusion.Shape.getElement(e) for e in ["Edge35", "Edge32", "Edge33", "Edge34"] | |
| ] | |
| square = Part.Wire(edges) | |
| sqrBB = square.BoundBox | |
| minPoint = FreeCAD.Vector(sqrBB.XMin, sqrBB.YMin, 0.0) | |
| maxPoint = FreeCAD.Vector(sqrBB.XMax, sqrBB.YMax, 0.0) | |
| for c in adaptive.Path.Commands: | |
| if pathOriginatesInBox(c, minPoint, maxPoint): | |
| isInBox = True | |
| break | |
| self.assertTrue(isInBox, "No paths originating within the inner hole.") | |
| def testModelStockAwareness(self): | |
| """testModelStockAwareness() Tests stock awareness- avoids cutting into the model regardless | |
| of bounding box selected.""" | |
| # Instantiate a Adaptive operation and set Base Geometry | |
| adaptive = PathAdaptive.Create("Adaptive") | |
| adaptive.Base = [(self.doc.Fusion, ["Face3", "Face10"])] # (base, subs_list) | |
| adaptive.Label = "testModelStockAwareness+" | |
| adaptive.Comment = "testModelStockAwareness() Verify path generated on adjacent, combined Face3 and Face10. The Z heights are different. Result should be the combination at Z=10 (faces from (0,0) to (40,25), minus tool radius), and only the lower face at Z=5: (15,0) to (40,25)." | |
| # Set additional operation properties | |
| setDepthsAndHeights(adaptive, 15, 0) | |
| adaptive.FinishingProfile = False | |
| adaptive.HelixAngle = 75.0 | |
| adaptive.LiftDistance.Value = 1.0 | |
| adaptive.StepOver = 75 | |
| adaptive.UseOutline = False | |
| adaptive.setExpression("StepDown", None) | |
| adaptive.ModelAwareExperiment = True | |
| adaptive.StepDown.Value = ( | |
| 5.0 # Have to set expression to None before numerical value assignment | |
| ) | |
| # Don't use helix entry- ensures helix moves are counted in the path | |
| # boundary calculation. This should be unnecessary, as the helices are | |
| # grown out of the cut area, and thus must be inside of it. | |
| adaptive.UseHelixArcs = False | |
| _addViewProvider(adaptive) | |
| self.doc.recompute() | |
| # Check: | |
| # - Bounding box at Z=10 stays within Face3 and Face10- so -X for Face3, | |
| # +X and +/-Y for Face10 | |
| # - bounding box at Z=5 stays within Face10 | |
| # - No toolpaths at Z=0 | |
| paths = [c for c in adaptive.Path.Commands if c.Name in ["G0", "G00", "G1", "G01"]] | |
| toolr = adaptive.OpToolDiameter.Value / 2 | |
| tol = adaptive.Tolerance | |
| # Make clean up math below- combine tool radius and tolerance into a | |
| # single field that can be added/subtracted to/from bounding boxes | |
| moffset = toolr - tol | |
| zDict = getPathBoundaries(paths, [10, 5, 0]) | |
| # NOTE: Face3 is at Z=10, Face10 is at Z=5 | |
| bbf3 = self.doc.Fusion.Shape.getElement("Face3").BoundBox | |
| bbf10 = self.doc.Fusion.Shape.getElement("Face10").BoundBox | |
| okAt10 = ( | |
| zDict[10] is not None | |
| and zDict[10]["min"][0] >= bbf3.XMin + moffset | |
| and zDict[10]["min"][1] >= bbf10.YMin + moffset | |
| and zDict[10]["max"][0] <= bbf10.XMax - moffset | |
| and zDict[10]["max"][1] <= bbf10.YMax - moffset | |
| ) | |
| okAt5 = ( | |
| zDict[5] is not None | |
| and zDict[5]["min"][0] >= bbf10.XMin + moffset | |
| and zDict[5]["min"][1] >= bbf10.YMin + moffset | |
| and zDict[5]["max"][0] < bbf10.XMax - moffset | |
| and zDict[5]["max"][1] < bbf10.YMax - moffset | |
| ) | |
| okAt0 = not zDict[0] | |
| self.assertTrue(okAt10 and okAt5 and okAt0, "Path boundaries outside of expected regions") | |
| def testZStockToLeave(self): | |
| """testZStockToLeave() Tests Z stock to leave- with 1mm Z stock to leave, machining | |
| at the top of the model should not touch the top model face""" | |
| # Instantiate a Adaptive operation and set Base Geometry | |
| adaptive = PathAdaptive.Create("Adaptive") | |
| adaptive.Base = [(self.doc.Fusion, ["Face3", "Face10"])] # (base, subs_list) | |
| adaptive.Label = "testZStockToLeave+" | |
| adaptive.Comment = "testZStockToLeave() Verify Z stock is left as requested" | |
| # Set additional operation properties | |
| setDepthsAndHeights(adaptive, 15, 10) | |
| adaptive.FinishingProfile = False | |
| adaptive.HelixAngle = 75.0 | |
| adaptive.LiftDistance.Value = 1.0 | |
| adaptive.StepOver = 75 | |
| adaptive.UseOutline = False | |
| adaptive.setExpression("StepDown", None) | |
| adaptive.StepDown.Value = ( | |
| 5.0 # Have to set expression to None before numerical value assignment | |
| ) | |
| # Add some Z stock to leave so we avoid Face3 in this stepdown at Z=10 | |
| adaptive.setExpression("ZStockToLeave", None) | |
| adaptive.ZStockToLeave.Value = 1 | |
| _addViewProvider(adaptive) | |
| self.doc.recompute() | |
| # Check: | |
| # - No feed path at depth Z=10 touchs Face3 | |
| toolr = adaptive.OpToolDiameter.Value / 2 | |
| tol = adaptive.Tolerance | |
| # Make clean up math below- combine tool radius and tolerance into a | |
| # single field that can be added/subtracted to/from bounding boxes | |
| moffset = toolr - tol | |
| # Offset the face we don't expect to touch, verify no move is within | |
| # that boundary | |
| # NOTE: This isn't a perfect test (won't catch moves that start and end | |
| # outside of our face, but cut through/across it), but combined with | |
| # other tests should be sufficient. | |
| noPathTouchesFace3 = True | |
| foffset = self.doc.Fusion.Shape.getElement("Face3").makeOffset2D(moffset) | |
| # NOTE: Face3 is at Z=10, and the only feed moves will be at Z=10 | |
| lastpt = FreeCAD.Vector(0, 0, 10) | |
| for p in [c.Parameters for c in adaptive.Path.Commands if c.Name in ["G1", "G01"]]: | |
| pt = FreeCAD.Vector(lastpt) | |
| if "X" in p: | |
| pt.x = p.get("X") | |
| if "Y" in p: | |
| pt.x = p.get("Y") | |
| if foffset.isInside(pt, 0.001, True): | |
| noPathTouchesFace3 = False | |
| break | |
| lastpt = pt | |
| self.assertTrue(noPathTouchesFace3, "No feed moves within the top face.") | |
| def testFullModelAdaptiveRoughing(self): | |
| """testFullModelAdaptiveRoughing() Tests full roughing- should machine entire model with no inputs""" | |
| # Instantiate a Adaptive operation and set Base Geometry | |
| adaptive = PathAdaptive.Create("Adaptive") | |
| adaptive.Base = [(self.doc.Fusion, [])] # (base, subs_list) | |
| adaptive.Label = "testFullModelAdaptiveRoughing+" | |
| adaptive.Comment = ( | |
| "testFullModelAdaptiveRoughing() Verify path generated with no subs roughs entire model" | |
| ) | |
| # Set additional operation properties | |
| setDepthsAndHeights(adaptive, 15, 0) | |
| adaptive.FinishingProfile = False | |
| adaptive.HelixAngle = 75.0 | |
| adaptive.LiftDistance.Value = 1.0 | |
| adaptive.StepOver = 75 | |
| adaptive.UseOutline = False | |
| adaptive.setExpression("StepDown", None) | |
| adaptive.ModelAwareExperiment = True | |
| adaptive.StepDown.Value = ( | |
| 5.0 # Have to set expression to None before numerical value assignment | |
| ) | |
| # Don't use helix entry- ensures helix moves are counted in the path | |
| # boundary calculation. This should be unnecessary, as the helices are | |
| # grown out of the cut area, and thus must be inside of it. | |
| adaptive.UseHelixArcs = False | |
| _addViewProvider(adaptive) | |
| self.doc.recompute() | |
| # Check: | |
| # - Bounding box at Z=0 goes outside the model box + tool diameter | |
| # (has to profile the model) | |
| # - Bounding box at Z=5 should go past the model in -X, but only up to the | |
| # stock edges in +X and Y | |
| # - Bounding box at Z=10 goes to at least stock bounding box edges, | |
| # minus tool diameter (has to machine the entire top of the stock off) | |
| # - [Should maybe check] At least one move Z = [10,5] is within the model | |
| # - [Should maybe check] No moves at Z = 0 are within the model | |
| paths = [c for c in adaptive.Path.Commands if c.Name in ["G0", "G00", "G1", "G01"]] | |
| toolr = adaptive.OpToolDiameter.Value / 2 | |
| tol = adaptive.Tolerance | |
| # Make clean up math below- combine tool radius and tolerance into a | |
| # single field that can be added/subtracted to/from bounding boxes | |
| moffset = toolr - tol | |
| zDict = getPathBoundaries(paths, [10, 5, 0]) | |
| mbb = self.doc.Fusion.Shape.BoundBox | |
| sbb = adaptive.Document.Stock.Shape.BoundBox | |
| okAt10 = ( | |
| zDict[10] is not None | |
| and zDict[10]["min"][0] <= sbb.XMin + moffset | |
| and zDict[10]["min"][1] <= sbb.YMin + moffset | |
| and zDict[10]["max"][0] >= sbb.XMax - moffset | |
| and zDict[10]["max"][1] >= sbb.YMax - moffset | |
| ) | |
| okAt5 = ( | |
| zDict[5] is not None | |
| and zDict[5]["min"][0] <= mbb.XMin - moffset | |
| and zDict[5]["min"][1] <= sbb.YMin + moffset | |
| and zDict[5]["max"][0] >= sbb.XMax - moffset | |
| and zDict[5]["max"][1] >= sbb.YMax - moffset | |
| ) | |
| okAt0 = ( | |
| zDict[0] is not None | |
| and zDict[0]["min"][0] <= mbb.XMin - moffset | |
| and zDict[0]["min"][1] <= mbb.YMin - moffset | |
| and zDict[0]["max"][0] >= mbb.XMax + moffset | |
| and zDict[0]["max"][1] >= mbb.YMax + moffset | |
| ) | |
| self.assertTrue( | |
| okAt10 and okAt5 and okAt0, "Path boundaries don't include expected regions" | |
| ) | |
| def testStockLimitsAwareness(self): | |
| """testStockLimitsAwareness() Tests stock handling- should rough full model, but not cut | |
| air excessively where there's not stock""" | |
| # Instantiate a Adaptive operation and set Base Geometry | |
| adaptive = PathAdaptive.Create("Adaptive") | |
| adaptive.Base = [(self.doc.Fusion, [])] # (base, subs_list) | |
| adaptive.Label = "testStockLimitsAwareness+" | |
| adaptive.Comment = ( | |
| "testStockLimitsAwareness() Verify machining region is limited to the stock" | |
| ) | |
| # Set additional operation properties | |
| setDepthsAndHeights(adaptive, 15, 5) | |
| adaptive.FinishingProfile = False | |
| adaptive.HelixAngle = 75.0 | |
| adaptive.LiftDistance.Value = 1.0 | |
| adaptive.StepOver = 75 | |
| adaptive.UseOutline = False | |
| adaptive.setExpression("StepDown", None) | |
| adaptive.ModelAwareExperiment = True | |
| adaptive.StepDown.Value = ( | |
| 5.0 # Have to set expression to None before numerical value assignment | |
| ) | |
| # Don't use helix entry- ensures helix moves are counted in the path | |
| # boundary calculation. This should be unnecessary, as the helices are | |
| # grown out of the cut area, and thus must be inside of it. | |
| adaptive.UseHelixArcs = False | |
| # Create and assign new stock that will create different bounds at | |
| # different stepdowns | |
| btall = Part.makeBox(17, 27, 11, FreeCAD.Vector(-1, -1, 0)) | |
| bshort = Part.makeBox(42, 27, 6, FreeCAD.Vector(-1, -1, 0)) | |
| adaptive.Document.Job.Stock.Shape = btall.fuse(bshort) | |
| _addViewProvider(adaptive) | |
| # NOTE: Do NOT recompute entire doc, which will undo our stock change! | |
| adaptive.recompute() | |
| # Check: | |
| # - Bounding box at Z=10 stays basically above "btall" | |
| # - Bounding box at Z=5 and Z=0 are outside of stock | |
| paths = [c for c in adaptive.Path.Commands if c.Name in ["G1", "G01"]] | |
| toolr = adaptive.OpToolDiameter.Value / 2 | |
| tol = adaptive.Tolerance | |
| # Make clean up math below- combine tool radius and tolerance into a | |
| # single field that can be added/subtracted to/from bounding boxes | |
| # NOTE: ADD tol here, since we're effectively flipping our normal | |
| # comparison and want tolerance to make our check looser | |
| moffset = toolr + tol | |
| zDict = getPathBoundaries(paths, [10, 5]) | |
| sbb = adaptive.Document.Stock.Shape.BoundBox | |
| sbb10 = btall.BoundBox | |
| # These should be no more than a tool radius outside of the "btall" | |
| # XY section of the stock | |
| okAt10 = ( | |
| zDict[10] is not None | |
| and zDict[10]["min"][0] >= sbb10.XMin - moffset | |
| and zDict[10]["min"][1] >= sbb10.YMin - moffset | |
| and zDict[10]["max"][0] <= sbb10.XMax + moffset | |
| and zDict[10]["max"][1] <= sbb10.YMax + moffset | |
| ) | |
| # These should be no more than a tool radius outside of the overall | |
| # stock bounding box | |
| okAt5 = ( | |
| zDict[5] is not None | |
| and zDict[5]["min"][0] >= sbb.XMin - moffset | |
| and zDict[5]["min"][1] >= sbb.YMin - moffset | |
| and zDict[5]["max"][0] <= sbb.XMax + moffset | |
| and zDict[5]["max"][1] <= sbb.YMax + moffset | |
| ) | |
| self.assertTrue(okAt10 and okAt5, "Path feeds extend excessively in +X") | |
| # POSSIBLY MISSING TESTS: | |
| # - Something for region ordering | |
| # - Known-edge cases: cones/spheres/cylinders (especially partials on edges | |
| # of model + strange angles- especially for cylinders) | |
| # - Multiple models/stock | |
| # - XY stock to leave | |
| # Eclass | |
| def getPathBoundaries(paths, zLevels): | |
| """getPathBoundaries(paths, zLevels): Takes the list of paths and list of Z | |
| depths of interest, and finds the bounding box of the paths at each depth. | |
| A dictionary of depth: {"min": (x,y), "max": (x,y)} entries is returned. | |
| NOTE: You'd think that using Path.BoundBox would give us what we want, | |
| but... no, for whatever reason it appears to always extend to (0,0,0) | |
| """ | |
| last = FreeCAD.Vector(0.0, 0.0, 0.0) | |
| # First make sure each element has X, Y, and Z coordinates | |
| for p in paths: | |
| params = p.Parameters | |
| last.x = p.X if "X" in params else last.x | |
| last.y = p.Y if "Y" in params else last.y | |
| last.z = p.Z if "Z" in params else last.z | |
| p.X = last.x | |
| p.Y = last.y | |
| p.Z = last.z | |
| zDict = {} | |
| for z in zLevels: | |
| zpaths = [k for k in paths if k.Z == z] | |
| if not zpaths: | |
| zDict[z] = None | |
| continue | |
| xmin = min([k.X for k in zpaths]) | |
| xmax = max([k.X for k in zpaths]) | |
| ymin = min([k.Y for k in zpaths]) | |
| ymax = max([k.Y for k in zpaths]) | |
| zDict[z] = {"min": (xmin, ymin), "max": (xmax, ymax)} | |
| return zDict | |
| def setDepthsAndHeights(op, strDep=20.0, finDep=0.0): | |
| """setDepthsAndHeights(op, strDep=20.0, finDep=0.0)... Sets default depths and heights for `op` passed to it""" | |
| # Set start and final depth in order to eliminate effects of stock (and its default values) | |
| op.setExpression("StartDepth", None) | |
| op.StartDepth.Value = strDep | |
| op.setExpression("FinalDepth", None) | |
| op.FinalDepth.Value = finDep | |
| # Set step down so as to only produce one layer path | |
| op.setExpression("StepDown", None) | |
| op.StepDown.Value = 20.0 | |
| # Set Heights | |
| # default values used | |
| def getGcodeMoves(cmdList, includeRapids=True, includeLines=True, includeArcs=True): | |
| """getGcodeMoves(cmdList, includeRapids=True, includeLines=True, includeArcs=True)... | |
| Accepts command dict and returns point string coordinate. | |
| """ | |
| # NOTE: Can NOT just check "if p.get("X")" or similar- that chokes when X is | |
| # zero. That becomes especially obvious when Z=0, and moves end up on the | |
| # wrong depth | |
| gcode_list = list() | |
| last = FreeCAD.Vector(0.0, 0.0, 0.0) | |
| for c in cmdList: | |
| p = c.Parameters | |
| name = c.Name | |
| if (includeRapids and name in ["G0", "G00"]) or (includeLines and name in ["G1", "G01"]): | |
| gcode = name | |
| x = last.x | |
| y = last.y | |
| z = last.z | |
| if "X" in p: | |
| x = round(p["X"], 2) | |
| gcode += " X" + str(x) | |
| if "Y" in p: | |
| y = round(p["Y"], 2) | |
| gcode += " Y" + str(y) | |
| if "Z" in p: | |
| z = round(p["Z"], 2) | |
| gcode += " Z" + str(z) | |
| last.x = x | |
| last.y = y | |
| last.z = z | |
| gcode_list.append(gcode) | |
| elif includeArcs and name in ["G2", "G3", "G02", "G03"]: | |
| gcode = name | |
| x = last.x | |
| y = last.y | |
| z = last.z | |
| i = 0.0 | |
| j = 0.0 | |
| k = 0.0 | |
| if "I" in p: | |
| i = round(p["I"], 2) | |
| gcode += " I" + str(i) | |
| if "J" in p: | |
| j = round(p["J"], 2) | |
| gcode += " J" + str(j) | |
| if "K" in p: | |
| k = round(p["K"], 2) | |
| gcode += " K" + str(k) | |
| if "X" in p: | |
| x = round(p["X"], 2) | |
| gcode += " X" + str(x) | |
| if "Y" in p: | |
| y = round(p["Y"], 2) | |
| gcode += " Y" + str(y) | |
| if "Z" in p: | |
| z = round(p["Z"], 2) | |
| gcode += " Z" + str(z) | |
| gcode_list.append(gcode) | |
| last.x = x | |
| last.y = y | |
| last.z = z | |
| return gcode_list | |
| def pathOriginatesInBox(cmd, minPoint, maxPoint): | |
| p = cmd.Parameters | |
| name = cmd.Name | |
| if name in ["G0", "G00", "G1", "G01"]: | |
| if "X" in p and "Y" in p: | |
| x = p.get("X") | |
| y = p.get("Y") | |
| if x > minPoint.x and y > minPoint.y and x < maxPoint.x and y < maxPoint.y: | |
| return True | |
| return False | |
| def _addViewProvider(adaptiveOp): | |
| if FreeCAD.GuiUp: | |
| PathOpGui = PathAdaptiveGui.PathOpGui | |
| cmdRes = PathAdaptiveGui.Command.res | |
| adaptiveOp.ViewObject.Proxy = PathOpGui.ViewProvider(adaptiveOp.ViewObject, cmdRes) | |
| # Example string literal of expected path moves from an operation | |
| # Expected moves for unit test01 | |
| expected_moves_test01 = "G1 X32.5 Y32.5 Z5.0; \ | |
| G1 X17.5 Y32.5 Z5.0; \ | |
| G1 X17.5 Y30.0 Z5.0; \ | |
| G1 X32.5 Y30.0 Z5.0; \ | |
| G1 X32.5 Y27.5 Z5.0; \ | |
| G1 X17.5 Y27.5 Z5.0; \ | |
| G1 X17.5 Y25.0 Z5.0; \ | |
| G1 X32.5 Y25.0 Z5.0; \ | |
| G1 X32.5 Y22.5 Z5.0; \ | |
| G1 X17.5 Y22.5 Z5.0; \ | |
| G1 X17.5 Y20.0 Z5.0; \ | |
| G1 X32.5 Y20.0 Z5.0; \ | |
| G1 X32.5 Y17.5 Z5.0; \ | |
| G1 X17.5 Y17.5 Z5.0" | |