diff --git a/doc/changelog.d/2356.added.md b/doc/changelog.d/2356.added.md new file mode 100644 index 0000000000..d2bd450fb8 --- /dev/null +++ b/doc/changelog.d/2356.added.md @@ -0,0 +1 @@ +Add Named Selections query to geometric entities diff --git a/src/ansys/geometry/core/_grpc/_services/v0/designs.py b/src/ansys/geometry/core/_grpc/_services/v0/designs.py index 5c902f5705..baa5d348ec 100644 --- a/src/ansys/geometry/core/_grpc/_services/v0/designs.py +++ b/src/ansys/geometry/core/_grpc/_services/v0/designs.py @@ -429,6 +429,14 @@ def serialize_beam(beam): "cross_section": serialize_beam_cross_section(beam.cross_section), } + def serialize_design_point(design_point): + return { + "id": design_point.id, + "name": design_point.owner_name, + "point": from_grpc_point_to_point3d(design_point.points[0]), + "parent_id": design_point.parent_id.id, + } + parts = getattr(response, "parts", []) transformed_parts = getattr(response, "transformed_parts", []) bodies = getattr(response, "bodies", []) @@ -438,6 +446,7 @@ def serialize_beam(beam): component_coordinate_systems = getattr(response, "component_coord_systems", []) component_shared_topologies = getattr(response, "component_shared_topologies", []) beams = getattr(response, "beams", []) + design_points = getattr(response, "design_points", []) return { "parts": [serialize_part(part) for part in parts] if len(parts) > 0 else [], "transformed_parts": [serialize_transformed_part(tp) for tp in transformed_parts], @@ -452,6 +461,7 @@ def serialize_beam(beam): component_shared_topologies ), "beams": [serialize_beam(beam) for beam in beams], + "design_points": [serialize_design_point(dp) for dp in design_points], } @protect_grpc diff --git a/src/ansys/geometry/core/_grpc/_services/v0/named_selection.py b/src/ansys/geometry/core/_grpc/_services/v0/named_selection.py index 68dbeb79ae..c57db7574e 100644 --- a/src/ansys/geometry/core/_grpc/_services/v0/named_selection.py +++ b/src/ansys/geometry/core/_grpc/_services/v0/named_selection.py @@ -26,7 +26,7 @@ from ansys.geometry.core.errors import protect_grpc from ..base.named_selection import GRPCNamedSelectionService -from .conversions import build_grpc_id, from_grpc_point_to_point3d +from .conversions import build_grpc_id class GRPCNamedSelectionServiceV0(GRPCNamedSelectionService): @@ -64,9 +64,7 @@ def get_named_selection(self, **kwargs): # noqa: D102 "faces": [face.id for face in response.faces], "edges": [edge.id for edge in response.edges], "beams": [beam.id.id for beam in response.beams], - "design_points": [ - (dp.id, from_grpc_point_to_point3d(dp.points[0])) for dp in response.design_points - ], + "design_points": [dp.id for dp in response.design_points], "components": [comp.id for comp in response.components], "vertices": [vertex.id.id for vertex in response.vertices], } diff --git a/src/ansys/geometry/core/designer/beam.py b/src/ansys/geometry/core/designer/beam.py index 5d13f4c2ba..9b036fc859 100644 --- a/src/ansys/geometry/core/designer/beam.py +++ b/src/ansys/geometry/core/designer/beam.py @@ -30,6 +30,7 @@ from ansys.geometry.core.math.frame import Frame from ansys.geometry.core.math.point import Point3D from ansys.geometry.core.math.vector import UnitVector3D +from ansys.geometry.core.misc.auxiliary import get_design_from_component from ansys.geometry.core.misc.checks import check_type from ansys.geometry.core.misc.measurements import Distance from ansys.geometry.core.shapes.curves.trimmed_curve import TrimmedCurve @@ -37,6 +38,7 @@ if TYPE_CHECKING: # pragma: no cover from ansys.geometry.core.designer.component import Component + from ansys.geometry.core.designer.selection import NamedSelection class BeamType(Enum): @@ -422,6 +424,21 @@ def is_alive(self) -> bool: """Flag indicating whether the beam is still alive on the server.""" return self._is_alive + def get_named_selections(self) -> list["NamedSelection"]: + """Get the named selections that include this beam. + + Returns + ------- + list[NamedSelection] + List of named selections that include this beam. + """ + included_ns = [] + for ns in get_design_from_component(self.parent_component).named_selections: + if any(beam.id == self.id for beam in ns.beams): + included_ns.append(ns) + + return included_ns + def __repr__(self) -> str: """Represent the beam as a string.""" lines = [f"ansys.geometry.core.designer.Beam {hex(id(self))}"] diff --git a/src/ansys/geometry/core/designer/body.py b/src/ansys/geometry/core/designer/body.py index 4475d803f7..8b9ca81673 100644 --- a/src/ansys/geometry/core/designer/body.py +++ b/src/ansys/geometry/core/designer/body.py @@ -67,6 +67,7 @@ from pyvista import MultiBlock, PolyData from ansys.geometry.core.designer.component import Component + from ansys.geometry.core.designer.selection import NamedSelection # TODO: Temporary fix for boolean operations # This is a temporary fix for the boolean operations issue. The issue is that the @@ -572,6 +573,17 @@ def copy(self, parent: "Component", name: str = None) -> "Body": """ return + @abstractmethod + def get_named_selections(self) -> list["NamedSelection"]: + """Get the named selections associated with the body. + + Returns + ------- + list[NamedSelection] + List of named selections associated with the body. + """ + return + @abstractmethod def get_raw_tessellation( self, @@ -1052,6 +1064,7 @@ def _get_vertices_from_id(self, body: Union["Body", "MasterBody"]) -> list[Verte Vertex( vertex_resp.get("id"), vertex_resp.get("position"), + body, ) for vertex_resp in response.get("vertices") ] @@ -1279,6 +1292,14 @@ def copy(self, parent: "Component", name: str = None) -> "Body": # noqa: D102 "Copy method is not implemented on the MasterBody. Call this method on a body instead." ) + def get_named_selections(self) -> list["NamedSelection"]: # noqa: D102 + raise NotImplementedError( + """ + get_named_selections is not implemented at the MasterBody level. + Instead, call this method on a body. + """ + ) + @min_backend_version(26, 1, 0) def get_raw_tessellation( # noqa: D102 self, @@ -1880,6 +1901,15 @@ def copy(self, parent: "Component", name: str = None) -> "Body": # noqa: D102 body_id = f"{parent.id}/{tb.id}" if parent.parent_component else tb.id return Body(body_id, response.get("name"), parent, tb) + @ensure_design_is_active + def get_named_selections(self) -> list["NamedSelection"]: # noqa: D102 + included_ns = [] + for ns in get_design_from_body(self).named_selections: + if any(body.id == self.id for body in ns.bodies): + included_ns.append(ns) + + return included_ns + @ensure_design_is_active def get_raw_tessellation( # noqa: D102 self, diff --git a/src/ansys/geometry/core/designer/component.py b/src/ansys/geometry/core/designer/component.py index 710f832986..f71ba6a6a9 100644 --- a/src/ansys/geometry/core/designer/component.py +++ b/src/ansys/geometry/core/designer/component.py @@ -67,6 +67,8 @@ if TYPE_CHECKING: # pragma: no cover from pyvista import MultiBlock, PolyData + from ansys.geometry.core.designer.selection import NamedSelection + @unique class SharedTopologyType(Enum): @@ -1980,3 +1982,18 @@ def make_independent(self, others: list["Component"] = None) -> None: """ ids = [self.id, *[o.id for o in others or []]] self._grpc_client._services.components.make_independent(ids=ids) + + def get_named_selections(self) -> list["NamedSelection"]: + """Get the named selections of the component. + + Returns + ------- + list[NamedSelection] + List of named selections belonging to the component. + """ + included_ns = [] + for ns in get_design_from_component(self).named_selections: + if any(comp.id == self.id for comp in ns.components): + included_ns.append(ns) + + return included_ns diff --git a/src/ansys/geometry/core/designer/design.py b/src/ansys/geometry/core/designer/design.py index a44cdb2c95..cb7baa278d 100644 --- a/src/ansys/geometry/core/designer/design.py +++ b/src/ansys/geometry/core/designer/design.py @@ -1308,6 +1308,18 @@ def __read_existing_design(self) -> None: component.coordinate_systems.append(new_cs) num_created_coord_systems += 1 + # Create DesignPoints + for dp in response.get("design_points"): + created_dp = DesignPoint( + dp.get("id"), + dp.get("name"), + dp.get("point"), + created_components.get(dp.get("parent_id"), self), + ) + + # Append the design point to the component to which it belongs + created_dp.parent_component._design_points.append(created_dp) + end = time.time() # Set SharedTopology diff --git a/src/ansys/geometry/core/designer/designpoint.py b/src/ansys/geometry/core/designer/designpoint.py index d062943bfa..932b680d55 100644 --- a/src/ansys/geometry/core/designer/designpoint.py +++ b/src/ansys/geometry/core/designer/designpoint.py @@ -24,6 +24,7 @@ from typing import TYPE_CHECKING, Union from ansys.geometry.core.math.point import Point3D +from ansys.geometry.core.misc.auxiliary import get_design_from_component from ansys.geometry.core.misc.checks import graphics_required from ansys.geometry.core.misc.units import UNITS @@ -31,6 +32,7 @@ import pyvista as pv from ansys.geometry.core.designer.component import Component + from ansys.geometry.core.designer.selection import NamedSelection class DesignPoint: @@ -44,14 +46,11 @@ class DesignPoint: User-defined label for the design points. points : Point3D 3D point constituting the design points. - parent_component : Component | None + parent_component : Component Parent component to place the new design point under within the design assembly. - Its default value is None. """ - def __init__( - self, id: str, name: str, point: Point3D, parent_component: Union["Component", None] = None - ): + def __init__(self, id: str, name: str, point: Point3D, parent_component: Union["Component"]): """Initialize the ``DesignPoints`` class.""" self._id = id self._name = name @@ -78,6 +77,21 @@ def parent_component(self) -> "Component": """Component node that the design point is under.""" return self._parent_component + def get_named_selections(self) -> list["NamedSelection"]: + """Get named selections that contain this design point. + + Returns + ------- + list[NamedSelection] + List of named selections that contain this design point. + """ + included_ns = [] + for ns in get_design_from_component(self.parent_component).named_selections: + if any(dp.id == self.id for dp in ns.design_points): + included_ns.append(ns) + + return included_ns + def __repr__(self) -> str: """Represent the design points as a string.""" lines = [f"ansys.geometry.core.designer.DesignPoints {hex(id(self))}"] diff --git a/src/ansys/geometry/core/designer/edge.py b/src/ansys/geometry/core/designer/edge.py index 11f8136c6b..e1fdf9bf1c 100644 --- a/src/ansys/geometry/core/designer/edge.py +++ b/src/ansys/geometry/core/designer/edge.py @@ -30,6 +30,7 @@ from ansys.geometry.core.errors import GeometryRuntimeError from ansys.geometry.core.math.bbox import BoundingBox from ansys.geometry.core.math.point import Point3D +from ansys.geometry.core.misc.auxiliary import get_design_from_body from ansys.geometry.core.misc.checks import ensure_design_is_active, min_backend_version from ansys.geometry.core.shapes.curves.trimmed_curve import ReversedTrimmedCurve, TrimmedCurve from ansys.geometry.core.shapes.parameterization import Interval @@ -37,6 +38,7 @@ if TYPE_CHECKING: # pragma: no cover from ansys.geometry.core.designer.body import Body from ansys.geometry.core.designer.face import Face + from ansys.geometry.core.designer.selection import NamedSelection from ansys.geometry.core.designer.vertex import Vertex @@ -185,6 +187,7 @@ def vertices(self) -> list["Vertex"]: Vertex( vertex_resp.get("id"), vertex_resp.get("position"), + self.body, ) for vertex_resp in response.get("vertices") ] @@ -229,3 +232,18 @@ def bounding_box(self) -> BoundingBox: return BoundingBox( response.get("min_corner"), response.get("max_corner"), response.get("center") ) + + def get_named_selections(self) -> list["NamedSelection"]: + """Get named selections associated with the edge. + + Returns + ------- + list[NamedSelection] + List of named selections that include the edge. + """ + included_ns = [] + for ns in get_design_from_body(self.body).named_selections: + if any(edge.id == self.id for edge in ns.edges): + included_ns.append(ns) + + return included_ns diff --git a/src/ansys/geometry/core/designer/face.py b/src/ansys/geometry/core/designer/face.py index 71d30097bf..3267b0fff4 100644 --- a/src/ansys/geometry/core/designer/face.py +++ b/src/ansys/geometry/core/designer/face.py @@ -39,6 +39,7 @@ DEFAULT_COLOR, convert_color_to_hex, convert_opacity_to_hex, + get_design_from_body, ) from ansys.geometry.core.misc.checks import ( ensure_design_is_active, @@ -58,6 +59,7 @@ import pyvista as pv from ansys.geometry.core.designer.body import Body + from ansys.geometry.core.designer.selection import NamedSelection @unique @@ -267,6 +269,7 @@ def vertices(self) -> list[Vertex]: Vertex( vertex_resp.get("id"), vertex_resp.get("position"), + self.body, ) for vertex_resp in response.get("vertices") ] @@ -530,6 +533,22 @@ def setup_offset_relationship( return result.get("success") + def get_named_selections(self) -> list["NamedSelection"]: + """Get named selections associated with the edge. + + Returns + ------- + list[NamedSelection] + List of named selections that include the edge. + """ + included_ns = [] + for ns in get_design_from_body(self.body).named_selections: + print([face.id for face in ns.faces]) + if any(face.id == self.id for face in ns.faces): + included_ns.append(ns) + + return included_ns + @graphics_required def tessellate(self, tess_options: TessellationOptions | None = None) -> "pv.PolyData": """Tessellate the face and return the geometry as triangles. diff --git a/src/ansys/geometry/core/designer/selection.py b/src/ansys/geometry/core/designer/selection.py index ec0a365417..837f955f30 100644 --- a/src/ansys/geometry/core/designer/selection.py +++ b/src/ansys/geometry/core/designer/selection.py @@ -35,6 +35,7 @@ get_beams_from_ids, get_bodies_from_ids, get_components_from_ids, + get_design_points_from_ids, get_edges_from_ids, get_faces_from_ids, get_vertices_from_ids, @@ -73,6 +74,8 @@ class NamedSelection: All design points to include in the named selection. components: list[Component], default: None All components to include in the named selection. + vertices: list[Vertex], default: None + All vertices to include in the named selection. """ def __init__( @@ -212,10 +215,10 @@ def design_points(self) -> list[DesignPoint]: self.__verify_ns() if self._design_points is None: # Get all design points from the named selection - self._design_points = [ - DesignPoint(dp_id, f"dp: {dp_id}", dp_point) - for dp_id, dp_point in self._ids_cached["design_points"] - ] + self._design_points = get_design_points_from_ids( + self._design, + self._ids_cached["design_points"], + ) return self._design_points diff --git a/src/ansys/geometry/core/designer/vertex.py b/src/ansys/geometry/core/designer/vertex.py index d8c1481a9f..f5afa20b5c 100644 --- a/src/ansys/geometry/core/designer/vertex.py +++ b/src/ansys/geometry/core/designer/vertex.py @@ -21,11 +21,18 @@ # SOFTWARE. """Module for managing a vertex.""" +from typing import TYPE_CHECKING + import numpy as np from ansys.geometry.core.math.point import Point3D +from ansys.geometry.core.misc.auxiliary import get_design_from_body from ansys.geometry.core.typing import RealSequence +if TYPE_CHECKING: # pragma: no cover + from ansys.geometry.core.designer.body import Body + from ansys.geometry.core.designer.selection import NamedSelection + class Vertex(Point3D): """Represents a single vertex of a body within the design assembly. @@ -44,6 +51,7 @@ def __new__( cls, id: str, position: np.ndarray | RealSequence, + body: "Body", ): """Initialize ``Vertex`` class.""" # Only pass position and unit to Point3D.__new__ @@ -54,9 +62,11 @@ def __init__( self, id: str, position: np.ndarray | RealSequence, + body: "Body", ): """Initialize the Vertex with a unique identifier.""" self._id = id + self._body = body super().__init__(position) # Make immutable @@ -67,9 +77,30 @@ def id(self) -> str: """Get the unique identifier of the vertex.""" return self._id + @property + def body(self) -> "Body": + """Get the body this vertex belongs to.""" + return self._body + + def get_named_selections(self) -> list["NamedSelection"]: + """Get all named selections that include this vertex. + + Returns + ------- + list["NamedSelection"] + List of named selections that include this vertex. + """ + included_ns = [] + for ns in get_design_from_body(self.body).named_selections: + if any(vertex.id == self.id for vertex in ns.vertices): + included_ns.append(ns) + + return included_ns + def __repr__(self) -> str: """Return a string representation of the vertex.""" lines = [f"ansys.geometry.core.designer.Vertex {hex(id(self))}"] lines.append(f" Id : {self.id}") - lines.append(f" Position : {self.position}") + lines.append(f" Position : {self.position}") + lines.append(f" Body Id : {self.body.id}") return "\n".join(lines) diff --git a/src/ansys/geometry/core/misc/auxiliary.py b/src/ansys/geometry/core/misc/auxiliary.py index 7f0080077c..2ddfbf73c0 100644 --- a/src/ansys/geometry/core/misc/auxiliary.py +++ b/src/ansys/geometry/core/misc/auxiliary.py @@ -28,6 +28,7 @@ from ansys.geometry.core.designer.body import Body from ansys.geometry.core.designer.component import Component from ansys.geometry.core.designer.design import Design + from ansys.geometry.core.designer.designpoint import DesignPoint from ansys.geometry.core.designer.edge import Edge from ansys.geometry.core.designer.face import Face from ansys.geometry.core.designer.vertex import Vertex @@ -142,6 +143,11 @@ def __traverse_all_beams(comp: Union["Design", "Component"]) -> list["Body"]: return __traverse_component_elem("beams", comp) +def __traverse_all_design_points(comp: Union["Design", "Component"]) -> list["DesignPoint"]: + """Traverse all design points in a design/component and all its subcomponents.""" + return __traverse_component_elem("design_points", comp) + + def get_all_bodies_from_design(design: "Design") -> list["Body"]: """Find all the ``Body`` objects inside a ``Design``. @@ -305,6 +311,31 @@ def get_beams_from_ids(design: "Design", beam_ids: list[str]) -> list["Beam"]: return [beam for beam in __traverse_all_beams(design) if beam.id in beam_ids] # noqa: E501 +def get_design_points_from_ids( + design: "Design", design_point_ids: list[str] +) -> list["DesignPoint"]: + """Find the ``DesignPoint`` objects inside a ``Design`` from its ids. + + Parameters + ---------- + design : Design + Parent design for the design points. + design_point_ids : list[str] + List of design point ids. + + Returns + ------- + list[DesignPoint] + List of DesignPoint objects. + + Notes + ----- + This method takes a design and design point ids, and gets their corresponding ``DesignPoint`` + objects. + """ + return [dp for dp in __traverse_all_design_points(design) if dp.id in design_point_ids] + + def convert_color_to_hex( color: str | tuple[float, float, float] | tuple[float, float, float, float], ) -> str: diff --git a/tests/_incompatible_tests.yml b/tests/_incompatible_tests.yml index ae4c4a61c3..79b761f236 100644 --- a/tests/_incompatible_tests.yml +++ b/tests/_incompatible_tests.yml @@ -73,6 +73,8 @@ backends: - tests/integration/test_design.py::test_named_selection_contents - tests/integration/test_design.py::test_named_selections_components - tests/integration/test_design.py::test_vertices + - tests/integration/test_design.py::test_components_get_named_selections + - tests/integration/test_design.py::test_vertices_get_named_selections # Bounding box center is only available from 25R2 onwards - tests/integration/test_design.py::test_get_body_bounding_box # Export to DSCO files is only available from 25R2 onwards @@ -195,6 +197,8 @@ backends: - tests/integration/test_design.py::test_named_selection_contents - tests/integration/test_design.py::test_named_selections_components - tests/integration/test_design.py::test_vertices + - tests/integration/test_design.py::test_components_get_named_selections + - tests/integration/test_design.py::test_vertices_get_named_selections # Bounding box center is only available from 25R2 onwards - tests/integration/test_design.py::test_get_body_bounding_box # Export to DSCO files is only available from 25R2 onwards @@ -264,6 +268,8 @@ backends: - tests/integration/test_design.py::test_named_selection_contents - tests/integration/test_design.py::test_named_selections_components - tests/integration/test_design.py::test_vertices + - tests/integration/test_design.py::test_components_get_named_selections + - tests/integration/test_design.py::test_vertices_get_named_selections # Potential problem in model/reading design for importing parameters - tests/integration/test_design.py::test_design_parameters # Bounding box center is only available from 25R2 onwards @@ -332,6 +338,8 @@ backends: - tests/integration/test_design.py::test_named_selections_components - tests/integration/test_design.py::test_component_make_independent - tests/integration/test_design.py::test_vertices + - tests/integration/test_design.py::test_components_get_named_selections + - tests/integration/test_design.py::test_vertices_get_named_selections # Insert/Import file operations caused problems prior to 26.1 - tests/integration/test_design_export.py::test_import_export_reimport_design_scdocx - tests/integration/test_design_export.py::test_import_export_reimport_design_x_t diff --git a/tests/integration/test_design.py b/tests/integration/test_design.py index fafdd557ba..dfb6eedb31 100644 --- a/tests/integration/test_design.py +++ b/tests/integration/test_design.py @@ -3840,8 +3840,6 @@ def test_vertices(modeler: Modeler, tmp_path_factory: pytest.TempPathFactory): assert design.bodies[1].vertices[0].y.magnitude == pytest.approx(-0.00288675, 1e-6, 1e-6) assert design.bodies[1].vertices[0].z.magnitude == pytest.approx(0.01, 1e-6, 1e-6) - print(design.bodies[1].vertices[0].id == "S,~sEbf61ff70-bc08-477a-8a5e-a7c7dc955f40.853__") - assert design.bodies[0].vertices == [] assert design.bodies[1].vertices[1].position == pytest.approx( Point3D([0.033, -0.0057735, 0.01]), 1e-6, 1e-6 @@ -4122,3 +4120,178 @@ def test_combine_merge(modeler: Modeler): design._update_design_inplace() assert len(design.bodies) == 1 assert box1.volume.m == pytest.approx(Quantity(2.5, UNITS.m**3).m, rel=1e-6, abs=1e-8) + + +def test_faces_get_named_selections(modeler: Modeler): + """Test getting named selections associated with faces.""" + design = modeler.create_design("faces_named_selections") + box = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1) + + # create named selection from faces + face_ns1 = [box.faces[0], box.faces[1]] + face_ns2 = [box.faces[2], box.faces[3]] + design.create_named_selection("face_ns_1", faces=face_ns1) + design.create_named_selection("face_ns_2", faces=face_ns2) + + # Check that faces return the correct named selections + for face in box.faces: + ns_list = face.get_named_selections() + if any(f.id == face.id for f in face_ns1): + assert len(ns_list) == 1 + assert any(ns.name == "face_ns_1" for ns in ns_list) + elif any(f.id == face.id for f in face_ns2): + assert len(ns_list) == 1 + assert any(ns.name == "face_ns_2" for ns in ns_list) + else: + assert len(ns_list) == 0 # No named selection for this face + + +def test_edges_get_named_selections(modeler: Modeler): + """Test getting named selections associated with edges.""" + design = modeler.create_design("edges_named_selections") + box = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1) + + # create named selection from edges + edge_ns1 = [box.edges[0], box.edges[1]] + edge_ns2 = [box.edges[2], box.edges[3]] + design.create_named_selection("edge_ns_1", edges=edge_ns1) + design.create_named_selection("edge_ns_2", edges=edge_ns2) + + # Check that edges return the correct named selections + for edge in box.edges: + ns_list = edge.get_named_selections() + if any(e.id == edge.id for e in edge_ns1): + assert len(ns_list) == 1 + assert any(ns.name == "edge_ns_1" for ns in ns_list) + elif any(e.id == edge.id for e in edge_ns2): + assert len(ns_list) == 1 + assert any(ns.name == "edge_ns_2" for ns in ns_list) + else: + assert len(ns_list) == 0 # No named selection for this edge + + +def test_body_get_named_selections(modeler: Modeler): + """Test getting named selections associated with bodies.""" + design = modeler.create_design("body_named_selections") + box1 = design.extrude_sketch("box1", Sketch().box(Point2D([0, 0]), 1, 1), 1) + box2 = design.extrude_sketch("box2", Sketch().box(Point2D([2, 2]), 1, 1), 1) + + # create named selection from bodies + design.create_named_selection("body_ns_1", bodies=[box1]) + design.create_named_selection("body_ns_2", bodies=[box2]) + + # Check that bodies return the correct named selections + for body in design.bodies: + ns_list = body.get_named_selections() + if body.id == box1.id: + assert len(ns_list) == 1 + assert any(ns.name == "body_ns_1" for ns in ns_list) + elif body.id == box2.id: + assert len(ns_list) == 1 + assert any(ns.name == "body_ns_2" for ns in ns_list) + else: + assert len(ns_list) == 0 # No named selection for this body + + +def test_beams_get_named_selections(modeler: Modeler): + """Test getting named selections associated with beams.""" + design = modeler.create_design("beam_named_selections") + profile = design.add_beam_circular_profile("profile1", Distance(0.1, UNITS.m)) + beam1 = design.create_beam(Point3D([0, 0, 0]), Point3D([1, 0, 0]), profile) + beam2 = design.create_beam(Point3D([0, 1, 0]), Point3D([1, 1, 0]), profile) + + # create named selection from beams + design.create_named_selection("beam_ns_1", beams=[beam1]) + design.create_named_selection("beam_ns_2", beams=[beam2]) + + # Check that beams return the correct named selections + for beam in design.beams: + ns_list = beam.get_named_selections() + if beam.id == beam1.id: + assert len(ns_list) == 1 + assert any(ns.name == "beam_ns_1" for ns in ns_list) + elif beam.id == beam2.id: + assert len(ns_list) == 1 + assert any(ns.name == "beam_ns_2" for ns in ns_list) + else: + assert len(ns_list) == 0 # No named selection for this beam + + +def test_vertices_get_named_selections(modeler: Modeler): + """Test getting named selections associated with vertices.""" + design = modeler.create_design("vertex_named_selections") + box = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1) + + # create named selection from vertices + vertex_ns1 = [box.vertices[0], box.vertices[1]] + vertex_ns2 = [box.vertices[2], box.vertices[3]] + vertex_ns3 = [box.vertices[4], box.vertices[5]] + vertex_ns4 = [box.vertices[4], box.vertices[5]] + + design.create_named_selection("vertex_ns_1", vertices=vertex_ns1) + design.create_named_selection("vertex_ns_2", vertices=vertex_ns2) + design.create_named_selection("vertex_ns_3", vertices=vertex_ns3) + design.create_named_selection("vertex_ns_4", vertices=vertex_ns4) + + # Check that vertices return the correct named selections + for vertex in box.vertices: + ns_list = vertex.get_named_selections() + if any(v.id == vertex.id for v in vertex_ns1): + assert len(ns_list) == 1 + assert any(ns.name == "vertex_ns_1" for ns in ns_list) + elif any(v.id == vertex.id for v in vertex_ns2): + assert len(ns_list) == 1 + assert any(ns.name == "vertex_ns_2" for ns in ns_list) + elif any(v.id == vertex.id for v in vertex_ns3): + assert len(ns_list) == 2 + assert any(ns.name == "vertex_ns_3" for ns in ns_list) + else: + assert len(ns_list) == 0 # No named selection for this vertex + + +def test_components_get_named_selections(modeler: Modeler): + """Test getting named selections associated with components.""" + design = modeler.create_design("component_named_selections") + comp1 = design.add_component("Component1") + comp2 = design.add_component("Component2") + design.add_component("Component3") + + # create named selection from components + design.create_named_selection("component_ns_1", components=[comp1]) + design.create_named_selection("component_ns_2", components=[comp2]) + + # Check that components return the correct named selections + for component in design.components: + ns_list = component.get_named_selections() + if component.id == comp1.id: + assert len(ns_list) == 1 + assert any(ns.name == "component_ns_1" for ns in ns_list) + elif component.id == comp2.id: + assert len(ns_list) == 1 + assert any(ns.name == "component_ns_2" for ns in ns_list) + else: + assert len(ns_list) == 0 # No named selection for this component + + +def test_design_point_get_named_selections(modeler: Modeler): + """Test getting named selections associated with design points.""" + design = modeler.create_design("design_point_named_selections") + dp1 = design.add_design_point("DesignPoint1", Point3D([0, 0, 0])) + dp2 = design.add_design_point("DesignPoint2", Point3D([1, 1, 1])) + design.add_design_point("DesignPoint3", Point3D([2, 2, 2])) + + # create named selection from design points + design.create_named_selection("design_point_ns_1", design_points=[dp1]) + design.create_named_selection("design_point_ns_2", design_points=[dp2]) + + # Check that design points return the correct named selections + for design_point in design.design_points: + ns_list = design_point.get_named_selections() + if design_point.id == dp1.id: + assert len(ns_list) == 1 + assert any(ns.name == "design_point_ns_1" for ns in ns_list) + elif design_point.id == dp2.id: + assert len(ns_list) == 1 + assert any(ns.name == "design_point_ns_2" for ns in ns_list) + else: + assert len(ns_list) == 0 # No named selection for this design point