From dc90a4b15a291b2584bd4917f9bd71eef7889769 Mon Sep 17 00:00:00 2001 From: Alex Verschoot Date: Sun, 16 Nov 2025 15:48:30 +0100 Subject: [PATCH] Changed the FilletPolyLine to be compatible with 0-radius fillets, where it should behave like a normal Polyline --- src/build123d/objects_curve.py | 109 ++++++++++++++++++++++++--------- 1 file changed, 80 insertions(+), 29 deletions(-) diff --git a/src/build123d/objects_curve.py b/src/build123d/objects_curve.py index 71d788b..61731bd 100644 --- a/src/build123d/objects_curve.py +++ b/src/build123d/objects_curve.py @@ -787,20 +787,22 @@ class Helix(BaseEdgeObject): class FilletPolyline(BaseLineObject): """Line Object: Fillet Polyline - Create a sequence of straight lines defined by successive points that are filleted to a given radius. Args: pts (VectorLike | Iterable[VectorLike]): sequence of two or more points - radius (float | Iterable[float]): radius to fillet at each vertex or a single value for all vertices + radius (float | Iterable[float]): radius to fillet at each vertex or a single value for all vertices. + A radius of 0 will create a sharp corner (vertex without fillet). + close (bool, optional): close end points with extra Edge and corner fillets. Defaults to False + mode (Mode, optional): combination mode. Defaults to Mode.ADD Raises: ValueError: Two or more points not provided - ValueError: radius must be positive + ValueError: radius must be non-negative """ _applies_to = [BuildLine._tag] @@ -812,9 +814,9 @@ class FilletPolyline(BaseLineObject): close: bool = False, mode: Mode = Mode.ADD, ): + context: BuildLine | None = BuildLine._get_context(self) validate_inputs(context, self) - points = flatten_sequence(*pts) if len(points) < 2: @@ -822,30 +824,35 @@ class FilletPolyline(BaseLineObject): if isinstance(radius, (int, float)): radius_list = [radius] * len(points) # Single radius for all points + else: radius_list = list(radius) if len(radius_list) != len(points) - int(not close) * 2: raise ValueError( f"radius list length ({len(radius_list)}) must match angle count ({ len(points) - int(not close) * 2})" ) + for r in radius_list: - if r <= 0: - raise ValueError(f"radius {r} must be positive") + if r < 0: + raise ValueError(f"radius {r} must be non-negative") lines_pts = WorkplaneList.localize(*points) - # Create the polyline + new_edges = [ Edge.make_line(lines_pts[i], lines_pts[i + 1]) for i in range(len(lines_pts) - 1) ] + if close and (new_edges[0] @ 0 - new_edges[-1] @ 1).length > 1e-5: new_edges.append(Edge.make_line(new_edges[-1] @ 1, new_edges[0] @ 0)) + wire_of_lines = Wire(new_edges) # Create a list of vertices from wire_of_lines in the same order as # the original points so the resulting fillet edges are ordered ordered_vertices = [] + for pnts in lines_pts: distance = { v: (Vector(pnts) - Vector(*v)).length for v in wire_of_lines.vertices() @@ -853,46 +860,90 @@ class FilletPolyline(BaseLineObject): ordered_vertices.append(sorted(distance.items(), key=lambda x: x[1])[0][0]) # Fillet the corners - # Create a map of vertices to edges containing that vertex vertex_to_edges = { v: [e for e in wire_of_lines.edges() if v in e.vertices()] for v in ordered_vertices } - # For each corner vertex create a new fillet Edge + # For each corner vertex create a new fillet Edge (or keep as vertex if radius is 0) fillets = [] + for i, (vertex, edges) in enumerate(vertex_to_edges.items()): if len(edges) != 2: continue - other_vertices = {ve for e in edges for ve in e.vertices() if ve != vertex} - third_edge = Edge.make_line(*[v for v in other_vertices]) - fillet_face = Face(Wire(edges + [third_edge])).fillet_2d( - radius_list[i - int(not close)], [vertex] - ) - fillets.append(fillet_face.edges().filter_by(GeomType.CIRCLE)[0]) + current_radius = radius_list[i - int(not close)] + + if current_radius == 0: + # For 0 radius, store the vertex as a marker for a sharp corner + fillets.append(None) + + else: + other_vertices = {ve for e in edges for ve in e.vertices() if ve != vertex} + third_edge = Edge.make_line(*[v for v in other_vertices]) + fillet_face = Face(Wire(edges + [third_edge])).fillet_2d( + current_radius, [vertex] + ) + fillets.append(fillet_face.edges().filter_by(GeomType.CIRCLE)[0]) # Create the Edges that join the fillets if close: - interior_edges = [ - Edge.make_line(fillets[i - 1] @ 1, fillets[i] @ 0) - for i in range(len(fillets)) - ] - end_edges = [] - else: - interior_edges = [ - Edge.make_line(fillets[i] @ 1, f @ 0) for i, f in enumerate(fillets[1:]) - ] - end_edges = [ - Edge.make_line(wire_of_lines @ 0, fillets[0] @ 0), - Edge.make_line(fillets[-1] @ 1, wire_of_lines @ 1), - ] + interior_edges = [] - new_wire = Wire(end_edges + interior_edges + fillets) + for i in range(len(fillets)): + prev_idx = i - 1 + curr_idx = i + # Determine start and end points + if fillets[prev_idx] is None: + start_pt = ordered_vertices[prev_idx] + else: + start_pt = fillets[prev_idx] @ 1 + + if fillets[curr_idx] is None: + end_pt = ordered_vertices[curr_idx] + else: + end_pt = fillets[curr_idx] @ 0 + interior_edges.append(Edge.make_line(start_pt, end_pt)) + + end_edges = [] + + else: + interior_edges = [] + for i in range(len(fillets) - 1): + curr_idx = i + next_idx = i + 1 + # Determine start and end points + if fillets[curr_idx] is None: + start_pt = ordered_vertices[curr_idx + 1] # +1 because first vertex has no fillet + else: + start_pt = fillets[curr_idx] @ 1 + + if fillets[next_idx] is None: + end_pt = ordered_vertices[next_idx + 1] + else: + end_pt = fillets[next_idx] @ 0 + interior_edges.append(Edge.make_line(start_pt, end_pt)) + + # Handle end edges + if fillets[0] is None: + start_edge = Edge.make_line(wire_of_lines @ 0, ordered_vertices[1]) + else: + start_edge = Edge.make_line(wire_of_lines @ 0, fillets[0] @ 0) + + if fillets[-1] is None: + end_edge = Edge.make_line(ordered_vertices[-2], wire_of_lines @ 1) + else: + end_edge = Edge.make_line(fillets[-1] @ 1, wire_of_lines @ 1) + end_edges = [start_edge, end_edge] + + # Filter out None values from fillets (these are 0-radius corners) + actual_fillets = [f for f in fillets if f is not None] + new_wire = Wire(end_edges + interior_edges + actual_fillets) super().__init__(new_wire, mode=mode) + class JernArc(BaseEdgeObject): """Line Object: Jern Arc