diff --git a/doc/changelog.d/2179.added.md b/doc/changelog.d/2179.added.md new file mode 100644 index 0000000000..8268ca0b1f --- /dev/null +++ b/doc/changelog.d/2179.added.md @@ -0,0 +1 @@ +Body sweep_with_guide diff --git a/src/ansys/geometry/core/_grpc/_services/base/bodies.py b/src/ansys/geometry/core/_grpc/_services/base/bodies.py index 42bf6b69ad..91cb28f8ec 100644 --- a/src/ansys/geometry/core/_grpc/_services/base/bodies.py +++ b/src/ansys/geometry/core/_grpc/_services/base/bodies.py @@ -59,6 +59,11 @@ def create_sweeping_chain(self, **kwargs) -> dict: """Create a sweeping chain.""" pass + @abstractmethod + def sweep_with_guide(self, **kwargs) -> dict: + """Sweep with a guide.""" + pass + @abstractmethod def create_extruded_body_from_face_profile(self, **kwargs) -> dict: """Create an extruded body from a face profile.""" diff --git a/src/ansys/geometry/core/_grpc/_services/v0/bodies.py b/src/ansys/geometry/core/_grpc/_services/v0/bodies.py index 03b08e8d21..fdc862eaf7 100644 --- a/src/ansys/geometry/core/_grpc/_services/v0/bodies.py +++ b/src/ansys/geometry/core/_grpc/_services/v0/bodies.py @@ -171,6 +171,48 @@ def create_sweeping_chain(self, **kwargs) -> dict: # noqa: D102 "is_surface": resp.is_surface, } + @protect_grpc + def sweep_with_guide(self, **kwargs) -> dict: # noqa: D102 + from ansys.api.dbu.v0.dbumodels_pb2 import EntityIdentifier + from ansys.api.geometry.v0.bodies_pb2 import ( + SweepWithGuideRequest, + SweepWithGuideRequestData, + ) + + # Create request object - assumes all inputs are valid and of the proper type + request = SweepWithGuideRequest( + request_data=[ + SweepWithGuideRequestData( + name=data.name, + parent=EntityIdentifier(id=data.parent_id), + plane=from_plane_to_grpc_plane(data.sketch.plane), + geometries=from_sketch_shapes_to_grpc_geometries( + data.sketch.plane, data.sketch.edges, data.sketch.faces + ), + path=from_trimmed_curve_to_grpc_trimmed_curve(data.path), + guide=from_trimmed_curve_to_grpc_trimmed_curve(data.guide), + tight_tolerance=data.tight_tolerance, + ) + for data in kwargs["sweep_data"] + ], + ) + + # Call the gRPC service + resp = self.stub.SweepWithGuide(request=request) + + # Return the response - formatted as a dictionary + return { + "bodies": [ + { + "id": body.id, + "name": body.name, + "master_id": body.master_id, + "is_surface": body.is_surface, + } + ] + for body in resp.bodies + } + @protect_grpc def create_extruded_body_from_face_profile(self, **kwargs) -> dict: # noqa: D102 from ansys.api.geometry.v0.bodies_pb2 import CreateExtrudedBodyFromFaceProfileRequest diff --git a/src/ansys/geometry/core/_grpc/_services/v0/conversions.py b/src/ansys/geometry/core/_grpc/_services/v0/conversions.py index f7dc96063e..ddd4a570ee 100644 --- a/src/ansys/geometry/core/_grpc/_services/v0/conversions.py +++ b/src/ansys/geometry/core/_grpc/_services/v0/conversions.py @@ -749,6 +749,44 @@ def from_nurbs_curve_to_grpc_nurbs_curve(curve: "NURBSCurve") -> GRPCNurbsCurve: ) +def from_grpc_nurbs_curve_to_nurbs_curve(curve: GRPCNurbsCurve) -> "NURBSCurve": + """Convert a NURBS curve gRPC message to a ``NURBSCurve``. + + Parameters + ---------- + curve : GRPCNurbsCurve + Geometry service gRPC NURBS curve message. + + Returns + ------- + NURBSCurve + Resulting converted NURBS curve. + """ + from ansys.geometry.core.shapes.curves.nurbs import NURBSCurve + + # Extract control points + control_points = [from_grpc_point_to_point3d(cp.position) for cp in curve.control_points] + + # Extract weights + weights = [cp.weight for cp in curve.control_points] + + # Extract degree + degree = curve.nurbs_data.degree + + # Convert gRPC knots to full knot vector + knots = [] + for grpc_knot in curve.nurbs_data.knots: + knots.extend([grpc_knot.parameter] * grpc_knot.multiplicity) + + # Create and return the NURBS curve + return NURBSCurve.from_control_points( + control_points=control_points, + degree=degree, + knots=knots, + weights=weights, + ) + + def from_knots_to_grpc_knots(knots: list[float]) -> list[GRPCKnot]: """Convert a list of knots to a list of gRPC knot messages. @@ -813,6 +851,8 @@ def from_grpc_curve_to_curve(curve: GRPCCurveGeometry) -> "Curve": result = Circle(origin, curve.radius, reference, axis) elif curve.major_radius != 0 and curve.minor_radius != 0: result = Ellipse(origin, curve.major_radius, curve.minor_radius, reference, axis) + elif curve.nurbs_curve.nurbs_data.degree != 0: + result = from_grpc_nurbs_curve_to_nurbs_curve(curve.nurbs_curve) elif curve.direction is not None: result = Line( origin, diff --git a/src/ansys/geometry/core/_grpc/_services/v1/bodies.py b/src/ansys/geometry/core/_grpc/_services/v1/bodies.py index 62e6cf1b4a..29f350bdcc 100644 --- a/src/ansys/geometry/core/_grpc/_services/v1/bodies.py +++ b/src/ansys/geometry/core/_grpc/_services/v1/bodies.py @@ -64,6 +64,10 @@ def create_sweeping_profile_body(self, **kwargs) -> dict: # noqa: D102 def create_sweeping_chain(self, **kwargs) -> dict: # noqa: D102 raise NotImplementedError + @protect_grpc + def sweep_with_guide(self, **kwargs) -> dict: # noqa: D102 + raise NotImplementedError + @protect_grpc def create_extruded_body_from_face_profile(self, **kwargs) -> dict: # noqa: D102 raise NotImplementedError diff --git a/src/ansys/geometry/core/designer/component.py b/src/ansys/geometry/core/designer/component.py index 627c1b5cdd..d1bee6cf09 100644 --- a/src/ansys/geometry/core/designer/component.py +++ b/src/ansys/geometry/core/designer/component.py @@ -21,6 +21,7 @@ # SOFTWARE. """Provides for managing components.""" +from dataclasses import dataclass from enum import Enum, unique from functools import cached_property from typing import TYPE_CHECKING, Any, Optional, Union @@ -135,6 +136,34 @@ def get_multiplier(self) -> int: return 1 if self is ExtrusionDirection.POSITIVE else -1 +@dataclass +class SweepWithGuideData: + """Data class for sweep with guide parameters. + + Parameters + ---------- + name : str + Name of the body to be generated by the sweep operation. + parent_id : str + ID of the parent component. + sketch : Sketch + Sketch to use for the sweep operation. + path : TrimmedCurve + Path to sweep along. + guide : TrimmedCurve + Guide curve for the sweep operation. + tight_tolerance : bool + Whether to use tight tolerance for the sweep operation. + """ + + name: str + parent_id: str + sketch: Sketch + path: TrimmedCurve + guide: TrimmedCurve + tight_tolerance: bool = False + + class Component: """Provides for creating and managing a component. @@ -707,6 +736,32 @@ def sweep_chain( ) return self.__build_body_from_response(response) + @min_backend_version(26, 1, 0) + @check_input_types + @ensure_design_is_active + def sweep_with_guide(self, sweep_data: list[SweepWithGuideData]) -> list[Body]: + """Create a body by sweeping a sketch along a path with a guide curve. + + The newly created body is placed under this component within the design assembly. + + Parameters + ---------- + sweep_data: list[SweepWithGuideData] + Data for the sweep operation, including the sketch, path, and guide curve. + + Returns + ------- + list[Body] + Created bodies from the given sweep data. + + Warnings + -------- + This method is only available starting on Ansys release 26R1. + """ + self._grpc_client.log.debug(f"Sweeping the profile {self.id}. Creating body...") + response = self._grpc_client.services.bodies.sweep_with_guide(sweep_data=sweep_data) + return [self.__build_body_from_response(body_data) for body_data in response.get("bodies")] + @min_backend_version(24, 2, 0) @check_input_types def revolve_sketch( diff --git a/tests/integration/test_design.py b/tests/integration/test_design.py index 17c2a5c3ba..ec2bcf14fe 100644 --- a/tests/integration/test_design.py +++ b/tests/integration/test_design.py @@ -41,6 +41,7 @@ SurfaceType, ) from ansys.geometry.core.designer.body import CollisionType, FillStyle, MasterBody +from ansys.geometry.core.designer.component import SweepWithGuideData from ansys.geometry.core.designer.face import FaceLoopType from ansys.geometry.core.designer.part import MasterComponent, Part from ansys.geometry.core.errors import GeometryExitedError, GeometryRuntimeError @@ -71,6 +72,7 @@ Torus, ) from ansys.geometry.core.shapes.box_uv import BoxUV +from ansys.geometry.core.shapes.curves.nurbs import NURBSCurve from ansys.geometry.core.shapes.parameterization import ( Interval, ) @@ -2809,6 +2811,55 @@ def test_sweep_chain(modeler: Modeler): assert body.volume.m == 0 +def test_sweep_with_guide(modeler: Modeler): + """Test creating a body by sweeping a profile with a guide curve.""" + design = modeler.create_design("SweepWithGuide") + + # Create path points for the sweep path + path_points = [ + Point3D([0.0, 0.0, 0.15]), + Point3D([0.05, 0.0, 0.1]), + Point3D([0.1, 0.0, 0.05]), + Point3D([0.15, 0.0, 0.1]), + Point3D([0.2, 0.0, 0.15]), + ] + nurbs_path = NURBSCurve.fit_curve_from_points(path_points, degree=3) + n_l_points = len(path_points) + path_interval = Interval(1.0 / (n_l_points - 1), (n_l_points - 2.0) / (n_l_points - 1)) + trimmed_path = nurbs_path.trim(path_interval) + + # Create a simple circular profile sketch + profile_plane = Plane(origin=path_points[1]) + profile_sketch = Sketch(profile_plane) + profile_sketch.circle(Point2D([0, 0]), 0.01) # 0.01 radius + + # Create guide curve points (offset from path) + guide_points = [Point3D([p.x.m, p.y.m + 0.01, p.z.m]) for p in path_points] + guide_curve = NURBSCurve.fit_curve_from_points(guide_points, degree=3) + guide_interval = Interval(1.0 / (n_l_points - 1), (n_l_points - 2.0) / (n_l_points - 1)) + trimmed_guide = guide_curve.trim(guide_interval) + + # Sweep the profile along the path with the guide curve + sweep_data = [ + SweepWithGuideData( + name="SweptBody", + parent_id=design.id, + sketch=profile_sketch, + path=trimmed_path, + guide=trimmed_guide, + tight_tolerance=True, + ) + ] + sweep_body = design.sweep_with_guide(sweep_data=sweep_data)[0] + + assert sweep_body is not None + assert sweep_body.name == "SweptBody" + assert sweep_body.is_surface + assert len(sweep_body.faces) == 1 + assert len(sweep_body.edges) == 2 + assert len(sweep_body.vertices) == 0 + + def test_create_body_from_loft_profile(modeler: Modeler): """Test the ``create_body_from_loft_profile()`` method to create a vase shape.