| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | import FreeCAD |
| | import Path |
| | import math |
| |
|
| | |
| | from lazy_loader.lazy_loader import LazyLoader |
| |
|
| | Part = LazyLoader("Part", globals(), "Part") |
| |
|
| | __title__ = "Util - Utility functions for CAM operations." |
| | __author__ = "sliptonic (Brad Collette)" |
| | __url__ = "https://www.freecad.org" |
| | __doc__ = "Collection of functions used by various operations. The functions are specific to CAM and the algorithms employed by CAM's operations." |
| |
|
| |
|
| | PrintWireDebug = False |
| |
|
| | 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()) |
| |
|
| | translate = FreeCAD.Qt.translate |
| |
|
| |
|
| | def debugEdge(label, e): |
| | """debugEdge(label, e) ... prints a python statement to create e |
| | Currently lines and arcs are supported.""" |
| | if not PrintWireDebug: |
| | return |
| | p0 = e.valueAt(e.FirstParameter) |
| | p1 = e.valueAt(e.LastParameter) |
| | if isinstance(e.Curve, Part.Line): |
| | print( |
| | "%s Part.makeLine((%.2f, %.2f, %.2f), (%.2f, %.2f, %.2f))" |
| | % (label, p0.x, p0.y, p0.z, p1.x, p1.y, p1.z) |
| | ) |
| | elif isinstance(e.Curve, Part.Circle): |
| | r = e.Curve.Radius |
| | c = e.Curve.Center |
| | a = e.Curve.Axis |
| | xu = e.Curve.AngleXU |
| | if a.z < 0: |
| | first = math.degrees(xu - e.FirstParameter) |
| | else: |
| | first = math.degrees(xu + e.FirstParameter) |
| | last = first + math.degrees(e.LastParameter - e.FirstParameter) |
| | print( |
| | "%s Part.makeCircle(%.2f, App.Vector(%.2f, %.2f, %.2f), App.Vector(%.2f, %.2f, %.2f), %.2f, %.2f)" |
| | % (label, r, c.x, c.y, c.z, a.x, a.y, a.z, first, last) |
| | ) |
| | else: |
| | print( |
| | "%s %s (%.2f, %.2f, %.2f) -> (%.2f, %.2f, %.2f)" |
| | % (label, type(e.Curve).__name__, p0.x, p0.y, p0.z, p1.x, p1.y, p1.z) |
| | ) |
| |
|
| |
|
| | def makeWires(inEdges): |
| | """makeWires ... function to make non-forking wires from a collection of edges""" |
| | edgelists = Part.sortEdges(inEdges) |
| | result = [Part.Wire(e) for e in edgelists] |
| | return result |
| |
|
| |
|
| | def debugWire(label, w): |
| | """debugWire(label, w) ... prints python statements for all edges of w to be added to the object tree in a group.""" |
| | if not PrintWireDebug: |
| | return |
| | print("#%s wire >>>>>>>>>>>>>>>>>>>>>>>>" % label) |
| | print("grp = FreeCAD.ActiveDocument.addObject('App::DocumentObjectGroup', '%s')" % label) |
| | for i, e in enumerate(w.Edges): |
| | edge = "%s_e%d" % (label, i) |
| | debugEdge("%s = " % edge, e) |
| | print("Part.show(%s, '%s')" % (edge, edge)) |
| | print("grp.addObject(FreeCAD.ActiveDocument.ActiveObject)") |
| | print("#%s wire <<<<<<<<<<<<<<<<<<<<<<<<" % label) |
| |
|
| |
|
| | def _orientEdges(inEdges): |
| | """_orientEdges(inEdges) ... internal worker function to orient edges so the last vertex of one edge connects to the first vertex of the next edge. |
| | Assumes the edges are in an order so they can be connected.""" |
| | Path.Log.track() |
| | |
| | e0 = inEdges[0] |
| | |
| | if 1 < len(inEdges): |
| | last = e0.valueAt(e0.LastParameter) |
| | e1 = inEdges[1] |
| | if not Path.Geom.pointsCoincide( |
| | last, e1.valueAt(e1.FirstParameter) |
| | ) and not Path.Geom.pointsCoincide(last, e1.valueAt(e1.LastParameter)): |
| | debugEdge("# _orientEdges - flip first", e0) |
| | e0 = Path.Geom.flipEdge(e0) |
| |
|
| | edges = [e0] |
| | last = e0.valueAt(e0.LastParameter) |
| | for e in inEdges[1:]: |
| | edge = ( |
| | e |
| | if Path.Geom.pointsCoincide(last, e.valueAt(e.FirstParameter)) |
| | else Path.Geom.flipEdge(e) |
| | ) |
| | edges.append(edge) |
| | last = edge.valueAt(edge.LastParameter) |
| | return edges |
| |
|
| |
|
| | def _isWireClockwise(w): |
| | """_isWireClockwise(w) ... return True if wire is oriented clockwise. |
| | Assumes the edges of w are already properly oriented - for generic access use isWireClockwise(w). |
| | """ |
| | |
| | |
| | |
| | if len(w.Edges) <= 2 and isinstance(w.Edges[0].Curve, Part.Circle): |
| | return 0 > w.Edges[0].Curve.Axis.z |
| | if len(w.Edges) == 2 and isinstance(w.Edges[1].Curve, Part.Circle): |
| | return 0 > w.Edges[1].Curve.Axis.z |
| |
|
| | |
| | |
| | area = 0 |
| | for e in w.Edges: |
| | v0 = e.valueAt(e.FirstParameter) |
| | v1 = e.valueAt(e.LastParameter) |
| | area = area + (v0.x * v1.y - v1.x * v0.y) |
| | Path.Log.track(area) |
| | return area < 0 |
| |
|
| |
|
| | def isWireClockwise(w): |
| | """isWireClockwise(w) ... returns True if the wire winds clockwise.""" |
| | return _isWireClockwise(Part.Wire(_orientEdges(w.Edges))) |
| |
|
| |
|
| | def orientWire(w, forward=True): |
| | """orientWire(w, forward=True) ... orients given wire in a specific direction. |
| | If forward = True (the default) the wire is oriented clockwise, looking down the negative Z axis. |
| | If forward = False the wire is oriented counter clockwise. |
| | If forward = None the orientation is determined by the order in which the edges appear in the wire. |
| | """ |
| | Path.Log.debug("orienting forward: {}: {} edges".format(forward, len(w.Edges))) |
| | wire = Part.Wire(_orientEdges(w.Edges)) |
| | if forward is not None: |
| | if forward != _isWireClockwise(wire): |
| | Path.Log.track("orientWire - needs flipping") |
| | return Path.Geom.flipWire(wire) |
| | Path.Log.track("orientWire - ok") |
| | return wire |
| |
|
| |
|
| | def offsetWire(wire, base, offset, forward, Side=None): |
| | """offsetWire(wire, base, offset, forward) ... offsets the wire away from base and orients the wire accordingly. |
| | The function tries to avoid most of the pitfalls of Part.makeOffset2D which is possible because all offsetting |
| | happens in the XY plane. |
| | """ |
| | Path.Log.track("offsetWire") |
| |
|
| | if len(wire.Edges) == 1: |
| | edge = wire.Edges[0] |
| | curve = edge.Curve |
| | if isinstance(curve, Part.Circle) and wire.isClosed(): |
| | |
| | |
| | |
| | z = -1 if forward else 1 |
| | new_edge = Part.makeCircle(curve.Radius + offset, curve.Center, FreeCAD.Vector(0, 0, z)) |
| | if base.isInside(new_edge.Vertexes[0].Point, offset / 2, True): |
| | if offset > curve.Radius or Path.Geom.isRoughly(offset, curve.Radius): |
| | |
| | return None |
| | if Side: |
| | Side[0] = "Inside" |
| | print("inside") |
| | new_edge = Part.makeCircle( |
| | curve.Radius - offset, curve.Center, FreeCAD.Vector(0, 0, -z) |
| | ) |
| |
|
| | return Part.Wire([new_edge]) |
| |
|
| | if isinstance(curve, Part.Circle) and not wire.isClosed(): |
| | |
| | z = -1 if forward else 1 |
| | l1 = math.sqrt( |
| | (edge.Vertexes[0].Point.x - curve.Center.x) ** 2 |
| | + (edge.Vertexes[0].Point.y - curve.Center.y) ** 2 |
| | ) |
| | l2 = math.sqrt( |
| | (edge.Vertexes[1].Point.x - curve.Center.x) ** 2 |
| | + (edge.Vertexes[1].Point.y - curve.Center.y) ** 2 |
| | ) |
| |
|
| | |
| | start_angle = math.acos((edge.Vertexes[0].Point.x - curve.Center.x) / l1) |
| | end_angle = math.acos((edge.Vertexes[1].Point.x - curve.Center.x) / l2) |
| |
|
| | |
| | if edge.Vertexes[0].Point.y < curve.Center.y: |
| | start_angle *= -1 |
| | if edge.Vertexes[1].Point.y < curve.Center.y: |
| | end_angle *= -1 |
| |
|
| | if ( |
| | edge.Vertexes[0].Point.x > curve.Center.x |
| | or edge.Vertexes[1].Point.x > curve.Center.x |
| | ) and curve.AngleXU < 0: |
| | tmp = start_angle |
| | start_angle = end_angle |
| | end_angle = tmp |
| |
|
| | |
| | if base.isInside(edge.Vertexes[0].Point, offset / 2, True): |
| | offset *= -1 |
| | if Side: |
| | Side[0] = "Inside" |
| |
|
| | |
| | if curve.AngleXU > 0: |
| | edge = Part.ArcOfCircle( |
| | Part.Circle(curve.Center, FreeCAD.Vector(0, 0, 1), curve.Radius + offset), |
| | start_angle, |
| | end_angle, |
| | ).toShape() |
| | else: |
| | edge = Part.ArcOfCircle( |
| | Part.Circle(curve.Center, FreeCAD.Vector(0, 0, 1), curve.Radius - offset), |
| | start_angle, |
| | end_angle, |
| | ).toShape() |
| |
|
| | return Part.Wire([edge]) |
| |
|
| | if isinstance(curve, (Part.Line, Part.LineSegment)): |
| | |
| | |
| | |
| | p0 = edge.Vertexes[0].Point |
| | v0 = edge.Vertexes[1].Point - p0 |
| | n = v0.cross(FreeCAD.Vector(0, 0, 1)) |
| | o = n.normalize() * offset |
| | edge.translate(o) |
| |
|
| | |
| | if base.isInside( |
| | edge.valueAt((edge.FirstParameter + edge.LastParameter) / 2), |
| | offset / 2, |
| | True, |
| | ): |
| | edge.translate(-2 * o) |
| |
|
| | |
| | if forward is not None: |
| | v1 = edge.Vertexes[1].Point - p0 |
| | left = Path.Geom.Side.Left == Path.Geom.Side.of(v0, v1) |
| | if left != forward: |
| | edge = Path.Geom.flipEdge(edge) |
| | return Part.Wire([edge]) |
| |
|
| | |
| |
|
| | owire = orientWire(wire.makeOffset2D(offset), True) |
| | debugWire("makeOffset2D_%d" % len(wire.Edges), owire) |
| |
|
| | if wire.isClosed(): |
| | if not base.isInside(owire.Edges[0].Vertexes[0].Point, offset / 2, True): |
| | Path.Log.track("closed - outside") |
| | if Side: |
| | Side[0] = "Outside" |
| | return orientWire(owire, forward) |
| | Path.Log.track("closed - inside") |
| | if Side: |
| | Side[0] = "Inside" |
| | try: |
| | owire = wire.makeOffset2D(-offset) |
| | except Exception: |
| | |
| | |
| | return None |
| | |
| | if forward is None: |
| | return orientWire(owire, None) |
| | return orientWire(owire, not forward) |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | edges = _orientEdges(wire.Edges) |
| |
|
| | |
| | start = edges[0].firstVertex().Point |
| | end = edges[-1].lastVertex().Point |
| | debugWire("wire", wire) |
| | debugWire("wedges", Part.Wire(edges)) |
| |
|
| | |
| | common = base.common(owire) |
| | insideEndpoints = [e.lastVertex().Point for e in common.Edges] |
| | insideEndpoints.append(common.Edges[0].firstVertex().Point) |
| |
|
| | def isInside(edge): |
| | p0 = edge.firstVertex().Point |
| | p1 = edge.lastVertex().Point |
| | for p in insideEndpoints: |
| | if Path.Geom.pointsCoincide(p, p0, 0.01) or Path.Geom.pointsCoincide(p, p1, 0.01): |
| | return True |
| | return False |
| |
|
| | outside = [e for e in owire.Edges if not isInside(e)] |
| | |
| | longestWire = None |
| | for w in [Part.Wire(el) for el in Part.sortEdges(outside)]: |
| | if not longestWire or longestWire.Length < w.Length: |
| | longestWire = w |
| |
|
| | debugWire("outside", Part.Wire(outside)) |
| | debugWire("longest", longestWire) |
| |
|
| | def isCircleAt(edge, center): |
| | """isCircleAt(edge, center) ... helper function returns True if edge is a circle at the given center.""" |
| | if isinstance(edge.Curve, (Part.Circle, Part.ArcOfCircle)): |
| | return Path.Geom.pointsCoincide(edge.Curve.Center, center) |
| | return False |
| |
|
| | |
| | collectLeft = False |
| | collectRight = False |
| | leftSideEdges = [] |
| | rightSideEdges = [] |
| |
|
| | |
| | |
| | |
| | |
| | for e in owire.Edges + owire.Edges: |
| | if isCircleAt(e, start): |
| | if Path.Geom.pointsCoincide(e.Curve.Axis, FreeCAD.Vector(0, 0, 1)): |
| | if not collectLeft and leftSideEdges: |
| | break |
| | collectLeft = True |
| | collectRight = False |
| | else: |
| | if not collectRight and rightSideEdges: |
| | break |
| | collectLeft = False |
| | collectRight = True |
| | elif isCircleAt(e, end): |
| | if Path.Geom.pointsCoincide(e.Curve.Axis, FreeCAD.Vector(0, 0, 1)): |
| | if not collectRight and rightSideEdges: |
| | break |
| | collectLeft = False |
| | collectRight = True |
| | else: |
| | if not collectLeft and leftSideEdges: |
| | break |
| | collectLeft = True |
| | collectRight = False |
| | elif collectLeft: |
| | leftSideEdges.append(e) |
| | elif collectRight: |
| | rightSideEdges.append(e) |
| |
|
| | debugWire("left", Part.Wire(leftSideEdges)) |
| | debugWire("right", Part.Wire(rightSideEdges)) |
| |
|
| | |
| | |
| | edges = leftSideEdges |
| | for e in longestWire.Edges: |
| | for e0 in rightSideEdges: |
| | if Path.Geom.edgesMatch(e, e0): |
| | edges = rightSideEdges |
| | Path.Log.debug("#use right side edges") |
| | if not forward: |
| | Path.Log.debug("#reverse") |
| | edges.reverse() |
| | return orientWire(Part.Wire(edges), None) |
| |
|
| | |
| | |
| | |
| | Path.Log.debug("#use left side edges") |
| | if not forward: |
| | Path.Log.debug("#reverse") |
| | edges.reverse() |
| |
|
| | return orientWire(Part.Wire(edges), None) |
| |
|