| | |
| |
|
| | 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): |
| | |
| | |
| | |
| |
|
| | 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): |
| | |
| |
|
| | for e in candidate.Edges: |
| | if isinstance(e.Curve, Part.Line): |
| | 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: |
| | 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 |
| | ): |
| | 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 |
| | ): |
| | 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: |
| | 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 |
| | |
| |
|
| |
|
| | 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 |
| |
|