diff --git a/AUTHORS.md b/AUTHORS.md index b148a985b5b1..814201bb0a62 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -43,3 +43,4 @@ - Panayiotis Papacharalambous <> [@papachap](https://github.com/papachap) - Oliver Bucklin <> [@obucklin](https://github.com/obucklin) - Dominik Reisach <> [@dominikreisach](https://github.com/dominikreisach) +- Eric Gozzi <> [@ericgozzi](https://github.com/ericgozzi) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99790add62cc..9cdadfdc9edb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added support for `.stp` file extension in addition to `.step` for `RhinoBrep.from_step()` and `RhinoBrep.to_step()` methods. * Added `volume()` method to `compas.datastructures.Mesh` for computing the volume of closed meshes using signed volume of triangles. * Added functions `warning`, `message`, `error` and `remark` to `compas_ghpython`. +* Added method `RhinoBrep.closest_point()`. +* Added attributes `RhinoBrepEdge.domain` and `RhinoBrepEdge.index` and methods `RhinoBrepEdge.closest_point()` and `RhinoBrepEdge.point_at`. +* Added method `RhinoBrepFace.point_at()`, `RhinoBrepFace.closest_point()`, `RhinoBrepFace.is_point_on_face()` and `RhinoBrepFace.is_point_on_boundary()`. +* Added method `RhinoBrepLoop.to_curve()`. +* Added attribute `RhinoBrepTrim.edge`. +* Added attribute `RhinoBrepVertex.index`. +* Added method `RhinoCurve.to_polyline()`. ### Changed diff --git a/src/compas/geometry/shapes/torus.py b/src/compas/geometry/shapes/torus.py index b2f7ac3908d4..af8388713d4e 100644 --- a/src/compas/geometry/shapes/torus.py +++ b/src/compas/geometry/shapes/torus.py @@ -294,4 +294,3 @@ def scale(self, factor): """ self.radius_axis *= factor self.radius_pipe *= factor - diff --git a/src/compas_rhino/geometry/brep/brep.py b/src/compas_rhino/geometry/brep/brep.py index dd3b27718990..c1dd826faac7 100644 --- a/src/compas_rhino/geometry/brep/brep.py +++ b/src/compas_rhino/geometry/brep/brep.py @@ -27,6 +27,7 @@ from compas_rhino.conversions import mesh_to_compas from compas_rhino.conversions import mesh_to_rhino from compas_rhino.conversions import plane_to_rhino +from compas_rhino.conversions import point_to_compas from compas_rhino.conversions import point_to_rhino from compas_rhino.conversions import polyline_to_rhino_curve from compas_rhino.conversions import sphere_to_rhino @@ -1106,3 +1107,21 @@ def cap_planar_holes(self, tolerance=None): self._brep = result else: raise BrepError("Failed to cap planar holes") + + def closest_point(self, point): + """ + Returns the closest point on the Brep to the given point. + + Parameters + ---------- + point : :class:`compas.geometry.Point` + The point to find the closest point on the Brep to. + + Returns + ------- + :class:`compas.geometry.Point` + The closest point on the Brep to the given point. + + """ + rgpoint = self._brep.ClosestPoint(point_to_rhino(point)) + return point_to_compas(rgpoint) diff --git a/src/compas_rhino/geometry/brep/edge.py b/src/compas_rhino/geometry/brep/edge.py index 41fd02817113..82dd88e6b1f0 100644 --- a/src/compas_rhino/geometry/brep/edge.py +++ b/src/compas_rhino/geometry/brep/edge.py @@ -49,6 +49,10 @@ class RhinoBrepEdge(BrepEdge): True if the geometry of this edge is a line, False otherwise. native_edge : :class:`Rhino.Geometry.BrepEdge` The underlying BrepEdge object. + domain : tuple + The domain of the edge. + index : int + The index of the edge. """ @@ -153,6 +157,17 @@ def is_ellipse(self): def length(self): return self._mass_props.Length + @property + def domain(self): + rhino_domain = self._edge.Domain + min = rhino_domain.Min + max = rhino_domain.Max + return (min, max) + + @property + def index(self): + return self._edge.EdgeIndex + # ============================================================================== # Methods # ============================================================================== @@ -200,3 +215,40 @@ def _create_curve__from_data__(curve_type, curve_data, frame_data, domain): raise ValueError("Unknown curve type: {}".format(curve_type)) curve.Domain = Rhino.Geometry.Interval(*domain) return curve + + def closest_point(self, point): + """ + Returns the parameter of the closest point on the edge to the given point. + + Parameters + ---------- + point : :class:`compas.geometry.Point` + The point to project onto the edge. + + Returns + ------- + float + The parameter of the closest point on the edge. + """ + rgpoint = Rhino.Geometry.Point3d(point.x, point.y, point.z) + success, parameter = self._edge.ClosestPoint(rgpoint) + if not success: + raise ValueError("Failed to find closest point on edge") + return parameter + + def point_at(self, parameter): + """ + Returns the point on the edge at the given parameter. + + Parameters + ---------- + parameter : float + The parameter of the point on the edge. + + Returns + ------- + :class:`compas.geometry.Point` + The point on the edge at the given parameter. + """ + rgpoint = self._edge.PointAt(parameter) + return point_to_compas(rgpoint) diff --git a/src/compas_rhino/geometry/brep/face.py b/src/compas_rhino/geometry/brep/face.py index b42e1b8c541b..e7153917248f 100644 --- a/src/compas_rhino/geometry/brep/face.py +++ b/src/compas_rhino/geometry/brep/face.py @@ -14,6 +14,7 @@ from compas_rhino.conversions import cylinder_to_rhino from compas_rhino.conversions import frame_to_rhino_plane from compas_rhino.conversions import plane_to_compas_frame +from compas_rhino.conversions import point_to_compas from compas_rhino.conversions import sphere_to_compas from compas_rhino.conversions import sphere_to_rhino from compas_rhino.geometry import RhinoNurbsSurface @@ -310,3 +311,86 @@ def frame_at(self, u, v): if not success: raise ValueError("Failed to get frame at uv parameters: ({},{}).".format(u, v)) return plane_to_compas_frame(rhino_plane) + + def point_at(self, u, v): + """Returns the point at the given uv parameters. + + Parameters + ---------- + u : float + The u parameter. + v : float + The v parameter. + + Returns + ------- + :class:`compas.geometry.Point` + The point at the given uv parameters. + """ + rgpoint = self._face.PointAt(u, v) + return point_to_compas(rgpoint) + + def closest_point(self, point): + """Returns the closest point on the face to a give point. + + Parameters + ---------- + point : :class:`compas.geometry.Point` + The point to find the closest point on the face to. + + Returns + ------- + tuple[float, float] + The u and v parameters of the closest point on the face. + + """ + rgpoint = Rhino.Geometry.Point3d(point.x, point.y, point.z) + success, u, v = self._face.ClosestPoint(rgpoint) + if success: + return (u, v) + else: + raise ValueError("Failed to find closest point on face.") + + def is_point_on_face(self, u, v): + """Returns True if the point at the given uv parameters is on the face (inside or on the boundary). + + Parameters + ---------- + u : float + The u parameter. + v : float + The v parameter. + + Returns + ------- + bool + True if the point is on the face (inside or on the boundary), False otherwise. + """ + relation = self._face.IsPointOnFace(u, v) + if relation == Rhino.Geometry.PointFaceRelation.Interior: + return True + if relation == Rhino.Geometry.PointFaceRelation.Boundary: + return True + if relation == Rhino.Geometry.PointFaceRelation.Exterior: + return False + + def is_point_on_boundary(self, u, v): + """Returns True if the point is on the boundary of the face. + + Parameters + ---------- + u : float + The u parameter. + v : float + The v parameter. + + Returns + ------- + bool + True if the point is on the boundary of the face, False otherwise. + """ + relation = self._face.IsPointOnFace(u, v) + if relation == Rhino.Geometry.PointFaceRelation.Boundary: + return True + else: + return False diff --git a/src/compas_rhino/geometry/brep/loop.py b/src/compas_rhino/geometry/brep/loop.py index 65a9c35c8fce..4f743ccaa161 100644 --- a/src/compas_rhino/geometry/brep/loop.py +++ b/src/compas_rhino/geometry/brep/loop.py @@ -5,6 +5,7 @@ import Rhino # type: ignore from compas.geometry import BrepLoop +from compas.geometry import Curve from .edge import RhinoBrepEdge from .trim import RhinoBrepTrim @@ -127,3 +128,8 @@ def native_loop(self, rhino_loop): self._loop = rhino_loop self._type = int(self._loop.LoopType) self._trims = [RhinoBrepTrim(trim) for trim in self._loop.Trims] + + def to_curve(self): + curve = self._loop.To3dCurve() + curve = Curve.from_native(curve) + return curve diff --git a/src/compas_rhino/geometry/brep/trim.py b/src/compas_rhino/geometry/brep/trim.py index b5570bd7e0e0..3270a71bd48a 100644 --- a/src/compas_rhino/geometry/brep/trim.py +++ b/src/compas_rhino/geometry/brep/trim.py @@ -6,6 +6,7 @@ from compas.geometry import BrepTrim from compas_rhino.geometry import RhinoNurbsCurve +from compas_rhino.geometry.brep.edge import RhinoBrepEdge from .vertex import RhinoBrepVertex @@ -29,7 +30,8 @@ class RhinoBrepTrim(BrepTrim): The end vertex of this trim. vertices : list[:class:`compas_rhino.geometry.RhinoBrepVertex`], read-only The list of vertices which comprise this trim (start and end). - + edge : :class:compas_rhino.geometry.RhinoBrepEdge + The edge associated with this trim. """ def __init__(self, rhino_trim=None): @@ -118,6 +120,10 @@ def iso_status(self): def native_trim(self): return self._trim + @property + def edge(self): + return RhinoBrepEdge(self._trim.Edge) + @native_trim.setter def native_trim(self, rhino_trim): self._trim = rhino_trim diff --git a/src/compas_rhino/geometry/brep/vertex.py b/src/compas_rhino/geometry/brep/vertex.py index 7ae9866efa29..c00a7fe26483 100644 --- a/src/compas_rhino/geometry/brep/vertex.py +++ b/src/compas_rhino/geometry/brep/vertex.py @@ -16,6 +16,8 @@ class RhinoBrepVertex(BrepVertex): The underlying Rhino BrepBertex object. point : :class:`compas.geometry.Point`, read-only The geometry of this vertex as a point in 3D space. + index : int + The index of the vertex. """ @@ -66,6 +68,10 @@ def __from_data__(cls, data, builder): def point(self): return self._point + @property + def index(self): + return self._vertex.VertexIndex + @property def native_vertex(self): return self._vertex diff --git a/src/compas_rhino/geometry/curves/curve.py b/src/compas_rhino/geometry/curves/curve.py index a3df7109fa78..31cae7a5afef 100644 --- a/src/compas_rhino/geometry/curves/curve.py +++ b/src/compas_rhino/geometry/curves/curve.py @@ -11,8 +11,10 @@ from compas_rhino.conversions import plane_to_rhino from compas_rhino.conversions import point_to_compas from compas_rhino.conversions import point_to_rhino +from compas_rhino.conversions import polyline_to_compas from compas_rhino.conversions import transformation_to_rhino from compas_rhino.conversions import vector_to_compas +from compas_rhino.conversions.exceptions import ConversionError class RhinoCurve(Curve): @@ -147,6 +149,33 @@ def from_rhino(cls, native_curve): # Conversions # ============================================================================== + def to_polyline(self, tolerance=1, angle_tolerance=1, minimum_lenght=0, maximum_length=1): + """ + Convert the curve to a polyline. + + Parameters + ---------- + tolerance : float, optional + The tolerance. This is the maximum deviation from line midpoints to the curve. + angle_tolerance : float, optional + The angle tolerance in radians. This is the maximum deviation of the line directions. + minimum_lenght : float, optional + The minimum segment length. + maximum_length : float, optional + The maximum segment length. + + Returns + ------- + :class:`compas.geometry.Polyline` + The polyline representation of the curve. + """ + curve_polyline = self.native_curve.ToPolyline(tolerance, angle_tolerance, minimum_lenght, maximum_length) + polyline_created, polyline = curve_polyline.TryGetPolyline() + if polyline_created: + return polyline_to_compas(polyline) + else: + raise ConversionError("The curve cannot be converted to a polyline.") + # ============================================================================== # Methods # ============================================================================== diff --git a/tests/compas/geometry/test_capsule.py b/tests/compas/geometry/test_capsule.py index e45b5378c18e..95fa0c089a98 100644 --- a/tests/compas/geometry/test_capsule.py +++ b/tests/compas/geometry/test_capsule.py @@ -19,18 +19,18 @@ def test_capsule_discretization(capsule): def test_capsule_scaled(): """Test that Capsule.scaled() returns a scaled copy without modifying the original.""" capsule = Capsule(radius=5.0, height=10.0) - + # Test uniform scaling scaled_capsule = capsule.scaled(0.5) - + # Original should be unchanged assert capsule.radius == 5.0 assert capsule.height == 10.0 - + # Scaled copy should have scaled dimensions assert scaled_capsule.radius == 2.5 assert scaled_capsule.height == 5.0 - + # Test scaling with factor > 1 scaled_capsule_2 = capsule.scaled(2.0) assert scaled_capsule_2.radius == 10.0 @@ -42,11 +42,10 @@ def test_capsule_scaled(): def test_capsule_scale(): """Test that Capsule.scale() modifies the capsule in place.""" capsule = Capsule(radius=5.0, height=10.0) - + # Test uniform scaling capsule.scale(0.5) - + # Capsule should be modified assert capsule.radius == 2.5 assert capsule.height == 5.0 - diff --git a/tests/compas/geometry/test_cone.py b/tests/compas/geometry/test_cone.py index d7bd82d6f53f..7f2f05e09f00 100644 --- a/tests/compas/geometry/test_cone.py +++ b/tests/compas/geometry/test_cone.py @@ -19,18 +19,18 @@ def test_cone_discretization(cone): def test_cone_scaled(): """Test that Cone.scaled() returns a scaled copy without modifying the original.""" cone = Cone(radius=5.0, height=10.0) - + # Test uniform scaling scaled_cone = cone.scaled(0.5) - + # Original should be unchanged assert cone.radius == 5.0 assert cone.height == 10.0 - + # Scaled copy should have scaled dimensions assert scaled_cone.radius == 2.5 assert scaled_cone.height == 5.0 - + # Test scaling with factor > 1 scaled_cone_2 = cone.scaled(2.0) assert scaled_cone_2.radius == 10.0 @@ -42,11 +42,10 @@ def test_cone_scaled(): def test_cone_scale(): """Test that Cone.scale() modifies the cone in place.""" cone = Cone(radius=5.0, height=10.0) - + # Test uniform scaling cone.scale(0.5) - + # Cone should be modified assert cone.radius == 2.5 assert cone.height == 5.0 - diff --git a/tests/compas/geometry/test_cylinder.py b/tests/compas/geometry/test_cylinder.py index b828062ee61b..465f006e37d6 100644 --- a/tests/compas/geometry/test_cylinder.py +++ b/tests/compas/geometry/test_cylinder.py @@ -18,18 +18,18 @@ def test_cylinder_discretization(cylinder): def test_cylinder_scaled(): """Test that Cylinder.scaled() returns a scaled copy without modifying the original.""" cylinder = Cylinder(radius=5.0, height=10.0) - + # Test uniform scaling scaled_cylinder = cylinder.scaled(0.5) - + # Original should be unchanged assert cylinder.radius == 5.0 assert cylinder.height == 10.0 - + # Scaled copy should have scaled dimensions assert scaled_cylinder.radius == 2.5 assert scaled_cylinder.height == 5.0 - + # Test scaling with factor > 1 scaled_cylinder_2 = cylinder.scaled(2.0) assert scaled_cylinder_2.radius == 10.0 @@ -41,11 +41,10 @@ def test_cylinder_scaled(): def test_cylinder_scale(): """Test that Cylinder.scale() modifies the cylinder in place.""" cylinder = Cylinder(radius=5.0, height=10.0) - + # Test uniform scaling cylinder.scale(0.5) - + # Cylinder should be modified assert cylinder.radius == 2.5 assert cylinder.height == 5.0 - diff --git a/tests/compas/geometry/test_shpere.py b/tests/compas/geometry/test_shpere.py index 68f42e9272da..db0903e3f3aa 100644 --- a/tests/compas/geometry/test_shpere.py +++ b/tests/compas/geometry/test_shpere.py @@ -22,16 +22,16 @@ def test_sphere_discretization(sphere): def test_sphere_scaled(): """Test that Sphere.scaled() returns a scaled copy without modifying the original.""" sphere = Sphere(radius=10.0) - + # Test uniform scaling scaled_sphere = sphere.scaled(0.5) - + # Original should be unchanged assert sphere.radius == 10.0 - + # Scaled copy should have scaled radius assert scaled_sphere.radius == 5.0 - + # Test scaling with factor > 1 scaled_sphere_2 = sphere.scaled(2.0) assert scaled_sphere_2.radius == 20.0 @@ -41,10 +41,9 @@ def test_sphere_scaled(): def test_sphere_scale(): """Test that Sphere.scale() modifies the sphere in place.""" sphere = Sphere(radius=10.0) - + # Test uniform scaling sphere.scale(0.5) - + # Sphere should be modified assert sphere.radius == 5.0 - diff --git a/tests/compas/geometry/test_torus.py b/tests/compas/geometry/test_torus.py index d08e526236a1..3b36f99e5903 100644 --- a/tests/compas/geometry/test_torus.py +++ b/tests/compas/geometry/test_torus.py @@ -19,18 +19,18 @@ def test_torus_discretization(torus): def test_torus_scaled(): """Test that Torus.scaled() returns a scaled copy without modifying the original.""" torus = Torus(radius_axis=10.0, radius_pipe=2.0) - + # Test uniform scaling scaled_torus = torus.scaled(0.5) - + # Original should be unchanged assert torus.radius_axis == 10.0 assert torus.radius_pipe == 2.0 - + # Scaled copy should have scaled dimensions assert scaled_torus.radius_axis == 5.0 assert scaled_torus.radius_pipe == 1.0 - + # Test scaling with factor > 1 scaled_torus_2 = torus.scaled(2.0) assert scaled_torus_2.radius_axis == 20.0 @@ -42,10 +42,10 @@ def test_torus_scaled(): def test_torus_scale(): """Test that Torus.scale() modifies the torus in place.""" torus = Torus(radius_axis=10.0, radius_pipe=2.0) - + # Test uniform scaling torus.scale(0.5) - + # Torus should be modified assert torus.radius_axis == 5.0 assert torus.radius_pipe == 1.0