Adding method Face.wrap_faces

This commit is contained in:
gumyr 2025-05-19 12:55:47 -04:00
parent 2efd21ff58
commit ccdfda88e9
2 changed files with 82 additions and 0 deletions

View file

@ -1754,6 +1754,71 @@ class Face(Mixin2D, Shape[TopoDS_Face]):
f"{type(planar_shape)}"
)
def wrap_faces(
self,
faces: Iterable[Face],
path: Wire | Edge,
start: float = 0.0,
) -> ShapeList[Face]:
"""wrap_faces
Wrap a sequence of 2D faces onto a 3D surface, aligned along a guiding path.
This method places multiple planar `Face` objects (defined in the XY plane) onto a
curved 3D surface (`self`), following a given path (Wire or Edge) that lies on or
closely follows the surface. Each face is spaced along the path according to its
original horizontal (X-axis) position, preserving the relative layout of the input
faces.
The wrapping process attempts to maintain the shape and size of each face while
minimizing distortion. Each face is repositioned to the origin, then individually
wrapped onto the surface starting at a specific point along the path. The face's
new orientation is defined using the path's tangent direction and the surface normal
at that point.
This is particularly useful for placing a series of featuressuch as embossed logos,
engraved labels, or patterned tilesonto a freeform or cylindrical surface, aligned
along a reference edge or curve.
Args:
faces (Iterable[Face]): An iterable of 2D planar faces to be wrapped.
path (Wire | Edge): A curve on the target surface that defines the alignment
direction. The X-position of each face is mapped to a relative position
along this path.
start (float, optional): The relative starting point on the path (between 0.0
and 1.0) where the first face should be placed. Defaults to 0.0.
Returns:
ShapeList[Face]: A list of wrapped face objects, aligned and conformed to the
surface.
"""
path_length = path.length
face_list = list(faces)
first_face_min_x = face_list[0].bounding_box().min.X
# Position each face at the origin and wrap onto surface
wrapped_faces: ShapeList[Face] = ShapeList()
for face in face_list:
bbox = face.bounding_box()
face_center_x = (bbox.min.X + bbox.max.X) / 2
delta_x = face_center_x - first_face_min_x
relative_position_on_wire = start + delta_x / path_length
path_position = path.position_at(relative_position_on_wire)
surface_location = Location(
Plane(
path_position,
x_dir=path.tangent_at(relative_position_on_wire),
z_dir=self.normal_at(path_position),
)
)
assert isinstance(face.position, Vector)
face.position -= (delta_x, 0, 0) # Shift back to origin
wrapped_face = Face.wrap(self, face, surface_location)
wrapped_faces.append(wrapped_face)
return wrapped_faces
def _uv_bounds(self) -> tuple[float, float, float, float]:
"""Return the u min, u max, v min, v max values"""
return BRepTools.UVBounds_s(self.wrapped)

View file

@ -50,6 +50,7 @@ from build123d.objects_sketch import (
Polygon,
Rectangle,
RegularPolygon,
Text,
Triangle,
)
from build123d.operations_generic import fillet, offset
@ -888,6 +889,22 @@ class TestFace(unittest.TestCase):
with self.assertRaises(RuntimeError):
surface.wrap(star, target)
def test_wrap_faces(self):
sphere = Solid.make_sphere(50, angle1=-90).face()
surface = sphere.face()
path: Edge = (
sphere.cut(
Solid.make_cylinder(80, 100, Plane.YZ).locate(Location((-50, 0, -70)))
)
.edges()
.sort_by(Axis.Z)[0]
.reversed()
)
text = Text(txt="ei", font_size=15, align=(Align.MIN, Align.CENTER))
wrapped_faces = surface.wrap_faces(text.faces(), path, 0.2)
self.assertEqual(len(wrapped_faces), 3)
self.assertTrue(all(not f.is_planar_face for f in wrapped_faces))
def test_revolve(self):
l1 = Edge.make_line((3, 0), (3, 2))
revolved = Face.revolve(l1, 360, Axis.Y)