Merge pull request #27585 from tarman3/holes_auto

CAM: Drillable - Auto-select optimizations
This commit is contained in:
sliptonic
2026-02-28 09:26:33 -06:00
committed by GitHub
3 changed files with 103 additions and 56 deletions
+3 -3
View File
@@ -251,10 +251,10 @@ class TestPathDrillable(PathTestUtils.PathTestBase):
def test20(self):
"""Test getDrillableTargets"""
results = Drillable.getDrillableTargets(self.obj)
self.assertEqual(len(results), 15)
self.assertEqual(len(results), 16)
results = Drillable.getDrillableTargets(self.obj, vector=None)
self.assertEqual(len(results), 20)
self.assertEqual(len(results), 21)
results = Drillable.getDrillableTargets(self.obj, ToolDiameter=20, vector=None)
results = Drillable.getDrillableTargets(self.obj, toolDiameter=20, vector=None)
self.assertEqual(len(results), 5)
+98 -51
View File
@@ -21,7 +21,7 @@ def checkForBlindHole(baseshape, selectedFace):
circularFaces = [
f
for f in baseshape.Faces
if len(f.OuterWire.Edges) == 1 and type(f.OuterWire.Edges[0].Curve) == Part.Circle
if len(f.OuterWire.Edges) == 1 and isinstance(f.OuterWire.Edges[0].Curve, Part.Circle)
]
circularFaceEdges = [f.OuterWire.Edges[0] for f in circularFaces]
@@ -38,7 +38,9 @@ def checkForBlindHole(baseshape, selectedFace):
return bottomface
def isDrillableCylinder(obj, candidate, tooldiameter=None, vector=App.Vector(0, 0, 1)):
def isDrillableCylinder(
obj, candidate, tooldiameter=None, vector=App.Vector(0, 0, 1), allowPartial=False
):
"""
checks if a candidate cylindrical face is drillable
"""
@@ -50,25 +52,6 @@ def isDrillableCylinder(obj, candidate, tooldiameter=None, vector=App.Vector(0,
"\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
@@ -82,10 +65,10 @@ def isDrillableCylinder(obj, candidate, tooldiameter=None, vector=App.Vector(0,
if not isinstance(candidate.Surface, Part.Cylinder):
raise TypeError("expected a cylinder")
if len(candidate.Edges) != 3:
if not (len(candidate.Edges) == 3 or allowPartial):
raise TypeError("cylinder does not have 3 edges. Not supported yet")
if raisedFeature(obj, candidate):
if obj.isInside(candidate.BoundBox.Center, Path.Geom.Tolerance, False):
Path.Log.debug("The cylindrical face is a raised feature")
return False
@@ -99,7 +82,7 @@ def isDrillableCylinder(obj, candidate, tooldiameter=None, vector=App.Vector(0,
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
if bottomface and matchVector: # blind holes only drillable at exact vector
result = compareVecs(bottomface.normalAt(0, 0), vector, exact=True)
Path.Log.track(result)
return result
@@ -121,19 +104,18 @@ def isDrillableFace(obj, candidate, tooldiameter=None, vector=App.Vector(0, 0, 1
"\n match tool diameter {} \n match vector {}".format(matchToolDiameter, matchVector)
)
if not type(candidate.Surface) == Part.Plane:
if not isinstance(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
if len(candidate.Edges) == 1 and isinstance(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
and isinstance(candidate.Edges[0].Curve, Part.Circle)
and isinstance(candidate.Edges[1].Curve, Part.Circle)
): # process a donut
Path.Log.debug("Face is a donut - 2 edges")
e1 = candidate.Edges[0]
@@ -225,7 +207,7 @@ def isDrillable(obj, candidate, tooldiameter=None, vector=App.Vector(0, 0, 1), a
)
)
if list == type(obj):
if isinstance(obj, list):
for shape in obj:
if isDrillable(shape, candidate, tooldiameter, vector):
return (True, shape)
@@ -237,7 +219,7 @@ def isDrillable(obj, candidate, tooldiameter=None, vector=App.Vector(0, 0, 1), a
try:
if candidate.ShapeType == "Face":
if isinstance(candidate.Surface, Part.Cylinder):
return isDrillableCylinder(obj, candidate, tooldiameter, vector)
return isDrillableCylinder(obj, candidate, tooldiameter, vector, allowPartial)
else:
return isDrillableFace(obj, candidate, tooldiameter, vector)
if candidate.ShapeType == "Edge":
@@ -269,35 +251,100 @@ def compareVecs(vec1, vec2, exact=False):
)
def getDrillableTargets(obj, ToolDiameter=None, vector=App.Vector(0, 0, 1)):
def getStraightEdge(candidate):
# Finds the seam edge in a cylinder
for e in candidate.Edges:
if isinstance(e.Curve, Part.Line):
return e
for e in candidate.Edges:
if isinstance(e.Curve, Part.BSplineCurve):
p0 = e.Vertexes[0].Point
p1 = e.Vertexes[-1].Point
length = p0.distanceToPoint(p1)
if Path.Geom.isRoughly(e.Length, length):
return Part.Edge(Part.LineSegment(p0, p1))
return App.Vector(0, 0, 1)
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
toolRadius = toolDiameter / 2 if toolDiameter else 0
shape = obj.Shape
drillables = []
candidates = [] # simple holes
candidatesExtra = [] # non regular holes
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):
for face in shape.Faces:
if not isinstance(face.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)
if toolRadius:
cylinderRadius = face.Surface.Radius
if toolRadius > cylinderRadius and not Path.Geom.isRoughly(cylinderRadius, toolRadius):
continue
if face.Volume > 0: # hole should have negative volume
continue
if len(face.Edges) > 3:
candidatesExtra.append(face)
else:
candidates.append(face)
if drillable:
results.append((obj, fname))
while candidatesExtra:
faces = [candidatesExtra.pop()]
center = faces[0].Surface.Center
for face in candidatesExtra:
# search faces with identical center point
if Path.Geom.pointsCoincide(face.Surface.Center, center):
faces.append(face)
return results
# check if faces creates a closed area
fzMin = min(e.BoundBox.ZMin for f in faces for e in f.Edges)
bottomEdges = [
e for f in faces for e in f.Edges if Path.Geom.isRoughly(e.BoundBox.ZMax, fzMin)
]
if bottomEdges:
wire = Part.Wire(Part.__sortEdges__(bottomEdges))
if wire and wire.isClosed():
candidates.append(faces[0])
for face in faces[1:]:
candidatesExtra.remove(face)
circularFaces = [] # faces which can be a bottom of hole
for face in shape.Faces:
if all(isinstance(e.Curve, Part.Circle) for e in face.OuterWire.Edges):
center = face.OuterWire.Edges[0].Curve.Center
if all(Path.Geom.pointsCoincide(center, e.Curve.Center) for e in face.OuterWire.Edges):
hashList = [e.hashCode() for e in face.OuterWire.Edges]
circularFaces.append((face, hashList))
for candidate in candidates:
if vector is None: # do not check direction
drillables.append(candidate)
continue
bottomFace = None
for face, hashList in circularFaces:
if any(
e.hashCode() in hashList
for e in candidate.Edges
if isinstance(e.Curve, Part.Circle)
):
bottomFace = face
break
if bottomFace: # blind holes only drillable at exact vector
if compareVecs(bottomFace.normalAt(0, 0), vector, exact=True):
drillables.append(candidate)
elif compareVecs(getStraightEdge(candidate).Curve.Direction, vector):
drillables.append(candidate)
if drillables:
hashList = [f.hashCode() for f in drillables]
faces = shape.Faces
return [(obj, f"Face{i + 1}") for i, f in enumerate(faces) if f.hashCode() in hashList]
return []
+2 -2
View File
@@ -210,12 +210,12 @@ class ObjectOp(PathOp.ObjectOp):
return
matchvector = None if job.JobType == "Multiaxis" else FreeCAD.Vector(0, 0, 1)
tooldiameter = obj.ToolController.Tool.Diameter
tooldiameter = obj.ToolController.Tool.Diameter.Value
features = []
for base in self.model:
features.extend(
Drillable.getDrillableTargets(base, ToolDiameter=tooldiameter, vector=matchvector)
Drillable.getDrillableTargets(base, toolDiameter=tooldiameter, vector=matchvector)
)
obj.Base = features
obj.Disabled = []