Make drillable detection ignore blind bottom facing holes
Toolchange gcode generation moved to generator
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Binary file not shown.
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user