From 453f676882f612d0b81a0b870128b5e02af74721 Mon Sep 17 00:00:00 2001 From: gumyr Date: Mon, 20 Oct 2025 18:50:14 -0400 Subject: [PATCH] Adding points to trim --- src/build123d/topology/one_d.py | 77 ++++++++++++++++++++++-------- tests/test_direct_api/test_edge.py | 25 ++++++++-- tests/test_direct_api/test_wire.py | 6 ++- 3 files changed, 83 insertions(+), 25 deletions(-) diff --git a/src/build123d/topology/one_d.py b/src/build123d/topology/one_d.py index 744a9ed..25b817a 100644 --- a/src/build123d/topology/one_d.py +++ b/src/build123d/topology/one_d.py @@ -358,6 +358,21 @@ class Mixin1D(Shape): """Unused - only here because Mixin1D is a subclass of Shape""" return NotImplemented + # ---- Static Methods ---- + + @staticmethod + def _to_param(edge_wire: Mixin1D, value: float | VectorLike, name: str) -> float: + """Convert a float or VectorLike into a curve parameter.""" + if isinstance(value, (int, float)): + return float(value) + try: + point = Vector(value) + except TypeError as exc: + raise TypeError( + f"{name} must be a float or VectorLike, not {value!r}" + ) from exc + return edge_wire.param_at_point(point) + # ---- Instance Methods ---- def __add__( @@ -2972,24 +2987,43 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]): ) return Wire([self]) - def trim(self, start: float, end: float) -> Edge: + def trim(self, start: float | VectorLike, end: float | VectorLike) -> Edge: + """_summary_ + + Args: + start (float | VectorLike): _description_ + end (float | VectorLike): _description_ + + Raises: + TypeError: _description_ + ValueError: _description_ + + Returns: + Edge: _description_ + """ """trim Create a new edge by keeping only the section between start and end. Args: - start (float): 0.0 <= start < 1.0 - end (float): 0.0 < end <= 1.0 + start (float | VectorLike): 0.0 <= start < 1.0 or point on edge + end (float | VectorLike): 0.0 < end <= 1.0 or point on edge Raises: - ValueError: start >= end + TypeError: invalid input, must be float or VectorLike ValueError: can't trim empty edge Returns: Edge: trimmed edge """ - if start >= end: - raise ValueError(f"start ({start}) must be less than end ({end})") + + start_u = Mixin1D._to_param(self, start, "start") + end_u = Mixin1D._to_param(self, end, "end") + + start_u, end_u = sorted([start_u, end_u]) + + # if start_u >= end_u: + # raise ValueError(f"start ({start_u}) must be less than end ({end_u})") if self.wrapped is None: raise ValueError("Can't trim empty edge") @@ -3000,8 +3034,8 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]): new_curve = BRep_Tool.Curve_s( self_copy.wrapped, self.param_at(0), self.param_at(1) ) - parm_start = self.param_at(start) - parm_end = self.param_at(end) + parm_start = self.param_at(start_u) + parm_end = self.param_at(end_u) trimmed_curve = Geom_TrimmedCurve( new_curve, parm_start, @@ -3010,14 +3044,14 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]): new_edge = BRepBuilderAPI_MakeEdge(trimmed_curve).Edge() return Edge(new_edge) - def trim_to_length(self, start: float, length: float) -> Edge: + def trim_to_length(self, start: float | VectorLike, length: float) -> Edge: """trim_to_length Create a new edge starting at the given normalized parameter of a given length. Args: - start (float): 0.0 <= start < 1.0 + start (float | VectorLike): 0.0 <= start < 1.0 or point on edge length (float): target length Raise: @@ -3029,6 +3063,8 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]): if self.wrapped is None: raise ValueError("Can't trim empty edge") + start_u = Mixin1D._to_param(self, start, "start") + self_copy = copy.deepcopy(self) assert self_copy.wrapped is not None @@ -3040,7 +3076,7 @@ class Edge(Mixin1D, Shape[TopoDS_Edge]): adaptor_curve = GeomAdaptor_Curve(new_curve) # Find the parameter corresponding to the desired length - parm_start = self.param_at(start) + parm_start = self.param_at(start_u) abscissa_point = GCPnts_AbscissaPoint(adaptor_curve, length, parm_start) # Get the parameter at the desired length @@ -3550,7 +3586,6 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]): return Wire.make_polygon(corners_world, close=True) # ---- Static Methods ---- - @staticmethod def order_chamfer_edges( reference_edge: Edge | None, edges: tuple[Edge, Edge] @@ -4066,29 +4101,31 @@ class Wire(Mixin1D, Shape[TopoDS_Wire]): ) return self - def trim(self: Wire, start: float, end: float) -> Wire: + def trim(self: Wire, start: float | VectorLike, end: float | VectorLike) -> Wire: """Trim a wire between [start, end] normalized over total length. Args: - start (float): normalized start position (0.0 to <1.0) - end (float): normalized end position (>0.0 to 1.0) + start (float | VectorLike): normalized start position (0.0 to <1.0) or point + end (float | VectorLike): normalized end position (>0.0 to 1.0) or point Returns: Wire: trimmed Wire """ - if start >= end: - raise ValueError("start must be less than end") + start_u = Mixin1D._to_param(self, start, "start") + end_u = Mixin1D._to_param(self, end, "end") + + start_u, end_u = sorted([start_u, end_u]) # Extract the edges in order ordered_edges = self.edges().sort_by(self) # If this is really just an edge, skip the complexity of a Wire if len(ordered_edges) == 1: - return Wire([ordered_edges[0].trim(start, end)]) + return Wire([ordered_edges[0].trim(start_u, end_u)]) total_length = self.length - start_len = start * total_length - end_len = end * total_length + start_len = start_u * total_length + end_len = end_u * total_length trimmed_edges = [] cur_length = 0.0 diff --git a/tests/test_direct_api/test_edge.py b/tests/test_direct_api/test_edge.py index fb60a7d..6f06f68 100644 --- a/tests/test_direct_api/test_edge.py +++ b/tests/test_direct_api/test_edge.py @@ -37,7 +37,7 @@ from build123d.geometry import Axis, Plane, Vector from build123d.objects_curve import CenterArc, EllipticalCenterArc from build123d.objects_sketch import Circle, Rectangle, RegularPolygon from build123d.operations_generic import sweep -from build123d.topology import Edge, Face, Wire +from build123d.topology import Edge, Face, Wire, Vertex from OCP.GeomProjLib import GeomProjLib @@ -183,8 +183,23 @@ class TestEdge(unittest.TestCase): line = Edge.make_line((-2, 0), (2, 0)) self.assertAlmostEqual(line.trim(0.25, 0.75).position_at(0), (-1, 0, 0), 5) self.assertAlmostEqual(line.trim(0.25, 0.75).position_at(1), (1, 0, 0), 5) - with self.assertRaises(ValueError): - line.trim(0.75, 0.25) + + l1 = CenterArc((0, 0), 1, 0, 180) + l2 = l1.trim(0, l1 @ 0.5) + self.assertAlmostEqual(l2 @ 0, (1, 0, 0), 5) + self.assertAlmostEqual(l2 @ 1, (0, 1, 0), 5) + + l3 = l1.trim((1, 0), (0, 1)) + self.assertAlmostEqual(l3 @ 0, (1, 0, 0), 5) + self.assertAlmostEqual(l3 @ 1, (0, 1, 0), 5) + + l4 = l1.trim(0.5, (-1, 0)) + self.assertAlmostEqual(l4 @ 0, (0, 1, 0), 5) + self.assertAlmostEqual(l4 @ 1, (-1, 0, 0), 5) + + l5 = l1.trim(0.5, Vertex(-1, 0)) + self.assertAlmostEqual(l5 @ 0, (0, 1, 0), 5) + self.assertAlmostEqual(l5 @ 1, (-1, 0, 0), 5) line.wrapped = None with self.assertRaises(ValueError): @@ -213,6 +228,10 @@ class TestEdge(unittest.TestCase): e4_trim = Edge(a4).trim_to_length(0.5, 2) self.assertAlmostEqual(e4_trim.length, 2, 5) + e5 = e1.trim_to_length((5, 5), 1) + self.assertAlmostEqual(e5 @ 0, (5, 5), 5) + self.assertAlmostEqual(e5.length, 1, 5) + e1.wrapped = None with self.assertRaises(ValueError): e1.trim_to_length(0.1, 2) diff --git a/tests/test_direct_api/test_wire.py b/tests/test_direct_api/test_wire.py index cbb9449..bbfb6fc 100644 --- a/tests/test_direct_api/test_wire.py +++ b/tests/test_direct_api/test_wire.py @@ -155,8 +155,10 @@ class TestWire(unittest.TestCase): t4 = o.trim(0.5, 0.75) self.assertAlmostEqual(t4.length, o.length * 0.25, 5) - with self.assertRaises(ValueError): - o.trim(0.75, 0.25) + w0 = Polyline((0, 0), (0, 1), (1, 1), (1, 0)) + w2 = w0.trim(0, (0.5, 1)) + self.assertAlmostEqual(w2 @ 1, (0.5, 1), 5) + spline = Spline( (0, 0, 0), (0, 10, 0),