Skip to content

Commit

Permalink
Merge pull request #397 from jdegenstein/loftvertices
Browse files Browse the repository at this point in the history
Add vertices support to loft
  • Loading branch information
gumyr authored Nov 30, 2023
2 parents 34db0aa + c168ba8 commit b9e6593
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 16 deletions.
36 changes: 30 additions & 6 deletions src/build123d/operations_part.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,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,
Expand All @@ -190,8 +190,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.
Expand All @@ -208,9 +209,32 @@ 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 any(isinstance(s, Vertex) for s in section_list[1:-1]):
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
Expand Down
31 changes: 21 additions & 10 deletions src/build123d/topology.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -5935,13 +5939,15 @@ def make_torus(
)

@classmethod
def make_loft(cls, wires: list[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.
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:
Expand All @@ -5950,13 +5956,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()

Expand Down Expand Up @@ -6274,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)]
Expand All @@ -6285,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)

Expand Down
44 changes: 44 additions & 0 deletions tests/test_build_part.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down

0 comments on commit b9e6593

Please sign in to comment.