| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | import FreeCAD |
| | import Path |
| | import math |
| |
|
| | from FreeCAD import Vector |
| |
|
| | |
| | from lazy_loader.lazy_loader import LazyLoader |
| |
|
| | Part = LazyLoader("Part", globals(), "Part") |
| |
|
| | __title__ = "Geom - geometry utilities for CAM" |
| | __author__ = "sliptonic (Brad Collette)" |
| | __url__ = "https://www.freecad.org" |
| | __doc__ = "Functions to extract and convert between Path.Command and Part.Edge and utility functions to reason about them." |
| |
|
| | Tolerance = 0.000001 |
| |
|
| | translate = FreeCAD.Qt.translate |
| |
|
| | 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()) |
| |
|
| |
|
| | class Side: |
| | """Class to determine and define the side a Path is on, or Vectors are in relation to each other.""" |
| |
|
| | Left = +1 |
| | Right = -1 |
| | Straight = 0 |
| | On = 0 |
| |
|
| | @classmethod |
| | def toString(cls, side): |
| | """toString(side) |
| | Returns a string representation of the enum value.""" |
| | if side == cls.Left: |
| | return "Left" |
| | if side == cls.Right: |
| | return "Right" |
| | return "On" |
| |
|
| | @classmethod |
| | def of(cls, ptRef, pt): |
| | """of(ptRef, pt) |
| | Determine the side of pt in relation to ptRef. |
| | If both Points are viewed as vectors with their origin in (0,0,0) |
| | then the two vectors either form a straight line (On) or pt |
| | lies in the left or right hemisphere in regards to ptRef.""" |
| | d = -ptRef.x * pt.y + ptRef.y * pt.x |
| | if d < 0: |
| | return cls.Left |
| | if d > 0: |
| | return cls.Right |
| | return cls.Straight |
| |
|
| |
|
| | CmdMoveRapid = ["G0", "G00"] |
| | CmdMoveStraight = ["G1", "G01"] |
| | CmdMoveCW = ["G2", "G02"] |
| | CmdMoveCCW = ["G3", "G03"] |
| | CmdMoveDrill = ["G73", "G81", "G82", "G83", "G85"] |
| | CmdMoveArc = CmdMoveCW + CmdMoveCCW |
| | CmdMove = CmdMoveStraight + CmdMoveArc + CmdMoveDrill |
| | CmdMoveAll = CmdMove + CmdMoveRapid |
| |
|
| |
|
| | def isRoughly(float1, float2, error=Tolerance): |
| | """isRoughly(float1, float2, [error=Tolerance]) |
| | Returns true if the two values are the same within a given error.""" |
| | return math.fabs(float1 - float2) <= error |
| |
|
| |
|
| | def pointsCoincide(p1, p2, error=Tolerance): |
| | """pointsCoincide(p1, p2, [error=Tolerance]) |
| | Return True if two points are roughly identical (see also isRoughly).""" |
| | return ( |
| | isRoughly(p1.x, p2.x, error) |
| | and isRoughly(p1.y, p2.y, error) |
| | and isRoughly(p1.z, p2.z, error) |
| | ) |
| |
|
| |
|
| | def edgesMatch(e0, e1, error=Tolerance): |
| | """edgesMatch(e0, e1, [error=Tolerance] |
| | Return true if the edges start and end at the same point and have the same type of curve.""" |
| | if type(e0.Curve) is not type(e1.Curve) or len(e0.Vertexes) != len(e1.Vertexes): |
| | return False |
| | return all( |
| | pointsCoincide(e0.Vertexes[i].Point, e1.Vertexes[i].Point, error) |
| | for i in range(len(e0.Vertexes)) |
| | ) |
| |
|
| |
|
| | def edgeConnectsTo(edge, vector, error=Tolerance): |
| | """edgeConnectsTop(edge, vector, error=Tolerance) |
| | Returns True if edge connects to given vector.""" |
| | return pointsCoincide(edge.valueAt(edge.FirstParameter), vector, error) or pointsCoincide( |
| | edge.valueAt(edge.LastParameter), vector, error |
| | ) |
| |
|
| |
|
| | def normalizeAngle(a): |
| | """normalizeAngle(a) ... return angle shifted into interval -pi <= a <= pi""" |
| | while a > math.pi: |
| | a = a - 2 * math.pi |
| | while a < -math.pi: |
| | a = a + 2 * math.pi |
| | return a |
| |
|
| |
|
| | def getAngle(vector): |
| | """getAngle(vector) |
| | Returns the angle [-pi,pi] of a vector using the X-axis as the reference. |
| | Positive angles for vertexes in the upper hemisphere (positive y values) |
| | and negative angles for the lower hemisphere.""" |
| | a = vector.getAngle(Vector(1, 0, 0)) |
| | if vector.y < 0: |
| | return -a |
| | return a |
| |
|
| |
|
| | def diffAngle(a1, a2, direction="CW"): |
| | """diffAngle(a1, a2, [direction='CW']) |
| | Returns the difference between two angles (a1 -> a2) into a given direction.""" |
| | if direction == "CW": |
| | while a1 < a2: |
| | a1 += 2 * math.pi |
| | a = a1 - a2 |
| | else: |
| | while a2 < a1: |
| | a2 += 2 * math.pi |
| | a = a2 - a1 |
| | return a |
| |
|
| |
|
| | def isVertical(obj): |
| | """isVertical(obj) ... answer True if obj points into Z""" |
| | if isinstance(obj, FreeCAD.Vector): |
| | return isRoughly(obj.x, 0) and isRoughly(obj.y, 0) |
| |
|
| | if obj.ShapeType == "Face": |
| | if isinstance(obj.Surface, Part.Plane): |
| | return isHorizontal(obj.Surface.Axis) |
| | if isinstance(obj.Surface, (Part.Cylinder, Part.Cone)): |
| | return isVertical(obj.Surface.Axis) |
| | if isinstance(obj.Surface, Part.Sphere): |
| | return True |
| | if isinstance(obj.Surface, Part.SurfaceOfExtrusion): |
| | return isVertical(obj.Surface.Direction) |
| | if isinstance(obj.Surface, Part.SurfaceOfRevolution): |
| | return isHorizontal(obj.Surface.Direction) |
| | if isinstance(obj.Surface, Part.BSplineSurface): |
| | |
| | vertEdges = [e for e in obj.Edges if isVertical(e)] |
| | return len(vertEdges) == 2 and len(obj.Edges) == 4 |
| |
|
| | Path.Log.info( |
| | translate("PathGeom", "face %s not handled, assuming not vertical") % type(obj.Surface) |
| | ) |
| | return None |
| |
|
| | if obj.ShapeType == "Edge": |
| | if isinstance(obj.Curve, (Part.Line, Part.LineSegment)): |
| | return isVertical(obj.Vertexes[1].Point - obj.Vertexes[0].Point) |
| | if isinstance(obj.Curve, (Part.Circle, Part.Ellipse)): |
| | return isHorizontal(obj.Curve.Axis) |
| | if isinstance(obj.Curve, (Part.BezierCurve, Part.BSplineCurve)): |
| | |
| | |
| | return isVertical(obj.Curve.EndPoint - obj.Curve.StartPoint) |
| |
|
| | Path.Log.info( |
| | translate("PathGeom", "edge %s not handled, assuming not vertical") % type(obj.Curve) |
| | ) |
| | return None |
| |
|
| | Path.Log.error(translate("PathGeom", "isVertical(%s) not supported") % obj) |
| | return None |
| |
|
| |
|
| | def isHorizontal(obj): |
| | """isHorizontal(obj) ... answer True if obj points into X or Y""" |
| | if isinstance(obj, FreeCAD.Vector): |
| | return isRoughly(obj.z, 0) |
| |
|
| | if obj.ShapeType == "Face": |
| | if isinstance(obj.Surface, Part.Plane): |
| | return isVertical(obj.Surface.Axis) |
| | if isinstance(obj.Surface, (Part.Cylinder, Part.Cone)): |
| | return isHorizontal(obj.Surface.Axis) |
| | if isinstance(obj.Surface, Part.Sphere): |
| | return True |
| | if isinstance(obj.Surface, Part.SurfaceOfExtrusion): |
| | return isHorizontal(obj.Surface.Direction) |
| | if isinstance(obj.Surface, Part.SurfaceOfRevolution): |
| | return isVertical(obj.Surface.Direction) |
| | return isRoughly(obj.BoundBox.ZLength, 0.0) |
| |
|
| | if obj.ShapeType == "Edge": |
| | if isinstance(obj.Curve, (Part.Line, Part.LineSegment)): |
| | return isHorizontal(obj.Vertexes[1].Point - obj.Vertexes[0].Point) |
| | if isinstance(obj.Curve, (Part.Circle, Part.Ellipse)): |
| | |
| | return isVertical(obj.Curve.Axis) |
| | return isRoughly(obj.BoundBox.ZLength, 0.0) |
| |
|
| | Path.Log.error(translate("PathGeom", "isHorizontal(%s) not supported") % obj) |
| | return None |
| |
|
| |
|
| | def commandEndPoint(cmd, defaultPoint=Vector(), X="X", Y="Y", Z="Z"): |
| | """commandEndPoint(cmd, [defaultPoint=Vector()], [X='X'], [Y='Y'], [Z='Z']) |
| | Extracts the end point from a Path Command.""" |
| | x = cmd.Parameters.get(X, defaultPoint.x) |
| | y = cmd.Parameters.get(Y, defaultPoint.y) |
| | z = cmd.Parameters.get(Z, defaultPoint.z) |
| | return Vector(x, y, z) |
| |
|
| |
|
| | def xy(point): |
| | """xy(point) |
| | Convenience function to return the projection of the Vector in the XY-plane.""" |
| | return Vector(point.x, point.y, 0) |
| |
|
| |
|
| | def speedBetweenPoints(p0, p1, hSpeed, vSpeed): |
| | if isRoughly(hSpeed, vSpeed): |
| | return hSpeed |
| |
|
| | d = p1 - p0 |
| | if isRoughly(0.0, d.z): |
| | return hSpeed |
| | if isRoughly(0.0, d.x) and isRoughly(0.0, d.y): |
| | return vSpeed |
| | |
| | pitch = 2 * math.atan2(xy(d).Length, math.fabs(d.z)) / math.pi |
| | while pitch < 0: |
| | pitch = pitch + 1 |
| | while pitch > 1: |
| | pitch = pitch - 1 |
| | Path.Log.debug( |
| | " pitch = %g %g (%.2f, %.2f, %.2f) -> %.2f" |
| | % (pitch, math.atan2(xy(d).Length, d.z), d.x, d.y, d.z, xy(d).Length) |
| | ) |
| | speed = vSpeed + pitch * (hSpeed - vSpeed) |
| | if speed > hSpeed and speed > vSpeed: |
| | return max(hSpeed, vSpeed) |
| | if speed < hSpeed and speed < vSpeed: |
| | return min(hSpeed, vSpeed) |
| | return speed |
| |
|
| |
|
| | def cmdsForEdge(edge, flip=False, hSpeed=0, vSpeed=0, tol=0.01): |
| | """cmdsForEdge(edge, flip=False) -> List(Path.Command) |
| | Returns a list of Path.Command representing the given edge. |
| | If flip is True the edge is considered to be backwards. |
| | Edge based on a Part.Line is results in G1 command. |
| | Horizontal Edge based on a Part.Circle is results in G2 or G3 command. |
| | Other edge has no direct Path.Command mapping |
| | and will be approximated by straight segments.""" |
| | pt = edge.valueAt(edge.LastParameter) if not flip else edge.valueAt(edge.FirstParameter) |
| | params = {"X": pt.x, "Y": pt.y, "Z": pt.z} |
| | if isinstance(edge.Curve, (Part.Line, Part.LineSegment)): |
| | if hSpeed > 0 and vSpeed > 0: |
| | pt2 = ( |
| | edge.valueAt(edge.FirstParameter) if not flip else edge.valueAt(edge.LastParameter) |
| | ) |
| | params.update({"F": speedBetweenPoints(pt, pt2, hSpeed, vSpeed)}) |
| | commands = [Path.Command("G1", params)] |
| | else: |
| | p1 = edge.valueAt(edge.FirstParameter) if not flip else edge.valueAt(edge.LastParameter) |
| | p2 = edge.valueAt((edge.FirstParameter + edge.LastParameter) / 2) |
| | p3 = pt |
| |
|
| | if ( |
| | hasattr(edge.Curve, "Axis") |
| | and isinstance(edge.Curve, Part.Circle) |
| | and isRoughly(edge.Curve.Axis.x, 0) |
| | and isRoughly(edge.Curve.Axis.y, 0) |
| | ): |
| | |
| | if edge.Curve.Axis.z < 0: |
| | cmd = "G2" if not flip else "G3" |
| | else: |
| | cmd = "G3" if not flip else "G2" |
| |
|
| | if pointsCoincide(p1, p3): |
| | |
| | offset = edge.Curve.Center - pt |
| | else: |
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | pa = xy(p1) |
| | pb = xy(p2) |
| | pc = xy(p3) |
| | offset = Part.Circle(pa, pb, pc).Center - pa |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | params.update({"I": offset.x, "J": offset.y, "K": (p3.z - p1.z) / 2}) |
| | |
| | if hSpeed > 0: |
| | params.update({"F": hSpeed}) |
| | commands = [Path.Command(cmd, params)] |
| |
|
| | else: |
| | |
| | |
| | points = edge.discretize(Deflection=tol) |
| | if flip: |
| | points = points[::-1] |
| |
|
| | commands = [] |
| | if points: |
| | p0 = points[0] |
| | for p in points[1:]: |
| | params = {"X": p.x, "Y": p.y, "Z": p.z} |
| | if hSpeed > 0 and vSpeed > 0: |
| | params["F"] = speedBetweenPoints(p0, p, hSpeed, vSpeed) |
| | cmd = Path.Command("G1", params) |
| | |
| | commands.append(cmd) |
| | p0 = p |
| | |
| | return commands |
| |
|
| |
|
| | def edgeForCmd(cmd, startPoint): |
| | """edgeForCmd(cmd, startPoint). |
| | Returns an Edge representing the given command, assuming a given startPoint.""" |
| |
|
| | |
| | |
| |
|
| | endPoint = commandEndPoint(cmd, startPoint) |
| | if (cmd.Name in CmdMoveStraight) or (cmd.Name in CmdMoveRapid) or (cmd.Name in CmdMoveDrill): |
| | if pointsCoincide(startPoint, endPoint): |
| | return None |
| | return Part.Edge(Part.LineSegment(startPoint, endPoint)) |
| |
|
| | if cmd.Name in CmdMoveArc: |
| | center = startPoint + commandEndPoint(cmd, Vector(0, 0, 0), "I", "J", "K") |
| | A = xy(startPoint - center) |
| | B = xy(endPoint - center) |
| | d = -B.x * A.y + B.y * A.x |
| |
|
| | if isRoughly(d, 0, 0.005): |
| | |
| | |
| | |
| | |
| | angle = getAngle(A) + math.pi / 2 |
| | if cmd.Name in CmdMoveCW: |
| | angle -= math.pi |
| | else: |
| | C = A + B |
| | angle = getAngle(C) |
| | |
| | |
| | |
| | |
| |
|
| | R = A.Length |
| | |
| | |
| | |
| | |
| | |
| | |
| | if isRoughly(startPoint.z, endPoint.z): |
| | midPoint = center + Vector(math.cos(angle), math.sin(angle), 0) * R |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | if pointsCoincide(startPoint, endPoint, 0.001): |
| | return Part.makeCircle(R, center, FreeCAD.Vector(0, 0, 1)) |
| | else: |
| | return Part.Edge(Part.Arc(startPoint, midPoint, endPoint)) |
| |
|
| | |
| | |
| | if cmd.Name in CmdMoveCW: |
| | cw = True |
| | else: |
| | cw = False |
| | angle = diffAngle(getAngle(A), getAngle(B), "CW" if cw else "CCW") |
| | height = endPoint.z - startPoint.z |
| | pitch = height * math.fabs(2 * math.pi / angle) |
| | if angle > 0: |
| | cw = not cw |
| | |
| | helix = Part.makeHelix(pitch, height, R, 0, not cw) |
| | helix.rotate(Vector(), Vector(0, 0, 1), 180 * getAngle(A) / math.pi) |
| | e = helix.Edges[0] |
| | helix.translate(startPoint - e.valueAt(e.FirstParameter)) |
| | return helix.Edges[0] |
| | return None |
| |
|
| |
|
| | def wireForPath(path, startPoint=Vector(0, 0, 0)): |
| | """wireForPath(path, [startPoint=Vector(0,0,0)]) |
| | Returns a wire representing all move commands found in the given path.""" |
| | edges = [] |
| | rapid = [] |
| | rapid_indexes = set() |
| | if hasattr(path, "Commands"): |
| | for cmd in path.Commands: |
| | edge = edgeForCmd(cmd, startPoint) |
| | if edge: |
| | if cmd.Name in CmdMoveRapid: |
| | rapid.append(edge) |
| | rapid_indexes.add(len(edges)) |
| | edges.append(edge) |
| | startPoint = commandEndPoint(cmd, startPoint) |
| | if not edges: |
| | return (None, rapid, rapid_indexes) |
| | return (Part.Wire(edges), rapid, rapid_indexes) |
| |
|
| |
|
| | def wiresForPath(path, startPoint=Vector(0, 0, 0)): |
| | """wiresForPath(path, [startPoint=Vector(0,0,0)]) |
| | Returns a collection of wires, each representing a continuous cutting Path in path.""" |
| | wires = [] |
| | if hasattr(path, "Commands"): |
| | edges = [] |
| | for cmd in path.Commands: |
| | if cmd.Name in CmdMove: |
| | edges.append(edgeForCmd(cmd, startPoint)) |
| | startPoint = commandEndPoint(cmd, startPoint) |
| | elif cmd.Name in CmdMoveRapid: |
| | if len(edges) > 0: |
| | wires.append(Part.Wire(edges)) |
| | edges = [] |
| | startPoint = commandEndPoint(cmd, startPoint) |
| | if edges: |
| | wires.append(Part.Wire(edges)) |
| | return wires |
| |
|
| |
|
| | def arcToHelix(edge, z0, z1): |
| | """arcToHelix(edge, z0, z1) |
| | Assuming edge is an arc it'll return a helix matching the arc starting at z0 and rising/falling to z1. |
| | """ |
| |
|
| | p1 = edge.valueAt(edge.FirstParameter) |
| | |
| |
|
| | cmd = cmdsForEdge(edge)[0] |
| | params = cmd.Parameters |
| | params.update({"Z": z1, "K": (z1 - z0) / 2}) |
| | command = Path.Command(cmd.Name, params) |
| |
|
| | |
| | |
| |
|
| | return edgeForCmd(command, Vector(p1.x, p1.y, z0)) |
| |
|
| |
|
| | def helixToArc(edge, z=0): |
| | """helixToArc(edge, z=0) |
| | Returns the projection of the helix onto the XY-plane with a given offset.""" |
| | p1 = edge.valueAt(edge.FirstParameter) |
| | p2 = edge.valueAt((edge.FirstParameter + edge.LastParameter) / 2) |
| | p3 = edge.valueAt(edge.LastParameter) |
| | p01 = Vector(p1.x, p1.y, z) |
| | p02 = Vector(p2.x, p2.y, z) |
| | p03 = Vector(p3.x, p3.y, z) |
| | return Part.Edge(Part.Arc(p01, p02, p03)) |
| |
|
| |
|
| | def splitArcAt(edge, pt): |
| | """splitArcAt(edge, pt) |
| | Returns a list of 2 edges which together form the original arc split at the given point. |
| | The Vector pt has to represent a point on the given arc.""" |
| | p = edge.Curve.parameter(pt) |
| | e0 = Part.Arc(edge.Curve.copy(), edge.FirstParameter, p).toShape() |
| | e1 = Part.Arc(edge.Curve.copy(), p, edge.LastParameter).toShape() |
| | return [e0, e1] |
| |
|
| |
|
| | def splitEdgeAt(edge, pt): |
| | """splitEdgeAt(edge, pt) |
| | Returns a list of 2 edges, forming the original edge split at the given point. |
| | The results are undefined if the Vector representing the point is not part of the edge.""" |
| | |
| | |
| | p1 = edge.valueAt(edge.FirstParameter) |
| | p2 = pt |
| | p3 = edge.valueAt(edge.LastParameter) |
| | |
| |
|
| | if isinstance(edge.Curve, (Part.Line, Part.LineSegment)): |
| | |
| | return [ |
| | Part.Edge(Part.LineSegment(p1, p2)), |
| | Part.Edge(Part.LineSegment(p2, p3)), |
| | ] |
| | elif isinstance(edge.Curve, Part.Circle): |
| | |
| | return splitArcAt(edge, pt) |
| | else: |
| | |
| | arc = helixToArc(edge, 0) |
| | aes = splitArcAt(arc, Vector(pt.x, pt.y, 0)) |
| | return [arcToHelix(aes[0], p1.z, p2.z), arcToHelix(aes[1], p2.z, p3.z)] |
| |
|
| |
|
| | def combineConnectedShapes(shapes): |
| | done = False |
| | while not done: |
| | done = True |
| | combined = [] |
| | Path.Log.debug("shapes: {}".format(shapes)) |
| | for shape in shapes: |
| | connected = [f for f in combined if isRoughly(shape.distToShape(f)[0], 0.0)] |
| | Path.Log.debug( |
| | " {}: connected: {} dist: {}".format( |
| | len(combined), |
| | connected, |
| | [shape.distToShape(f)[0] for f in combined], |
| | ) |
| | ) |
| | if connected: |
| | combined = [f for f in combined if f not in connected] |
| | connected.append(shape) |
| | combined.append(Part.makeCompound(connected)) |
| | done = False |
| | else: |
| | combined.append(shape) |
| | shapes = combined |
| | return shapes |
| |
|
| |
|
| | def removeDuplicateEdges(wire): |
| | unique = [] |
| | for e in wire.Edges: |
| | if not any(edgesMatch(e, u) for u in unique): |
| | unique.append(e) |
| | return Part.Wire(unique) |
| |
|
| |
|
| | def flipEdge(edge): |
| | """flipEdge(edge) |
| | Flips given edge around so the new Vertexes[0] was the old Vertexes[-1] and vice versa, without changing the shape. |
| | Currently only lines, line segments, circles and arcs are supported.""" |
| |
|
| | if isinstance(edge.Curve, Part.Line) and not edge.Vertexes: |
| | return Part.Edge( |
| | Part.Line(edge.valueAt(edge.LastParameter), edge.valueAt(edge.FirstParameter)) |
| | ) |
| | elif isinstance(edge.Curve, (Part.Line, Part.LineSegment)): |
| | return Part.Edge(Part.LineSegment(edge.Vertexes[-1].Point, edge.Vertexes[0].Point)) |
| | elif isinstance(edge.Curve, Part.Circle): |
| | |
| | circle = Part.Circle(edge.Curve.Center, -edge.Curve.Axis, edge.Curve.Radius) |
| | |
| | circle.rotate( |
| | FreeCAD.Placement( |
| | circle.Center, |
| | circle.Axis, |
| | 180 - math.degrees(edge.LastParameter + edge.Curve.AngleXU), |
| | ) |
| | ) |
| | |
| | arc = Part.Edge(circle, 0, edge.LastParameter - edge.FirstParameter) |
| | return arc |
| | elif isinstance(edge.Curve, (Part.BSplineCurve, Part.BezierCurve)): |
| | if isinstance(edge.Curve, Part.BSplineCurve): |
| | spline = edge.Curve |
| | else: |
| | spline = edge.Curve.toBSpline() |
| |
|
| | mults = spline.getMultiplicities() |
| | weights = spline.getWeights() |
| | knots = spline.getKnots() |
| | poles = spline.getPoles() |
| | perio = spline.isPeriodic() |
| | ratio = spline.isRational() |
| | degree = spline.Degree |
| |
|
| | ma = max(knots) |
| | mi = min(knots) |
| | knots = [ma + mi - k for k in knots] |
| |
|
| | mults.reverse() |
| | weights.reverse() |
| | poles.reverse() |
| | knots.reverse() |
| |
|
| | flipped = Part.BSplineCurve() |
| | flipped.buildFromPolesMultsKnots(poles, mults, knots, perio, degree, weights, ratio) |
| |
|
| | return Part.Edge(flipped, ma + mi - edge.LastParameter, ma + mi - edge.FirstParameter) |
| | elif isinstance(edge.Curve, Part.OffsetCurve): |
| | return edge.reversed() |
| |
|
| | Path.Log.warning(translate("PathGeom", "%s not supported for flipping") % type(edge.Curve)) |
| |
|
| |
|
| | def flipWire(wire): |
| | """Flip the entire wire and all its edges so it is being processed the other way around.""" |
| | edges = [flipEdge(e) for e in wire.Edges] |
| | edges.reverse() |
| | Path.Log.debug(edges) |
| | return Part.Wire(edges) |
| |
|
| |
|
| | def makeBoundBoxFace(bBox, offset=0.0, zHeight=0.0): |
| | """makeBoundBoxFace(bBox, offset=0.0, zHeight=0.0)... |
| | Function to create boundbox face, with possible extra offset and custom Z-height.""" |
| | p1 = FreeCAD.Vector(bBox.XMin - offset, bBox.YMin - offset, zHeight) |
| | p2 = FreeCAD.Vector(bBox.XMax + offset, bBox.YMin - offset, zHeight) |
| | p3 = FreeCAD.Vector(bBox.XMax + offset, bBox.YMax + offset, zHeight) |
| | p4 = FreeCAD.Vector(bBox.XMin - offset, bBox.YMax + offset, zHeight) |
| |
|
| | L1 = Part.makeLine(p1, p2) |
| | L2 = Part.makeLine(p2, p3) |
| | L3 = Part.makeLine(p3, p4) |
| | L4 = Part.makeLine(p4, p1) |
| |
|
| | return Part.Face(Part.Wire([L1, L2, L3, L4])) |
| |
|
| |
|
| | |
| | def combineHorizontalFaces(faces): |
| | """combineHorizontalFaces(faces)... |
| | This function successfully identifies and combines multiple connected faces and |
| | works on multiple independent faces with multiple connected faces within the list. |
| | The return value is a list of simplified faces. |
| | The Adaptive op is not concerned with which hole edges belong to which face. |
| | |
| | Attempts to do the same shape connecting failed with TechDraw.findShapeOutline() and |
| | Path.Geom.combineConnectedShapes(), so this algorithm was created. |
| | """ |
| | horizontal = list() |
| | offset = 10.0 |
| | topFace = None |
| | innerFaces = list() |
| |
|
| | |
| | for f in faces: |
| | if f.BoundBox.ZMin != 0.0: |
| | f.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - f.BoundBox.ZMin)) |
| |
|
| | |
| | allFaces = Part.makeCompound(faces) |
| | if hasattr(allFaces, "Area") and isRoughly(allFaces.Area, 0.0): |
| | msg = translate( |
| | "PathGeom", |
| | "Zero working area to process. Check your selection and settings.", |
| | ) |
| | Path.Log.info(msg) |
| | return horizontal |
| |
|
| | afbb = allFaces.BoundBox |
| | bboxFace = makeBoundBoxFace(afbb, offset, -5.0) |
| | bboxSolid = bboxFace.extrude(FreeCAD.Vector(0.0, 0.0, 10.0)) |
| | extrudedFaces = list() |
| | for f in faces: |
| | extrudedFaces.append(f.extrude(FreeCAD.Vector(0.0, 0.0, 6.0))) |
| |
|
| | |
| | allFacesSolid = extrudedFaces.pop() |
| | for i in range(len(extrudedFaces)): |
| | temp = extrudedFaces.pop().fuse(allFacesSolid) |
| | allFacesSolid = temp |
| | cut = bboxSolid.cut(allFacesSolid) |
| |
|
| | |
| | |
| | |
| |
|
| | |
| | for f in cut.Faces: |
| | fbb = f.BoundBox |
| | if isRoughly(fbb.ZMin, 5.0) and isRoughly(fbb.ZMax, 5.0): |
| | if ( |
| | isRoughly(afbb.XMin - offset, fbb.XMin) |
| | and isRoughly(afbb.XMax + offset, fbb.XMax) |
| | and isRoughly(afbb.YMin - offset, fbb.YMin) |
| | and isRoughly(afbb.YMax + offset, fbb.YMax) |
| | ): |
| | topFace = f |
| | else: |
| | innerFaces.append(f) |
| |
|
| | if not topFace: |
| | return horizontal |
| |
|
| | outer = [Part.Face(w) for w in topFace.Wires[1:]] |
| |
|
| | if outer: |
| | for f in outer: |
| | f.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - f.BoundBox.ZMin)) |
| |
|
| | if innerFaces: |
| | |
| | inner = innerFaces |
| |
|
| | for f in inner: |
| | f.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - f.BoundBox.ZMin)) |
| | innerComp = Part.makeCompound(inner) |
| | outerComp = Part.makeCompound(outer) |
| | cut = outerComp.cut(innerComp) |
| | for f in cut.Faces: |
| | horizontal.append(f) |
| | else: |
| | horizontal = outer |
| |
|
| | return horizontal |
| |
|