Make drillable detection ignore blind bottom facing holes

Toolchange gcode generation moved to generator
This commit is contained in:
sliptonic
2022-01-31 13:09:24 -06:00
parent 586dd83663
commit 98d804ef52
6 changed files with 107 additions and 86 deletions

View File

@@ -22,9 +22,7 @@
import PathScripts.PathLog as PathLog
import math
import Path
import FreeCAD
from enum import Enum
__title__ = "Toolchange Path Generator"
@@ -33,7 +31,7 @@ __url__ = "https://www.freecadweb.org"
__doc__ = "Generates the rotation toolpath"
if True:
if False:
PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
PathLog.trackModule(PathLog.thisModule())
else:
@@ -59,7 +57,8 @@ def generate(
)
if spindledirection is not SpindleDirection.OFF and spindlespeed == 0:
raise ValueError("Turning on spindle with zero speed is invalid")
spindledirection = SpindleDirection.OFF
# raise ValueError("Turning on spindle with zero speed is invalid")
if spindlespeed < 0:
raise ValueError("Spindle speed must be a positive value")
@@ -74,4 +73,5 @@ def generate(
else:
commands.append(Path.Command(spindledirection.value, {"S": spindlespeed}))
PathLog.track(commands)
return commands

View File

@@ -277,6 +277,8 @@ class ToolController:
args["spindledirection"] = SpindleDirection.CW
else:
args["spindledirection"] = SpindleDirection.CCW
elif obj.SpindleDir == "None":
args["spindledirection"] = SpindleDirection.OFF
else:
if obj.SpindleDir == "Forward":
args["spindledirection"] = SpindleDirection.CW

View File

@@ -4,7 +4,7 @@ import Part
import numpy
import math
if True:
if False:
PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
PathLog.trackModule(PathLog.thisModule())
else:
@@ -101,22 +101,24 @@ def isDrillableCylinder(obj, candidate, tooldiameter=None, vector=App.Vector(0,
PathLog.debug("The tool is larger than the target")
return False
if matchVector and not (compareVecs(getSeam(candidate).Curve.Direction, vector)):
PathLog.debug("The feature is not aligned with the given vector")
return False
bottomface = checkForBlindHole(obj, candidate)
PathLog.track(bottomface)
if bottomface is None: # thru hole
return True
else:
PathLog.track(bottomface.normalAt(0,0))
result = compareVecs(bottomface.normalAt(0, 0), App.Vector(0, 0, 1), exact=True)
PathLog.track("candidate is a blind hole")
if (
bottomface is not None and matchVector
): # blind holes only drillable at exact vector
result = compareVecs(bottomface.normalAt(0, 0), vector, exact=True)
PathLog.track(result)
return result
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)):
def isDrillableFace(obj, candidate, tooldiameter=None, vector=App.Vector(0, 0, 1)):
"""
checks if a flat face or edge is drillable
"""
@@ -129,41 +131,57 @@ def isDrillableCircle(obj, candidate, tooldiameter=None, vector=App.Vector(0, 0,
)
)
if candidate.ShapeType == "Face":
PathLog.track()
if not type(candidate.Surface) == Part.Plane:
PathLog.debug("Drilling on non-planar faces not supported")
return False
PathLog.track()
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)
)
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
if vector is not None: # Check for blind hole alignment
if not compareVecs(candidate.normalAt(0, 0), vector, exact=True):
return False
if vector is not None: # Check for blind hole alignment
if not compareVecs(candidate.normalAt(0,0), vector, exact=True):
PathLog.track(False)
return False
if matchToolDiameter and edge.Curve.Radius < tooldiameter / 2:
PathLog.track()
return False
else:
return True
else: # edge
edge = candidate
if not (isinstance(edge.Curve, Part.Circle) and edge.isClosed()):
PathLog.debug("expected a closed circular edge")
return False
def isDrillableEdge(obj, candidate, tooldiameter=None, vector=App.Vector(0, 0, 1)):
"""
checks if an 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
)
)
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.")
@@ -216,12 +234,16 @@ def isDrillable(obj, candidate, tooldiameter=None, vector=App.Vector(0, 0, 1)):
raise TypeError("expected a Face or Edge. Got a {}".format(candidate.ShapeType))
try:
if candidate.ShapeType == "Face" and isinstance(
candidate.Surface, Part.Cylinder
):
return isDrillableCylinder(obj, candidate, tooldiameter, vector)
if candidate.ShapeType == "Face":
if isinstance(candidate.Surface, Part.Cylinder):
return isDrillableCylinder(obj, candidate, tooldiameter, vector)
else:
return isDrillableFace(obj, candidate, tooldiameter, vector)
if candidate.ShapeType == "Edge":
return isDrillableEdge(obj, candidate, tooldiameter, vector)
else:
return isDrillableCircle(obj, candidate, tooldiameter, vector)
return False
except TypeError as e:
PathLog.debug(e)
return False

View File

@@ -33,10 +33,11 @@ if False:
else:
PathLog.setLevel(PathLog.Level.INFO, 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")
self.obj = self.doc.getObject("Pocket011")
def tearDown(self):
App.closeDocument(self.doc.Name)
@@ -68,16 +69,10 @@ class TestPathDrillable(PathTestUtils.PathTestBase):
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")
candidate = self.obj.getSubObject("Face30")
# Typical drilling
self.assertTrue(drillableLib.isDrillable(self.obj.Shape, candidate))
@@ -93,7 +88,7 @@ class TestPathDrillable(PathTestUtils.PathTestBase):
)
# off-axis hole
candidate = self.obj.getSubObject("Face42")
candidate = self.obj.getSubObject("Face44")
# Typical drilling
self.assertFalse(drillableLib.isDrillable(self.obj.Shape, candidate))
@@ -106,26 +101,26 @@ class TestPathDrillable(PathTestUtils.PathTestBase):
# Passing explicit vector
self.assertTrue(
drillableLib.isDrillable(
self.obj.Shape, candidate, vector=App.Vector(0, 1, 0)
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)
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)
self.obj.Shape, candidate, tooldiameter=30, vector=App.Vector(0, -1, 0)
)
)
# ellipse hole
candidate = self.obj.getSubObject("Face20")
candidate = self.obj.getSubObject("Face29")
# Typical drilling
self.assertFalse(drillableLib.isDrillable(self.obj.Shape, candidate))
@@ -136,7 +131,7 @@ class TestPathDrillable(PathTestUtils.PathTestBase):
)
# raised cylinder
candidate = self.obj.getSubObject("Face30")
candidate = self.obj.getSubObject("Face32")
# Typical drilling
self.assertFalse(drillableLib.isDrillable(self.obj.Shape, candidate))
@@ -146,9 +141,8 @@ class TestPathDrillable(PathTestUtils.PathTestBase):
drillableLib.isDrillable(self.obj.Shape, candidate, vector=None)
)
# cylinder on slope
candidate = self.obj.getSubObject("Face26")
candidate = self.obj.getSubObject("Face24")
# Typical drilling
self.assertTrue(drillableLib.isDrillable(self.obj.Shape, candidate))
@@ -158,7 +152,7 @@ class TestPathDrillable(PathTestUtils.PathTestBase):
)
# Circular Faces
candidate = self.obj.getSubObject("Face51")
candidate = self.obj.getSubObject("Face54")
# Typical drilling
self.assertTrue(drillableLib.isDrillable(self.obj.Shape, candidate))
@@ -186,7 +180,7 @@ class TestPathDrillable(PathTestUtils.PathTestBase):
)
# off-axis circular face hole
candidate = self.obj.getSubObject("Face54")
candidate = self.obj.getSubObject("Face58")
# Typical drilling
self.assertFalse(drillableLib.isDrillable(self.obj.Shape, candidate))
@@ -199,12 +193,12 @@ class TestPathDrillable(PathTestUtils.PathTestBase):
# Passing explicit vector
self.assertTrue(
drillableLib.isDrillable(
self.obj.Shape, candidate, vector=App.Vector(0, 1, 0)
self.obj.Shape, candidate, vector=App.Vector(0, -1, 0)
)
)
# raised face
candidate = self.obj.getSubObject("Face45")
candidate = self.obj.getSubObject("Face49")
# Typical drilling
self.assertTrue(drillableLib.isDrillable(self.obj.Shape, candidate))
@@ -214,7 +208,7 @@ class TestPathDrillable(PathTestUtils.PathTestBase):
)
# interrupted Face
candidate = self.obj.getSubObject("Face46")
candidate = self.obj.getSubObject("Face50")
# Typical drilling
self.assertFalse(drillableLib.isDrillable(self.obj.Shape, candidate))
@@ -224,7 +218,7 @@ class TestPathDrillable(PathTestUtils.PathTestBase):
)
# donut face
candidate = self.obj.getSubObject("Face44")
candidate = self.obj.getSubObject("Face48")
# Typical drilling
self.assertTrue(drillableLib.isDrillable(self.obj.Shape, candidate))
@@ -235,7 +229,7 @@ class TestPathDrillable(PathTestUtils.PathTestBase):
# Test edges
# circular edge
candidate = self.obj.getSubObject("Edge53")
candidate = self.obj.getSubObject("Edge55")
# Typical drilling
self.assertTrue(drillableLib.isDrillable(self.obj.Shape, candidate))
@@ -259,11 +253,13 @@ class TestPathDrillable(PathTestUtils.PathTestBase):
# Drilling with bit too large
self.assertFalse(
drillableLib.isDrillable(self.obj.Shape, candidate, tooldiameter=30)
drillableLib.isDrillable(
self.obj.Shape, candidate, tooldiameter=30, vector=None
)
)
# off-axis circular edge
candidate = self.obj.getSubObject("Edge72")
candidate = self.obj.getSubObject("Edge74")
# Typical drilling
self.assertFalse(drillableLib.isDrillable(self.obj.Shape, candidate))
@@ -281,7 +277,7 @@ class TestPathDrillable(PathTestUtils.PathTestBase):
)
# incomplete circular edge
candidate = self.obj.getSubObject("Edge108")
candidate = self.obj.getSubObject("Edge39")
# Typical drilling
self.assertFalse(drillableLib.isDrillable(self.obj.Shape, candidate))
@@ -291,7 +287,7 @@ class TestPathDrillable(PathTestUtils.PathTestBase):
)
# elliptical edge
candidate = self.obj.getSubObject("Edge54")
candidate = self.obj.getSubObject("Edge56")
# Typical drilling
self.assertFalse(drillableLib.isDrillable(self.obj.Shape, candidate))
@@ -300,14 +296,15 @@ class TestPathDrillable(PathTestUtils.PathTestBase):
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)
self.assertEqual(len(results), 20)
results = drillableLib.getDrillableTargets(self.obj, ToolDiameter= 20, vector=None)
results = drillableLib.getDrillableTargets(
self.obj, ToolDiameter=20, vector=None
)
self.assertEqual(len(results), 5)

View File

@@ -21,15 +21,13 @@
# ***************************************************************************
import Path
import FreeCAD
import Generators.toolchange_generator as generator
from Generators.toolchange_generator import SpindleDirection
import PathScripts.PathLog as PathLog
import PathTests.PathTestUtils as PathTestUtils
import numpy as np
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
PathLog.trackModule(PathLog.thisModule())
@@ -67,7 +65,9 @@ class TestPathToolChangeGenerator(PathTestUtils.PathTestBase):
# speed zero with spindle on
args["spindlespeed"] = 0
self.assertRaises(ValueError, generator.generate, **args)
results = generator.generate(**args)
self.assertTrue(len(results) == 2)
PathLog.track(results)
# negative spindlespeed
args["spindlespeed"] = -10