# SPDX-License-Identifier: LGPL-2.1-or-later 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