From 78a30a76766d2ab9f57333079a8eb7251f2bc85b Mon Sep 17 00:00:00 2001 From: gumyr Date: Sat, 23 Mar 2024 15:17:40 -0400 Subject: [PATCH] Face.normal_at takes point or u,v Issue #410 --- src/build123d/topology.py | 58 +++++++++++++++++++++++++++++++++++++-- tests/test_direct_api.py | 13 +++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/build123d/topology.py b/src/build123d/topology.py index ca5b9c6d..6f3e1cdb 100644 --- a/src/build123d/topology.py +++ b/src/build123d/topology.py @@ -5337,7 +5337,37 @@ def offset(self, amount: float) -> Face: """Return a copy of self moved along the normal by amount""" return copy.deepcopy(self).moved(Location(self.normal_at() * amount)) + @overload def normal_at(self, surface_point: VectorLike = None) -> Vector: + """normal_at point on surface + + Args: + surface_point (VectorLike, optional): a point that lies on the surface where + the normal. Defaults to the center (None). + + Returns: + Vector: surface normal direction + """ + + @overload + def normal_at(self, u: float = None, v: float = None) -> Vector: + """normal_at u, v values on Face + + Args: + u (float, optional): the horizontal coordinate in the parameter space of the Face, + between 0.0 and 1.0 + v (float, optional): the vertical coordinate in the parameter space of the Face, + between 0.0 and 1.0 + Defaults to the center (None/None) + + Raises: + ValueError: Either neither or both u v values must be provided + + Returns: + Vector: surface normal direction + """ + + def normal_at(self, *args, **kwargs) -> Vector: """normal_at Computes the normal vector at the desired location on the face. @@ -5349,13 +5379,37 @@ def normal_at(self, surface_point: VectorLike = None) -> Vector: Returns: Vector: surface normal direction """ + surface_point, u, v = (None,) * 3 + + if args: + if isinstance(args[0], Iterable): + surface_point = args[0] + elif isinstance(args[0], (int, float)): + u = args[0] + if len(args) == 2 and isinstance(args[1], (int, float)): + v = args[1] + + unknown_args = ", ".join( + set(kwargs.keys()).difference(["surface_point", "u", "v"]) + ) + if unknown_args: + raise ValueError(f"Unexpected argument(s) {unknown_args}") + + surface_point = kwargs.get("surface_point", surface_point) + u = kwargs.get("u", u) + v = kwargs.get("v", v) + if surface_point is None and u is None and v is None: + u, v = 0.5, 0.5 + elif surface_point is None and sum(i is None for i in [u, v]) == 1: + raise ValueError("Both u & v values must be specified") + # get the geometry surface = self._geom_adaptor() if surface_point is None: u_val0, u_val1, v_val0, v_val1 = self._uv_bounds() - u_val = 0.5 * (u_val0 + u_val1) - v_val = 0.5 * (v_val0 + v_val1) + u_val = u * (u_val0 + u_val1) + v_val = v * (v_val0 + v_val1) else: # project point on surface projector = GeomAPI_ProjectPointOnSurf( diff --git a/tests/test_direct_api.py b/tests/test_direct_api.py index a391a6a5..b3129574 100644 --- a/tests/test_direct_api.py +++ b/tests/test_direct_api.py @@ -1376,6 +1376,19 @@ def test_constructor(self): with self.assertRaises(ValueError): Face(bob="fred") + def test_normal_at(self): + face = Face.make_rect(1, 1) + self.assertVectorAlmostEquals(face.normal_at(0, 0), (0, 0, 1), 5) + self.assertVectorAlmostEquals( + face.normal_at(face.position_at(0, 0)), (0, 0, 1), 5 + ) + with self.assertRaises(ValueError): + face.normal_at(0) + with self.assertRaises(ValueError): + face.normal_at(center=(0, 0)) + face = Cylinder(1, 1).faces().filter_by(GeomType.CYLINDER)[0] + self.assertVectorAlmostEquals(face.normal_at(0, 1), (1, 0, 0), 5) + class TestFunctions(unittest.TestCase): def test_edges_to_wires(self):