diff --git a/src/build123d/topology.py b/src/build123d/topology.py index 2888748a..c57c3f81 100644 --- a/src/build123d/topology.py +++ b/src/build123d/topology.py @@ -7903,7 +7903,7 @@ def param_at_point(self, point: VectorLike) -> float: return distance / wire_length - def trim(self, start: float, end: float) -> Wire: + def trim(self: Wire, start: float, end: float) -> Wire: """trim Create a new wire by keeping only the section between start and end. @@ -7922,72 +7922,59 @@ def trim(self, start: float, end: float) -> Wire: if start >= end: raise ValueError("start must be less than end") - trim_start_point = self.position_at(start) - trim_end_point = self.position_at(end) + edges = self.edges() # If this is really just an edge, skip the complexity of a Wire - if len(self.edges()) == 1: - return Wire([self.edge().trim(start, end)]) - - # Get all the edges - modified_edges: list[Edge] = [] - unmodified_edges: list[Edge] = [] - for edge in self.edges(): - # Is edge flipped - flipped = self.param_at_point(edge.position_at(0)) > self.param_at_point( - edge.position_at(1) - ) - # Does this edge contain the start/end points - contains_start = edge.distance_to(trim_start_point) <= TOLERANCE - contains_end = edge.distance_to(trim_end_point) <= TOLERANCE - - # Trim edges containing start or end points - degenerate = False - if contains_start and contains_end: - u_start = edge.param_at_point(trim_start_point) - u_end = edge.param_at_point(trim_end_point) - edge = edge.trim(u_start, u_end) - elif contains_start: - u_value = edge.param_at_point(trim_start_point) - if not flipped: - degenerate = u_value == 1.0 - if not degenerate: - edge = edge.trim(u_value, 1.0) - elif flipped: - degenerate = u_value == 0.0 - if not degenerate: - edge = edge.trim(0.0, u_value) - elif contains_end: - u_value = edge.param_at_point(trim_end_point) - if not flipped: - degenerate = u_value == 0.0 - if not degenerate: - edge = edge.trim(0.0, u_value) - elif flipped: - degenerate = u_value == 1.0 - if not degenerate: - edge = edge.trim(u_value, 1.0) - if not degenerate: - if contains_start or contains_end: - modified_edges.append(edge) - else: - unmodified_edges.append(edge) + if len(edges) == 1: + return Wire([edges[0].trim(start, end)]) + + # For each Edge determine the beginning and end wire parameters + # Note that u, v values are parameters along the Wire + edges_uv_values: list[tuple[float, float, Edge]] = [] + found_end_of_wire = False # for finding ends of closed wires + + for e in edges: + u = self.param_at_point(e.position_at(0)) + v = self.param_at_point(e.position_at(1)) + if self.is_closed: # Avoid two beginnings or ends + u = 1 - u if found_end_of_wire and (u == 0 or u == 1) else u + v = 1 - v if found_end_of_wire and (v == 0 or v == 1) else v + found_end_of_wire = ( + u == 0 or u == 1 or v == 0 or v == 1 or found_end_of_wire + ) - # Select the wire containing the start and end points - wire_segments = edges_to_wires(modified_edges + unmodified_edges) - trimmed_wire = filter( - lambda w: all( - [ - w.distance_to(p) <= TOLERANCE - for p in [trim_start_point, trim_end_point] - ] - ), - wire_segments, - ) - try: - return next(trimmed_wire) - except StopIteration as exc: - raise RuntimeError("Invalid trim result") from exc + # Edge might be reversed and require flipping parms + u, v = (v, u) if u > v else (u, v) + + edges_uv_values.append((u, v, e)) + + new_edges = [] + for u, v, e in edges_uv_values: + if v < start or u > end: # Edge not needed + continue + + elif start <= u and v <= end: # keep whole Edge + new_edges.append(e) + + elif start >= u and end <= v: # Wire trimmed to single Edge + u_edge = e.param_at_point(self.position_at(start)) + v_edge = e.param_at_point(self.position_at(end)) + u_edge, v_edge = ( + (v_edge, u_edge) if u_edge > v_edge else (u_edge, v_edge) + ) + new_edges.append(e.trim(u_edge, v_edge)) + + elif start <= u: # keep start of Edge + u_edge = e.param_at_point(self.position_at(end)) + if u_edge != 0: + new_edges.append(e.trim(0, u_edge)) + + else: # v <= end keep end of Edge + v_edge = e.param_at_point(self.position_at(start)) + if v_edge != 1: + new_edges.append(e.trim(v_edge, 1)) + + return Wire(new_edges) def order_edges(self) -> ShapeList[Edge]: """Return the edges in self ordered by wire direction and orientation""" diff --git a/tests/test_direct_api.py b/tests/test_direct_api.py index 9b80172c..95fd7c51 100644 --- a/tests/test_direct_api.py +++ b/tests/test_direct_api.py @@ -3990,6 +3990,20 @@ def test_trim(self): self.assertVectorAlmostEquals(spline @ 0.5, half @ 0, 4) self.assertVectorAlmostEquals(spline @ 1, half @ 1, 4) + w = Rectangle(3, 1).wire() + t5 = w.trim(0, 0.5) + self.assertAlmostEqual(t5.length, 4, 5) + t6 = w.trim(0.5, 1) + self.assertAlmostEqual(t6.length, 4, 5) + + p = RegularPolygon(10, 20).wire() + t7 = p.trim(0.1, 0.2) + self.assertAlmostEqual(p.length * 0.1, t7.length, 5) + + c = Circle(10).wire() + t8 = c.trim(0.4, 0.9) + self.assertAlmostEqual(c.length * 0.5, t8.length, 5) + def test_param_at_point(self): e = Edge.make_three_point_arc((0, -20), (5, 0), (0, 20)) # Three edges are created 0->0.5->0.75->1.0