diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 56e0cf70b..ee09abda0 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -265,6 +265,8 @@ from OCP.BRepAlgo import BRepAlgo +from OCP.ChFi2d import ChFi2d_FilletAPI # For Wire.Fillet() + from math import pi, sqrt, inf, radians, cos import warnings @@ -2379,6 +2381,90 @@ def chamfer2D(self, d: float, vertices: Iterable[Vertex]) -> "Wire": return f.chamfer2D(d, vertices).outerWire() + def fillet( + self, radius: float, vertices: Optional[Iterable[Vertex]] = None + ) -> "Wire": + """ + Apply 2D or 3D fillet to a wire + :param wire: The input wire to fillet. Currently only open wires are supported + :param radius: the radius of the fillet, must be > zero + :param vertices: Optional list of vertices to fillet. By default all vertices are fillet. + :return: A wire with filleted corners + """ + + edges = list(self) + all_vertices = self.Vertices() + newEdges = [] + currentEdge = edges[0] + + verticesSet = set(vertices) if vertices else set() + + for i in range(len(edges) - 1): + nextEdge = edges[i + 1] + + # Create a plane that is spanned by currentEdge and nextEdge + currentDir = currentEdge.tangentAt(1) + nextDir = nextEdge.tangentAt(0) + normalDir = currentDir.cross(nextDir) + + # Check conditions for skipping fillet: + # 1. The edges are parallel + # 2. The vertex is not in the vertices white list + if normalDir.Length == 0 or ( + all_vertices[i + 1] not in verticesSet and bool(verticesSet) + ): + newEdges.append(currentEdge) + currentEdge = nextEdge + continue + + # Prepare for using ChFi2d_FilletAPI + pointInPlane = currentEdge.Center().toPnt() + cornerPlane = gp_Pln(pointInPlane, normalDir.toDir()) + + filletMaker = ChFi2d_FilletAPI( + currentEdge.wrapped, nextEdge.wrapped, cornerPlane + ) + + ok = filletMaker.Perform(radius) + if not ok: + raise ValueError(f"Failed fillet at vertex {i+1}!") + + # Get the result of the fillet operation + thePoint = next(iter(nextEdge)).Center().toPnt() + res_arc = filletMaker.Result( + thePoint, currentEdge.wrapped, nextEdge.wrapped + ) + + newEdges.append(currentEdge) + newEdges.append(Edge(res_arc)) + + currentEdge = nextEdge + + # Add the last edge + newEdges.append(currentEdge) + + return Wire.assembleEdges(newEdges) + + def Vertices(self) -> List[Vertex]: + """ + Ordered list of vertices of the wire. + """ + + rv = [] + + exp = BRepTools_WireExplorer(self.wrapped) + rv.append(Vertex(exp.CurrentVertex())) + + while exp.More(): + exp.Next() + rv.append(Vertex(exp.CurrentVertex())) + + # handle closed wires correclty + if self.IsClosed(): + rv = rv[:-1] + + return rv + def __iter__(self) -> Iterator[Edge]: """ Iterate over edges in an ordered way. diff --git a/tests/test_cad_objects.py b/tests/test_cad_objects.py index f1ab63c7a..3ced2314b 100644 --- a/tests/test_cad_objects.py +++ b/tests/test_cad_objects.py @@ -742,6 +742,30 @@ def testEdgeWrapperRadius(self): ) self.assertAlmostEqual(many_rad.radius(), 1.0) + def testWireFillet(self): + points = [ + (0.000, 0.000, 0.000), + (-0.287, 1.183, -0.592), + (-1.404, 4.113, -2.787), + (-1.332, 1.522, 0.553), + (7.062, 0.433, -0.097), + (8.539, -0.000, -0.000), + ] + wire = Wire.makePolygon(points, close=False) + + # Fillet the wire + wfillet = wire.fillet(radius=0.560) + assert len(wfillet.Edges()) == 2 * len(points) - 3 + + # Fillet a single vertex + wfillet = wire.fillet(radius=0.560, vertices=wire.Vertices()[1:2]) + assert len(wfillet.Edges()) == len(points) + + # Assert exception if trying to fillet with too big + # a radius + with self.assertRaises(ValueError): + wfillet = wire.fillet(radius=1.0) + @pytest.mark.parametrize( "points, close, expected_edges",