diff --git a/doc/changelog.d/2273.added.md b/doc/changelog.d/2273.added.md new file mode 100644 index 0000000000..b12343eaa1 --- /dev/null +++ b/doc/changelog.d/2273.added.md @@ -0,0 +1 @@ +NURBS surface body creation diff --git a/pyproject.toml b/pyproject.toml index 15f4c2dfd5..962464d35e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ ] dependencies = [ - "ansys-api-geometry==0.4.77", + "ansys-api-geometry==0.4.78", "ansys-tools-path>=0.3,<1", "beartype>=0.11.0,<0.22", "geomdl>=5,<6", diff --git a/src/ansys/geometry/core/_grpc/_services/v0/conversions.py b/src/ansys/geometry/core/_grpc/_services/v0/conversions.py index 79c1baae87..324033b800 100644 --- a/src/ansys/geometry/core/_grpc/_services/v0/conversions.py +++ b/src/ansys/geometry/core/_grpc/_services/v0/conversions.py @@ -44,6 +44,7 @@ MaterialProperty as GRPCMaterialProperty, Matrix as GRPCMatrix, NurbsCurve as GRPCNurbsCurve, + NurbsSurface as GRPCNurbsSurface, Plane as GRPCPlane, Point as GRPCPoint, Polygon as GRPCPolygon, @@ -58,6 +59,7 @@ from ansys.geometry.core.errors import GeometryRuntimeError from ansys.geometry.core.misc.checks import graphics_required +from ansys.geometry.core.shapes.surfaces.nurbs import NURBSSurface if TYPE_CHECKING: # pragma: no cover import pyvista as pv @@ -770,6 +772,53 @@ def from_nurbs_curve_to_grpc_nurbs_curve(curve: "NURBSCurve") -> GRPCNurbsCurve: ) +def from_nurbs_surface_to_grpc_nurbs_surface(surface: "NURBSSurface") -> GRPCNurbsSurface: + """Convert a ``NURBSSurface`` to a NURBS surface gRPC message. + + Parameters + ---------- + surface : NURBSSurface + Surface to convert. + + Returns + ------- + GRPCNurbsSurface + Geometry service gRPC ``NURBSSurface`` message. + """ + from ansys.api.geometry.v0.models_pb2 import ( + ControlPoint as GRPCControlPoint, + NurbsData as GRPCNurbsData, + ) + + # Convert control points + control_points = [ + GRPCControlPoint( + position=from_point3d_to_grpc_point(point), + weight=weight, + ) + for weight, point in zip(surface.weights, surface.control_points) + ] + + # Convert nurbs data + nurbs_data_u = GRPCNurbsData( + degree=surface.degree_u, + knots=from_knots_to_grpc_knots(surface.knotvector_u), + order=surface.degree_u + 1, + ) + + nurbs_data_v = GRPCNurbsData( + degree=surface.degree_v, + knots=from_knots_to_grpc_knots(surface.knotvector_v), + order=surface.degree_v + 1, + ) + + return GRPCNurbsSurface( + control_points=control_points, + nurbs_data_u=nurbs_data_u, + nurbs_data_v=nurbs_data_v, + ) + + def from_grpc_nurbs_curve_to_nurbs_curve(curve: GRPCNurbsCurve) -> "NURBSCurve": """Convert a NURBS curve gRPC message to a ``NURBSCurve``. @@ -976,6 +1025,14 @@ def from_surface_to_grpc_surface(surface: "Surface") -> tuple[GRPCSurface, GRPCS minor_radius=surface.minor_radius.m, ) surface_type = GRPCSurfaceType.SURFACETYPE_TORUS + elif isinstance(surface, NURBSSurface): + grpc_surface = GRPCSurface( + origin=origin, + reference=reference, + axis=axis, + nurbs_surface=from_nurbs_surface_to_grpc_nurbs_surface(surface), + ) + surface_type = GRPCSurfaceType.SURFACETYPE_NURBS return grpc_surface, surface_type diff --git a/src/ansys/geometry/core/designer/component.py b/src/ansys/geometry/core/designer/component.py index 90d5dafd4b..c792f73ec2 100644 --- a/src/ansys/geometry/core/designer/component.py +++ b/src/ansys/geometry/core/designer/component.py @@ -61,6 +61,7 @@ from ansys.geometry.core.shapes.curves.trimmed_curve import TrimmedCurve from ansys.geometry.core.shapes.parameterization import Interval from ansys.geometry.core.shapes.surfaces import TrimmedSurface +from ansys.geometry.core.shapes.surfaces.nurbs import NURBSSurface from ansys.geometry.core.sketch.sketch import Sketch from ansys.geometry.core.typing import Real @@ -1041,7 +1042,15 @@ def create_body_from_surface(self, name: str, trimmed_surface: TrimmedSurface) - Warnings -------- This method is only available starting on Ansys release 25R1. + NURBS surface bodies are only supported starting on Ansys release 26R1. """ + if (self._grpc_client.backend_version < (26, 1, 0)) and ( + isinstance(trimmed_surface.geometry, NURBSSurface) + ): + raise ValueError( + "NURBS surface bodies are only supported starting on Ansys release 26R1." + ) + self._grpc_client.log.debug( f"Creating surface body from trimmed surface provided on {self.id}. Creating body..." ) diff --git a/src/ansys/geometry/core/shapes/surfaces/nurbs.py b/src/ansys/geometry/core/shapes/surfaces/nurbs.py index 46b76b139b..a6a4ed7252 100644 --- a/src/ansys/geometry/core/shapes/surfaces/nurbs.py +++ b/src/ansys/geometry/core/shapes/surfaces/nurbs.py @@ -26,7 +26,8 @@ from beartype import beartype as check_input_types -from ansys.geometry.core.math import Point3D +from ansys.geometry.core.math import ZERO_POINT3D, Point3D +from ansys.geometry.core.math.constants import UNITVECTOR3D_X, UNITVECTOR3D_Z from ansys.geometry.core.math.matrix import Matrix44 from ansys.geometry.core.math.vector import UnitVector3D, Vector3D from ansys.geometry.core.shapes.parameterization import ( @@ -57,7 +58,13 @@ class NURBSSurface(Surface): """ - def __init__(self, geomdl_object: "geomdl_nurbs.Surface" = None): + def __init__( + self, + origin: Point3D = ZERO_POINT3D, + reference: UnitVector3D = UNITVECTOR3D_X, + axis: UnitVector3D = UNITVECTOR3D_Z, + geomdl_object: "geomdl_nurbs.Surface" = None, + ): """Initialize ``NURBSSurface`` class.""" try: import geomdl.NURBS as geomdl_nurbs # noqa: N811 @@ -68,6 +75,9 @@ def __init__(self, geomdl_object: "geomdl_nurbs.Surface" = None): ) from e self._nurbs_surface = geomdl_object if geomdl_object else geomdl_nurbs.Surface() + self._origin = origin + self._reference = reference + self._axis = axis @property def geomdl_nurbs_surface(self) -> "geomdl_nurbs.Surface": @@ -110,6 +120,21 @@ def weights(self) -> list[Real]: """Get the weights of the control points.""" return self._nurbs_surface.weights + @property + def origin(self) -> Point3D: + """Get the origin of the surface.""" + return self._origin + + @property + def dir_x(self) -> UnitVector3D: + """Get the reference direction of the surface.""" + return self._reference + + @property + def dir_z(self) -> UnitVector3D: + """Get the axis direction of the surface.""" + return self._axis + @classmethod @check_input_types def from_control_points( @@ -120,6 +145,9 @@ def from_control_points( knots_v: list[Real], control_points: list[Point3D], weights: list[Real] = None, + origin: Point3D = ZERO_POINT3D, + reference: UnitVector3D = UNITVECTOR3D_X, + axis: UnitVector3D = UNITVECTOR3D_Z, ) -> "NURBSSurface": """Create a NURBS surface from control points and knot vectors. @@ -139,13 +167,19 @@ def from_control_points( Weights for the control points. If not provided, all weights are set to 1. delta : float, optional Evaluation delta for the surface. Default is 0.01. + origin : Point3D, optional + Origin of the surface. Default is (0, 0, 0). + reference : UnitVector3D, optional + Reference direction of the surface. Default is (1, 0, 0). + axis : UnitVector3D, optional + Axis direction of the surface. Default is (0, 0, 1). Returns ------- NURBSSurface Created NURBS surface. """ - nurbs_surface = cls() + nurbs_surface = cls(origin, reference, axis) nurbs_surface._nurbs_surface.degree_u = degree_u nurbs_surface._nurbs_surface.degree_v = degree_v @@ -182,6 +216,9 @@ def fit_surface_from_points( size_v: int, degree_u: int, degree_v: int, + origin: Point3D = ZERO_POINT3D, + reference: UnitVector3D = UNITVECTOR3D_X, + axis: UnitVector3D = UNITVECTOR3D_Z, ) -> "NURBSSurface": """Fit a NURBS surface to a set of points. @@ -197,6 +234,12 @@ def fit_surface_from_points( Degree of the surface in the U direction. degree_v : int Degree of the surface in the V direction. + origin : Point3D, optional + Origin of the surface. Default is (0, 0, 0). + reference : UnitVector3D, optional + Reference direction of the surface. Default is (1, 0, 0). + axis : UnitVector3D, optional + Axis direction of the surface. Default is (0, 0, 1). Returns ------- @@ -215,7 +258,7 @@ def fit_surface_from_points( degree_v=degree_v, ) - nurbs_surface = cls() + nurbs_surface = cls(origin, reference, axis) nurbs_surface._nurbs_surface.degree_u = degree_u nurbs_surface._nurbs_surface.degree_v = degree_v diff --git a/tests/_incompatible_tests.yml b/tests/_incompatible_tests.yml index f341e46e87..ee4a8d5752 100644 --- a/tests/_incompatible_tests.yml +++ b/tests/_incompatible_tests.yml @@ -296,6 +296,8 @@ backends: # Export body facets add in 26.1 - tests/integration/test_design.py::test_write_body_facets_on_save[scdocx-None] - tests/integration/test_design.py::test_write_body_facets_on_save[dsco-DISCO] + # NURBS surface creation only available from 26.1 onwards + - tests/integration/test_design.py::test_nurbs_surface_body_creation - version: "25.2" incompatible_tests: @@ -330,3 +332,5 @@ backends: # Export body facets add in 26.1 - tests/integration/test_design.py::test_write_body_facets_on_save[scdocx-None] - tests/integration/test_design.py::test_write_body_facets_on_save[dsco-DISCO] + # NURBS surface creation only available from 26.1 onwards + - tests/integration/test_design.py::test_nurbs_surface_body_creation diff --git a/tests/integration/test_design.py b/tests/integration/test_design.py index dcf93e9674..4db76fe19b 100644 --- a/tests/integration/test_design.py +++ b/tests/integration/test_design.py @@ -76,6 +76,7 @@ from ansys.geometry.core.shapes.parameterization import ( Interval, ) +from ansys.geometry.core.shapes.surfaces.nurbs import NURBSSurface from ansys.geometry.core.sketch import Sketch from ..conftest import are_graphics_available @@ -3264,6 +3265,34 @@ def test_surface_body_creation(modeler: Modeler): assert body.faces[0].area.m == pytest.approx(39.4784176044 * 2) +def test_nurbs_surface_body_creation(modeler: Modeler): + """Test surface body creation from NURBS surfaces.""" + design = modeler.create_design("Design1") + + points = [ + Point3D([0, 0, 0]), + Point3D([0, 1, 1]), + Point3D([0, 2, 0]), + Point3D([1, 0, 1]), + Point3D([1, 1, 2]), + Point3D([1, 2, 1]), + Point3D([2, 0, 0]), + Point3D([2, 1, 1]), + Point3D([2, 2, 0]), + ] + degree_u = 2 + degree_v = 2 + surface = NURBSSurface.fit_surface_from_points( + points=points, size_u=3, size_v=3, degree_u=degree_u, degree_v=degree_v + ) + + trimmed_surface = surface.trim(BoxUV(Interval(0, 1), Interval(0, 1))) + body = design.create_body_from_surface("nurbs_surface", trimmed_surface) + assert len(design.bodies) == 1 + assert body.is_surface + assert body.faces[0].area.m == pytest.approx(7.44626609) + + def test_create_surface_from_nurbs_sketch(modeler: Modeler): """Test creating a surface from a NURBS sketch.""" design = modeler.create_design("NURBS_Sketch_Surface")