From f20501ed0ccdd7a38038e2a4a1ee6ce005c4c86f Mon Sep 17 00:00:00 2001 From: jdegenstein Date: Sat, 25 Nov 2023 20:59:52 -0600 Subject: [PATCH 1/5] add vertex support to make_loft --- src/build123d/topology.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/build123d/topology.py b/src/build123d/topology.py index 94a240e5..74cab0a8 100644 --- a/src/build123d/topology.py +++ b/src/build123d/topology.py @@ -5935,13 +5935,13 @@ def make_torus( ) @classmethod - def make_loft(cls, wires: list[Wire], ruled: bool = False) -> Solid: + def make_loft(objs: list[Vertex, Wire], ruled: bool = False) -> Solid: """make loft - Makes a loft from a list of wires. + Makes a loft from a list of wires and vertices, where vertices can be the first, last, or first and last elements. Args: - wires (list[Wire]): section perimeters + objs (list[Vertex, Wire]): wire perimeters or vertices ruled (bool, optional): stepped or smooth. Defaults to False (smooth). Raises: @@ -5950,13 +5950,18 @@ def make_loft(cls, wires: list[Wire], ruled: bool = False) -> Solid: Returns: Solid: Lofted object """ + + if len(objs) < 2: + raise ValueError("More than one wire, or a wire and a vertex is required") + # the True flag requests building a solid instead of a shell. - if len(wires) < 2: - raise ValueError("More than one wire is required") loft_builder = BRepOffsetAPI_ThruSections(True, ruled) - for wire in wires: - loft_builder.AddWire(wire.wrapped) + for obj in objs: + if isinstance(obj, Vertex): + loft_builder.AddVertex(obj.wrapped) + elif isinstance(obj, Wire): + loft_builder.AddWire(obj.wrapped) loft_builder.Build() From b4b3b2b0e8e3cbe96ff43a634a0a6e7302f040db Mon Sep 17 00:00:00 2001 From: jdegenstein Date: Sat, 25 Nov 2023 21:02:33 -0600 Subject: [PATCH 2/5] add vertex, sketch support to loft() and input handling --- src/build123d/operations_part.py | 50 ++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/src/build123d/operations_part.py b/src/build123d/operations_part.py index 60400250..58adf686 100644 --- a/src/build123d/operations_part.py +++ b/src/build123d/operations_part.py @@ -175,7 +175,7 @@ def extrude( def loft( - sections: Union[Face, Iterable[Face]] = None, + sections: Union[Face, Sketch, Iterable[Union[Vertex, Face, Sketch]]] = None, ruled: bool = False, clean: bool = True, mode: Mode = Mode.ADD, @@ -185,8 +185,9 @@ def loft( Loft the pending sketches/faces, across all workplanes, into a solid. Args: - sections (Face): slices to loft into object. If not provided, pending_faces - will be used. + sections (Vertex, Face, Sketch): slices to loft into object. If not provided, pending_faces + will be used. If vertices are to be used, a vertex can be the first, last, or + first and last elements. ruled (bool, optional): discontiguous layer tangents. Defaults to False. clean (bool, optional): Remove extraneous internal structure. Defaults to True. mode (Mode, optional): combination mode. Defaults to Mode.ADD. @@ -205,9 +206,46 @@ def loft( context.pending_faces = [] context.pending_face_planes = [] else: - loft_wires = [ - face.outer_wire() for section in section_list for face in section.faces() - ] + if all(isinstance(s, (Face, Sketch)) for s in section_list): + loft_wires = [ + face.outer_wire() + for section in section_list + for face in section.faces() + ] + elif any(isinstance(s, Vertex) for s in section_list) and any( + isinstance(s, (Face, Sketch)) for s in section_list + ): + if len(section_list) == 2: + pass + elif isinstance(section_list[0], Vertex) and isinstance( + section_list[-1], Vertex + ): + pass + elif isinstance(section_list[0], Vertex) and isinstance( + section_list[-1], (Face, Sketch) + ): + pass + elif isinstance(section_list[0], (Face, Sketch)) and isinstance( + section_list[-1], Vertex + ): + pass + else: + raise ValueError( + "Vertices must be the first, last, or first and last elements" + ) + loft_wires = [] + for s in section_list: + if isinstance(s, Vertex): + loft_wires.append(s) + elif isinstance(s, Face): + loft_wires.append(s.outer_wire()) + elif isinstance(s, Sketch): + loft_wires.append(s.face().outer_wire()) + elif all(isinstance(s, Vertex) for s in section_list): + raise ValueError( + "At least one face/sketch is required if vertices are the first, last, or first and last elements" + ) + new_solid = Solid.make_loft(loft_wires, ruled) # Try to recover an invalid loft From b947152216a5c76313863e16f5bbff7d4f6b6ff5 Mon Sep 17 00:00:00 2001 From: jdegenstein Date: Sat, 25 Nov 2023 21:23:15 -0600 Subject: [PATCH 3/5] make_loft accept iterable of vertex/wire --- src/build123d/topology.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/build123d/topology.py b/src/build123d/topology.py index 74cab0a8..b4b36b46 100644 --- a/src/build123d/topology.py +++ b/src/build123d/topology.py @@ -5935,7 +5935,7 @@ def make_torus( ) @classmethod - def make_loft(objs: list[Vertex, Wire], ruled: bool = False) -> Solid: + def make_loft(objs: Iterable[Union[Vertex, Wire]], ruled: bool = False) -> Solid: """make loft Makes a loft from a list of wires and vertices, where vertices can be the first, last, or first and last elements. From 594deefb70bfbb4df5b9076e78dbcce1390372ee Mon Sep 17 00:00:00 2001 From: jdegenstein Date: Sun, 26 Nov 2023 20:50:06 -0600 Subject: [PATCH 4/5] fix missing cls in make_loft --- src/build123d/topology.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/build123d/topology.py b/src/build123d/topology.py index b4b36b46..476a5862 100644 --- a/src/build123d/topology.py +++ b/src/build123d/topology.py @@ -1390,6 +1390,7 @@ class Shape(NodeMixin): topo_parent (Shape): assembly parent of this object """ + # pylint: disable=too-many-instance-attributes, too-many-public-methods _dim = None @@ -1763,7 +1764,7 @@ def clean(self) -> Self: try: upgrader.Build() self.wrapped = downcast(upgrader.Shape()) - except: # pylint: disable=bare-except + except: # pylint: disable=bare-except warnings.warn(f"Unable to clean {self}") return self @@ -3256,6 +3257,7 @@ def __call__(self, shape: Shape) -> bool: class ShapeList(list[T]): """Subclass of list with custom filter and sort methods appropriate to CAD""" + # pylint: disable=too-many-public-methods @property @@ -4244,6 +4246,7 @@ def wires(self) -> list[Wire]: class Edge(Mixin1D, Shape): """A trimmed curve that represents the border of a face""" + # pylint: disable=too-many-public-methods _dim = 1 @@ -4999,6 +5002,7 @@ def to_axis(self) -> Axis: class Face(Shape): """a bounded surface that represents part of the boundary of a solid""" + # pylint: disable=too-many-public-methods _dim = 2 @@ -5935,7 +5939,9 @@ def make_torus( ) @classmethod - def make_loft(objs: Iterable[Union[Vertex, Wire]], ruled: bool = False) -> Solid: + def make_loft( + cls, objs: Iterable[Union[Vertex, Wire]], ruled: bool = False + ) -> Solid: """make loft Makes a loft from a list of wires and vertices, where vertices can be the first, last, or first and last elements. @@ -6279,7 +6285,7 @@ def extrude_until( .solids() .sort_by(direction_axis)[0] ) - except: # pylint: disable=bare-except + except: # pylint: disable=bare-except warnings.warn("clipping error - extrusion may be incorrect") else: extrusion_parts = [extrusion.intersect(target_object)] @@ -6290,7 +6296,7 @@ def extrude_until( .solids() .sort_by(direction_axis)[0] ) - except: # pylint: disable=bare-except + except: # pylint: disable=bare-except warnings.warn("clipping error - extrusion may be incorrect") extrusion = Shape.fuse(*extrusion_parts) From b7a68a87c334394e26b609edcbae8efc79a9f729 Mon Sep 17 00:00:00 2001 From: jdegenstein Date: Mon, 27 Nov 2023 10:07:24 -0600 Subject: [PATCH 5/5] add tests, simplify and improve loft logic --- src/build123d/operations_part.py | 18 ++----------- tests/test_build_part.py | 44 ++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 16 deletions(-) diff --git a/src/build123d/operations_part.py b/src/build123d/operations_part.py index 58adf686..3fd9fe75 100644 --- a/src/build123d/operations_part.py +++ b/src/build123d/operations_part.py @@ -50,7 +50,7 @@ def extrude( to_extrude: Union[Face, Sketch] = None, amount: float = None, - dir: VectorLike = None, # pylint: disable=redefined-builtin + dir: VectorLike = None, # pylint: disable=redefined-builtin until: Until = None, target: Union[Compound, Solid] = None, both: bool = False, @@ -215,21 +215,7 @@ def loft( elif any(isinstance(s, Vertex) for s in section_list) and any( isinstance(s, (Face, Sketch)) for s in section_list ): - if len(section_list) == 2: - pass - elif isinstance(section_list[0], Vertex) and isinstance( - section_list[-1], Vertex - ): - pass - elif isinstance(section_list[0], Vertex) and isinstance( - section_list[-1], (Face, Sketch) - ): - pass - elif isinstance(section_list[0], (Face, Sketch)) and isinstance( - section_list[-1], Vertex - ): - pass - else: + if any(isinstance(s, Vertex) for s in section_list[1:-1]): raise ValueError( "Vertices must be the first, last, or first and last elements" ) diff --git a/tests/test_build_part.py b/tests/test_build_part.py index 6ad2d8f1..71e38b52 100644 --- a/tests/test_build_part.py +++ b/tests/test_build_part.py @@ -325,6 +325,50 @@ def test_simple_loft(self): self.assertLess(test.part.volume, 225 * pi * 30, 5) self.assertGreater(test.part.volume, 25 * pi * 30, 5) + def test_loft_vertex(self): + with BuildPart() as test: + v1 = Vertex(0, 0, 3) + with BuildSketch() as s: + Rectangle(1, 1) + loft(sections=[s.sketch, v1], ruled=True) + self.assertAlmostEqual(test.part.volume, 1, 5) + + def test_loft_vertices(self): + with BuildPart() as test: + v1 = Vertex(0, 0, 3) + v2 = Vertex(0, 0, -3) + with BuildSketch() as s: + Rectangle(1, 1) + loft(sections=[v2, s.sketch, v1], ruled=True) + self.assertAlmostEqual(test.part.volume, 2, 5) + + def test_loft_vertex_face(self): + v1 = Vertex(0, 0, 3) + r = Rectangle(1, 1) + test = loft(sections=[r.face(), v1], ruled=True) + self.assertAlmostEqual(test.volume, 1, 5) + + def test_loft_no_sections_assert(self): + with BuildPart() as test: + with self.assertRaises(ValueError): + loft(sections=[None]) + + def test_loft_all_vertices_assert(self): + with BuildPart() as test: + v1 = Vertex(0, 0, -1) + v2 = Vertex(0, 0, 2) + with self.assertRaises(ValueError): + loft(sections=[v1, v2]) + + def test_loft_vertex_middle_assert(self): + with BuildPart() as test: + v1 = Vertex(0, 0, -1) + v2 = Vertex(0, 0, 2) + with BuildSketch() as s: + Circle(1) + with self.assertRaises(ValueError): + loft(sections=[v1, v2, s.sketch]) + class TestRevolve(unittest.TestCase): def test_simple_revolve(self):