freecad-cam/Mod/CAM/Path/Base/Drillable.py
2026-02-01 01:59:24 +01:00

302 lines
9.6 KiB
Python

import FreeCAD as App
import Part
import Path
import numpy
import math
if False:
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
else:
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
def checkForBlindHole(baseshape, selectedFace):
"""
check for blind holes, returns the bottom face if found, none
if the hole is a thru-hole
"""
circularFaces = [
f
for f in baseshape.Faces
if len(f.OuterWire.Edges) == 1 and type(f.OuterWire.Edges[0].Curve) == Part.Circle
]
circularFaceEdges = [f.OuterWire.Edges[0] for f in circularFaces]
commonedges = [i for i in selectedFace.Edges for x in circularFaceEdges if i.isSame(x)]
bottomface = None
for f in circularFaces:
for e in f.Edges:
for i in commonedges:
if e.isSame(i):
bottomface = f
break
return bottomface
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
Path.Log.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 isinstance(candidate.Surface, Part.Cylinder):
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):
Path.Log.debug("The cylindrical face is a raised feature")
return False
if not matchToolDiameter and not matchVector:
return True
if matchToolDiameter and tooldiameter / 2 > candidate.Surface.Radius:
Path.Log.debug("The tool is larger than the target")
return False
bottomface = checkForBlindHole(obj, candidate)
Path.Log.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)
Path.Log.track(result)
return result
elif matchVector and not (compareVecs(getSeam(candidate).Curve.Direction, vector)):
Path.Log.debug("The feature is not aligned with the given vector")
return False
else:
return True
def isDrillableFace(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
Path.Log.debug(
"\n match tool diameter {} \n match vector {}".format(matchToolDiameter, matchVector)
)
if not type(candidate.Surface) == Part.Plane:
Path.Log.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
Path.Log.debug("Face is circular - 1 edge")
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
Path.Log.debug("Face is a donut - 2 edges")
e1 = candidate.Edges[0]
e2 = candidate.Edges[1]
edge = e1 if e1.Curve.Radius < e2.Curve.Radius else e2
else:
Path.Log.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):
Path.Log.debug("Vector not aligned")
return False
if matchToolDiameter and edge.Curve.Radius < tooldiameter / 2:
Path.Log.debug("Failed diameter check")
return False
else:
Path.Log.debug("Face is drillable")
return True
def isDrillableEdge(
obj, candidate, tooldiameter=None, vector=App.Vector(0, 0, 1), allowPartial=False
):
"""
checks if an edge is drillable
"""
matchToolDiameter = tooldiameter is not None
matchVector = vector is not None
Path.Log.debug(
"\n match tool diameter {} \n match vector {}".format(matchToolDiameter, matchVector)
)
edge = candidate
if not (isinstance(edge.Curve, Part.Circle)):
Path.Log.debug("expected a circular edge")
return False
if isinstance(edge.Curve, Part.Circle):
if not (allowPartial or edge.isClosed()):
Path.Log.debug("expected a closed circular edge or allow partial")
return False
if not hasattr(edge.Curve, "Radius"):
Path.Log.debug("The Feature edge has no radius - Ellipse.")
return False
if not matchToolDiameter and not matchVector:
return True
if matchToolDiameter and tooldiameter / 2 > edge.Curve.Radius:
Path.Log.debug("The tool is larger than the target")
return False
if matchVector and not (compareVecs(edge.Curve.Axis, vector)):
Path.Log.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), allowPartial=False):
"""
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.
allowPartial will permit selecting partial circular arcs manually.
obj=Shape
candidate = Face or Edge
tooldiameter=float
vector=App.Vector or None
allowPartial boolean
"""
Path.Log.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":
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, allowPartial)
else:
return False
except TypeError as e:
Path.Log.debug(e)
return False
# raise TypeError("{}".format(e))
def compareVecs(vec1, vec2, exact=False):
"""
compare the two vectors to see if they are aligned for drilling.
if exact is True, vectors must match direction. Otherwise,
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)
Path.Log.debug("vector angle: {}".format(angle))
if exact:
return numpy.isclose(angle, 0, rtol=1e-05, atol=1e-06)
else:
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) + 1):
fname = "Face{}".format(i)
Path.Log.debug(fname)
candidate = obj.getSubObject(fname)
if not isinstance(candidate.Surface, Part.Cylinder):
continue
try:
drillable = isDrillable(shp, candidate, tooldiameter=ToolDiameter, vector=vector)
Path.Log.debug("fname: {} : drillable {}".format(fname, drillable))
except Exception as e:
Path.Log.debug(e)
continue
if drillable:
results.append((obj, fname))
return results