Helpers and tests

Drillable lib and Tests
This commit is contained in:
sliptonic
2021-12-02 22:12:18 -06:00
parent d42f8af3a0
commit 69d2ce2501
8 changed files with 696 additions and 26 deletions

View File

@@ -10,6 +10,8 @@ set(Path_Scripts
Init.py
PathCommands.py
TestPathApp.py
PathMachineState.py
PathFeedRate.py
)
if(BUILD_GUI)
@@ -144,6 +146,7 @@ SET(PathScripts_SRCS
SET(Generator_SRCS
Generators/drill_generator.py
Generators/rotation_generator.py
)
SET(PathScripts_post_SRCS
@@ -221,8 +224,10 @@ SET(PathTests_SRCS
PathTests/TestPathDressupDogbone.py
PathTests/TestPathDressupHoldingTags.py
PathTests/TestPathDrillGenerator.py
PathTests/TestPathRotationGenerator.py
PathTests/TestPathGeom.py
PathTests/TestPathHelix.py
PathTests/TestPathHelpers.py
PathTests/TestPathLog.py
PathTests/TestPathOpTools.py
PathTests/TestPathPost.py

View File

@@ -73,16 +73,16 @@ def setFeedRate(commandlist, ToolController):
if command.Name not in feedcommands + rapidcommands:
continue
if _isVertical(machine.getPosition(), command):
if _isVertical(FreeCAD.Vector(machine.X, machine.Y, machine.Z), command):
rate = (
ToolController.VertRapid.Value
if command in rapidcommands
if command.Name in rapidcommands
else ToolController.VertFeed.Value
)
else:
rate = (
ToolController.HorizRapid.Value
if command in rapidcommands
if command.Name in rapidcommands
else ToolController.HorizFeed.Value
)

View File

@@ -58,48 +58,61 @@ class MachineState:
"G59.9",
]
X: float = field(default=None)
Y: float = field(default=None)
Z: float = field(default=None)
A: float = field(default=None)
B: float = field(default=None)
C: float = field(default=None)
X: float = field(default=0)
Y: float = field(default=0)
Z: float = field(default=0)
A: float = field(default=0)
B: float = field(default=0)
C: float = field(default=0)
F: float = field(default=None)
Coolant: bool = field(default=False)
WCS: str = field(default="G54")
Spindle: str = field(default="off")
S: int = field(default=0)
T: str = field(default=None)
T: int = field(default=None)
def addCommand(self, command):
"""Processes a command and updates the internal state of the machine. Returns true if the command has alterned the machine state"""
oldstate = self.getState()
if command.Name == "M6":
self.T = command.Parameters["T"]
return
self.T = int(command.Parameters["T"])
return not oldstate == self.getState()
if command.Name in ["M3", "M4"]:
self.S = command.Parameters["S"]
self.SpindApple = "CW" if command.Name == "M3" else "CCW"
return
self.Spindle = "CW" if command.Name == "M3" else "CCW"
return not oldstate == self.getState()
if command.Name in ["M2", "M5"]:
self.S = 0
self.Spindle = "off"
return
return not oldstate == self.getState()
if command.Name in self.WCSLIST:
self.WCS = command.Name
return
return not oldstate == self.getState()
for p in command.Parameters:
self.__setattr__(p, command.Parameters[p])
def getPosition(self):
"""
Returns an App.Vector of the current location
"""
x = 0 if self.X is None else self.X
y = 0 if self.Y is None else self.Y
z = 0 if self.Z is None else self.Z
return not oldstate == self.getState()
return FreeCAD.Vector(x, y, z)
def getState(self):
"""
Returns a dictionary of the current machine state
"""
state = {}
state['X'] = self.X
state['Y'] = self.Y
state['Z'] = self.Z
state['A'] = self.A
state['B'] = self.B
state['C'] = self.C
state['F'] = self.F
state['Coolant'] = self.Coolant
state['WCS'] = self.WCS
state['Spindle'] = self.Spindle
state['S'] = self.S
state['T'] = self.T
return state

View File

@@ -0,0 +1,235 @@
import PathScripts.PathLog as PathLog
import FreeCAD as App
import Part
import numpy
import math
if False:
PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
PathLog.trackModule(PathLog.thisModule())
else:
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
def isDrillableCylinder(obj, candidate, tooldiameter=None, vector=App.Vector(0, 0, 1)):
"""
checks if a candidate cylindrical face is drillable
"""
matchToolDiameter = tooldiameter is not None
matchVector = vector is not None
PathLog.debug(
"\n match tool diameter {} \n match vector {}".format(
matchToolDiameter, matchVector
)
)
def raisedFeature(obj, candidate):
# check if the cylindrical 'lids' are inside the base
# object. This eliminates extruded circles but allows
# actual holes.
startLidCenter = App.Vector(
candidate.BoundBox.Center.x,
candidate.BoundBox.Center.y,
candidate.BoundBox.ZMax,
)
endLidCenter = App.Vector(
candidate.BoundBox.Center.x,
candidate.BoundBox.Center.y,
candidate.BoundBox.ZMin,
)
return obj.isInside(startLidCenter, 1e-6, False) or obj.isInside(
endLidCenter, 1e-6, False
)
def getSeam(candidate):
# Finds the vertical seam edge in a cylinder
for e in candidate.Edges:
if isinstance(e.Curve, Part.Line): # found the seam
return e
if not candidate.ShapeType == "Face":
raise TypeError("expected a Face")
if not str(candidate.Surface) == "<Cylinder object>":
raise TypeError("expected a cylinder")
if len(candidate.Edges) != 3:
raise TypeError("cylinder does not have 3 edges. Not supported yet")
if raisedFeature(obj, candidate):
PathLog.debug("The cylindrical face is a raised feature")
return False
if not matchToolDiameter and not matchVector:
return True
elif matchToolDiameter and tooldiameter / 2 > candidate.Surface.Radius:
PathLog.debug("The tool is larger than the target")
return False
elif matchVector and not (compareVecs(getSeam(candidate).Curve.Direction, vector)):
PathLog.debug("The feature is not aligned with the given vector")
return False
else:
return True
def isDrillableCircle(obj, candidate, tooldiameter=None, vector=App.Vector(0, 0, 1)):
"""
checks if a flat face or edge is drillable
"""
matchToolDiameter = tooldiameter is not None
matchVector = vector is not None
PathLog.debug(
"\n match tool diameter {} \n match vector {}".format(
matchToolDiameter, matchVector
)
)
if candidate.ShapeType == "Face":
if not type(candidate.Surface) == Part.Plane:
PathLog.debug("Drilling on non-planar faces not supported")
return False
if (
len(candidate.Edges) == 1 and type(candidate.Edges[0].Curve) == Part.Circle
): # Regular circular face
edge = candidate.Edges[0]
elif (
len(candidate.Edges) == 2
and type(candidate.Edges[0].Curve) == Part.Circle
and type(candidate.Edges[1].Curve) == Part.Circle
): # process a donut
e1 = candidate.Edges[0]
e2 = candidate.Edges[1]
edge = e1 if e1.Curve.Radius < e2.Curve.Radius else e2
else:
PathLog.debug(
"expected a Face with one or two circular edges got a face with {} edges".format(
len(candidate.Edges)
)
)
return False
else: # edge
edge = candidate
if not (isinstance(edge.Curve, Part.Circle) and edge.isClosed()):
PathLog.debug("expected a closed circular edge")
return False
if not hasattr(edge.Curve, "Radius"):
PathLog.debug("The Feature edge has no radius - Ellipse.")
return False
if not matchToolDiameter and not matchVector:
return True
elif matchToolDiameter and tooldiameter / 2 > edge.Curve.Radius:
PathLog.debug("The tool is larger than the target")
return False
elif matchVector and not (compareVecs(edge.Curve.Axis, vector)):
PathLog.debug("The feature is not aligned with the given vector")
return False
else:
return True
def isDrillable(obj, candidate, tooldiameter=None, vector=App.Vector(0, 0, 1)):
"""
Checks candidates to see if they can be drilled at the given vector.
Candidates can be either faces - circular or cylindrical or circular edges.
The tooldiameter can be optionally passed. if passed, the check will return
False for any holes smaller than the tooldiameter.
vector defaults to (0,0,1) which aligns with the Z axis. By default will return False
for any candidate not drillable in this orientation. Pass 'None' to vector to test whether
the hole is drillable at any orientation.
obj=Shape
candidate = Face or Edge
tooldiameter=float
vector=App.Vector or None
"""
PathLog.debug(
"obj: {} candidate: {} tooldiameter {} vector {}".format(
obj, candidate, tooldiameter, vector
)
)
if list == type(obj):
for shape in obj:
if isDrillable(shape, candidate, tooldiameter, vector):
return (True, shape)
return (False, None)
if candidate.ShapeType not in ["Face", "Edge"]:
raise TypeError("expected a Face or Edge. Got a {}".format(candidate.ShapeType))
try:
if (
candidate.ShapeType == "Face"
and str(candidate.Surface) == "<Cylinder object>"
):
return isDrillableCylinder(obj, candidate, tooldiameter, vector)
else:
return isDrillableCircle(obj, candidate, tooldiameter, vector)
except TypeError as e:
PathLog.debug(e)
return False
# raise TypeError("{}".format(e))
def compareVecs(vec1, vec2):
"""
compare the two vectors to see if they are aligned for drilling
alignment can indicate the vectors are the same or exactly opposite
"""
angle = vec1.getAngle(vec2)
angle = 0 if math.isnan(angle) else math.degrees(angle)
PathLog.debug("vector angle: {}".format(angle))
return numpy.isclose(angle, 0, rtol=1e-05, atol=1e-06) or numpy.isclose(
angle, 180, rtol=1e-05, atol=1e-06
)
def getDrillableTargets(obj, ToolDiameter=None, vector=App.Vector(0, 0, 1)):
"""
Returns a list of tuples for drillable subelements from the given object
[(obj,'Face1'),(obj,'Face3')]
Finds cylindrical faces that are larger than the tool diameter (if provided) and
oriented with the vector. If vector is None, all drillables are returned
"""
shp = obj.Shape
results = []
for i in range(1, len(shp.Faces)):
fname = 'Face{}'.format(i)
PathLog.debug(fname)
candidate = obj.getSubObject(fname)
if not str(candidate.Surface) == '<Cylinder object>':
continue
try:
drillable = isDrillable(shp, candidate, tooldiameter=ToolDiameter, vector=vector)
PathLog.debug("fname: {} : drillable {}".format(fname, drillable))
except Exception as e:
PathLog.debug(e)
continue
if drillable:
results.append((obj,fname))
return results

Binary file not shown.

View File

@@ -0,0 +1,310 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2021 sliptonic <shopinthewoods@gmail.com> *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the 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 Path
import FreeCAD as App
import PathScripts.PathLog as PathLog
import PathTests.PathTestUtils as PathTestUtils
import PathScripts.drillableLib as drillableLib
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
PathLog.trackModule(PathLog.thisModule())
class TestPathDrillable(PathTestUtils.PathTestBase):
def setUp(self):
self.doc = App.open(App.getHomePath() + "/Mod/Path/PathTests/Drilling_1.FCStd")
self.obj = self.doc.getObject("Pocket010")
def tearDown(self):
App.closeDocument(self.doc.Name)
def test00(self):
"""Test CompareVecs"""
# Vec and origin
v1 = App.Vector(0, 0, 10)
v2 = App.Vector(0, 0, 0)
self.assertTrue(drillableLib.compareVecs(v1, v2))
# two valid vectors
v1 = App.Vector(0, 10, 0)
v2 = App.Vector(0, 20, 0)
self.assertTrue(drillableLib.compareVecs(v1, v2))
# two valid vectors not aligned
v1 = App.Vector(0, 10, 0)
v2 = App.Vector(10, 0, 0)
self.assertFalse(drillableLib.compareVecs(v1, v2))
def test10(self):
"""Test isDrillable"""
# Invalid types
candidate = self.obj.getSubObject("Vertex1")
self.assertRaises(
TypeError, lambda: drillableLib.isDrillable(self.obj.Shape, candidate)
)
# # partial cylinder
# candidate = self.obj.getSubObject("Face10")
# self.assertRaises(
# TypeError, lambda: drillableLib.isDrillable(self.obj.Shape, candidate)
# )
# Test cylinder faces
# thru-hole
candidate = self.obj.getSubObject("Face25")
# Typical drilling
self.assertTrue(drillableLib.isDrillable(self.obj.Shape, candidate))
# Drilling with smaller bit
self.assertTrue(
drillableLib.isDrillable(self.obj.Shape, candidate, tooldiameter=20)
)
# Drilling with bit too large
self.assertFalse(
drillableLib.isDrillable(self.obj.Shape, candidate, tooldiameter=30)
)
# off-axis hole
candidate = self.obj.getSubObject("Face42")
# Typical drilling
self.assertFalse(drillableLib.isDrillable(self.obj.Shape, candidate))
# Passing None as vector
self.assertTrue(
drillableLib.isDrillable(self.obj.Shape, candidate, vector=None)
)
# Passing explicit vector
self.assertTrue(
drillableLib.isDrillable(
self.obj.Shape, candidate, vector=App.Vector(0, 1, 0)
)
)
# Drilling with smaller bit
self.assertTrue(
drillableLib.isDrillable(
self.obj.Shape, candidate, tooldiameter=10, vector=App.Vector(0, 1, 0)
)
)
# Drilling with bit too large
self.assertFalse(
drillableLib.isDrillable(
self.obj.Shape, candidate, tooldiameter=30, vector=App.Vector(0, 1, 0)
)
)
# ellipse hole
candidate = self.obj.getSubObject("Face20")
# Typical drilling
self.assertFalse(drillableLib.isDrillable(self.obj.Shape, candidate))
# Passing None as vector
self.assertFalse(
drillableLib.isDrillable(self.obj.Shape, candidate, vector=None)
)
# raised cylinder
candidate = self.obj.getSubObject("Face30")
# Typical drilling
self.assertFalse(drillableLib.isDrillable(self.obj.Shape, candidate))
# Passing None as vector
self.assertFalse(
drillableLib.isDrillable(self.obj.Shape, candidate, vector=None)
)
# cylinder on slope
candidate = self.obj.getSubObject("Face26")
# Typical drilling
self.assertTrue(drillableLib.isDrillable(self.obj.Shape, candidate))
# Passing None as vector
self.assertTrue(
drillableLib.isDrillable(self.obj.Shape, candidate, vector=None)
)
# Circular Faces
candidate = self.obj.getSubObject("Face51")
# Typical drilling
self.assertTrue(drillableLib.isDrillable(self.obj.Shape, candidate))
# Passing None as vector
self.assertTrue(
drillableLib.isDrillable(self.obj.Shape, candidate, vector=None)
)
# Passing explicit vector
self.assertTrue(
drillableLib.isDrillable(
self.obj.Shape, candidate, vector=App.Vector(0, 0, 1)
)
)
# Drilling with smaller bit
self.assertTrue(
drillableLib.isDrillable(self.obj.Shape, candidate, tooldiameter=10)
)
# Drilling with bit too large
self.assertFalse(
drillableLib.isDrillable(self.obj.Shape, candidate, tooldiameter=30)
)
# off-axis circular face hole
candidate = self.obj.getSubObject("Face54")
# Typical drilling
self.assertFalse(drillableLib.isDrillable(self.obj.Shape, candidate))
# Passing None as vector
self.assertTrue(
drillableLib.isDrillable(self.obj.Shape, candidate, vector=None)
)
# Passing explicit vector
self.assertTrue(
drillableLib.isDrillable(
self.obj.Shape, candidate, vector=App.Vector(0, 1, 0)
)
)
# raised face
candidate = self.obj.getSubObject("Face45")
# Typical drilling
self.assertTrue(drillableLib.isDrillable(self.obj.Shape, candidate))
# Passing None as vector
self.assertTrue(
drillableLib.isDrillable(self.obj.Shape, candidate, vector=None)
)
# interrupted Face
candidate = self.obj.getSubObject("Face46")
# Typical drilling
self.assertFalse(drillableLib.isDrillable(self.obj.Shape, candidate))
# Passing None as vector
self.assertFalse(
drillableLib.isDrillable(self.obj.Shape, candidate, vector=None)
)
# donut face
candidate = self.obj.getSubObject("Face44")
# Typical drilling
self.assertTrue(drillableLib.isDrillable(self.obj.Shape, candidate))
# Passing None as vector
self.assertTrue(
drillableLib.isDrillable(self.obj.Shape, candidate, vector=None)
)
# Test edges
# circular edge
candidate = self.obj.getSubObject("Edge53")
# Typical drilling
self.assertTrue(drillableLib.isDrillable(self.obj.Shape, candidate))
# Passing None as vector
self.assertTrue(
drillableLib.isDrillable(self.obj.Shape, candidate, vector=None)
)
# Passing explicit vector
self.assertTrue(
drillableLib.isDrillable(
self.obj.Shape, candidate, vector=App.Vector(0, 0, 1)
)
)
# Drilling with smaller bit
self.assertTrue(
drillableLib.isDrillable(self.obj.Shape, candidate, tooldiameter=10)
)
# Drilling with bit too large
self.assertFalse(
drillableLib.isDrillable(self.obj.Shape, candidate, tooldiameter=30)
)
# off-axis circular edge
candidate = self.obj.getSubObject("Edge72")
# Typical drilling
self.assertFalse(drillableLib.isDrillable(self.obj.Shape, candidate))
# Passing None as vector
self.assertTrue(
drillableLib.isDrillable(self.obj.Shape, candidate, vector=None)
)
# Passing explicit vector
self.assertTrue(
drillableLib.isDrillable(
self.obj.Shape, candidate, vector=App.Vector(0, 1, 0)
)
)
# incomplete circular edge
candidate = self.obj.getSubObject("Edge108")
# Typical drilling
self.assertFalse(drillableLib.isDrillable(self.obj.Shape, candidate))
# Passing None as vector
self.assertFalse(
drillableLib.isDrillable(self.obj.Shape, candidate, vector=None)
)
# elliptical edge
candidate = self.obj.getSubObject("Edge54")
# Typical drilling
self.assertFalse(drillableLib.isDrillable(self.obj.Shape, candidate))
# Passing None as vector
self.assertFalse(
drillableLib.isDrillable(self.obj.Shape, candidate, vector=None)
)
def test20(self):
"""Test getDrillableTargets"""
results = drillableLib.getDrillableTargets(self.obj)
self.assertEqual(len(results), 15)
results = drillableLib.getDrillableTargets(self.obj, vector=None)
self.assertEqual(len(results), 18)
results = drillableLib.getDrillableTargets(self.obj, ToolDiameter= 20, vector=None)
self.assertEqual(len(results), 5)

View File

@@ -0,0 +1,103 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2021 sliptonic <shopinthewoods@gmail.com> *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the 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 PathFeedRate
import PathMachineState
import Path
import PathScripts.PathToolController as PathToolController
from PathTests.PathTestUtils import PathTestBase
class TestPathHelpers(PathTestBase):
def setUp(self):
self.doc = FreeCAD.newDocument("TestPathUtils")
c1 = Path.Command("G0 Z10")
c2 = Path.Command("G0 X20 Y10")
c3 = Path.Command("G1 X20 Y10 Z5")
c4 = Path.Command("G1 X20 Y20")
self.commandlist = [c1, c2, c3, c4]
def tearDown(self):
FreeCAD.closeDocument("TestPathUtils")
def test00(self):
"""Test that FeedRate Helper populates horiz and vert feed rate based on TC"""
t = Path.Tool("test", "5.0")
tc = PathToolController.Create("TC0", t)
tc.VertRapid = 5
tc.HorizRapid = 10
tc.VertFeed = 15
tc.HorizFeed = 20
resultlist = PathFeedRate.setFeedRate(self.commandlist, tc)
self.assertTrue(resultlist[0].Parameters["F"] == 5)
self.assertTrue(resultlist[1].Parameters["F"] == 10)
self.assertTrue(resultlist[2].Parameters["F"] == 15)
self.assertTrue(resultlist[3].Parameters["F"] == 20)
def test01(self):
"""Test that Machine State initializes and stores position correctly"""
machine = PathMachineState.MachineState()
state = machine.getState()
self.assertTrue(state['X'] == 0 )
self.assertTrue(state['Y'] == 0 )
self.assertTrue(state['Z'] == 0 )
self.assertTrue(machine.WCS == "G54")
for c in self.commandlist:
result = machine.addCommand(c)
state = machine.getState()
self.assertTrue(state['X'] == 20 )
self.assertTrue(state['Y'] == 20 )
self.assertTrue(state['Z'] == 5 )
machine.addCommand(Path.Command("M3 S200"))
self.assertTrue(machine.S == 200)
self.assertTrue(machine.Spindle == "CW")
machine.addCommand(Path.Command("M4 S200"))
self.assertTrue(machine.Spindle == "CCW")
machine.addCommand(Path.Command("M2"))
self.assertTrue(machine.Spindle == "off")
self.assertTrue(machine.S == 0)
machine.addCommand(Path.Command("G57"))
self.assertTrue(machine.WCS == "G57")
machine.addCommand(Path.Command("M6 T5"))
self.assertTrue(machine.T == 5)
# Test that non-change commands return false
result = machine.addCommand(Path.Command("G0 X20"))
self.assertFalse(result)
result = machine.addCommand(Path.Command("G0 X30"))
self.assertTrue(result)

View File

@@ -22,6 +22,8 @@
import TestApp
# from PathTests.TestPathHelix import TestPathHelix
# from PathTests.TestPathPost import PathPostTestCases
from PathTests.TestPathAdaptive import TestPathAdaptive
from PathTests.TestPathCore import TestPathCore
from PathTests.TestPathDeburr import TestPathDeburr
@@ -30,12 +32,12 @@ from PathTests.TestPathDressupDogbone import TestDressupDogbone
from PathTests.TestPathDressupHoldingTags import TestHoldingTags
from PathTests.TestPathDrillGenerator import TestPathDrillGenerator
from PathTests.TestPathGeom import TestPathGeom
# from PathTests.TestPathHelix import TestPathHelix
from PathTests.TestPathHelpers import TestPathHelpers
from PathTests.TestPathLog import TestPathLog
from PathTests.TestPathOpTools import TestPathOpTools
# from PathTests.TestPathPost import PathPostTestCases
from PathTests.TestPathPreferences import TestPathPreferences
from PathTests.TestPathPropertyBag import TestPathPropertyBag
from PathTests.TestPathRotationGenerator import TestPathRotationGenerator
from PathTests.TestPathSetupSheet import TestPathSetupSheet
from PathTests.TestPathStock import TestPathStock
from PathTests.TestPathThreadMilling import TestPathThreadMilling
@@ -57,10 +59,12 @@ False if TestPathCore.__name__ else True
False if TestPathDeburr.__name__ else True
False if TestPathGeom.__name__ else True
# False if TestPathHelix.__name__ else True
False if TestPathHelpers.__name__ else True
False if TestPathLog.__name__ else True
False if TestPathOpTools.__name__ else True
False if TestPathPreferences.__name__ else True
False if TestPathPropertyBag.__name__ else True
False if TestPathRotationGenerator.__name__ else True
False if TestPathSetupSheet.__name__ else True
False if TestPathStock.__name__ else True
False if TestPathThreadMilling.__name__ else True