From 8dda53e86a8d6159a2e0a80e9b684043bdad4e2c Mon Sep 17 00:00:00 2001 From: Jacob Kerstetter Date: Wed, 13 Aug 2025 15:43:20 -0400 Subject: [PATCH 1/6] body remove_assigned_material working and tested --- .../core/_grpc/_services/base/bodies.py | 5 + .../core/_grpc/_services/v0/bodies.py | 15 + .../core/_grpc/_services/v1/bodies.py | 4 + src/ansys/geometry/core/designer/body.py | 13 + tests/integration/test_design.py | 3521 +++++++---------- 5 files changed, 1513 insertions(+), 2045 deletions(-) diff --git a/src/ansys/geometry/core/_grpc/_services/base/bodies.py b/src/ansys/geometry/core/_grpc/_services/base/bodies.py index 42bf6b69ad..0901bace56 100644 --- a/src/ansys/geometry/core/_grpc/_services/base/bodies.py +++ b/src/ansys/geometry/core/_grpc/_services/base/bodies.py @@ -144,6 +144,11 @@ def get_assigned_material(self, **kwargs) -> dict: """Get the assigned material of a body.""" pass + @abstractmethod + def remove_assigned_material(self, **kwargs) -> dict: + """Remove the assigned material of a body.""" + pass + @abstractmethod def set_name(self, **kwargs) -> dict: """Set the name of a body.""" diff --git a/src/ansys/geometry/core/_grpc/_services/v0/bodies.py b/src/ansys/geometry/core/_grpc/_services/v0/bodies.py index 03b08e8d21..b585c73301 100644 --- a/src/ansys/geometry/core/_grpc/_services/v0/bodies.py +++ b/src/ansys/geometry/core/_grpc/_services/v0/bodies.py @@ -452,6 +452,21 @@ def get_assigned_material(self, **kwargs) -> dict: # noqa: D102 # Return the response - formatted as a dictionary return {"material": from_grpc_material_to_material(resp)} + + @protect_grpc + def remove_assigned_material(self, **kwargs) -> dict: # noqa: D102 + from ansys.api.geometry.v0.bodies_pb2 import RemoveAssignedMaterialRequest + + # Create the request - assumes all inputs are valid and of the proper type + request = RemoveAssignedMaterialRequest(ids=[build_grpc_id(id) for id in kwargs["ids"]]) + + # Call the gRPC service + resp = self.stub.RemoveAssignedMaterial(request=request) + + # Return the response - formatted as a dictionary + return { + "successfully_removed": [id for id in resp.successfully_removed] + } @protect_grpc def set_name(self, **kwargs) -> dict: # noqa: D102 diff --git a/src/ansys/geometry/core/_grpc/_services/v1/bodies.py b/src/ansys/geometry/core/_grpc/_services/v1/bodies.py index 62e6cf1b4a..85c2892c76 100644 --- a/src/ansys/geometry/core/_grpc/_services/v1/bodies.py +++ b/src/ansys/geometry/core/_grpc/_services/v1/bodies.py @@ -131,6 +131,10 @@ def set_assigned_material(self, **kwargs) -> dict: # noqa: D102 @protect_grpc def get_assigned_material(self, **kwargs) -> dict: # noqa: D102 raise NotImplementedError + + @protect_grpc + def remove_assigned_material(self, **kwargs): # noqa: D102 + raise NotImplementedError @protect_grpc def set_name(self, **kwargs) -> dict: # noqa: D102 diff --git a/src/ansys/geometry/core/designer/body.py b/src/ansys/geometry/core/designer/body.py index c40589f5fd..09bbeb6e48 100644 --- a/src/ansys/geometry/core/designer/body.py +++ b/src/ansys/geometry/core/designer/body.py @@ -337,6 +337,11 @@ def get_assigned_material(self) -> Material: Material assigned to the body. """ return + + @abstractmethod + def remove_assigned_material(self) -> None: + """Remove the material assigned to the body.""" + return @abstractmethod def add_midsurface_thickness(self, thickness: Quantity) -> None: @@ -1060,6 +1065,10 @@ def get_assigned_material(self) -> Material: # noqa: D102 self._grpc_client.log.debug(f"Retrieving assigned material for body {self.id}.") response = self._grpc_client.services.bodies.get_assigned_material(id=self.id) return response.get("material") + + def remove_assigned_material(self) -> None: # noqa: D102 + self._grpc_client.log.debug(f"Removing assigned material for body {self.id}.") + self._grpc_client.services.bodies.remove_assigned_material(ids=[self.id]) @protect_grpc @check_input_types @@ -1592,6 +1601,10 @@ def assign_material(self, material: Material) -> None: # noqa: D102 @ensure_design_is_active def get_assigned_material(self) -> Material: # noqa: D102 return self._template.get_assigned_material() + + @ensure_design_is_active + def remove_assigned_material(self): # noqa: D102 + self._template.remove_assigned_material() @ensure_design_is_active def add_midsurface_thickness(self, thickness: Quantity) -> None: # noqa: D102 diff --git a/tests/integration/test_design.py b/tests/integration/test_design.py index 17c2a5c3ba..e002556fb7 100644 --- a/tests/integration/test_design.py +++ b/tests/integration/test_design.py @@ -320,6 +320,37 @@ def test_get_empty_material(modeler: Modeler): assert len(mat_service.properties) == 1 +def test_remove_material_from_body(modeler: Modeler): + """Test removing a material from a body.""" + # Create a design and a sketch + design = modeler.create_design("RemoveMaterialTest") + sketch = Sketch() + sketch.circle(Point2D([0, 0], UNITS.mm), Quantity(10, UNITS.mm)) + + # Extrude the sketch to create a body + body = design.extrude_sketch("CircleBody", sketch, Quantity(10, UNITS.mm)) + + # Create and assign a material + density = Quantity(7850, UNITS.kg / (UNITS.m ** 3)) + material = Material( + "Steel", + density, + [MaterialProperty(MaterialPropertyType.POISSON_RATIO, "Poisson", Quantity(0.3))], + ) + design.add_material(material) + body.assign_material(material) + assert body.material.name == "Steel" + + # Remove the material from the body + body.remove_assigned_material() + + # Check that the body no longer has a material assigned + assert body.material.name == "" + assert len(body.material.properties) == 1 + assert body.material.properties[MaterialPropertyType.DENSITY].quantity == Quantity( + 0, UNITS.kg / (UNITS.m ** 3)) + + def test_face_to_body_creation(modeler: Modeler): """Test in charge of validating the extrusion of an existing face.""" # Create a Sketch and draw a circle (all client side) @@ -850,1769 +881,1777 @@ def test_delete_body_component(modeler: Modeler): _ = comp_2.add_component("Nested_1_Component_2") # Create the bodies - body_1 = comp_3.extrude_sketch(name="comp_3_circle", sketch=sketch, distance=distance) - body_2 = nested_2_comp_1.extrude_sketch( + _ = comp_3.extrude_sketch(name="comp_3_circle", sketch=sketch, distance=distance) + _ = nested_2_comp_1.extrude_sketch( name="nested_2_comp_1_circle", sketch=sketch, distance=distance ) _ = nested_1_nested_1_comp_1.extrude_sketch( name="nested_1_nested_1_comp_1_circle", sketch=sketch, distance=distance ) - # Let's start by doing something impossible - trying to delete body_1 from comp_1 - comp_1.delete_body(body_1) - - # Check that all the underlying objects are still alive - assert comp_1.is_alive - assert comp_1.components[0].is_alive - assert comp_1.components[0].components[0].is_alive - assert comp_1.components[0].components[0].bodies[0].is_alive - assert comp_1.components[1].is_alive - assert comp_1.components[1].bodies[0].is_alive - assert comp_2.is_alive - assert comp_2.components[0].is_alive - assert comp_3.is_alive - assert comp_3.bodies[0].is_alive - - # Do the same checks but calling them from the design object - assert design.is_alive - assert design.components[0].is_alive - assert design.components[0].components[0].is_alive - assert design.components[0].components[0].components[0].is_alive - assert design.components[0].components[0].components[0].bodies[0].is_alive - assert design.components[0].components[1].is_alive - assert design.components[0].components[1].bodies[0].is_alive - assert design.components[1].is_alive - assert design.components[1].components[0].is_alive - assert design.components[2].is_alive - assert design.components[2].bodies[0].is_alive - - # Let's do another impossible thing - trying to delete comp_3 from comp_1 - comp_1.delete_component(comp_3) - - # Check that all the underlying objects are still alive - assert comp_1.is_alive - assert comp_1.components[0].is_alive - assert comp_1.components[0].components[0].is_alive - assert comp_1.components[0].components[0].bodies[0].is_alive - assert comp_1.components[1].is_alive - assert comp_1.components[1].bodies[0].is_alive - assert comp_2.is_alive - assert comp_2.components[0].is_alive - assert comp_3.is_alive - assert comp_3.bodies[0].is_alive - - # Do the same checks but calling them from the design object - assert design.is_alive - assert design.components[0].is_alive - assert design.components[0].components[0].is_alive - assert design.components[0].components[0].components[0].is_alive - assert design.components[0].components[0].components[0].bodies[0].is_alive - assert design.components[0].components[1].is_alive - assert design.components[0].components[1].bodies[0].is_alive - assert design.components[1].is_alive - assert design.components[1].components[0].is_alive - assert design.components[2].is_alive - assert design.components[2].bodies[0].is_alive - - # Let's delete now the entire comp_2 component - comp_2.delete_component(comp_2) - - # Check that all the underlying objects are still alive except for comp_2 - assert comp_1.is_alive - assert comp_1.components[0].is_alive - assert comp_1.components[0].components[0].is_alive - assert comp_1.components[0].components[0].bodies[0].is_alive - assert comp_1.components[1].is_alive - assert comp_1.components[1].bodies[0].is_alive - assert not comp_2.is_alive - assert not comp_2.components[0].is_alive - assert comp_3.is_alive - assert comp_3.bodies[0].is_alive - - # Do the same checks but calling them from the design object - assert design.is_alive - assert design.components[0].is_alive - assert design.components[0].components[0].is_alive - assert design.components[0].components[0].components[0].is_alive - assert design.components[0].components[0].components[0].bodies[0].is_alive - assert design.components[0].components[1].is_alive - assert design.components[0].components[1].bodies[0].is_alive - assert not design.components[1].is_alive - assert not design.components[1].components[0].is_alive - assert design.components[2].is_alive - assert design.components[2].bodies[0].is_alive - - # Let's delete now the body_2 object - design.delete_body(body_2) - - # Check that all the underlying objects are still alive except for comp_2 and body_2 - assert comp_1.is_alive - assert comp_1.components[0].is_alive - assert comp_1.components[0].components[0].is_alive - assert comp_1.components[0].components[0].bodies[0].is_alive - assert comp_1.components[1].is_alive - assert not body_2.is_alive - assert not comp_2.is_alive - assert not comp_2.components[0].is_alive - assert comp_3.is_alive - - # Do the same checks but calling them from the design object - assert design.is_alive - assert design.components[0].is_alive - assert design.components[0].components[0].is_alive - assert design.components[0].components[0].components[0].is_alive - assert design.components[0].components[0].components[0].bodies[0].is_alive - assert design.components[0].components[1].is_alive - assert not design.components[1].is_alive - assert not design.components[1].components[0].is_alive - assert design.components[2].is_alive - assert design.components[2].bodies[0].is_alive - - # Finally, let's delete the most complex one - comp_1 - design.delete_component(comp_1) - - # Check that all the underlying objects are still alive except for comp_2, body_2 and comp_1 - assert not comp_1.is_alive - assert not comp_1.components[0].is_alive - assert not comp_1.components[0].components[0].is_alive - assert not comp_1.components[1].is_alive - assert not comp_2.is_alive - assert not comp_2.components[0].is_alive - assert comp_3.is_alive - assert comp_3.bodies[0].is_alive - - # Do the same checks but calling them from the design object - assert design.is_alive - assert not design.components[0].is_alive - assert not design.components[0].components[0].is_alive - assert not design.components[0].components[0].components[0].is_alive - assert not design.components[0].components[1].is_alive - assert not design.components[1].is_alive - assert not design.components[1].components[0].is_alive - assert design.components[2].is_alive - assert design.components[2].bodies[0].is_alive - - # Finally, let's delete the entire design - design.delete_component(comp_3) - - # Check everything is dead - assert design.is_alive - assert not design.components[0].is_alive - assert not design.components[0].components[0].is_alive - assert not design.components[0].components[0].components[0].is_alive - assert not design.components[0].components[1].is_alive - assert not design.components[1].is_alive - assert not design.components[1].components[0].is_alive - assert not design.components[2].is_alive - - # Try deleting the Design object itself - this is forbidden - with pytest.raises(ValueError, match="The design itself cannot be deleted."): - design.delete_component(design) - - # Let's try out the representation methods - design_str = str(design) - assert "ansys.geometry.core.designer.Design" in design_str - assert "Name : Deletion_Test" in design_str - assert "N Bodies : 0" in design_str - assert "N Components : 0" in design_str - assert "N Coordinate Systems : 0" in design_str - assert "N Named Selections : 0" in design_str - assert "N Materials : 0" in design_str - assert "N Beam Profiles : 0" in design_str - assert "N Design Points : 0" in design_str - - comp_1_str = str(comp_1) - assert "ansys.geometry.core.designer.Component" in comp_1_str - assert "Name : Component_1" in comp_1_str - assert "Exists : False" in comp_1_str - assert "Parent component : Deletion_Test" in comp_1_str - assert "N Bodies : 0" in comp_1_str - assert "N Beams : 0" in comp_1_str - assert "N Components : 0" in comp_1_str - assert "N Design Points : 0" in comp_1_str - assert "N Coordinate Systems : 0" in comp_1_str - - body_1_str = str(body_1) - assert "ansys.geometry.core.designer.Body" in body_1_str - assert "Name : comp_3_circle" in body_1_str - assert "Exists : False" in body_1_str - assert "Surface body : False" in body_1_str - assert "Parent component : Component_3" in body_1_str - assert "Color : None" in body_1_str - - -def test_shared_topology(modeler: Modeler): - """Test for checking the correct setting of shared topology on the server. + # Create beams (in design) + circle_profile_1 = design.add_beam_circular_profile( + "CircleProfile1", Quantity(10, UNITS.mm), Point3D([0, 0, 0]), UNITVECTOR3D_X, UNITVECTOR3D_Y + ) + _ = design.create_beam( + Point3D([9, 99, 999], UNITS.mm), Point3D([8, 88, 888], UNITS.mm), circle_profile_1 + ) - Notes - ----- - Requires storing scdocx file and checking manually (for now). - """ - # Create your design on the server side - design = modeler.create_design("SharedTopology_Test") + # Test the tree print - by default + ################################## + lines = design.tree_print(return_list=True) + ref = [ + ">>> Tree print view of component 'TreePrintComponent'", + "", + "Location", + "--------", + "Root component (Design)", + "", + "Subtree", + "-------", + "(comp) TreePrintComponent", + "|---(beam) 0:", + "|---(comp) Component_1", + ": |---(comp) Nested_1_Component_1", + ": : |---(comp) Nested_1_Nested_1_Component_1", + ": : |---(body) nested_1_nested_1_comp_1_circle", + ": |---(comp) Nested_2_Component_1", + ": |---(body) nested_2_comp_1_circle", + "|---(comp) Component_2", + ": |---(comp) Nested_1_Component_2", + "|---(comp) Component_3", + " |---(body) comp_3_circle", + ] + assert check_list_equality(lines, ref) is True - # Create a Sketch object and draw a circle (all client side) - sketch = Sketch() - sketch.circle(Point2D([-30, -30], UNITS.mm), Quantity(10, UNITS.mm)) - distance = Quantity(30, UNITS.mm) + # Test - request depth 1, and show only components + ################################################## + lines = design.tree_print( + return_list=True, depth=1, consider_bodies=False, consider_beams=False + ) + ref = [ + ">>> Tree print view of component 'TreePrintComponent'", + "", + "Location", + "--------", + "Root component (Design)", + "", + "Subtree", + "-------", + "(comp) TreePrintComponent", + "|---(comp) Component_1", + "|---(comp) Component_2", + "|---(comp) Component_3", + ] + assert check_list_equality(lines, ref) is True - # Create a component - comp_1 = design.add_component("Component_1") - comp_1.extrude_sketch(name="Body_1", sketch=sketch, distance=distance) + # Test - request depth 2, indent 1 (which will default to 2) + # and sort the components alphabetically + ############################################################ + lines = design.tree_print(return_list=True, depth=2, indent=1, sort_keys=True) + ref = [ + ">>> Tree print view of component 'TreePrintComponent'", + "", + "Location", + "--------", + "Root component (Design)", + "", + "Subtree", + "-------", + "(comp) TreePrintComponent", + "|-(beam) 0:", + "|-(comp) Component_1", + ": |-(comp) Nested_1_Component_1", + ": |-(comp) Nested_2_Component_1", + "|-(comp) Component_2", + ": |-(comp) Nested_1_Component_2", + "|-(comp) Component_3", + " |-(body) comp_3_circle", + ] + assert check_list_equality(lines, ref) is True + + # Test - request from Nested_1_Component_1 + ########################################## + lines = nested_1_comp_1.tree_print(return_list=True) + ref = [ + ">>> Tree print view of component 'Nested_1_Component_1'", + "", + "Location", + "--------", + "TreePrintComponent > Component_1 > Nested_1_Component_1", + "", + "Subtree", + "-------", + "(comp) Nested_1_Component_1", + "|---(comp) Nested_1_Nested_1_Component_1", + " |---(body) nested_1_nested_1_comp_1_circle", + ] + assert check_list_equality(lines, ref) is True - # Now that the component is created, let's try to assign a SharedTopology - assert comp_1.shared_topology is None - # Set the shared topology - comp_1.set_shared_topology(SharedTopologyType.SHARETYPE_SHARE) - assert comp_1.shared_topology == SharedTopologyType.SHARETYPE_SHARE +def test_surface_body_creation(modeler: Modeler): + """Test surface body creation from trimmed surfaces.""" + design = modeler.create_design("Design1") + + # half sphere + surface = Sphere([0, 0, 0], 1) + trimmed_surface = surface.trim(BoxUV(Interval(0, np.pi * 2), Interval(0, np.pi / 2))) + body = design.create_body_from_surface("sphere", trimmed_surface) + assert len(design.bodies) == 1 + assert body.is_surface + assert body.faces[0].area.m == pytest.approx(np.pi * 2) - # Try to assign it to the entire design - assert design.shared_topology is None - with pytest.raises(ValueError, match="The design itself cannot have a shared topology."): - design.set_shared_topology(SharedTopologyType.SHARETYPE_NONE) + # cylinder + surface = Cylinder([0, 0, 0], 1) + trimmed_surface = surface.trim(BoxUV(Interval(0, np.pi * 2), Interval(0, 1))) + body = design.create_body_from_surface("cylinder", trimmed_surface) + assert len(design.bodies) == 2 + assert body.is_surface + assert body.faces[0].area.m == pytest.approx(np.pi * 2) -def test_single_body_translation(modeler: Modeler): - """Test for verifying the correct translation of a ``Body``. + # cone + surface = Cone([0, 0, 0], 1, np.pi / 4) + trimmed_surface = surface.trim(BoxUV(Interval(0, np.pi * 2), Interval(surface.apex.z.m, 0))) + body = design.create_body_from_surface("cone", trimmed_surface) - Notes - ----- - Requires storing scdocx file and checking manually (for now). - """ - # Create your design on the server side - design = modeler.create_design("SingleBodyTranslation_Test") + assert len(design.bodies) == 3 + assert body.is_surface + assert body.faces[0].area.m == pytest.approx(4.44288293816) - # Create 2 Sketch objects and draw a circle and a polygon (all client side) - sketch_1 = Sketch() - sketch_1.circle(Point2D([10, 10], UNITS.mm), Quantity(10, UNITS.mm)) - sketch_2 = Sketch() - sketch_2.polygon(Point2D([-30, -30], UNITS.mm), Quantity(10, UNITS.mm), sides=5) + # half torus + surface = Torus([0, 0, 0], 2, 1) + trimmed_surface = surface.trim(BoxUV(Interval(0, np.pi), Interval(0, np.pi * 2))) + body = design.create_body_from_surface("torus", trimmed_surface) - # Build 2 independent components and bodies - circle_comp = design.add_component("CircleComponent") - body_circle_comp = circle_comp.extrude_sketch("Circle", sketch_1, Quantity(50, UNITS.mm)) - polygon_comp = design.add_component("PolygonComponent") - body_polygon_comp = polygon_comp.extrude_sketch("Polygon", sketch_2, Quantity(30, UNITS.mm)) + assert len(design.bodies) == 4 + assert body.is_surface + assert body.faces[0].area.m == pytest.approx(39.4784176044) - body_circle_comp.translate(UnitVector3D([1, 0, 0]), Distance(50, UNITS.mm)) - body_polygon_comp.translate(UnitVector3D([-1, 1, -1]), Quantity(88, UNITS.mm)) - body_polygon_comp.translate(UnitVector3D([-1, 1, -1]), 101) + # SOLID BODIES + # sphere + surface = Sphere([0, 0, 0], 1) + trimmed_surface = surface.trim(BoxUV(Interval(0, np.pi * 2), Interval(-np.pi / 2, np.pi / 2))) + body = design.create_body_from_surface("sphere_solid", trimmed_surface) + assert len(design.bodies) == 5 + assert not body.is_surface + assert body.faces[0].area.m == pytest.approx(np.pi * 4) -def test_bodies_translation(modeler: Modeler): - """Test for verifying the correct translation of list of ``Body``. + # torus + surface = Torus([0, 0, 0], 2, 1) + trimmed_surface = surface.trim(BoxUV(Interval(0, np.pi * 2), Interval(0, np.pi * 2))) + body = design.create_body_from_surface("torus_solid", trimmed_surface) - Notes - ----- - Requires storing scdocx file and checking manually (for now). - """ - # Create your design on the server side - design = modeler.create_design("MultipleBodyTranslation_Test") + assert len(design.bodies) == 6 + assert not body.is_surface + assert body.faces[0].area.m == pytest.approx(39.4784176044 * 2) - # Create 2 Sketch objects and draw a circle and a polygon (all client side) - sketch_1 = Sketch() - sketch_1.circle(Point2D([10, 10], UNITS.mm), Quantity(10, UNITS.mm)) - sketch_2 = Sketch() - sketch_2.polygon(Point2D([-30, -30], UNITS.mm), Quantity(10, UNITS.mm), sides=5) - # Build 2 independent components and bodies - circle_comp = design.add_component("CircleComponent") - body_circle_comp = circle_comp.extrude_sketch("Circle", sketch_1, Quantity(50, UNITS.mm)) - polygon_comp = design.add_component("PolygonComponent") - body_polygon_comp = polygon_comp.extrude_sketch("Polygon", sketch_2, Quantity(30, UNITS.mm)) +def test_create_surface_from_nurbs_sketch(modeler: Modeler): + """Test creating a surface from a NURBS sketch.""" + design = modeler.create_design("NURBS_Sketch_Surface") - design.translate_bodies( - [body_circle_comp, body_polygon_comp], UnitVector3D([1, 0, 0]), Distance(48, UNITS.mm) + # Create a NURBS sketch + sketch = Sketch() + sketch.nurbs_from_2d_points( + points=[ + Point2D([0, 0]), + Point2D([1, 0]), + Point2D([1, 1]), + Point2D([0, 1]), + ], + tag="nurbs_sketch", ) - design.translate_bodies( - [body_circle_comp, body_polygon_comp], UnitVector3D([0, -1, 1]), Quantity(88, UNITS.mm) + sketch.segment( + start=Point2D([0, -1]), + end=Point2D([0, 2]), + tag="segment_1", ) - design.translate_bodies([body_circle_comp, body_polygon_comp], UnitVector3D([0, -1, 1]), 101) - # Try translating a body that does not belong to this component - no error thrown, - # but no operation performed either. - circle_comp.translate_bodies( - [body_polygon_comp], UnitVector3D([0, -1, 1]), Quantity(88, UNITS.mm) + # Create a surface from the NURBS sketch + surface_body = design.create_surface( + name="nurbs_surface", + sketch=sketch, ) + assert len(design.bodies) == 1 + assert surface_body.is_surface + assert surface_body.faces[0].area.m > 0 -def test_body_rotation(modeler: Modeler): - """Test for verifying the correct rotation of a ``Body``.""" - # Create your design on the server side - design = modeler.create_design("BodyRotation_Test") - - body = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1) - original_vertices = [] - for edge in body.edges: - original_vertices.extend([edge.shape.start, edge.shape.end]) +def test_design_parameters(modeler: Modeler): + """Test the design parameter's functionality.""" + design = modeler.open_file(FILES_DIR / "blockswithparameters.dsco") + test_parameters = design.parameters - body.rotate(Point3D([0, 0, 0]), UnitVector3D([0, 0, 1]), np.pi / 4) + # Verify the initial parameters + assert len(test_parameters) == 2 + assert test_parameters[0].name == "p1" + assert abs(test_parameters[0].dimension_value - 0.00010872999999999981) < 1e-8 + assert test_parameters[0].dimension_type == ParameterType.DIMENSIONTYPE_AREA - new_vertices = [] - for edge in body.edges: - new_vertices.extend([edge.shape.start, edge.shape.end]) + assert test_parameters[1].name == "p2" + assert abs(test_parameters[1].dimension_value - 0.0002552758322160813) < 1e-8 + assert test_parameters[1].dimension_type == ParameterType.DIMENSIONTYPE_AREA - # Make sure no vertices are in the same position as in before rotation - for old_vertex, new_vertex in zip(original_vertices, new_vertices): - assert not np.allclose(old_vertex, new_vertex) + # Update the second parameter and verify the status + test_parameters[1].dimension_value = 0.0006 + status = design.set_parameter(test_parameters[1]) + assert status == ParameterUpdateStatus.SUCCESS + # Attempt to update the first parameter and expect a constrained status + test_parameters[0].dimension_value = 0.0006 + status = design.set_parameter(test_parameters[0]) + assert status == ParameterUpdateStatus.CONSTRAINED_PARAMETERS -def test_download_file(modeler: Modeler, tmp_path_factory: pytest.TempPathFactory): - """Test for downloading a design in multiple modes and verifying the - correct download. - """ - # Create your design on the server side - design = modeler.create_design("MultipleBodyTranslation_Test") + test_parameters[0].name = "NewName" + assert test_parameters[0].name == "NewName" - # Create a Sketch object and draw a circle - sketch = Sketch() - sketch.circle(Point2D([10, 10], UNITS.mm), Quantity(10, UNITS.mm)) + test_parameters[0].dimension_type = ParameterType.DIMENSIONTYPE_AREA + assert test_parameters[0].dimension_type == ParameterType.DIMENSIONTYPE_AREA - # Extrude the sketch - design.extrude_sketch(name="MyCylinder", sketch=sketch, distance=Quantity(50, UNITS.mm)) - # Download the design - file = tmp_path_factory.mktemp("scdoc_files_download") / "dummy_folder" / "cylinder.scdocx" - design.download(file) +def test_cached_bodies(modeler: Modeler): + """Test that bodies are cached correctly. - # Check that the file exists - assert file.exists() - - # Check that we can also save it (even if it is not accessible on the server) - if BackendType.is_linux_service(modeler.client.backend_type): - file_save = "/tmp/cylinder-temp.scdocx" - else: - file_save = tmp_path_factory.mktemp("scdoc_files_save") / "cylinder.scdocx" - - design.save(file_location=file_save) + Whenever a new body is created, modified etc. we should make sure that the cache is updated. + """ + design = modeler.create_design("ModelingDemo") - # Check for other exports - Windows backend... - if not BackendType.is_core_service(modeler.client.backend_type): - binary_parasolid_file = tmp_path_factory.mktemp("scdoc_files_download") / "cylinder.x_b" - text_parasolid_file = tmp_path_factory.mktemp("scdoc_files_download") / "cylinder.x_t" + # Define a sketch + origin = Point3D([0, 0, 10]) + plane = Plane(origin, direction_x=[1, 0, 0], direction_y=[0, 1, 0]) - # Windows-only HOOPS exports for now - step_file = tmp_path_factory.mktemp("scdoc_files_download") / "cylinder.stp" - design.download(step_file, format=DesignFileFormat.STEP) - assert step_file.exists() + # Create a sketch + sketch_box = Sketch(plane) + sketch_box.box(Point2D([20, 20]), 30 * UNITS.m, 30 * UNITS.m) - iges_file = tmp_path_factory.mktemp("scdoc_files_download") / "cylinder.igs" - design.download(iges_file, format=DesignFileFormat.IGES) - assert iges_file.exists() + sketch_cylinder = Sketch(plane) + sketch_cylinder.circle(Point2D([20, 20]), 5 * UNITS.m) - # Linux backend... - else: - binary_parasolid_file = tmp_path_factory.mktemp("scdoc_files_download") / "cylinder.xmt_bin" - text_parasolid_file = tmp_path_factory.mktemp("scdoc_files_download") / "cylinder.xmt_txt" + design.extrude_sketch(name="BoxBody", sketch=sketch_box, distance=Distance(30, unit=UNITS.m)) + design.extrude_sketch( + name="CylinderBody", + sketch=sketch_cylinder, + distance=Distance(60, unit=UNITS.m), + ) - # FMD - fmd_file = tmp_path_factory.mktemp("scdoc_files_download") / "cylinder.fmd" - design.download(fmd_file, format=DesignFileFormat.FMD) - assert fmd_file.exists() + my_bodies = design.bodies + my_bodies_2 = design.bodies - # PMDB - pmdb_file = tmp_path_factory.mktemp("scdoc_files_download") / "cylinder.pmdb" + # We should make sure that the object memory addresses are the same + for body1, body2 in zip(my_bodies, my_bodies_2): + assert body1 is body2 # We are comparing the memory addresses + assert id(body1) == id(body2) - design.download(binary_parasolid_file, format=DesignFileFormat.PARASOLID_BIN) - design.download(text_parasolid_file, format=DesignFileFormat.PARASOLID_TEXT) - design.download(pmdb_file, format=DesignFileFormat.PMDB) + design.extrude_sketch( + name="CylinderBody2", + sketch=sketch_cylinder, + distance=Distance(20, unit=UNITS.m), + direction="-", + ) + my_bodies_3 = design.bodies - assert binary_parasolid_file.exists() - assert text_parasolid_file.exists() - assert pmdb_file.exists() + for body1, body3 in zip(my_bodies, my_bodies_3): + assert body1 is not body3 + assert id(body1) != id(body3) -def test_upload_file(modeler: Modeler, tmp_path_factory: pytest.TempPathFactory): - """Test uploading a file to the server.""" - file = tmp_path_factory.mktemp("test_design") / "upload_example.scdocx" - file_size = 1024 +def test_extrude_sketch_with_cut_request(modeler: Modeler): + """Test the cut argument when performing a sketch extrusion. - # Write random bytes - with file.open(mode="wb") as fout: - fout.write(os.urandom(file_size)) + This method mimics a cut operation. - assert file.exists() + Behind the scenes, a subtraction operation is performed on the bodies. After extruding the + sketch, the resulting body should be a cut body. + """ + # Define a sketch + origin = Point3D([0, 0, 10]) + plane = Plane(origin, direction_x=[1, 0, 0], direction_y=[0, 1, 0]) - # Upload file - path_on_server = modeler._upload_file(file) - assert path_on_server is not None + # Create a sketch + sketch_box = Sketch(plane) + sketch_box.box(Point2D([20, 20]), 30 * UNITS.m, 30 * UNITS.m) + sketch_cylinder = Sketch(plane) + sketch_cylinder.circle(Point2D([20, 20]), 5 * UNITS.m) -def test_stream_upload_file(tmp_path_factory: pytest.TempPathFactory): - """Test uploading a file to the server.""" - # Define a new maximum message length - import ansys.geometry.core.connection.defaults as pygeom_defaults + # Create a design + design = modeler.create_design("ExtrudeSketchWithCut") - old_value = pygeom_defaults.MAX_MESSAGE_LENGTH - try: - # Set the new maximum message length - pygeom_defaults.MAX_MESSAGE_LENGTH = 1024**2 # 1 MB + box_body = design.extrude_sketch( + name="BoxBody", sketch=sketch_box, distance=Distance(30, unit=UNITS.m) + ) + design.extrude_sketch( + name="CylinderBody", sketch=sketch_cylinder, distance=Distance(60, unit=UNITS.m), cut=True + ) - file = tmp_path_factory.mktemp("test_design") / "upload_stream_example.scdocx" - file_size = 5 * 1024**2 # stream five messages + # Verify there is only one body + assert len(design.bodies) == 1 - # Write random bytes - with file.open(mode="wb") as fout: - fout.write(os.urandom(file_size)) - assert file.exists() + # Verify the volume of the resulting body is less than the volume of the box + assert design.bodies[0].volume.m < box_body.volume.m - # Upload file - necessary to import the Modeler class and create an instance - from ansys.geometry.core import Modeler - modeler = Modeler() - path_on_server = modeler._upload_file_stream(file) - assert path_on_server is not None - finally: - pygeom_defaults.MAX_MESSAGE_LENGTH = old_value +def test_extrude_sketch_with_cut_request_no_collision(modeler: Modeler): + """Test the cut argument when performing a sketch extrusion (with no collision). + This method mimics an unsuccessful cut operation. -def test_slot_extrusion(modeler: Modeler): - """Test the extrusion of a slot.""" - # Create your design on the server side - design = modeler.create_design("ExtrudeSlot") + The sketch extrusion should not result in a cut body since there is no collision between the + original body and the extruded body. + """ + # Define a sketch + origin = Point3D([0, 0, 10]) + plane = Plane(origin, direction_x=[1, 0, 0], direction_y=[0, 1, 0]) - # Create a Sketch object and draw a slot - sketch = Sketch() - sketch.slot(Point2D([10, 10], UNITS.mm), Quantity(10, UNITS.mm), Quantity(5, UNITS.mm)) + # Create a sketch + sketch_box = Sketch(plane) + sketch_box.box(Point2D([20, 20]), 30 * UNITS.m, 30 * UNITS.m) - # Extrude the sketch - body = design.extrude_sketch(name="MySlot", sketch=sketch, distance=Distance(50, UNITS.mm)) + sketch_cylinder = Sketch(plane) + sketch_cylinder.circle(Point2D([100, 100]), 5 * UNITS.m) - # A slot has 6 faces and 12 edges - assert len(body.faces) == 6 - assert len(body.edges) == 12 + # Create a design + design = modeler.create_design("ExtrudeSketchWithCutNoCollision") + box_body = design.extrude_sketch( + name="BoxBody", sketch=sketch_box, distance=Distance(30, unit=UNITS.m) + ) + design.extrude_sketch( + name="CylinderBody", sketch=sketch_cylinder, distance=Distance(60, unit=UNITS.m), cut=True + ) -def test_project_and_imprint_curves(modeler: Modeler): - """Test the projection of a set of curves on a body.""" - # Create your design on the server side - design = modeler.create_design("ExtrudeSlot") - comp = design.add_component("Comp1") + # Verify there is only one body... the cut operation should delete it + assert len(design.bodies) == 1 - # Create a Sketch object and draw a couple of slots - imprint_sketch = Sketch() - imprint_sketch.slot(Point2D([10, 10], UNITS.mm), Quantity(10, UNITS.mm), Quantity(5, UNITS.mm)) - imprint_sketch.slot(Point2D([50, 50], UNITS.mm), Quantity(10, UNITS.mm), Quantity(5, UNITS.mm)) + # Verify the volume of the resulting body is exactly the same + assert design.bodies[0].volume.m == box_body.volume.m - # Extrude the sketch - sketch = Sketch() - sketch.box(Point2D([0, 0], UNITS.mm), Quantity(150, UNITS.mm), Quantity(150, UNITS.mm)) - body = comp.extrude_sketch(name="MyBox", sketch=sketch, distance=Quantity(50, UNITS.mm)) - body_faces = body.faces - body_copy = body.copy(design, "copy") +def test_create_surface_body_from_trimmed_curves(modeler: Modeler): + design = modeler.create_design("surface") - # Project the curves on the box - faces = body.project_curves(direction=UNITVECTOR3D_Z, sketch=imprint_sketch, closest_face=True) - assert len(faces) == 1 - # With the previous dir, the curves will be imprinted on the - # bottom face (closest one), i.e. the first one. - assert faces[0].id == body_faces[0].id + # pill shape + circle1 = Circle(Point3D([0, 0, 0]), 1).trim(Interval(0, np.pi)) + line1 = Line(Point3D([-1, 0, 0]), UnitVector3D([0, -1, 0])).trim(Interval(0, 1)) + circle2 = Circle(Point3D([0, -1, 0]), 1).trim(Interval(np.pi, np.pi * 2)) + line2 = Line(Point3D([1, 0, 0]), UnitVector3D([0, -1, 0])).trim(Interval(0, 1)) - # If we now draw our curves on a higher plane, the upper face should be selected - imprint_sketch_2 = Sketch(plane=Plane(Point3D([0, 0, 50], UNITS.mm))) - imprint_sketch_2.slot( - Point2D([10, 10], UNITS.mm), Quantity(10, UNITS.mm), Quantity(5, UNITS.mm) - ) - imprint_sketch_2.slot( - Point2D([50, 50], UNITS.mm), Quantity(10, UNITS.mm), Quantity(5, UNITS.mm) - ) - faces = body.project_curves( - direction=UNITVECTOR3D_Z, sketch=imprint_sketch_2, closest_face=True + body = design.create_surface_from_trimmed_curves("body", [circle1, line1, line2, circle2]) + assert body.is_surface + assert body.faces[0].area.m == pytest.approx( + Quantity(2 + np.pi, UNITS.m**2).m, rel=1e-6, abs=1e-8 ) - assert len(faces) == 1 - # With the previous dir, the curves will be imprinted on the - # top face (closest one), i.e. the first one. - assert faces[0].id == body_faces[1].id - # Now, let's try projecting only a single curve (i.e. one of the slots only) - faces = body.project_curves( - direction=UNITVECTOR3D_Z, sketch=imprint_sketch_2, closest_face=True, only_one_curve=True + # create from edges (by getting their trimmed curves) + trimmed_curves_from_edges = [edge.shape for edge in body.edges] + body = design.create_surface_from_trimmed_curves("body2", trimmed_curves_from_edges) + assert body.is_surface + assert body.faces[0].area.m == pytest.approx( + Quantity(2 + np.pi, UNITS.m**2).m, rel=1e-6, abs=1e-8 ) - assert len(faces) == 1 - # With the previous dir, the curves will be imprinted on the - # top face (closest one), i.e. the first one. - assert faces[0].id == body_faces[1].id - # Verify that the surface and curve types are of the correct type - related to PR - # https://github.com/ansys/pyansys-geometry/pull/1096 - assert isinstance(faces[0].surface_type, SurfaceType) - # Now once the previous curves have been projected, let's try imprinting our sketch - # - # It should generate two additional faces to our box = 6 + 2 - new_edges, new_faces = body.imprint_curves(faces=faces, sketch=imprint_sketch_2) +def test_shell_body(modeler: Modeler): + """Test shell command.""" + design = modeler.create_design("shell") + base = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1) - assert len(new_faces) == 2 - assert len(body.faces) == 8 + assert base.volume.m == pytest.approx(Quantity(1, UNITS.m**3).m, rel=1e-6, abs=1e-8) + assert len(base.faces) == 6 - # Verify that the surface and curve types are of the correct type - related to PR - # https://github.com/ansys/pyansys-geometry/pull/1096 - assert isinstance(new_faces[0].surface_type, SurfaceType) - assert isinstance(new_edges[0].curve_type, CurveType) + # shell + success = base.shell_body(0.1) + assert success + assert base.volume.m == pytest.approx(Quantity(0.728, UNITS.m**3).m, rel=1e-6, abs=1e-8) + assert len(base.faces) == 12 - # Make sure we have occurrence faces, not master - assert faces[0].id not in [face.id for face in body._template.faces] - assert new_faces[0].id not in [face.id for face in body._template.faces] + base = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1) + success = base.shell_body(-0.1) + assert success + assert base.volume.m == pytest.approx(Quantity(0.488, UNITS.m**3).m, rel=1e-6, abs=1e-8) + assert len(base.faces) == 12 - faces = body_copy.imprint_projected_curves( - direction=UNITVECTOR3D_Z, sketch=imprint_sketch, closest_face=True - ) - assert len(faces) == 2 - assert len(body_copy.faces) == 8 +def test_shell_faces(modeler: Modeler): + """Test shell commands for a single face.""" + design = modeler.create_design("shell") + base = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1) -def test_imprint_trimmed_curves(modeler: Modeler): - """ - Test the imprinting of trimmed curves onto a specified face of a body. - """ - unit = DEFAULT_UNITS.LENGTH + assert base.volume.m == pytest.approx(Quantity(1, UNITS.m**3).m, rel=1e-6, abs=1e-8) + assert len(base.faces) == 6 - wx = 1 - wy = 1 - wz = 1 - design = modeler.create_design("test imprint") + # shell + success = base.remove_faces(base.faces[0], 0.1) + assert success + assert base.volume.m == pytest.approx(Quantity(0.584, UNITS.m**3).m, rel=1e-6, abs=1e-8) + assert len(base.faces) == 11 - # create box - start_at = Point3D([wx / 2, wy / 2, 0.0], unit=unit) - plane = Plane( - start_at, - UNITVECTOR3D_X, - UNITVECTOR3D_Y, - ) +def test_shell_multiple_faces(modeler: Modeler): + """Test shell commands for multiple faces.""" + design = modeler.create_design("shell") + base = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1) - box_plane = Sketch(plane) - box_plane.box(Point2D([0.0, 0.0], unit=unit), width=wx, height=wy) - box = design.extrude_sketch("box", box_plane, wz) + assert base.volume.m == pytest.approx(Quantity(1, UNITS.m**3).m, rel=1e-6, abs=1e-8) + assert len(base.faces) == 6 - assert len(box.faces) == 6 - assert len(box.edges) == 12 + # shell + success = base.remove_faces([base.faces[0], base.faces[2]], 0.1) + assert success + assert base.volume.m == pytest.approx(Quantity(0.452, UNITS.m**3).m, rel=1e-6, abs=1e-8) + assert len(base.faces) == 10 - # create cylinder - point = Point3D([0.5, 0.5, 0.5]) - ortho_1, ortho_2 = UNITVECTOR3D_X, UNITVECTOR3D_Y - plane = Plane(point, ortho_1, ortho_2) - sketch_cylinder = Sketch(plane) - sketch_cylinder.circle(Point2D([0.0, 0.0], unit=unit), radius=0.1) - cylinder = design.extrude_sketch("cylinder", sketch_cylinder, 0.5) - edges = cylinder.faces[1].edges - trimmed_curves = [edges[0].shape] - new_edges, new_faces = box.imprint_curves(faces=[box.faces[1]], trimmed_curves=trimmed_curves) +def test_set_component_name(modeler: Modeler): + """Test the setting of component names.""" - # the new edge is coming from the circular top edge of the cylinder. - assert new_edges[0].start == new_edges[0].end - # verify that there is one new edge coming from the circle. - assert len(new_faces) == 1 - # verify that there is one new face coming from the circle. - assert len(new_edges) == 1 - # verify that there are 7 faces in total. - assert len(box.faces) == 7 - # verify that there are 14 edges in total. - assert len(box.edges) == 13 + design = modeler.create_design("ComponentNameTest") + component = design.add_component("Component1") + assert component.name == "Component1" + component.name = "ChangedComponentName" + assert component.name == "ChangedComponentName" -def test_copy_body(modeler: Modeler): - """Test copying a body.""" - # Create your design on the server side - design = modeler.create_design("Design") - sketch_1 = Sketch().circle(Point2D([10, 10], UNITS.mm), Quantity(10, UNITS.mm)) - body = design.extrude_sketch("Original", sketch_1, Distance(1, UNITS.mm)) +def test_get_face_bounding_box(modeler: Modeler): + """Test getting the bounding box of a face.""" + design = modeler.create_design("face_bounding_box") + body = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1) - # Copy body at same design level - copy = body.copy(design, "Copy") - assert len(design.bodies) == 2 - assert design.bodies[-1].id == copy.id + bounding_box = body.faces[0].bounding_box + assert bounding_box.min_corner.x.m == bounding_box.min_corner.y.m == -0.5 + assert bounding_box.max_corner.x.m == 0.5 + assert bounding_box.max_corner.y.m == 0.5 - # Bodies should be distinct - assert body.id != copy.id - assert body != copy + bounding_box = body.faces[1].bounding_box + assert bounding_box.min_corner.x.m == bounding_box.min_corner.y.m == -0.5 + assert bounding_box.max_corner.x.m == 0.5 + assert bounding_box.max_corner.y.m == -0.5 - # Copy body into sub-component - comp1 = design.add_component("comp1") - copy2 = body.copy(comp1, "Subcopy") - assert len(comp1.bodies) == 1 - assert comp1.bodies[-1].id == copy2.id - # Bodies should be distinct - assert body.id != copy2.id - assert body != copy2 +def test_get_edge_bounding_box(modeler: Modeler): + """Test getting the bounding box of an edge.""" + design = modeler.create_design("edge_bounding_box") + body = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1) - # Copy a copy - comp2 = comp1.add_component("comp2") - copy3 = copy2.copy(comp2, "Copy3") - assert len(comp2.bodies) == 1 - assert comp2.bodies[-1].id == copy3.id + # Edge 0 goes from (-0.5, -0.5, 1) to (0.5, -0.5, 1) + bounding_box = body.edges[0].bounding_box + assert bounding_box.min_corner.x.m == bounding_box.min_corner.y.m == -0.5 + assert bounding_box.min_corner.z.m == 1 + assert bounding_box.max_corner.x.m == 0.5 + assert bounding_box.max_corner.y.m == -0.5 + assert bounding_box.max_corner.z.m == 1 - # Bodies should be distinct - assert copy2.id != copy3.id - assert copy2 != copy3 + # Test center + center = bounding_box.center + assert center.x.m == 0 + assert center.y.m == -0.5 + assert center.z.m == 1 - # Ensure deleting original doesn't affect the copies - design.delete_body(body) - assert not body.is_alive - assert copy.is_alive +def test_get_body_bounding_box(modeler: Modeler): + """Test getting the bounding box of a body.""" + design = modeler.create_design("body_bounding_box") + body = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1) -def test_beams(modeler: Modeler): - """Test beam creation.""" - # Create your design on the server side - design = modeler.create_design("BeamCreation") + bounding_box = body.bounding_box + assert bounding_box.min_corner.x.m == bounding_box.min_corner.y.m == -0.5 + assert bounding_box.min_corner.z.m == 0 + assert bounding_box.max_corner.x.m == bounding_box.max_corner.y.m == 0.5 + assert bounding_box.max_corner.z.m == 1 - circle_profile_1 = design.add_beam_circular_profile( - "CircleProfile1", Quantity(10, UNITS.mm), Point3D([0, 0, 0]), UNITVECTOR3D_X, UNITVECTOR3D_Y - ) + # Test center + center = bounding_box.center + assert center.x.m == 0 + assert center.y.m == 0 + assert center.z.m == 0.5 - assert circle_profile_1.id is not None - assert circle_profile_1.center == Point3D([0, 0, 0]) - assert circle_profile_1.radius.value.m_as(DEFAULT_UNITS.LENGTH) == 0.01 - assert circle_profile_1.direction_x == UNITVECTOR3D_X - assert circle_profile_1.direction_y == UNITVECTOR3D_Y - circle_profile_2 = design.add_beam_circular_profile( - "CircleProfile2", - Distance(20, UNITS.mm), - Point3D([10, 20, 30], UNITS.mm), - UnitVector3D([1, 1, 1]), - UnitVector3D([0, -1, 1]), +def test_extrude_faces_failure_log_to_file(modeler: Modeler): + """Test that the failure to extrude faces logs the correct message to a file.""" + # Create a design and a body for testing + design = modeler.create_design("test_design") + body = design.extrude_sketch("test_body", Sketch().box(Point2D([0, 0]), 1, 1), 1) + + # Call the method with invalid parameters to trigger failure + result = modeler.geometry_commands.extrude_faces( + faces=[body.faces[0]], + distance=-10.0, # Invalid distance to trigger failure + direction=UnitVector3D([0, 0, 1]), ) + # Assert the result is an empty list + assert result == [] - assert circle_profile_2.id is not None - assert circle_profile_2.id is not circle_profile_1.id + result = modeler.geometry_commands.extrude_faces_up_to( + faces=[body.faces[0]], + up_to_selection=body.faces[0], # Using the same face as target to trigger failure + direction=UnitVector3D([0, 0, 1]), + seed_point=Point3D([0, 0, 0]), + ) + # Assert the result is an empty list + assert result == [] - with pytest.raises(ValueError, match="Radius must be a real positive value."): - design.add_beam_circular_profile( - "InvalidProfileRadius", - Quantity(-10, UNITS.mm), - Point3D([0, 0, 0]), - UNITVECTOR3D_X, - UNITVECTOR3D_Y, - ) - with pytest.raises(ValueError, match="Direction X and direction Y must be perpendicular."): - design.add_beam_circular_profile( - "InvalidUnitVectorAlignment", - Quantity(10, UNITS.mm), - Point3D([0, 0, 0]), - UNITVECTOR3D_X, - UnitVector3D([-1, -1, -1]), +def test_extrude_edges_missing_parameters(modeler: Modeler): + """Test that extrude_edges raises a ValueError when required parameters are missing.""" + # Create a design and body for testing + design = modeler.create_design("test_design") + body = design.extrude_sketch("test_body", Sketch().box(Point2D([0, 0]), 1, 1), 1) + + # Test case: Missing both `from_face` and `from_point`/`direction` + with pytest.raises( + ValueError, + match="To extrude edges, either a face or a direction and point must be provided.", + ): + modeler.geometry_commands.extrude_edges( + edges=[body.edges[0]], # Using the first edge of the body + distance=10.0, + from_face=None, + from_point=None, + direction=None, ) - # Create a beam at the root component level - beam_1 = design.create_beam( - Point3D([9, 99, 999], UNITS.mm), Point3D([8, 88, 888], UNITS.mm), circle_profile_1 - ) - assert beam_1.id is not None - assert beam_1.start == Point3D([9, 99, 999], UNITS.mm) - assert beam_1.end == Point3D([8, 88, 888], UNITS.mm) - assert beam_1.profile == circle_profile_1 - assert beam_1.parent_component.id == design.id - assert beam_1.is_alive - assert len(design.beams) == 1 - assert design.beams[0] == beam_1 +def test_import_component_named_selections(modeler: Modeler): + """Test importing named selections from an inserted design component.""" + # This file had a component inserted into it that has named selections that we need to import + design = modeler.open_file(Path(FILES_DIR, "import_component_groups.scdocx")) + component = design.components[0] - beam_1_str = str(beam_1) - assert "ansys.geometry.core.designer.Beam" in beam_1_str - assert " Exists : True" in beam_1_str - assert " Start : [0.009" in beam_1_str - assert " End : [0.008" in beam_1_str - assert " Parent component : BeamCreation" in beam_1_str - assert " Beam Profile info" in beam_1_str - assert " -----------------" in beam_1_str - assert "ansys.geometry.core.designer.BeamCircularProfile " in beam_1_str - assert " Name : CircleProfile1" in beam_1_str - assert " Radius : 10.0 millimeter" in beam_1_str - assert " Center : [0.0,0.0,0.0] in meters" in beam_1_str - assert " Direction x : [1.0,0.0,0.0]" in beam_1_str - assert " Direction y : [0.0,1.0,0.0]" in beam_1_str + assert len(design.named_selections) == 0 + component.import_named_selections() + assert len(design.named_selections) == 3 - # Now, let's create two beams at a nested component, with the same profile - nested_component = design.add_component("NestedComponent") - beam_2 = nested_component.create_beam( - Point3D([7, 77, 777], UNITS.mm), Point3D([6, 66, 666], UNITS.mm), circle_profile_2 - ) - beam_3 = nested_component.create_beam( - Point3D([8, 88, 888], UNITS.mm), Point3D([7, 77, 777], UNITS.mm), circle_profile_2 - ) - assert beam_2.id is not None - assert beam_2.profile == circle_profile_2 - assert beam_2.parent_component.id == nested_component.id - assert beam_2.is_alive - assert beam_3.id is not None - assert beam_3.profile == circle_profile_2 - assert beam_3.parent_component.id == nested_component.id - assert beam_3.is_alive - assert beam_2.id != beam_3.id - assert len(nested_component.beams) == 2 - assert nested_component.beams[0] == beam_2 - assert nested_component.beams[1] == beam_3 +def test_component_make_independent(modeler: Modeler): + """Test making components independent.""" - # Once the beams are created, let's try deleting it. - # For example, we shouldn't be able to delete beam_1 from the nested component. - nested_component.delete_beam(beam_1) + design = modeler.open_file(Path(FILES_DIR, "cars.scdocx")) + face = next((ns for ns in design.named_selections if ns.name == "to_pull"), None).faces[0] + comp = next( + (ns for ns in design.named_selections if ns.name == "make_independent"), None + ).components[0] - assert beam_2.is_alive - assert nested_component.beams[0].is_alive - assert beam_3.is_alive - assert nested_component.beams[1].is_alive - assert beam_1.is_alive - assert design.beams[0].is_alive + comp.make_independent() - # Let's try deleting one of the beams from the nested component - nested_component.delete_beam(beam_2) - assert not beam_2.is_alive - assert not nested_component.beams[0].is_alive - assert beam_3.is_alive - assert nested_component.beams[1].is_alive - assert beam_1.is_alive - assert design.beams[0].is_alive + assert Accuracy.length_is_equal(comp.bodies[0].volume.m, face.body.volume.m) - # Now, let's try deleting it from the design directly - this should be possible - design.delete_beam(beam_3) - assert not beam_2.is_alive - assert not nested_component.beams[0].is_alive - assert not beam_3.is_alive - assert not nested_component.beams[1].is_alive - assert beam_1.is_alive - assert design.beams[0].is_alive + modeler.geometry_commands.extrude_faces(face, 1) + comp = design.components[0].components[-1].components[-1] # stale from update-design-in-place - # Finally, let's delete the beam from the root component - design.delete_beam(beam_1) - assert not beam_2.is_alive - assert not nested_component.beams[0].is_alive - assert not beam_3.is_alive - assert not nested_component.beams[1].is_alive - assert not beam_1.is_alive - assert not design.beams[0].is_alive + assert not Accuracy.length_is_equal(comp.bodies[0].volume.m, face.body.volume.m) - # Now, let's try deleting the beam profiles! - assert len(design.beam_profiles) == 2 - design.delete_beam_profile("MyInventedBeamProfile") - assert len(design.beam_profiles) == 2 - design.delete_beam_profile(circle_profile_1) - assert len(design.beam_profiles) == 1 - design.delete_beam_profile(circle_profile_2) - assert len(design.beam_profiles) == 0 +def test_write_body_facets_on_save(modeler: Modeler, tmp_path_factory: pytest.TempPathFactory): + design = modeler.open_file(Path(FILES_DIR, "cars.scdocx")) -def test_midsurface_properties(modeler: Modeler): - """Test mid-surface properties assignment.""" - # Create your design on the server side - design = modeler.create_design("MidSurfaceProperties") + # First file without body facets + filepath_no_facets = tmp_path_factory.mktemp("test_design") / "cars_no_facets.scdocx" + design.download(filepath_no_facets) - # Create a Sketch object and draw a slot - sketch = Sketch() - sketch.slot(Point2D([10, 10], UNITS.mm), Quantity(10, UNITS.mm), Quantity(5, UNITS.mm)) + # Second file with body facets + filepath_with_facets = tmp_path_factory.mktemp("test_design") / "cars_with_facets.scdocx" + design.download(filepath_with_facets, write_body_facets=True) - # Create an actual body from the slot, and translate it - slot_body = design.extrude_sketch("MySlot", sketch, Quantity(10, UNITS.mm)) - slot_body.translate(UNITVECTOR3D_X, Quantity(40, UNITS.mm)) + # Compare file sizes + size_no_facets = filepath_no_facets.stat().st_size + size_with_facets = filepath_with_facets.stat().st_size - # Create a surface body as well - slot_surf = design.create_surface("MySlotSurface", sketch) + assert size_with_facets > size_no_facets - surf_repr = str(slot_surf) - assert "ansys.geometry.core.designer.Body" in surf_repr - assert "Name : MySlotSurface" in surf_repr - assert "Exists : True" in surf_repr - assert "Parent component : MidSurfaceProperties" in surf_repr - assert "Surface body : True" in surf_repr - assert "Surface thickness : None" in surf_repr - assert "Surface offset : None" in surf_repr - assert f"Color : {DEFAULT_COLOR}" in surf_repr + # Ensure facets.bin and renderlist.xml files exist + with zipfile.ZipFile(filepath_with_facets, "r") as zip_ref: + namelist = set(zip_ref.namelist()) - # Let's assign a thickness to both bodies - design.add_midsurface_thickness( - thickness=Quantity(10, UNITS.mm), - bodies=[slot_body, slot_surf], - ) + expected_files = { + "SpaceClaim/Graphics/facets.bin", + "SpaceClaim/Graphics/renderlist.xml", + } - # Let's also assign a mid-surface offset to both bodies - design.add_midsurface_offset( - offset_type=MidSurfaceOffsetType.TOP, bodies=[slot_body, slot_surf] - ) + missing = expected_files - namelist + assert not missing - # Let's check the values now - assert slot_body.surface_thickness is None - assert slot_body.surface_offset is None - assert slot_surf.surface_thickness == Quantity(10, UNITS.mm) - assert slot_surf.surface_offset == MidSurfaceOffsetType.TOP - # Let's check that the design-stored values are also updated - assert design.bodies[0].surface_thickness is None - assert design.bodies[0].surface_offset is None - assert design.bodies[1].surface_thickness == Quantity(10, UNITS.mm) - assert design.bodies[1].surface_offset == MidSurfaceOffsetType.TOP +def test_upload_file(modeler: Modeler, tmp_path_factory: pytest.TempPathFactory): + """Test uploading a file to the server.""" + file = tmp_path_factory.mktemp("test_design") / "upload_example.scdocx" + file_size = 1024 - surf_repr = str(slot_surf) - assert "ansys.geometry.core.designer.Body" in surf_repr - assert "Name : MySlotSurface" in surf_repr - assert "Exists : True" in surf_repr - assert "Parent component : MidSurfaceProperties" in surf_repr - assert "Surface body : True" in surf_repr - assert "Surface thickness : 10 millimeter" in surf_repr - assert "Surface offset : MidSurfaceOffsetType.TOP" in surf_repr - assert f"Color : {DEFAULT_COLOR}" in surf_repr + # Write random bytes + with file.open(mode="wb") as fout: + fout.write(os.urandom(file_size)) - # Let's try reassigning values directly to slot_body - this shouldn't do anything - slot_body.add_midsurface_thickness(Quantity(10, UNITS.mm)) - slot_body.add_midsurface_offset(MidSurfaceOffsetType.TOP) + assert file.exists() - body_repr = str(slot_body) - assert "ansys.geometry.core.designer.Body" in body_repr - assert "Name : MySlot" in body_repr - assert "Exists : True" in body_repr - assert "Parent component : MidSurfaceProperties" in body_repr - assert "Surface body : False" in body_repr - assert f"Color : {DEFAULT_COLOR}" in surf_repr - assert slot_body.surface_thickness is None - assert slot_body.surface_offset is None + # Upload file + path_on_server = modeler._upload_file(file) + assert path_on_server is not None - # Let's try reassigning values directly to slot_surf - this should work - # TODO : at the moment the server does not allow to reassign - put in try/catch block - # https://github.com/ansys/pyansys-geometry/issues/1146 - try: - slot_surf.add_midsurface_thickness(Quantity(30, UNITS.mm)) - slot_surf.add_midsurface_offset(MidSurfaceOffsetType.BOTTOM) - surf_repr = str(slot_surf) - assert "ansys.geometry.core.designer.Body" in surf_repr - assert "Name : MySlotSurface" in surf_repr - assert "Exists : True" in surf_repr - assert "Parent component : MidSurfaceProperties" in surf_repr - assert "Surface body : True" in surf_repr - assert "Surface thickness : 30 millimeter" in surf_repr - assert "Surface offset : MidSurfaceOffsetType.BOTTOM" in surf_repr - assert f"Color : {DEFAULT_COLOR}" in surf_repr - except GeometryExitedError: - pass +def test_stream_upload_file(tmp_path_factory: pytest.TempPathFactory): + """Test uploading a file to the server.""" + # Define a new maximum message length + import ansys.geometry.core.connection.defaults as pygeom_defaults - # Let's create a new surface body and assign them from body methods directly - slot_surf2 = design.create_surface("MySlotSurface2", sketch) + old_value = pygeom_defaults.MAX_MESSAGE_LENGTH + try: + # Set the new maximum message length + pygeom_defaults.MAX_MESSAGE_LENGTH = 1024**2 # 1 MB - slot_surf2.add_midsurface_thickness(Quantity(30, UNITS.mm)) - slot_surf2.add_midsurface_offset(MidSurfaceOffsetType.BOTTOM) + file = tmp_path_factory.mktemp("test_design") / "upload_stream_example.scdocx" + file_size = 5 * 1024**2 # stream five messages - surf_repr = str(slot_surf2) - assert "ansys.geometry.core.designer.Body" in surf_repr - assert "Name : MySlotSurface2" in surf_repr - assert "Exists : True" in surf_repr - assert "Parent component : MidSurfaceProperties" in surf_repr - assert "Surface body : True" in surf_repr - assert "Surface thickness : 30 millimeter" in surf_repr - assert "Surface offset : MidSurfaceOffsetType.BOTTOM" in surf_repr - assert f"Color : {DEFAULT_COLOR}" in surf_repr + # Write random bytes + with file.open(mode="wb") as fout: + fout.write(os.urandom(file_size)) + assert file.exists() + # Upload file - necessary to import the Modeler class and create an instance + from ansys.geometry.core import Modeler -def test_design_points(modeler: Modeler): - """Test for verifying the ``DesignPoints``""" - # Create your design on the server side - design = modeler.create_design("DesignPoints") - point = Point3D([6, 66, 666], UNITS.mm) - design_points_1 = design.add_design_point("FirstPointSet", point) + modeler = Modeler() + path_on_server = modeler._upload_file_stream(file) + assert path_on_server is not None + finally: + pygeom_defaults.MAX_MESSAGE_LENGTH = old_value - # Check the design points - assert len(design.design_points) == 1 - assert design_points_1.id is not None - assert design_points_1.name == "FirstPointSet" - assert design_points_1.value == point - # Create another set of design points - point_set_2 = [Point3D([10, 10, 10], UNITS.m), Point3D([20, 20, 20], UNITS.m)] - design_points_2 = design.add_design_points("SecondPointSet", point_set_2) +def test_slot_extrusion(modeler: Modeler): + """Test the extrusion of a slot.""" + # Create your design on the server side + design = modeler.create_design("ExtrudeSlot") - assert len(design.design_points) == 3 + # Create a Sketch object and draw a slot + sketch = Sketch() + sketch.slot(Point2D([10, 10], UNITS.mm), Quantity(10, UNITS.mm), Quantity(5, UNITS.mm)) - nested_component = design.add_component("NestedComponent") - design_point_3 = nested_component.add_design_point("Nested", Point3D([7, 77, 777], UNITS.mm)) + # Extrude the sketch + body = design.extrude_sketch(name="MySlot", sketch=sketch, distance=Distance(50, UNITS.mm)) - assert design_point_3.id is not None - assert design_point_3.value == Point3D([7, 77, 777], UNITS.mm) - assert design_point_3.parent_component.id == nested_component.id - assert len(nested_component.design_points) == 1 - assert nested_component.design_points[0] == design_point_3 + # A slot has 6 faces and 12 edges + assert len(body.faces) == 6 + assert len(body.edges) == 12 - design_point_1_str = str(design_points_1) - assert "ansys.geometry.core.designer.DesignPoint" in design_point_1_str - assert " Name : FirstPointSet" in design_point_1_str - assert " Design Point : [0.006 0.066 0.666]" in design_point_1_str - design_point_2_str = str(design_points_2) - assert "ansys.geometry.core.designer.DesignPoint" in design_point_2_str - assert " Name : SecondPointSet" in design_point_2_str - assert " Design Point : [10. 10. 10.]" in design_point_2_str - assert "ansys.geometry.core.designer.DesignPoint" in design_point_2_str - assert " Name : SecondPointSet" in design_point_2_str - assert " Design Point : [20. 20. 20.]" in design_point_2_str +def test_project_and_imprint_curves(modeler: Modeler): + """Test the projection of a set of curves on a body.""" + # Create your design on the server side + design = modeler.create_design("ExtrudeSlot") + comp = design.add_component("Comp1") - # SKIPPING IF GRAPHICS REQUIRED - if are_graphics_available(): - # make sure it can create polydata - pd = design_points_1._to_polydata() + # Create a Sketch object and draw a couple of slots + imprint_sketch = Sketch() + imprint_sketch.slot(Point2D([10, 10], UNITS.mm), Quantity(10, UNITS.mm), Quantity(5, UNITS.mm)) + imprint_sketch.slot(Point2D([50, 50], UNITS.mm), Quantity(10, UNITS.mm), Quantity(5, UNITS.mm)) - import pyvista as pv + # Extrude the sketch + sketch = Sketch() + sketch.box(Point2D([0, 0], UNITS.mm), Quantity(150, UNITS.mm), Quantity(150, UNITS.mm)) + body = comp.extrude_sketch(name="MyBox", sketch=sketch, distance=Quantity(50, UNITS.mm)) + body_faces = body.faces - assert isinstance(pd, pv.PolyData) + body_copy = body.copy(design, "copy") + # Project the curves on the box + faces = body.project_curves(direction=UNITVECTOR3D_Z, sketch=imprint_sketch, closest_face=True) + assert len(faces) == 1 + # With the previous dir, the curves will be imprinted on the + # bottom face (closest one), i.e. the first one. + assert faces[0].id == body_faces[0].id -def test_named_selections_beams(modeler: Modeler): - """Test for verifying the correct creation of ``NamedSelection`` with - beams. - """ - # Create your design on the server side - design = modeler.create_design("NamedSelectionBeams_Test") - - # Test creating a named selection out of beams - circle_profile_1 = design.add_beam_circular_profile( - "CircleProfile1", Quantity(10, UNITS.mm), Point3D([0, 0, 0]), UNITVECTOR3D_X, UNITVECTOR3D_Y + # If we now draw our curves on a higher plane, the upper face should be selected + imprint_sketch_2 = Sketch(plane=Plane(Point3D([0, 0, 50], UNITS.mm))) + imprint_sketch_2.slot( + Point2D([10, 10], UNITS.mm), Quantity(10, UNITS.mm), Quantity(5, UNITS.mm) ) - beam_1 = design.create_beam( - Point3D([9, 99, 999], UNITS.mm), Point3D([8, 88, 888], UNITS.mm), circle_profile_1 + imprint_sketch_2.slot( + Point2D([50, 50], UNITS.mm), Quantity(10, UNITS.mm), Quantity(5, UNITS.mm) ) - ns_beams = design.create_named_selection("CircleProfile", beams=[beam_1]) - assert len(design.named_selections) == 1 - assert design.named_selections[0].name == "CircleProfile" + faces = body.project_curves( + direction=UNITVECTOR3D_Z, sketch=imprint_sketch_2, closest_face=True + ) + assert len(faces) == 1 + # With the previous dir, the curves will be imprinted on the + # top face (closest one), i.e. the first one. + assert faces[0].id == body_faces[1].id - # Try deleting this named selection - design.delete_named_selection(ns_beams) - assert len(design.named_selections) == 0 + # Now, let's try projecting only a single curve (i.e. one of the slots only) + faces = body.project_curves( + direction=UNITVECTOR3D_Z, sketch=imprint_sketch_2, closest_face=True, only_one_curve=True + ) + assert len(faces) == 1 + # With the previous dir, the curves will be imprinted on the + # top face (closest one), i.e. the first one. + assert faces[0].id == body_faces[1].id + # Verify that the surface and curve types are of the correct type - related to PR + # https://github.com/ansys/pyansys-geometry/pull/1096 + assert isinstance(faces[0].surface_type, SurfaceType) -def test_named_selections_design_points(modeler: Modeler): - """Test for verifying the correct creation of ``NamedSelection`` with - design points. - """ - # Create your design on the server side - design = modeler.create_design("NamedSelectionDesignPoints_Test") + # Now once the previous curves have been projected, let's try imprinting our sketch + # + # It should generate two additional faces to our box = 6 + 2 + new_edges, new_faces = body.imprint_curves(faces=faces, sketch=imprint_sketch_2) - # Test creating a named selection out of design_points - point_set_1 = Point3D([10, 10, 0], UNITS.m) - design_points_1 = design.add_design_point("FirstPointSet", point_set_1) - ns_despoint = design.create_named_selection("FirstPointSet", design_points=[design_points_1]) - assert len(design.named_selections) == 1 - assert design.named_selections[0].name == "FirstPointSet" + assert len(new_faces) == 2 + assert len(body.faces) == 8 - # Try deleting this named selection - design.delete_named_selection(ns_despoint) - assert len(design.named_selections) == 0 + # Verify that the surface and curve types are of the correct type - related to PR + # https://github.com/ansys/pyansys-geometry/pull/1096 + assert isinstance(new_faces[0].surface_type, SurfaceType) + assert isinstance(new_edges[0].curve_type, CurveType) + + # Make sure we have occurrence faces, not master + assert faces[0].id not in [face.id for face in body._template.faces] + assert new_faces[0].id not in [face.id for face in body._template.faces] + + faces = body_copy.imprint_projected_curves( + direction=UNITVECTOR3D_Z, sketch=imprint_sketch, closest_face=True + ) + assert len(faces) == 2 + assert len(body_copy.faces) == 8 -def test_named_selections_components(modeler: Modeler): - """Test for verifying the correct creation of ``NamedSelection`` with - components. +def test_imprint_trimmed_curves(modeler: Modeler): """ - # Create your design on the server side - design = modeler.create_design("NamedSelectionComponents_Test") + Test the imprinting of trimmed curves onto a specified face of a body. + """ + unit = DEFAULT_UNITS.LENGTH - # Test creating a named selection out of components - comp1 = design.add_component("Comp1") - comp2 = design.add_component("Comp2") - ns_components = design.create_named_selection("Components", components=[comp1, comp2]) - assert len(design.named_selections) == 1 - assert design.named_selections[0].name == "Components" + wx = 1 + wy = 1 + wz = 1 + design = modeler.create_design("test imprint") - # Fetch the component from the named selection - assert ns_components.components[0].id == comp1.id - assert ns_components.components[1].id == comp2.id + # create box + start_at = Point3D([wx / 2, wy / 2, 0.0], unit=unit) - # Try deleting this named selection - design.delete_named_selection(ns_components) - assert len(design.named_selections) == 0 + plane = Plane( + start_at, + UNITVECTOR3D_X, + UNITVECTOR3D_Y, + ) + box_plane = Sketch(plane) + box_plane.box(Point2D([0.0, 0.0], unit=unit), width=wx, height=wy) + box = design.extrude_sketch("box", box_plane, wz) -def test_component_instances(modeler: Modeler): - """Test creation of ``Component`` instances and the effects this has.""" - design_name = "ComponentInstance_Test" - design = modeler.create_design(design_name) + assert len(box.faces) == 6 + assert len(box.edges) == 12 - # Create a car - car1 = design.add_component("Car1") - comp1 = car1.add_component("A") - comp2 = car1.add_component("B") - wheel1 = comp2.add_component("Wheel1") + # create cylinder + point = Point3D([0.5, 0.5, 0.5]) + ortho_1, ortho_2 = UNITVECTOR3D_X, UNITVECTOR3D_Y + plane = Plane(point, ortho_1, ortho_2) + sketch_cylinder = Sketch(plane) + sketch_cylinder.circle(Point2D([0.0, 0.0], unit=unit), radius=0.1) + cylinder = design.extrude_sketch("cylinder", sketch_cylinder, 0.5) - # Create car base frame - sketch = Sketch().box(Point2D([5, 10]), 10, 20) - comp2.extrude_sketch("Base", sketch, 5) + edges = cylinder.faces[1].edges + trimmed_curves = [edges[0].shape] + new_edges, new_faces = box.imprint_curves(faces=[box.faces[1]], trimmed_curves=trimmed_curves) - # Create first wheel - sketch = Sketch(Plane(direction_x=Vector3D([0, 1, 0]), direction_y=Vector3D([0, 0, 1]))) - sketch.circle(Point2D([0, 0]), 5) - wheel1.extrude_sketch("Wheel", sketch, -5) + # the new edge is coming from the circular top edge of the cylinder. + assert new_edges[0].start == new_edges[0].end + # verify that there is one new edge coming from the circle. + assert len(new_faces) == 1 + # verify that there is one new face coming from the circle. + assert len(new_edges) == 1 + # verify that there are 7 faces in total. + assert len(box.faces) == 7 + # verify that there are 14 edges in total. + assert len(box.edges) == 13 - # Create 3 other wheels and move them into position - rotation_origin = Point3D([0, 0, 0]) - rotation_direction = UnitVector3D([0, 0, 1]) - wheel2 = comp2.add_component("Wheel2", wheel1) - wheel2.modify_placement(Vector3D([0, 20, 0])) +def test_copy_body(modeler: Modeler): + """Test copying a body.""" + # Create your design on the server side + design = modeler.create_design("Design") - wheel3 = comp2.add_component("Wheel3", wheel1) - wheel3.modify_placement(Vector3D([10, 0, 0]), rotation_origin, rotation_direction, np.pi) + sketch_1 = Sketch().circle(Point2D([10, 10], UNITS.mm), Quantity(10, UNITS.mm)) + body = design.extrude_sketch("Original", sketch_1, Distance(1, UNITS.mm)) - wheel4 = comp2.add_component("Wheel4", wheel1) - wheel4.modify_placement(Vector3D([10, 20, 0]), rotation_origin, rotation_direction, np.pi) + # Copy body at same design level + copy = body.copy(design, "Copy") + assert len(design.bodies) == 2 + assert design.bodies[-1].id == copy.id - # Assert all components have unique IDs - comp_ids = [wheel1.id, wheel2.id, wheel3.id, wheel4.id] - assert len(comp_ids) == len(set(comp_ids)) + # Bodies should be distinct + assert body.id != copy.id + assert body != copy - # Assert all bodies have unique IDs - body_ids = [wheel1.bodies[0].id, wheel2.bodies[0].id, wheel3.bodies[0].id, wheel4.bodies[0].id] - assert len(body_ids) == len(set(body_ids)) + # Copy body into sub-component + comp1 = design.add_component("comp1") + copy2 = body.copy(comp1, "Subcopy") + assert len(comp1.bodies) == 1 + assert comp1.bodies[-1].id == copy2.id - # Assert all instances have unique MasterComponents - comp_templates = [wheel2._master_component, wheel3._master_component, wheel4._master_component] - assert len(comp_templates) == len(set(comp_templates)) + # Bodies should be distinct + assert body.id != copy2.id + assert body != copy2 - # Assert all instances have the same Part - comp_parts = [ - wheel2._master_component.part, - wheel3._master_component.part, - wheel4._master_component.part, - ] - assert len(set(comp_parts)) == 1 + # Copy a copy + comp2 = comp1.add_component("comp2") + copy3 = copy2.copy(comp2, "Copy3") + assert len(comp2.bodies) == 1 + assert comp2.bodies[-1].id == copy3.id - assert wheel1.get_world_transform() == IDENTITY_MATRIX44 - assert wheel2.get_world_transform() != IDENTITY_MATRIX44 + # Bodies should be distinct + assert copy2.id != copy3.id + assert copy2 != copy3 - # Create 2nd car - car2 = design.add_component("Car2", car1) - car2.modify_placement(Vector3D([30, 0, 0])) + # Ensure deleting original doesn't affect the copies + design.delete_body(body) + assert not body.is_alive + assert copy.is_alive - # Create top of car - applies to BOTH cars - sketch = Sketch(Plane(Point3D([0, 5, 5]))).box(Point2D([5, 2.5]), 10, 5) - comp1.extrude_sketch("Top", sketch, 5) - # Show the body also got added to Car2, and they are distinct, but - # not independent - assert car1.components[0].bodies[0].id != car2.components[0].bodies[0].id +def test_beams(modeler: Modeler): + """Test beam creation.""" + # Create your design on the server side + design = modeler.create_design("BeamCreation") - # If monikers were formatted properly, you should be able to use them - assert len(car2.components[1].components[1].bodies[0].faces) > 0 + circle_profile_1 = design.add_beam_circular_profile( + "CircleProfile1", Quantity(10, UNITS.mm), Point3D([0, 0, 0]), UNITVECTOR3D_X, UNITVECTOR3D_Y + ) + assert circle_profile_1.id is not None + assert circle_profile_1.center == Point3D([0, 0, 0]) + assert circle_profile_1.radius.value.m_as(DEFAULT_UNITS.LENGTH) == 0.01 + assert circle_profile_1.direction_x == UNITVECTOR3D_X + assert circle_profile_1.direction_y == UNITVECTOR3D_Y -def test_boolean_body_operations(modeler: Modeler): - """ - Test cases: + circle_profile_2 = design.add_beam_circular_profile( + "CircleProfile2", + Distance(20, UNITS.mm), + Point3D([10, 20, 30], UNITS.mm), + UnitVector3D([1, 1, 1]), + UnitVector3D([0, -1, 1]), + ) - 1) master/master - a) intersect - i) normal - x) identity - y) transform - ii) empty failure - b) subtract - i) normal - x) identity - y) transform - ii) empty failure - iii) disjoint - c) unite - i) normal - x) identity - y) transform - ii) disjoint - 2) instance/instance - a) intersect - i) normal - x) identity - y) transform - ii) empty failure - b) subtract - i) normal - x) identity - y) transform - ii) empty failure - c) unite - i) normal - x) identity - y) transform - """ - design = modeler.create_design("TestBooleanOperations") - - comp1 = design.add_component("Comp1") - comp2 = design.add_component("Comp2") - comp3 = design.add_component("Comp3") - - body1 = comp1.extrude_sketch("Body1", Sketch().box(Point2D([0, 0]), 1, 1), 1) - body2 = comp2.extrude_sketch("Body2", Sketch().box(Point2D([0.5, 0]), 1, 1), 1) - body3 = comp3.extrude_sketch("Body3", Sketch().box(Point2D([5, 0]), 1, 1), 1) - - # 1.a.i.x - copy1 = body1.copy(comp1, "Copy1") - copy2 = body2.copy(comp2, "Copy2") - copy1.intersect(copy2) - - assert not copy2.is_alive - assert body2.is_alive - assert Accuracy.length_is_equal(copy1.volume.m, 0.5) + assert circle_profile_2.id is not None + assert circle_profile_2.id is not circle_profile_1.id - # 1.a.i.y - copy1 = body1.copy(comp1, "Copy1") - copy2 = body2.copy(comp2, "Copy2") - copy2.translate(UnitVector3D([1, 0, 0]), 0.25) - copy1.intersect(copy2) + with pytest.raises(ValueError, match="Radius must be a real positive value."): + design.add_beam_circular_profile( + "InvalidProfileRadius", + Quantity(-10, UNITS.mm), + Point3D([0, 0, 0]), + UNITVECTOR3D_X, + UNITVECTOR3D_Y, + ) - assert not copy2.is_alive - assert Accuracy.length_is_equal(copy1.volume.m, 0.25) + with pytest.raises(ValueError, match="Direction X and direction Y must be perpendicular."): + design.add_beam_circular_profile( + "InvalidUnitVectorAlignment", + Quantity(10, UNITS.mm), + Point3D([0, 0, 0]), + UNITVECTOR3D_X, + UnitVector3D([-1, -1, -1]), + ) - # 1.a.ii - copy1 = body1.copy(comp1, "Copy1") - copy3 = body3.copy(comp3, "Copy3") - with pytest.raises(ValueError, match="bodies do not intersect"): - copy1.intersect(copy3) + # Create a beam at the root component level + beam_1 = design.create_beam( + Point3D([9, 99, 999], UNITS.mm), Point3D([8, 88, 888], UNITS.mm), circle_profile_1 + ) - assert copy1.is_alive - assert copy3.is_alive + assert beam_1.id is not None + assert beam_1.start == Point3D([9, 99, 999], UNITS.mm) + assert beam_1.end == Point3D([8, 88, 888], UNITS.mm) + assert beam_1.profile == circle_profile_1 + assert beam_1.parent_component.id == design.id + assert beam_1.is_alive + assert len(design.beams) == 1 + assert design.beams[0] == beam_1 - # 1.b.i.x - copy1 = body1.copy(comp1, "Copy1") - copy2 = body2.copy(comp2, "Copy2") - copy1.subtract(copy2) + beam_1_str = str(beam_1) + assert "ansys.geometry.core.designer.Beam" in beam_1_str + assert " Exists : True" in beam_1_str + assert " Start : [0.009" in beam_1_str + assert " End : [0.008" in beam_1_str + assert " Parent component : BeamCreation" in beam_1_str + assert " Beam Profile info" in beam_1_str + assert " -----------------" in beam_1_str + assert "ansys.geometry.core.designer.BeamCircularProfile " in beam_1_str + assert " Name : CircleProfile1" in beam_1_str + assert " Radius : 10.0 millimeter" in beam_1_str + assert " Center : [0.0,0.0,0.0] in meters" in beam_1_str + assert " Direction x : [1.0,0.0,0.0]" in beam_1_str + assert " Direction y : [0.0,1.0,0.0]" in beam_1_str - assert not copy2.is_alive - assert body2.is_alive - assert Accuracy.length_is_equal(copy1.volume.m, 0.5) + # Now, let's create two beams at a nested component, with the same profile + nested_component = design.add_component("NestedComponent") + beam_2 = nested_component.create_beam( + Point3D([7, 77, 777], UNITS.mm), Point3D([6, 66, 666], UNITS.mm), circle_profile_2 + ) + beam_3 = nested_component.create_beam( + Point3D([8, 88, 888], UNITS.mm), Point3D([7, 77, 777], UNITS.mm), circle_profile_2 + ) - # 1.b.i.y - copy1 = body1.copy(comp1, "Copy1") - copy2 = body2.copy(comp2, "Copy2") - copy2.translate(UnitVector3D([1, 0, 0]), 0.25) - copy1.subtract(copy2) + assert beam_2.id is not None + assert beam_2.profile == circle_profile_2 + assert beam_2.parent_component.id == nested_component.id + assert beam_2.is_alive + assert beam_3.id is not None + assert beam_3.profile == circle_profile_2 + assert beam_3.parent_component.id == nested_component.id + assert beam_3.is_alive + assert beam_2.id != beam_3.id + assert len(nested_component.beams) == 2 + assert nested_component.beams[0] == beam_2 + assert nested_component.beams[1] == beam_3 - assert not copy2.is_alive - assert Accuracy.length_is_equal(copy1.volume.m, 0.75) + # Once the beams are created, let's try deleting it. + # For example, we shouldn't be able to delete beam_1 from the nested component. + nested_component.delete_beam(beam_1) - # 1.b.ii - copy1 = body1.copy(comp1, "Copy1") - copy1a = body1.copy(comp1, "Copy1a") - with pytest.raises(ValueError): - copy1.subtract(copy1a) + assert beam_2.is_alive + assert nested_component.beams[0].is_alive + assert beam_3.is_alive + assert nested_component.beams[1].is_alive + assert beam_1.is_alive + assert design.beams[0].is_alive - assert copy1.is_alive - assert copy1a.is_alive + # Let's try deleting one of the beams from the nested component + nested_component.delete_beam(beam_2) + assert not beam_2.is_alive + assert not nested_component.beams[0].is_alive + assert beam_3.is_alive + assert nested_component.beams[1].is_alive + assert beam_1.is_alive + assert design.beams[0].is_alive - # 1.b.iii - copy1 = body1.copy(comp1, "Copy1") - copy3 = body3.copy(comp3, "Copy3") - copy1.subtract(copy3) + # Now, let's try deleting it from the design directly - this should be possible + design.delete_beam(beam_3) + assert not beam_2.is_alive + assert not nested_component.beams[0].is_alive + assert not beam_3.is_alive + assert not nested_component.beams[1].is_alive + assert beam_1.is_alive + assert design.beams[0].is_alive - assert Accuracy.length_is_equal(copy1.volume.m, 1) - assert copy1.volume - assert not copy3.is_alive + # Finally, let's delete the beam from the root component + design.delete_beam(beam_1) + assert not beam_2.is_alive + assert not nested_component.beams[0].is_alive + assert not beam_3.is_alive + assert not nested_component.beams[1].is_alive + assert not beam_1.is_alive + assert not design.beams[0].is_alive - # 1.c.i.x - copy1 = body1.copy(comp1, "Copy1") - copy2 = body2.copy(comp2, "Copy2") - copy1.unite(copy2) + # Now, let's try deleting the beam profiles! + assert len(design.beam_profiles) == 2 + design.delete_beam_profile("MyInventedBeamProfile") + assert len(design.beam_profiles) == 2 + design.delete_beam_profile(circle_profile_1) + assert len(design.beam_profiles) == 1 + design.delete_beam_profile(circle_profile_2) + assert len(design.beam_profiles) == 0 - assert not copy2.is_alive - assert body2.is_alive - assert Accuracy.length_is_equal(copy1.volume.m, 1.5) - # 1.c.i.y - copy1 = body1.copy(comp1, "Copy1") - copy2 = body2.copy(comp2, "Copy2") - copy2.translate(UnitVector3D([1, 0, 0]), 0.25) - copy1.unite(copy2) +def test_midsurface_properties(modeler: Modeler): + """Test mid-surface properties assignment.""" + # Create your design on the server side + design = modeler.create_design("MidSurfaceProperties") - assert not copy2.is_alive - assert Accuracy.length_is_equal(copy1.volume.m, 1.75) + # Create a Sketch object and draw a slot + sketch = Sketch() + sketch.slot(Point2D([10, 10], UNITS.mm), Quantity(10, UNITS.mm), Quantity(5, UNITS.mm)) - # 1.c.ii - copy1 = body1.copy(comp1, "Copy1") - copy3 = body3.copy(comp3, "Copy3") - copy1.unite(copy3) + # Create an actual body from the slot, and translate it + slot_body = design.extrude_sketch("MySlot", sketch, Quantity(10, UNITS.mm)) + slot_body.translate(UNITVECTOR3D_X, Quantity(40, UNITS.mm)) - assert not copy3.is_alive - assert body3.is_alive - assert Accuracy.length_is_equal(copy1.volume.m, 1) + # Create a surface body as well + slot_surf = design.create_surface("MySlotSurface", sketch) - # Test instance/instance - comp1_i = design.add_component("Comp1_i", comp1) - comp2_i = design.add_component("Comp2_i", comp2) - comp3_i = design.add_component("Comp3_i", comp3) + surf_repr = str(slot_surf) + assert "ansys.geometry.core.designer.Body" in surf_repr + assert "Name : MySlotSurface" in surf_repr + assert "Exists : True" in surf_repr + assert "Parent component : MidSurfaceProperties" in surf_repr + assert "Surface body : True" in surf_repr + assert "Surface thickness : None" in surf_repr + assert "Surface offset : None" in surf_repr + assert f"Color : {DEFAULT_COLOR}" in surf_repr - comp1_i.modify_placement( - Vector3D([52, 61, -43]), Point3D([-4, 26, 66]), UnitVector3D([-21, 20, 87]), np.pi / 4 - ) - comp2_i.modify_placement( - Vector3D([52, 61, -43]), Point3D([-4, 26, 66]), UnitVector3D([-21, 20, 87]), np.pi / 4 + # Let's assign a thickness to both bodies + design.add_midsurface_thickness( + thickness=Quantity(10, UNITS.mm), + bodies=[slot_body, slot_surf], ) - comp3_i.modify_placement( - Vector3D([52, 61, -43]), Point3D([-4, 26, 66]), UnitVector3D([-21, 20, 87]), np.pi / 4 + + # Let's also assign a mid-surface offset to both bodies + design.add_midsurface_offset( + offset_type=MidSurfaceOffsetType.TOP, bodies=[slot_body, slot_surf] ) - body1 = comp1_i.bodies[0] - body2 = comp2_i.bodies[0] - body3 = comp3_i.bodies[0] + # Let's check the values now + assert slot_body.surface_thickness is None + assert slot_body.surface_offset is None + assert slot_surf.surface_thickness == Quantity(10, UNITS.mm) + assert slot_surf.surface_offset == MidSurfaceOffsetType.TOP - # 2.a.i.x - copy1 = body1.copy(comp1_i, "Copy1") - copy2 = body2.copy(comp2_i, "Copy2") - copy1.intersect(copy2) + # Let's check that the design-stored values are also updated + assert design.bodies[0].surface_thickness is None + assert design.bodies[0].surface_offset is None + assert design.bodies[1].surface_thickness == Quantity(10, UNITS.mm) + assert design.bodies[1].surface_offset == MidSurfaceOffsetType.TOP - assert not copy2.is_alive - assert body2.is_alive - assert Accuracy.length_is_equal(copy1.volume.m, 0.5) + surf_repr = str(slot_surf) + assert "ansys.geometry.core.designer.Body" in surf_repr + assert "Name : MySlotSurface" in surf_repr + assert "Exists : True" in surf_repr + assert "Parent component : MidSurfaceProperties" in surf_repr + assert "Surface body : True" in surf_repr + assert "Surface thickness : 10 millimeter" in surf_repr + assert "Surface offset : MidSurfaceOffsetType.TOP" in surf_repr + assert f"Color : {DEFAULT_COLOR}" in surf_repr - # 2.a.i.y - copy1 = body1.copy(comp1_i, "Copy1") - copy2 = body2.copy(comp2_i, "Copy2") - copy2.translate(UnitVector3D([1, 0, 0]), 0.25) - copy1.intersect(copy2) - - assert not copy2.is_alive - assert Accuracy.length_is_equal(copy1.volume.m, 0.25) - - # 2.a.ii - copy1 = body1.copy(comp1_i, "Copy1") - copy3 = body3.copy(comp3_i, "Copy3") - with pytest.raises(ValueError, match="bodies do not intersect"): - copy1.intersect(copy3) - - assert copy1.is_alive - assert copy3.is_alive - - # 2.b.i.x - copy1 = body1.copy(comp1_i, "Copy1") - copy2 = body2.copy(comp2_i, "Copy2") - copy1.subtract(copy2) + # Let's try reassigning values directly to slot_body - this shouldn't do anything + slot_body.add_midsurface_thickness(Quantity(10, UNITS.mm)) + slot_body.add_midsurface_offset(MidSurfaceOffsetType.TOP) - assert not copy2.is_alive - assert body2.is_alive - assert Accuracy.length_is_equal(copy1.volume.m, 0.5) + body_repr = str(slot_body) + assert "ansys.geometry.core.designer.Body" in body_repr + assert "Name : MySlot" in body_repr + assert "Exists : True" in body_repr + assert "Parent component : MidSurfaceProperties" in body_repr + assert "Surface body : False" in body_repr + assert f"Color : {DEFAULT_COLOR}" in surf_repr + assert slot_body.surface_thickness is None + assert slot_body.surface_offset is None - # 2.b.i.y - copy1 = body1.copy(comp1_i, "Copy1") - copy2 = body2.copy(comp2_i, "Copy2") - copy2.translate(UnitVector3D([1, 0, 0]), 0.25) - copy1.subtract(copy2) + # Let's try reassigning values directly to slot_surf - this should work + # TODO : at the moment the server does not allow to reassign - put in try/catch block + # https://github.com/ansys/pyansys-geometry/issues/1146 + try: + slot_surf.add_midsurface_thickness(Quantity(30, UNITS.mm)) + slot_surf.add_midsurface_offset(MidSurfaceOffsetType.BOTTOM) - assert not copy2.is_alive - assert Accuracy.length_is_equal(copy1.volume.m, 0.75) + surf_repr = str(slot_surf) + assert "ansys.geometry.core.designer.Body" in surf_repr + assert "Name : MySlotSurface" in surf_repr + assert "Exists : True" in surf_repr + assert "Parent component : MidSurfaceProperties" in surf_repr + assert "Surface body : True" in surf_repr + assert "Surface thickness : 30 millimeter" in surf_repr + assert "Surface offset : MidSurfaceOffsetType.BOTTOM" in surf_repr + assert f"Color : {DEFAULT_COLOR}" in surf_repr + except GeometryExitedError: + pass - # 2.b.ii - copy1 = body1.copy(comp1_i, "Copy1") - copy1a = body1.copy(comp1_i, "Copy1a") - with pytest.raises(ValueError): - copy1.subtract(copy1a) + # Let's create a new surface body and assign them from body methods directly + slot_surf2 = design.create_surface("MySlotSurface2", sketch) - assert copy1.is_alive - assert copy1a.is_alive + slot_surf2.add_midsurface_thickness(Quantity(30, UNITS.mm)) + slot_surf2.add_midsurface_offset(MidSurfaceOffsetType.BOTTOM) - # 2.b.iii - copy1 = body1.copy(comp1_i, "Copy1") - copy3 = body3.copy(comp3_i, "Copy3") - copy1.subtract(copy3) + surf_repr = str(slot_surf2) + assert "ansys.geometry.core.designer.Body" in surf_repr + assert "Name : MySlotSurface2" in surf_repr + assert "Exists : True" in surf_repr + assert "Parent component : MidSurfaceProperties" in surf_repr + assert "Surface body : True" in surf_repr + assert "Surface thickness : 30 millimeter" in surf_repr + assert "Surface offset : MidSurfaceOffsetType.BOTTOM" in surf_repr + assert f"Color : {DEFAULT_COLOR}" in surf_repr - assert Accuracy.length_is_equal(copy1.volume.m, 1) - assert copy1.volume - assert not copy3.is_alive - # 2.c.i.x - copy1 = body1.copy(comp1_i, "Copy1") - copy2 = body2.copy(comp2_i, "Copy2") - copy1.unite(copy2) +def test_design_points(modeler: Modeler): + """Test for verifying the ``DesignPoints``""" + # Create your design on the server side + design = modeler.create_design("DesignPoints") + point = Point3D([6, 66, 666], UNITS.mm) + design_points_1 = design.add_design_point("FirstPointSet", point) - assert not copy2.is_alive - assert body2.is_alive - assert Accuracy.length_is_equal(copy1.volume.m, 1.5) + # Check the design points + assert len(design.design_points) == 1 + assert design_points_1.id is not None + assert design_points_1.name == "FirstPointSet" + assert design_points_1.value == point - # 2.c.i.y - copy1 = body1.copy(comp1_i, "Copy1") - copy2 = body2.copy(comp2_i, "Copy2") - copy2.translate(UnitVector3D([1, 0, 0]), 0.25) - copy1.unite(copy2) + # Create another set of design points + point_set_2 = [Point3D([10, 10, 10], UNITS.m), Point3D([20, 20, 20], UNITS.m)] + design_points_2 = design.add_design_points("SecondPointSet", point_set_2) - assert not copy2.is_alive - assert Accuracy.length_is_equal(copy1.volume.m, 1.75) + assert len(design.design_points) == 3 - # 2.c.ii - copy1 = body1.copy(comp1_i, "Copy1") - copy3 = body3.copy(comp3_i, "Copy3") - copy1.unite(copy3) + nested_component = design.add_component("NestedComponent") + design_point_3 = nested_component.add_design_point("Nested", Point3D([7, 77, 777], UNITS.mm)) - assert not copy3.is_alive - assert body3.is_alive - assert Accuracy.length_is_equal(copy1.volume.m, 1) + assert design_point_3.id is not None + assert design_point_3.value == Point3D([7, 77, 777], UNITS.mm) + assert design_point_3.parent_component.id == nested_component.id + assert len(nested_component.design_points) == 1 + assert nested_component.design_points[0] == design_point_3 + design_point_1_str = str(design_points_1) + assert "ansys.geometry.core.designer.DesignPoint" in design_point_1_str + assert " Name : FirstPointSet" in design_point_1_str + assert " Design Point : [0.006 0.066 0.666]" in design_point_1_str -def test_multiple_bodies_boolean_operations(modeler: Modeler): - """Test boolean operations with multiple bodies.""" - design = modeler.create_design("TestBooleanOperationsMultipleBodies") + design_point_2_str = str(design_points_2) + assert "ansys.geometry.core.designer.DesignPoint" in design_point_2_str + assert " Name : SecondPointSet" in design_point_2_str + assert " Design Point : [10. 10. 10.]" in design_point_2_str + assert "ansys.geometry.core.designer.DesignPoint" in design_point_2_str + assert " Name : SecondPointSet" in design_point_2_str + assert " Design Point : [20. 20. 20.]" in design_point_2_str - comp1 = design.add_component("Comp1") - comp2 = design.add_component("Comp2") - comp3 = design.add_component("Comp3") + # SKIPPING IF GRAPHICS REQUIRED + if are_graphics_available(): + # make sure it can create polydata + pd = design_points_1._to_polydata() - body1 = comp1.extrude_sketch("Body1", Sketch().box(Point2D([0, 0]), 1, 1), 1) - body2 = comp2.extrude_sketch("Body2", Sketch().box(Point2D([0.5, 0]), 1, 1), 1) - body3 = comp3.extrude_sketch("Body3", Sketch().box(Point2D([5, 0]), 1, 1), 1) + import pyvista as pv - ################# Check subtract operation ################# - copy1_sub = body1.copy(comp1, "Copy1_subtract") - copy2_sub = body2.copy(comp2, "Copy2_subtract") - copy3_sub = body3.copy(comp3, "Copy3_subtract") - copy1_sub.subtract([copy2_sub, copy3_sub]) + assert isinstance(pd, pv.PolyData) - assert not copy2_sub.is_alive - assert not copy3_sub.is_alive - assert body2.is_alive - assert body3.is_alive - assert len(comp1.bodies) == 2 - assert len(comp2.bodies) == 1 - assert len(comp3.bodies) == 1 - # Cleanup previous subtest - comp1.delete_body(copy1_sub) - assert len(comp1.bodies) == 1 +def test_named_selections_beams(modeler: Modeler): + """Test for verifying the correct creation of ``NamedSelection`` with + beams. + """ + # Create your design on the server side + design = modeler.create_design("NamedSelectionBeams_Test") - ################# Check unite operation ################# - copy1_uni = body1.copy(comp1, "Copy1_unite") - copy2_uni = body2.copy(comp2, "Copy2_unite") - copy3_uni = body3.copy(comp3, "Copy3_unite") - copy1_uni.unite([copy2_uni, copy3_uni]) + # Test creating a named selection out of beams + circle_profile_1 = design.add_beam_circular_profile( + "CircleProfile1", Quantity(10, UNITS.mm), Point3D([0, 0, 0]), UNITVECTOR3D_X, UNITVECTOR3D_Y + ) + beam_1 = design.create_beam( + Point3D([9, 99, 999], UNITS.mm), Point3D([8, 88, 888], UNITS.mm), circle_profile_1 + ) + ns_beams = design.create_named_selection("CircleProfile", beams=[beam_1]) + assert len(design.named_selections) == 1 + assert design.named_selections[0].name == "CircleProfile" - assert not copy2_uni.is_alive - assert not copy3_uni.is_alive - assert body2.is_alive - assert body3.is_alive - assert len(comp1.bodies) == 2 - assert len(comp2.bodies) == 1 - assert len(comp3.bodies) == 1 + # Try deleting this named selection + design.delete_named_selection(ns_beams) + assert len(design.named_selections) == 0 - # Cleanup previous subtest - comp1.delete_body(copy1_uni) - assert len(comp1.bodies) == 1 - ################# Check intersect operation ################# - copy1_int = body1.copy(comp1, "Copy1_intersect") - copy2_int = body2.copy(comp2, "Copy2_intersect") - copy3_int = body3.copy(comp3, "Copy3_intersect") # Body 3 does not intersect them - copy1_int.intersect([copy2_int]) +def test_named_selections_design_points(modeler: Modeler): + """Test for verifying the correct creation of ``NamedSelection`` with + design points. + """ + # Create your design on the server side + design = modeler.create_design("NamedSelectionDesignPoints_Test") - assert not copy2_int.is_alive - assert copy3_int.is_alive - assert body2.is_alive - assert body3.is_alive - assert len(comp1.bodies) == 2 - assert len(comp2.bodies) == 1 - assert len(comp3.bodies) == 2 + # Test creating a named selection out of design_points + point_set_1 = Point3D([10, 10, 0], UNITS.m) + design_points_1 = design.add_design_point("FirstPointSet", point_set_1) + ns_despoint = design.create_named_selection("FirstPointSet", design_points=[design_points_1]) + assert len(design.named_selections) == 1 + assert design.named_selections[0].name == "FirstPointSet" - # Cleanup previous subtest - comp1.delete_body(copy1_int) - comp3.delete_body(copy3_int) - assert len(comp1.bodies) == 1 - assert len(comp3.bodies) == 1 + # Try deleting this named selection + design.delete_named_selection(ns_despoint) + assert len(design.named_selections) == 0 -def test_bool_operations_with_keep_other(modeler: Modeler): - """Test boolean operations with keep other option.""" - # Create the design and bodies - design = modeler.create_design("TestBooleanOperationsWithKeepOther") +def test_named_selections_components(modeler: Modeler): + """Test for verifying the correct creation of ``NamedSelection`` with + components. + """ + # Create your design on the server side + design = modeler.create_design("NamedSelectionComponents_Test") + # Test creating a named selection out of components comp1 = design.add_component("Comp1") comp2 = design.add_component("Comp2") - comp3 = design.add_component("Comp3") - - body1 = comp1.extrude_sketch("Body1", Sketch().box(Point2D([0, 0]), 1, 1), 1) - body2 = comp2.extrude_sketch("Body2", Sketch().box(Point2D([0.5, 0]), 1, 1), 1) - body3 = comp3.extrude_sketch("Body3", Sketch().box(Point2D([5, 0]), 1, 1), 1) - - # ---- Verify subtract operation ---- - body1.subtract([body2, body3], keep_other=True) + ns_components = design.create_named_selection("Components", components=[comp1, comp2]) + assert len(design.named_selections) == 1 + assert design.named_selections[0].name == "Components" - assert body2.is_alive - assert body3.is_alive - assert len(comp1.bodies) == 1 - assert len(comp2.bodies) == 1 - assert len(comp3.bodies) == 1 + # Fetch the component from the named selection + assert ns_components.components[0].id == comp1.id + assert ns_components.components[1].id == comp2.id - # ---- Verify unite operation ---- - body1.unite([body2, body3], keep_other=True) + # Try deleting this named selection + design.delete_named_selection(ns_components) + assert len(design.named_selections) == 0 - assert body2.is_alive - assert body3.is_alive - assert len(comp1.bodies) == 1 - assert len(comp2.bodies) == 1 - assert len(comp3.bodies) == 1 - # ---- Verify intersect operation ---- - body1.intersect(body2, keep_other=True) +def test_component_instances(modeler: Modeler): + """Test creation of ``Component`` instances and the effects this has.""" + design_name = "ComponentInstance_Test" + design = modeler.create_design(design_name) - assert body1.is_alive - assert body2.is_alive - assert body3.is_alive - assert len(comp1.bodies) == 1 - assert len(comp2.bodies) == 1 - assert len(comp3.bodies) == 1 - - -def test_child_component_instances(modeler: Modeler): - """Test creation of child ``Component`` instances and check the data model - reflects that. - """ - design_name = "ChildComponentInstances_Test" - design = modeler.create_design(design_name) - # Create a base component - base1 = design.add_component("Base1") - comp1 = base1.add_component("A") - comp2 = base1.add_component("B") + # Create a car + car1 = design.add_component("Car1") + comp1 = car1.add_component("A") + comp2 = car1.add_component("B") + wheel1 = comp2.add_component("Wheel1") - # Create the solid body for the base + # Create car base frame sketch = Sketch().box(Point2D([5, 10]), 10, 20) - comp2.extrude_sketch("Bottom", sketch, 5) + comp2.extrude_sketch("Base", sketch, 5) - # Create the 2nd base - base2 = design.add_component("Base2", base1) - base2.modify_placement(Vector3D([30, 0, 0])) + # Create first wheel + sketch = Sketch(Plane(direction_x=Vector3D([0, 1, 0]), direction_y=Vector3D([0, 0, 1]))) + sketch.circle(Point2D([0, 0]), 5) + wheel1.extrude_sketch("Wheel", sketch, -5) - # Create top part (applies to both Base1 and Base2) - sketch = Sketch(Plane(Point3D([0, 5, 5]))).box(Point2D([5, 2.5]), 10, 5) - comp1.extrude_sketch("Top", sketch, 5) + # Create 3 other wheels and move them into position + rotation_origin = Point3D([0, 0, 0]) + rotation_direction = UnitVector3D([0, 0, 1]) - # create the first child component - comp1.add_component("Child1") - comp1.extrude_sketch("Child1_body", Sketch(Plane([5, 7.5, 10])).box(Point2D([0, 0]), 1, 1), 1) + wheel2 = comp2.add_component("Wheel2", wheel1) + wheel2.modify_placement(Vector3D([0, 20, 0])) - assert len(comp1.components) == 1 - assert len(base2.components[0].components) == 1 - assert len(comp1.components) == len(base2.components[0].components) + wheel3 = comp2.add_component("Wheel3", wheel1) + wheel3.modify_placement(Vector3D([10, 0, 0]), rotation_origin, rotation_direction, np.pi) - # create the second child component - comp1.add_component("Child2") - comp1.extrude_sketch("Child2_body", Sketch(Plane([5, 7.5, 10])).box(Point2D([0, 0]), 1, 1), -1) + wheel4 = comp2.add_component("Wheel4", wheel1) + wheel4.modify_placement(Vector3D([10, 20, 0]), rotation_origin, rotation_direction, np.pi) - assert len(comp1.components) == 2 - assert len(base2.components[0].components) == 2 - assert len(comp1.components) == len(base2.components[0].components) + # Assert all components have unique IDs + comp_ids = [wheel1.id, wheel2.id, wheel3.id, wheel4.id] + assert len(comp_ids) == len(set(comp_ids)) + # Assert all bodies have unique IDs + body_ids = [wheel1.bodies[0].id, wheel2.bodies[0].id, wheel3.bodies[0].id, wheel4.bodies[0].id] + assert len(body_ids) == len(set(body_ids)) -def test_multiple_designs(modeler: Modeler, tmp_path_factory: pytest.TempPathFactory): - """Generate multiple designs, make sure they are all separate, and once - a design is deactivated, the next one is activated. - """ - # Initiate expected output images - scshot_dir = tmp_path_factory.mktemp("test_multiple_designs") - scshot_1 = scshot_dir / "design1.png" - scshot_2 = scshot_dir / "design2.png" + # Assert all instances have unique MasterComponents + comp_templates = [wheel2._master_component, wheel3._master_component, wheel4._master_component] + assert len(comp_templates) == len(set(comp_templates)) - # Create your design on the server side - design1 = modeler.create_design("Design1") + # Assert all instances have the same Part + comp_parts = [ + wheel2._master_component.part, + wheel3._master_component.part, + wheel4._master_component.part, + ] + assert len(set(comp_parts)) == 1 - # Create a Sketch object and draw a slot - sketch1 = Sketch() - sketch1.slot(Point2D([10, 10], UNITS.mm), Quantity(10, UNITS.mm), Quantity(5, UNITS.mm)) + assert wheel1.get_world_transform() == IDENTITY_MATRIX44 + assert wheel2.get_world_transform() != IDENTITY_MATRIX44 - # Extrude the sketch to create a body - design1.extrude_sketch("MySlot", sketch1, Quantity(10, UNITS.mm)) + # Create 2nd car + car2 = design.add_component("Car2", car1) + car2.modify_placement(Vector3D([30, 0, 0])) - # SKIPPING IF GRAPHICS REQUIRED - if are_graphics_available(): - # Request plotting and store images - design1.plot(screenshot=scshot_1) + # Create top of car - applies to BOTH cars + sketch = Sketch(Plane(Point3D([0, 5, 5]))).box(Point2D([5, 2.5]), 10, 5) + comp1.extrude_sketch("Top", sketch, 5) - # Create a second design - design2 = modeler.create_design("Design2") + # Show the body also got added to Car2, and they are distinct, but + # not independent + assert car1.components[0].bodies[0].id != car2.components[0].bodies[0].id - # Create a Sketch object and draw a rectangle - sketch2 = Sketch() - sketch2.box(Point2D([-30, -30], UNITS.mm), 5 * UNITS.mm, 8 * UNITS.mm) + # If monikers were formatted properly, you should be able to use them + assert len(car2.components[1].components[1].bodies[0].faces) > 0 - # Extrude the sketch to create a body - design2.extrude_sketch("MyRectangle", sketch2, Quantity(10, UNITS.mm)) - # SKIPPING IF GRAPHICS REQUIRED - if are_graphics_available(): - # Request plotting and store images - design2.plot(screenshot=scshot_2) +def test_boolean_body_operations(modeler: Modeler): + """ + Test cases: - # Check that the images are different - assert scshot_1.exists() - assert scshot_2.exists() + 1) master/master + a) intersect + i) normal + x) identity + y) transform + ii) empty failure + b) subtract + i) normal + x) identity + y) transform + ii) empty failure + iii) disjoint + c) unite + i) normal + x) identity + y) transform + ii) disjoint + 2) instance/instance + a) intersect + i) normal + x) identity + y) transform + ii) empty failure + b) subtract + i) normal + x) identity + y) transform + ii) empty failure + c) unite + i) normal + x) identity + y) transform + """ + design = modeler.create_design("TestBooleanOperations") - from pyvista.plotting.utilities.regression import compare_images as pv_compare_images + comp1 = design.add_component("Comp1") + comp2 = design.add_component("Comp2") + comp3 = design.add_component("Comp3") - err = pv_compare_images(str(scshot_1), str(scshot_2)) - assert not err < 0.1 + body1 = comp1.extrude_sketch("Body1", Sketch().box(Point2D([0, 0]), 1, 1), 1) + body2 = comp2.extrude_sketch("Body2", Sketch().box(Point2D([0.5, 0]), 1, 1), 1) + body3 = comp3.extrude_sketch("Body3", Sketch().box(Point2D([5, 0]), 1, 1), 1) - # Check that design1 is not active and design2 is active - assert not design1.is_active - assert design2.is_active + # 1.a.i.x + copy1 = body1.copy(comp1, "Copy1") + copy2 = body2.copy(comp2, "Copy2") + copy1.intersect(copy2) + assert not copy2.is_alive + assert body2.is_alive + assert Accuracy.length_is_equal(copy1.volume.m, 0.5) -def test_get_active_design(modeler: Modeler): - """Return the active design from the designs dictionary of the modeler.""" - design1 = modeler.create_design("Design1") - d1_id = design1.design_id - active_design = modeler.get_active_design() - assert active_design.design_id == d1_id + # 1.a.i.y + copy1 = body1.copy(comp1, "Copy1") + copy2 = body2.copy(comp2, "Copy2") + copy2.translate(UnitVector3D([1, 0, 0]), 0.25) + copy1.intersect(copy2) + assert not copy2.is_alive + assert Accuracy.length_is_equal(copy1.volume.m, 0.25) -def test_get_collision(modeler: Modeler): - """Test the collision state between two bodies.""" - design = modeler.open_file(FILES_DIR / "MixingTank.scdocx") - body1 = design.bodies[0] - body2 = design.bodies[1] - body3 = design.bodies[2] + # 1.a.ii + copy1 = body1.copy(comp1, "Copy1") + copy3 = body3.copy(comp3, "Copy3") + with pytest.raises(ValueError, match="bodies do not intersect"): + copy1.intersect(copy3) - assert body1.get_collision(body2) == CollisionType.TOUCH - assert body2.get_collision(body3) == CollisionType.NONE + assert copy1.is_alive + assert copy3.is_alive + # 1.b.i.x + copy1 = body1.copy(comp1, "Copy1") + copy2 = body2.copy(comp2, "Copy2") + copy1.subtract(copy2) -def test_set_body_name(modeler: Modeler): - """Test the setting the name of a body.""" - design = modeler.create_design("simple_cube") - unit = DEFAULT_UNITS.LENGTH - plane = Plane( - Point3D([1 / 2, 1 / 2, 0.0], unit=unit), - UNITVECTOR3D_X, - UNITVECTOR3D_Y, - ) - box_plane = Sketch(plane) - box_plane.box(Point2D([0.0, 0.0]), width=1 * unit, height=1 * unit) - box = design.extrude_sketch("first_name", box_plane, 1 * unit) - assert box.name == "first_name" - box.set_name("updated_name") - assert box.name == "updated_name" - box.name = "updated_name2" - assert box.name == "updated_name2" + assert not copy2.is_alive + assert body2.is_alive + assert Accuracy.length_is_equal(copy1.volume.m, 0.5) + # 1.b.i.y + copy1 = body1.copy(comp1, "Copy1") + copy2 = body2.copy(comp2, "Copy2") + copy2.translate(UnitVector3D([1, 0, 0]), 0.25) + copy1.subtract(copy2) -def test_set_fill_style(modeler: Modeler): - """Test the setting the fill style of a body.""" - design = modeler.create_design("RVE") - unit = DEFAULT_UNITS.LENGTH + assert not copy2.is_alive + assert Accuracy.length_is_equal(copy1.volume.m, 0.75) - plane = Plane( - Point3D([1 / 2, 1 / 2, 0.0], unit=unit), - UNITVECTOR3D_X, - UNITVECTOR3D_Y, - ) + # 1.b.ii + copy1 = body1.copy(comp1, "Copy1") + copy1a = body1.copy(comp1, "Copy1a") + with pytest.raises(ValueError): + copy1.subtract(copy1a) - box_plane = Sketch(plane) - box_plane.box(Point2D([0.0, 0.0]), width=1 * unit, height=1 * unit) - box = design.extrude_sketch("Matrix", box_plane, 1 * unit) - - assert box.fill_style == FillStyle.DEFAULT - box.set_fill_style(FillStyle.TRANSPARENT) - assert box.fill_style == FillStyle.TRANSPARENT - box.fill_style = FillStyle.OPAQUE - assert box.fill_style == FillStyle.OPAQUE + assert copy1.is_alive + assert copy1a.is_alive + # 1.b.iii + copy1 = body1.copy(comp1, "Copy1") + copy3 = body3.copy(comp3, "Copy3") + copy1.subtract(copy3) -def test_body_suppression(modeler: Modeler): - """Test the suppression of a body.""" + assert Accuracy.length_is_equal(copy1.volume.m, 1) + assert copy1.volume + assert not copy3.is_alive - design = modeler.create_design("RVE") - unit = DEFAULT_UNITS.LENGTH + # 1.c.i.x + copy1 = body1.copy(comp1, "Copy1") + copy2 = body2.copy(comp2, "Copy2") + copy1.unite(copy2) - plane = Plane( - Point3D([1 / 2, 1 / 2, 0.0], unit=unit), - UNITVECTOR3D_X, - UNITVECTOR3D_Y, - ) + assert not copy2.is_alive + assert body2.is_alive + assert Accuracy.length_is_equal(copy1.volume.m, 1.5) - box_plane = Sketch(plane) - box_plane.box(Point2D([0.0, 0.0]), width=1 * unit, height=1 * unit) - box = design.extrude_sketch("Matrix", box_plane, 1 * unit) + # 1.c.i.y + copy1 = body1.copy(comp1, "Copy1") + copy2 = body2.copy(comp2, "Copy2") + copy2.translate(UnitVector3D([1, 0, 0]), 0.25) + copy1.unite(copy2) - assert box.is_suppressed is False - box.set_suppressed(True) - assert box.is_suppressed is True - box.is_suppressed = False - assert box.is_suppressed is False + assert not copy2.is_alive + assert Accuracy.length_is_equal(copy1.volume.m, 1.75) + # 1.c.ii + copy1 = body1.copy(comp1, "Copy1") + copy3 = body3.copy(comp3, "Copy3") + copy1.unite(copy3) -def test_set_body_color(modeler: Modeler): - """Test the getting and setting of body color.""" + assert not copy3.is_alive + assert body3.is_alive + assert Accuracy.length_is_equal(copy1.volume.m, 1) - design = modeler.create_design("RVE2") - unit = DEFAULT_UNITS.LENGTH - plane = Plane( - Point3D([1 / 2, 1 / 2, 0.0], unit=unit), - UNITVECTOR3D_X, - UNITVECTOR3D_Y, - ) - box_plane = Sketch(plane) - box_plane.box(Point2D([0.0, 0.0]), width=1 * unit, height=1 * unit) - box = design.extrude_sketch("Block", box_plane, 1 * unit) +def test_multiple_bodies_boolean_operations(modeler: Modeler): + """Test boolean operations with multiple bodies.""" + design = modeler.create_design("TestBooleanOperationsMultipleBodies") - # Default body color is if it is not set on server side. - assert box.color == DEFAULT_COLOR + comp1 = design.add_component("Comp1") + comp2 = design.add_component("Comp2") + comp3 = design.add_component("Comp3") - # Set the color of the body using hex code. - box.color = "#0000ff" - assert box.color[0:7] == "#0000ff" + body1 = comp1.extrude_sketch("Body1", Sketch().box(Point2D([0, 0]), 1, 1), 1) + body2 = comp2.extrude_sketch("Body2", Sketch().box(Point2D([0.5, 0]), 1, 1), 1) + body3 = comp3.extrude_sketch("Body3", Sketch().box(Point2D([5, 0]), 1, 1), 1) - box.color = "#ffc000" - assert box.color[0:7] == "#ffc000" + ################# Check subtract operation ################# + copy1_sub = body1.copy(comp1, "Copy1_subtract") + copy2_sub = body2.copy(comp2, "Copy2_subtract") + copy3_sub = body3.copy(comp3, "Copy3_subtract") + copy1_sub.subtract([copy2_sub, copy3_sub]) - # Set the color of the body using color name. - box.set_color("green") - box.color[0:7] == "#008000" + assert not copy2_sub.is_alive + assert not copy3_sub.is_alive + assert body2.is_alive + assert body3.is_alive + assert len(comp1.bodies) == 2 + assert len(comp2.bodies) == 1 + assert len(comp3.bodies) == 1 - # Set the color of the body using RGB values between (0,1) as floats. - box.set_color((1.0, 0.0, 0.0)) - box.color[0:7] == "#ff0000" + # Cleanup previous subtest + comp1.delete_body(copy1_sub) + assert len(comp1.bodies) == 1 - # Set the color of the body using RGB values between (0,255) as integers). - box.set_color((0, 255, 0)) - box.color[0:7] == "#00ff00" + ################# Check unite operation ################# + copy1_uni = body1.copy(comp1, "Copy1_unite") + copy2_uni = body2.copy(comp2, "Copy2_unite") + copy3_uni = body3.copy(comp3, "Copy3_unite") + copy1_uni.unite([copy2_uni, copy3_uni]) - # Assigning color object directly - blue_color = mcolors.to_rgba("#0000FF") - box.color = blue_color - assert box.color[0:7] == "#0000ff" + assert not copy2_uni.is_alive + assert not copy3_uni.is_alive + assert body2.is_alive + assert body3 is not None and body3.is_alive + assert len(comp1.bodies) == 2 + assert len(comp2.bodies) == 1 + assert len(comp3.bodies) == 1 - # Test an RGBA color - box.color = "#ff00003c" - assert box.color == "#ff00003c" + # Cleanup previous subtest + comp1.delete_body(copy1_uni) + assert len(comp1.bodies) == 1 - # Test setting the opacity separately - box.opacity = 0.8 - assert box.color == "#ff0000cc" + ################# Check intersect operation ################# + copy1_int = body1.copy(comp1, "Copy1_intersect") + copy2_int = body2.copy(comp2, "Copy2_intersect") + copy3_int = body3.copy(comp3, "Copy3_intersect") # Body 3 does not intersect them + copy1_int.intersect([copy2_int]) - # Try setting the opacity to an invalid value - with pytest.raises( - ValueError, match="Invalid color value: Opacity value must be between 0 and 1." - ): - box.opacity = 255 + assert not copy2_int.is_alive + assert copy3_int.is_alive + assert body2.is_alive + assert body3.is_alive + assert len(comp1.bodies) == 2 + assert len(comp2.bodies) == 1 + assert len(comp3.bodies) == 2 + # Cleanup previous subtest + comp1.delete_body(copy1_int) + comp3.delete_body(copy3_int) + assert len(comp1.bodies) == 1 + assert len(comp3.bodies) == 1 -def test_body_scale(modeler: Modeler): - """Verify the correct scaling of a body.""" - design = modeler.create_design("BodyScale_Test") - body = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1) - assert Accuracy.length_is_equal(body.volume.m, 1) +def test_bool_operations_with_keep_other(modeler: Modeler): + """Test boolean operations with keep other option.""" + # Create the design and bodies + design = modeler.create_design("TestBooleanOperationsWithKeepOther") - body.scale(2) - assert Accuracy.length_is_equal(body.volume.m, 8) + comp1 = design.add_component("Comp1") + comp2 = design.add_component("Comp2") + comp3 = design.add_component("Comp3") - body.scale(0.25) - assert Accuracy.length_is_equal(body.volume.m, 1 / 8) + body1 = comp1.extrude_sketch("Body1", Sketch().box(Point2D([0, 0]), 1, 1), 1) + body2 = comp2.extrude_sketch("Body2", Sketch().box(Point2D([0.5, 0]), 1, 1), 1) + body3 = comp3.extrude_sketch("Body3", Sketch().box(Point2D([5, 0]), 1, 1), 1) + # ---- Verify subtract operation ---- + body1.subtract([body2, body3], keep_other=True) -def test_body_mapping(modeler: Modeler): - """Verify the correct mapping of a body.""" - design = modeler.create_design("BodyMap_Test") + assert body2.is_alive + assert body3.is_alive + assert len(comp1.bodies) == 1 + assert len(comp2.bodies) == 1 + assert len(comp3.bodies) == 1 - # non-symmetric shape to allow determination of mirroring - body = design.extrude_sketch( - "box", - Sketch() - .segment(Point2D([1, 1]), Point2D([-1, 1])) - .segment_to_point(Point2D([0, 0.5])) - .segment_to_point(Point2D([-1, -1])) - .segment_to_point(Point2D([1, -1])) - .segment_to_point(Point2D([1, 1])), - 1, - ) + # ---- Verify unite operation ---- + body1.unite([body2, body3], keep_other=True) - # Test 1: identity mapping - everything should be the same - copy = body.copy(body.parent_component, "copy") - copy.map(Frame(Point3D([0, 0, 0]), UnitVector3D([1, 0, 0]), UnitVector3D([0, 1, 0]))) + assert body2.is_alive + assert body3.is_alive + assert len(comp1.bodies) == 1 + assert len(comp2.bodies) == 1 + assert len(comp3.bodies) == 1 - vertices = [] - for edge in body.edges: - vertices.extend([edge.shape.start, edge.shape.end]) + # ---- Verify intersect operation ---- + body1.intersect(body2, keep_other=True) - copy_vertices = [] - for edge in copy.edges: - copy_vertices.extend([edge.shape.start, edge.shape.end]) + assert body1.is_alive + assert body2.is_alive + assert body3.is_alive + assert len(comp1.bodies) == 1 + assert len(comp2.bodies) == 1 + assert len(comp3.bodies) == 1 - assert np.allclose(vertices, copy_vertices) - # Test 2: mirror the body - flips only the x direction - copy = body.copy(body.parent_component, "copy") - copy.map(Frame(Point3D([-4, 0, 1]), UnitVector3D([-1, 0, 0]), UnitVector3D([0, 1, 0]))) +def test_child_component_instances(modeler: Modeler): + """Test creation of child ``Component`` instances and check the data model + reflects that. + """ + design_name = "ChildComponentInstances_Test" + design = modeler.create_design(design_name) + # Create a base component + base1 = design.add_component("Base1") + comp1 = base1.add_component("A") + comp2 = base1.add_component("B") - copy_vertices = [] - for edge in copy.edges: - copy_vertices.extend([edge.shape.start, edge.shape.end]) + # Create the solid body for the base + sketch = Sketch().box(Point2D([5, 10]), 10, 20) + comp2.extrude_sketch("Bottom", sketch, 5) - # expected vertices from confirmed mirror - expected_vertices = [ - Point3D([-3.0, -1.0, 0.0]), - Point3D([-5.0, -1.0, 0.0]), - Point3D([-3.0, -1.0, 1.0]), - Point3D([-3.0, -1.0, 0.0]), - Point3D([-4.0, 0.5, 0.0]), - Point3D([-3.0, -1.0, 0.0]), - Point3D([-4.0, 0.5, 1.0]), - Point3D([-4.0, 0.5, 0.0]), - Point3D([-3.0, 1.0, 0.0]), - Point3D([-4.0, 0.5, 0.0]), - Point3D([-3.0, 1.0, 1.0]), - Point3D([-3.0, 1.0, 0.0]), - Point3D([-5.0, 1.0, 0.0]), - Point3D([-3.0, 1.0, 0.0]), - Point3D([-5.0, 1.0, 1.0]), - Point3D([-5.0, 1.0, 0.0]), - Point3D([-5.0, -1.0, 0.0]), - Point3D([-5.0, 1.0, 0.0]), - Point3D([-5.0, -1.0, 1.0]), - Point3D([-5.0, -1.0, 0.0]), - Point3D([-3.0, -1.0, 1.0]), - Point3D([-5.0, -1.0, 1.0]), - Point3D([-4.0, 0.5, 1.0]), - Point3D([-3.0, -1.0, 1.0]), - Point3D([-3.0, 1.0, 1.0]), - Point3D([-4.0, 0.5, 1.0]), - Point3D([-5.0, 1.0, 1.0]), - Point3D([-3.0, 1.0, 1.0]), - Point3D([-5.0, -1.0, 1.0]), - Point3D([-5.0, 1.0, 1.0]), - ] + # Create the 2nd base + base2 = design.add_component("Base2", base1) + base2.modify_placement(Vector3D([30, 0, 0])) - assert np.allclose(expected_vertices, copy_vertices) + # Create top part (applies to both Base1 and Base2) + sketch = Sketch(Plane(Point3D([0, 5, 5]))).box(Point2D([5, 2.5]), 10, 5) + comp1.extrude_sketch("Top", sketch, 5) - # Test 3: rotate body 180 degrees - flip x and y direction - map_copy = body.copy(body.parent_component, "copy") - map_copy.map(Frame(Point3D([0, 0, 0]), UnitVector3D([-1, 0, 0]), UnitVector3D([0, -1, 0]))) + # create the first child component + comp1.add_component("Child1") + comp1.extrude_sketch("Child1_body", Sketch(Plane([5, 7.5, 10])).box(Point2D([0, 0]), 1, 1), 1) - rotate_copy = body.copy(body.parent_component, "copy") - rotate_copy.rotate(Point3D([0, 0, 0]), UnitVector3D([0, 0, 1]), np.pi) + assert len(comp1.components) == 1 + assert len(base2.components[0].components) == 1 + assert len(comp1.components) == len(base2.components[0].components) - map_vertices = [] - for edge in map_copy.edges: - map_vertices.extend([edge.shape.start, edge.shape.end]) + # create the second child component + comp1.add_component("Child2") + comp1.extrude_sketch("Child2_body", Sketch(Plane([5, 7.5, 10])).box(Point2D([0, 0]), 1, 1), -1) - rotate_vertices = [] - for edge in rotate_copy.edges: - rotate_vertices.extend([edge.shape.start, edge.shape.end]) + assert len(comp1.components) == 2 + assert len(base2.components[0].components) == 2 + assert len(comp1.components) == len(base2.components[0].components) - assert np.allclose(map_vertices, rotate_vertices) +def test_multiple_designs(modeler: Modeler, tmp_path_factory: pytest.TempPathFactory): + """Generate multiple designs, make sure they are all separate, and once + a design is deactivated, the next one is activated. + """ + # Initiate expected output images + scshot_dir = tmp_path_factory.mktemp("test_multiple_designs") + scshot_1 = scshot_dir / "design1.png" + scshot_2 = scshot_dir / "design2.png" -def test_sphere_creation(modeler: Modeler): - """Test the creation of a sphere body with a given radius.""" - design = modeler.create_design("Spheretest") - center_point = Point3D([10, 10, 10], UNITS.m) - radius = Distance(1, UNITS.m) - spherebody = design.create_sphere("testspherebody", center_point, radius) - assert spherebody.name == "testspherebody" - assert len(spherebody.faces) == 1 - assert round(spherebody.volume._magnitude, 3) == round(4.1887902, 3) + # Create your design on the server side + design1 = modeler.create_design("Design1") + # Create a Sketch object and draw a slot + sketch1 = Sketch() + sketch1.slot(Point2D([10, 10], UNITS.mm), Quantity(10, UNITS.mm), Quantity(5, UNITS.mm)) -def test_body_mirror(modeler: Modeler): - """Test the mirroring of a body.""" - design = modeler.create_design("Design1") + # Extrude the sketch to create a body + design1.extrude_sketch("MySlot", sketch1, Quantity(10, UNITS.mm)) - # Create shape with no lines of symmetry in any axis - body = design.extrude_sketch( - "box", + # SKIPPING IF GRAPHICS REQUIRED + if are_graphics_available(): + # Request plotting and store images + design1.plot(screenshot=scshot_1) + + # Create a second design + design2 = modeler.create_design("Design2") + + # Create a Sketch object and draw a rectangle + sketch2 = Sketch() + sketch2.box(Point2D([-30, -30], UNITS.mm), 5 * UNITS.mm, 8 * UNITS.mm) + + # Extrude the sketch to create a body + design2.extrude_sketch("MyRectangle", sketch2, Quantity(10, UNITS.mm)) + + # SKIPPING IF GRAPHICS REQUIRED + if are_graphics_available(): + # Request plotting and store images + design2.plot(screenshot=scshot_2) + + # Check that the images are different + assert scshot_1.exists() + assert scshot_2.exists() + + from pyvista.plotting.utilities.regression import compare_images as pv_compare_images + + err = pv_compare_images(str(scshot_1), str(scshot_2)) + assert not err < 0.1 + + # Check that design1 is not active and design2 is active + assert not design1.is_active + assert design2.is_active + + +def test_get_active_design(modeler: Modeler): + """Return the active design from the designs dictionary of the modeler.""" + design1 = modeler.create_design("Design1") + d1_id = design1.design_id + active_design = modeler.get_active_design() + assert active_design.design_id == d1_id + + +def test_get_collision(modeler: Modeler): + """Test the collision state between two bodies.""" + design = modeler.open_file(FILES_DIR / "MixingTank.scdocx") + body1 = design.bodies[0] + body2 = design.bodies[1] + body3 = design.bodies[2] + + assert body1.get_collision(body2) == CollisionType.TOUCH + assert body2.get_collision(body3) == CollisionType.NONE + + +def test_set_body_name(modeler: Modeler): + """Test the setting the name of a body.""" + design = modeler.create_design("simple_cube") + unit = DEFAULT_UNITS.LENGTH + plane = Plane( + Point3D([1 / 2, 1 / 2, 0.0], unit=unit), + UNITVECTOR3D_X, + UNITVECTOR3D_Y, + ) + box_plane = Sketch(plane) + box_plane.box(Point2D([0.0, 0.0]), width=1 * unit, height=1 * unit) + box = design.extrude_sketch("first_name", box_plane, 1 * unit) + assert box.name == "first_name" + box.set_name("updated_name") + assert box.name == "updated_name" + box.name = "updated_name2" + assert box.name == "updated_name2" + + +def test_set_fill_style(modeler: Modeler): + """Test the setting the fill style of a body.""" + design = modeler.create_design("RVE") + unit = DEFAULT_UNITS.LENGTH + + plane = Plane( + Point3D([1 / 2, 1 / 2, 0.0], unit=unit), + UNITVECTOR3D_X, + UNITVECTOR3D_Y, + ) + + box_plane = Sketch(plane) + box_plane.box(Point2D([0.0, 0.0]), width=1 * unit, height=1 * unit) + box = design.extrude_sketch("Matrix", box_plane, 1 * unit) + + assert box.fill_style == FillStyle.DEFAULT + box.set_fill_style(FillStyle.TRANSPARENT) + assert box.fill_style == FillStyle.TRANSPARENT + box.fill_style = FillStyle.OPAQUE + assert box.fill_style == FillStyle.OPAQUE + + +def test_body_suppression(modeler: Modeler): + """Test the suppression of a body.""" + + design = modeler.create_design("RVE") + unit = DEFAULT_UNITS.LENGTH + + plane = Plane( + Point3D([1 / 2, 1 / 2, 0.0], unit=unit), + UNITVECTOR3D_X, + UNITVECTOR3D_Y, + ) + + box_plane = Sketch(plane) + box_plane.box(Point2D([0.0, 0.0]), width=1 * unit, height=1 * unit) + box = design.extrude_sketch("Matrix", box_plane, 1 * unit) + + assert box.is_suppressed is False + box.set_suppressed(True) + assert box.is_suppressed is True + box.is_suppressed = False + assert box.is_suppressed is False + + +def test_set_body_color(modeler: Modeler): + """Test the getting and setting of body color.""" + + design = modeler.create_design("RVE2") + unit = DEFAULT_UNITS.LENGTH + + plane = Plane( + Point3D([1 / 2, 1 / 2, 0.0], unit=unit), + UNITVECTOR3D_X, + UNITVECTOR3D_Y, + ) + box_plane = Sketch(plane) + box_plane.box(Point2D([0.0, 0.0]), width=1 * unit, height=1 * unit) + box = design.extrude_sketch("Block", box_plane, 1 * unit) + + # Default body color is if it is not set on server side. + assert box.color == DEFAULT_COLOR + + # Set the color of the body using hex code. + box.color = "#0000ff" + assert box.color[0:7] == "#0000ff" + + box.color = "#ffc000" + assert box.color[0:7] == "#ffc000" + + # Set the color of the body using color name. + box.set_color("green") + box.color[0:7] == "#008000" + + # Set the color of the body using RGB values between (0,1) as floats. + box.set_color((1.0, 0.0, 0.0)) + box.color[0:7] == "#ff0000" + + # Set the color of the body using RGB values between (0,255) as integers). + box.set_color((0, 255, 0)) + box.color[0:7] == "#00ff00" + + # Assigning color object directly + blue_color = mcolors.to_rgba("#0000FF") + box.color = blue_color + assert box.color[0:7] == "#0000ff" + + # Test an RGBA color + box.color = "#ff00003c" + assert box.color == "#ff00003c" + + # Test setting the opacity separately + box.opacity = 0.8 + assert box.color == "#ff0000cc" + + # Try setting the opacity to an invalid value + with pytest.raises( + ValueError, match="Invalid color value: Opacity value must be between 0 and 1." + ): + box.opacity = 255 + + +def test_body_scale(modeler: Modeler): + """Verify the correct scaling of a body.""" + design = modeler.create_design("BodyScale_Test") + + body = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1) + assert Accuracy.length_is_equal(body.volume.m, 1) + + body.scale(2) + assert Accuracy.length_is_equal(body.volume.m, 8) + + body.scale(0.25) + assert Accuracy.length_is_equal(body.volume.m, 1 / 8) + + +def test_body_mapping(modeler: Modeler): + """Verify the correct mapping of a body.""" + design = modeler.create_design("BodyMap_Test") + + # non-symmetric shape to allow determination of mirroring + body = design.extrude_sketch( + "box", Sketch() .segment(Point2D([1, 1]), Point2D([-1, 1])) .segment_to_point(Point2D([0, 0.5])) @@ -2740,6 +2779,7 @@ def test_sweep_sketch(modeler: Modeler): # create a circle on the XY-plane centered at (0, 0, 0) with radius 5 path = [Circle(Point3D([0, 0, 0]), path_radius).trim(Interval(0, 2 * np.pi))] + # create the donut body body = design_sketch.sweep_sketch("donutsweep", profile, path) assert body.is_surface is False @@ -2760,78 +2800,6 @@ def test_sweep_sketch(modeler: Modeler): assert Accuracy.length_is_equal(body.volume.m, 394.7841760435743) -def test_sweep_chain(modeler: Modeler): - """Test revolving a semi-elliptical profile around a circular axis to make - a bowl. - """ - design_chain = modeler.create_design("bowl") - - radius = 10 - - # create quarter-ellipse profile with major radius = 10, minor radius = 5 - profile = [ - Ellipse( - Point3D([0, 0, radius / 2]), radius, radius / 2, reference=[1, 0, 0], axis=[0, 1, 0] - ).trim(Interval(0, np.pi / 2)) - ] - - # create circle on the plane parallel to the XY-plane but moved up by 5 units with radius 10 - path = [Circle(Point3D([0, 0, radius / 2]), radius).trim(Interval(0, 2 * np.pi))] - - # create the bowl body - body = design_chain.sweep_chain("bowlsweep", path, profile) - - assert body.is_surface is True - - # check edges - assert len(body.edges) == 1 - - # check length of edge - # compute expected circumference (circle with radius 10) - expected_edge_cirumference = 2 * np.pi * 10 - assert body.edges[0].length.m == pytest.approx(expected_edge_cirumference) - - # check faces - assert len(body.faces) == 1 - - # check area of face - # compute expected area (half a spheroid) - minor_rad = radius / 2 - e_squared = 1 - (minor_rad**2 / radius**2) - e = np.sqrt(e_squared) - expected_face_area = ( - 2 * np.pi * radius**2 + (minor_rad**2 / e) * np.pi * np.log((1 + e) / (1 - e)) - ) / 2 - assert body.faces[0].area.m == pytest.approx(expected_face_area) - - # check volume of body - # expected is 0 since it's not a closed surface - assert body.volume.m == 0 - - -def test_create_body_from_loft_profile(modeler: Modeler): - """Test the ``create_body_from_loft_profile()`` method to create a vase - shape. - """ - design_sketch = modeler.create_design("loftprofile") - - profile1 = Circle(origin=[0, 0, 0], radius=8).trim(Interval(0, 2 * np.pi)) - profile2 = Circle(origin=[0, 0, 10], radius=10).trim(Interval(0, 2 * np.pi)) - profile3 = Circle(origin=[0, 0, 20], radius=5).trim(Interval(0, 2 * np.pi)) - - # Call the method - result = design_sketch.create_body_from_loft_profile( - "vase", [[profile1], [profile2], [profile3]], False, False - ) - - # Assert that the resulting body has only one face. - assert len(result.faces) == 1 - - # check volume of body - # expected is 0 since it's not a closed surface - assert result.volume.m == 0 - - def test_revolve_sketch(modeler: Modeler): """Test revolving a circular profile for a quarter donut.""" # Initialize the donut sketch design @@ -2955,6 +2923,7 @@ def check_list_equality(lines, expected_lines): # Now, only "comp_3", "nested_2_comp_1" and "nested_1_nested_1_comp_1" # will have a body associated. # + # # Create the components comp_1 = design.add_component("Component_1") @@ -3071,542 +3040,4 @@ def check_list_equality(lines, expected_lines): "|---(comp) Nested_1_Nested_1_Component_1", " |---(body) nested_1_nested_1_comp_1_circle", ] - assert check_list_equality(lines, ref) is True - - -def test_surface_body_creation(modeler: Modeler): - """Test surface body creation from trimmed surfaces.""" - design = modeler.create_design("Design1") - - # half sphere - surface = Sphere([0, 0, 0], 1) - trimmed_surface = surface.trim(BoxUV(Interval(0, np.pi * 2), Interval(0, np.pi / 2))) - body = design.create_body_from_surface("sphere", trimmed_surface) - assert len(design.bodies) == 1 - assert body.is_surface - assert body.faces[0].area.m == pytest.approx(np.pi * 2) - - # cylinder - surface = Cylinder([0, 0, 0], 1) - trimmed_surface = surface.trim(BoxUV(Interval(0, np.pi * 2), Interval(0, 1))) - body = design.create_body_from_surface("cylinder", trimmed_surface) - - assert len(design.bodies) == 2 - assert body.is_surface - assert body.faces[0].area.m == pytest.approx(np.pi * 2) - - # cone - surface = Cone([0, 0, 0], 1, np.pi / 4) - trimmed_surface = surface.trim(BoxUV(Interval(0, np.pi * 2), Interval(surface.apex.z.m, 0))) - body = design.create_body_from_surface("cone", trimmed_surface) - - assert len(design.bodies) == 3 - assert body.is_surface - assert body.faces[0].area.m == pytest.approx(4.44288293816) - - # half torus - surface = Torus([0, 0, 0], 2, 1) - trimmed_surface = surface.trim(BoxUV(Interval(0, np.pi), Interval(0, np.pi * 2))) - body = design.create_body_from_surface("torus", trimmed_surface) - - assert len(design.bodies) == 4 - assert body.is_surface - assert body.faces[0].area.m == pytest.approx(39.4784176044) - - # SOLID BODIES - - # sphere - surface = Sphere([0, 0, 0], 1) - trimmed_surface = surface.trim(BoxUV(Interval(0, np.pi * 2), Interval(-np.pi / 2, np.pi / 2))) - body = design.create_body_from_surface("sphere_solid", trimmed_surface) - assert len(design.bodies) == 5 - assert not body.is_surface - assert body.faces[0].area.m == pytest.approx(np.pi * 4) - - # torus - surface = Torus([0, 0, 0], 2, 1) - trimmed_surface = surface.trim(BoxUV(Interval(0, np.pi * 2), Interval(0, np.pi * 2))) - body = design.create_body_from_surface("torus_solid", trimmed_surface) - - assert len(design.bodies) == 6 - assert not body.is_surface - assert body.faces[0].area.m == pytest.approx(39.4784176044 * 2) - - -def test_create_surface_from_nurbs_sketch(modeler: Modeler): - """Test creating a surface from a NURBS sketch.""" - design = modeler.create_design("NURBS_Sketch_Surface") - - # Create a NURBS sketch - sketch = Sketch() - sketch.nurbs_from_2d_points( - points=[ - Point2D([0, 0]), - Point2D([1, 0]), - Point2D([1, 1]), - Point2D([0, 1]), - ], - tag="nurbs_sketch", - ) - sketch.segment( - start=Point2D([0, -1]), - end=Point2D([0, 2]), - tag="segment_1", - ) - - # Create a surface from the NURBS sketch - surface_body = design.create_surface( - name="nurbs_surface", - sketch=sketch, - ) - - assert len(design.bodies) == 1 - assert surface_body.is_surface - assert surface_body.faces[0].area.m > 0 - - -def test_design_parameters(modeler: Modeler): - """Test the design parameter's functionality.""" - design = modeler.open_file(FILES_DIR / "blockswithparameters.dsco") - test_parameters = design.parameters - - # Verify the initial parameters - assert len(test_parameters) == 2 - assert test_parameters[0].name == "p1" - assert abs(test_parameters[0].dimension_value - 0.00010872999999999981) < 1e-8 - assert test_parameters[0].dimension_type == ParameterType.DIMENSIONTYPE_AREA - - assert test_parameters[1].name == "p2" - assert abs(test_parameters[1].dimension_value - 0.0002552758322160813) < 1e-8 - assert test_parameters[1].dimension_type == ParameterType.DIMENSIONTYPE_AREA - - # Update the second parameter and verify the status - test_parameters[1].dimension_value = 0.0006 - status = design.set_parameter(test_parameters[1]) - assert status == ParameterUpdateStatus.SUCCESS - - # Attempt to update the first parameter and expect a constrained status - test_parameters[0].dimension_value = 0.0006 - status = design.set_parameter(test_parameters[0]) - assert status == ParameterUpdateStatus.CONSTRAINED_PARAMETERS - - test_parameters[0].name = "NewName" - assert test_parameters[0].name == "NewName" - - test_parameters[0].dimension_type = ParameterType.DIMENSIONTYPE_AREA - assert test_parameters[0].dimension_type == ParameterType.DIMENSIONTYPE_AREA - - -def test_cached_bodies(modeler: Modeler): - """Test that bodies are cached correctly. - - Whenever a new body is created, modified etc. we should make sure that the cache is updated. - """ - design = modeler.create_design("ModelingDemo") - - # Define a sketch - origin = Point3D([0, 0, 10]) - plane = Plane(origin, direction_x=[1, 0, 0], direction_y=[0, 1, 0]) - - # Create a sketch - sketch_box = Sketch(plane) - sketch_box.box(Point2D([20, 20]), 30 * UNITS.m, 30 * UNITS.m) - - sketch_cylinder = Sketch(plane) - sketch_cylinder.circle(Point2D([20, 20]), 5 * UNITS.m) - - design.extrude_sketch(name="BoxBody", sketch=sketch_box, distance=Distance(30, unit=UNITS.m)) - design.extrude_sketch( - name="CylinderBody", - sketch=sketch_cylinder, - distance=Distance(60, unit=UNITS.m), - ) - - my_bodies = design.bodies - my_bodies_2 = design.bodies - - # We should make sure that the object memory addresses are the same - for body1, body2 in zip(my_bodies, my_bodies_2): - assert body1 is body2 # We are comparing the memory addresses - assert id(body1) == id(body2) - - design.extrude_sketch( - name="CylinderBody2", - sketch=sketch_cylinder, - distance=Distance(20, unit=UNITS.m), - direction="-", - ) - my_bodies_3 = design.bodies - - for body1, body3 in zip(my_bodies, my_bodies_3): - assert body1 is not body3 - assert id(body1) != id(body3) - - -def test_extrude_sketch_with_cut_request(modeler: Modeler): - """Test the cut argument when performing a sketch extrusion. - - This method mimics a cut operation. - - Behind the scenes, a subtraction operation is performed on the bodies. After extruding the - sketch, the resulting body should be a cut body. - """ - # Define a sketch - origin = Point3D([0, 0, 10]) - plane = Plane(origin, direction_x=[1, 0, 0], direction_y=[0, 1, 0]) - - # Create a sketch - sketch_box = Sketch(plane) - sketch_box.box(Point2D([20, 20]), 30 * UNITS.m, 30 * UNITS.m) - - sketch_cylinder = Sketch(plane) - sketch_cylinder.circle(Point2D([20, 20]), 5 * UNITS.m) - - # Create a design - design = modeler.create_design("ExtrudeSketchWithCut") - - box_body = design.extrude_sketch( - name="BoxBody", sketch=sketch_box, distance=Distance(30, unit=UNITS.m) - ) - volume_box = box_body.volume - - design.extrude_sketch( - name="CylinderBody", sketch=sketch_cylinder, distance=Distance(60, unit=UNITS.m), cut=True - ) - - # Verify there is only one body - assert len(design.bodies) == 1 - - # Verify the volume of the resulting body is less than the volume of the box - assert design.bodies[0].volume < volume_box - - -def test_extrude_sketch_with_cut_request_no_collision(modeler: Modeler): - """Test the cut argument when performing a sketch extrusion (with no collision). - - This method mimics an unsuccessful cut operation. - - The sketch extrusion should not result in a cut body since there is no collision between the - original body and the extruded body. - """ - # Define a sketch - origin = Point3D([0, 0, 10]) - plane = Plane(origin, direction_x=[1, 0, 0], direction_y=[0, 1, 0]) - - # Create a sketch - sketch_box = Sketch(plane) - sketch_box.box(Point2D([20, 20]), 30 * UNITS.m, 30 * UNITS.m) - - sketch_cylinder = Sketch(plane) - sketch_cylinder.circle(Point2D([100, 100]), 5 * UNITS.m) - - # Create a design - design = modeler.create_design("ExtrudeSketchWithCutNoCollision") - - box_body = design.extrude_sketch( - name="BoxBody", sketch=sketch_box, distance=Distance(30, unit=UNITS.m) - ) - volume_box = box_body.volume - - design.extrude_sketch( - name="CylinderBody", sketch=sketch_cylinder, distance=Distance(60, unit=UNITS.m), cut=True - ) - - # Verify there is only one body... the cut operation should delete it - assert len(design.bodies) == 1 - - # Verify the volume of the resulting body is exactly the same - assert design.bodies[0].volume == volume_box - - -def test_create_surface_body_from_trimmed_curves(modeler: Modeler): - design = modeler.create_design("surface") - - # pill shape - circle1 = Circle(Point3D([0, 0, 0]), 1).trim(Interval(0, np.pi)) - line1 = Line(Point3D([-1, 0, 0]), UnitVector3D([0, -1, 0])).trim(Interval(0, 1)) - circle2 = Circle(Point3D([0, -1, 0]), 1).trim(Interval(np.pi, np.pi * 2)) - line2 = Line(Point3D([1, 0, 0]), UnitVector3D([0, -1, 0])).trim(Interval(0, 1)) - - body = design.create_surface_from_trimmed_curves("body", [circle1, line1, line2, circle2]) - assert body.is_surface - assert body.faces[0].area.m == pytest.approx( - Quantity(2 + np.pi, UNITS.m**2).m, rel=1e-6, abs=1e-8 - ) - - # create from edges (by getting their trimmed curves) - trimmed_curves_from_edges = [edge.shape for edge in body.edges] - body = design.create_surface_from_trimmed_curves("body2", trimmed_curves_from_edges) - assert body.is_surface - assert body.faces[0].area.m == pytest.approx( - Quantity(2 + np.pi, UNITS.m**2).m, rel=1e-6, abs=1e-8 - ) - - -def test_shell_body(modeler: Modeler): - """Test shell command.""" - design = modeler.create_design("shell") - base = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1) - - assert base.volume.m == pytest.approx(Quantity(1, UNITS.m**3).m, rel=1e-6, abs=1e-8) - assert len(base.faces) == 6 - - # shell - success = base.shell_body(0.1) - assert success - assert base.volume.m == pytest.approx(Quantity(0.728, UNITS.m**3).m, rel=1e-6, abs=1e-8) - assert len(base.faces) == 12 - - base = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1) - success = base.shell_body(-0.1) - assert success - assert base.volume.m == pytest.approx(Quantity(0.488, UNITS.m**3).m, rel=1e-6, abs=1e-8) - assert len(base.faces) == 12 - - -def test_shell_faces(modeler: Modeler): - """Test shell commands for a single face.""" - design = modeler.create_design("shell") - base = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1) - - assert base.volume.m == pytest.approx(Quantity(1, UNITS.m**3).m, rel=1e-6, abs=1e-8) - assert len(base.faces) == 6 - - # shell - success = base.remove_faces(base.faces[0], 0.1) - assert success - assert base.volume.m == pytest.approx(Quantity(0.584, UNITS.m**3).m, rel=1e-6, abs=1e-8) - assert len(base.faces) == 11 - - -def test_shell_multiple_faces(modeler: Modeler): - """Test shell commands for multiple faces.""" - design = modeler.create_design("shell") - base = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1) - - assert base.volume.m == pytest.approx(Quantity(1, UNITS.m**3).m, rel=1e-6, abs=1e-8) - assert len(base.faces) == 6 - - # shell - success = base.remove_faces([base.faces[0], base.faces[2]], 0.1) - assert success - assert base.volume.m == pytest.approx(Quantity(0.452, UNITS.m**3).m, rel=1e-6, abs=1e-8) - assert len(base.faces) == 10 - - -def test_set_face_color(modeler: Modeler): - """Test the getting and setting of face colors.""" - - design = modeler.create_design("FaceColorTest") - box = design.extrude_sketch("Body1", Sketch().box(Point2D([0, 0]), 1, 1), 1) - faces = box.faces - assert len(faces) == 6 - - # Default body color is if it is not set on server side. - assert faces[0].color == DEFAULT_COLOR - - # Set the color of the body using hex code. - faces[0].color = "#0000ffff" - assert faces[0].color == "#0000ffff" - - faces[1].color = "#ffc000ff" - assert faces[1].color == "#ffc000ff" - - # Set the color of the body using color name. - faces[2].set_color("green") - assert faces[2].color == "#008000ff" - - # Set the color of the body using RGB values between (0,1) as floats. - faces[0].set_color((1.0, 0.0, 0.0)) - assert faces[0].color == "#ff0000ff" - - # Set the color of the body using RGB values between (0,255) as integers). - faces[1].set_color((0, 255, 0)) - assert faces[1].color == "#00ff00ff" - - # Assigning color object directly - blue_color = mcolors.to_rgba("#0000FF") - faces[2].color = blue_color - assert faces[2].color == "#0000ffff" - - # Assign a color with opacity - faces[3].color = (255, 0, 0, 80) - assert faces[3].color == "#ff000050" - - # Test setting the opacity separately - faces[3].opacity = 0.8 - assert faces[3].color == "#ff0000cc" - - # Try setting the opacity to an invalid value - with pytest.raises( - ValueError, match="Invalid color value: Opacity value must be between 0 and 1." - ): - faces[3].opacity = 255 - - -def test_set_component_name(modeler: Modeler): - """Test the setting of component names.""" - - design = modeler.create_design("ComponentNameTest") - component = design.add_component("Component1") - assert component.name == "Component1" - - component.name = "ChangedComponentName" - assert component.name == "ChangedComponentName" - - -def test_get_face_bounding_box(modeler: Modeler): - """Test getting the bounding box of a face.""" - design = modeler.create_design("face_bounding_box") - body = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1) - - bounding_box = body.faces[0].bounding_box - assert bounding_box.min_corner.x.m == bounding_box.min_corner.y.m == -0.5 - assert bounding_box.max_corner.x.m == bounding_box.max_corner.y.m == 0.5 - - bounding_box = body.faces[1].bounding_box - assert bounding_box.min_corner.x.m == bounding_box.min_corner.y.m == -0.5 - assert bounding_box.max_corner.x.m == bounding_box.max_corner.y.m == 0.5 - - -def test_get_edge_bounding_box(modeler: Modeler): - """Test getting the bounding box of an edge.""" - design = modeler.create_design("edge_bounding_box") - body = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1) - - # Edge 0 goes from (-0.5, -0.5, 1) to (0.5, -0.5, 1) - bounding_box = body.edges[0].bounding_box - assert bounding_box.min_corner.x.m == bounding_box.min_corner.y.m == -0.5 - assert bounding_box.min_corner.z.m == 1 - assert bounding_box.max_corner.x.m == 0.5 - assert bounding_box.max_corner.y.m == -0.5 - assert bounding_box.max_corner.z.m == 1 - - # Test center - center = bounding_box.center - assert center.x.m == 0 - assert center.y.m == -0.5 - assert center.z.m == 1 - - -def test_get_body_bounding_box(modeler: Modeler): - """Test getting the bounding box of a body.""" - design = modeler.create_design("body_bounding_box") - body = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1) - - bounding_box = body.bounding_box - assert bounding_box.min_corner.x.m == bounding_box.min_corner.y.m == -0.5 - assert bounding_box.min_corner.z.m == 0 - assert bounding_box.max_corner.x.m == bounding_box.max_corner.y.m == 0.5 - assert bounding_box.max_corner.z.m == 1 - - # Test center - center = bounding_box.center - assert center.x.m == 0 - assert center.y.m == 0 - assert center.z.m == 0.5 - - -def test_extrude_faces_failure_log_to_file(modeler: Modeler): - """Test that the failure to extrude faces logs the correct message to a file.""" - # Create a design and body for testing - design = modeler.create_design("test_design") - body = design.extrude_sketch("test_body", Sketch().box(Point2D([0, 0]), 1, 1), 1) - - # Call the method with invalid parameters to trigger failure - result = modeler.geometry_commands.extrude_faces( - faces=[body.faces[0]], - distance=-10.0, # Invalid distance to trigger failure - direction=UnitVector3D([0, 0, 1]), - ) - # Assert the result is an empty list - assert result == [] - - result = modeler.geometry_commands.extrude_faces_up_to( - faces=[body.faces[0]], - up_to_selection=body.faces[0], # Using the same face as target to trigger failure - direction=UnitVector3D([0, 0, 1]), - seed_point=Point3D([0, 0, 0]), - ) - # Assert the result is an empty list - assert result == [] - - -def test_extrude_edges_missing_parameters(modeler: Modeler): - """Test that extrude_edges raises a ValueError when required parameters are missing.""" - # Create a design and body for testing - design = modeler.create_design("test_design") - body = design.extrude_sketch("test_body", Sketch().box(Point2D([0, 0]), 1, 1), 1) - - # Test case: Missing both `from_face` and `from_point`/`direction` - with pytest.raises( - ValueError, - match="To extrude edges, either a face or a direction and point must be provided.", - ): - modeler.geometry_commands.extrude_edges( - edges=[body.edges[0]], # Using the first edge of the body - distance=10.0, - from_face=None, - from_point=None, - direction=None, - ) - - -def test_import_component_named_selections(modeler: Modeler): - """Test importing named selections from an inserted design component.""" - # This file had a component inserted into it that has named selections that we need to import - design = modeler.open_file(Path(FILES_DIR, "import_component_groups.scdocx")) - component = design.components[0] - - assert len(design.named_selections) == 0 - component.import_named_selections() - assert len(design.named_selections) == 3 - - -def test_component_make_independent(modeler: Modeler): - """Test making components independent.""" - - design = modeler.open_file(Path(FILES_DIR, "cars.scdocx")) - face = next((ns for ns in design.named_selections if ns.name == "to_pull"), None).faces[0] - comp = next( - (ns for ns in design.named_selections if ns.name == "make_independent"), None - ).components[0] - - comp.make_independent() - - assert Accuracy.length_is_equal(comp.bodies[0].volume.m, face.body.volume.m) - - modeler.geometry_commands.extrude_faces(face, 1) - comp = design.components[0].components[-1].components[-1] # stale from update-design-in-place - - assert not Accuracy.length_is_equal(comp.bodies[0].volume.m, face.body.volume.m) - - -def test_write_body_facets_on_save(modeler: Modeler, tmp_path_factory: pytest.TempPathFactory): - design = modeler.open_file(Path(FILES_DIR, "cars.scdocx")) - - # First file without body facets - filepath_no_facets = tmp_path_factory.mktemp("test_design") / "cars_no_facets.scdocx" - design.download(filepath_no_facets) - - # Second file with body facets - filepath_with_facets = tmp_path_factory.mktemp("test_design") / "cars_with_facets.scdocx" - design.download(filepath_with_facets, write_body_facets=True) - - # Compare file sizes - size_no_facets = filepath_no_facets.stat().st_size - size_with_facets = filepath_with_facets.stat().st_size - - assert size_with_facets > size_no_facets - - # Ensure facets.bin and renderlist.xml files exist - with zipfile.ZipFile(filepath_with_facets, "r") as zip_ref: - namelist = set(zip_ref.namelist()) - - expected_files = { - "SpaceClaim/Graphics/facets.bin", - "SpaceClaim/Graphics/renderlist.xml", - } - - missing = expected_files - namelist - assert not missing + assert check_list_equality(lines, ref) is True \ No newline at end of file From 6f930e1a7533124054070d56ebbe16f5eeac8088 Mon Sep 17 00:00:00 2001 From: Jacob Kerstetter Date: Mon, 18 Aug 2025 14:13:46 -0400 Subject: [PATCH 2/6] remove_material implemented and tested --- .../core/_grpc/_services/base/materials.py | 5 +++ .../core/_grpc/_services/v0/materials.py | 15 ++++++++ .../core/_grpc/_services/v1/materials.py | 4 ++ src/ansys/geometry/core/designer/design.py | 17 +++++++++ tests/integration/test_material.py | 38 +++++++++++++++++++ 5 files changed, 79 insertions(+) diff --git a/src/ansys/geometry/core/_grpc/_services/base/materials.py b/src/ansys/geometry/core/_grpc/_services/base/materials.py index bceb66e8fc..c504bf63ce 100644 --- a/src/ansys/geometry/core/_grpc/_services/base/materials.py +++ b/src/ansys/geometry/core/_grpc/_services/base/materials.py @@ -43,3 +43,8 @@ def __init__(self, channel: grpc.Channel): def add_material(self, **kwargs) -> dict: """Add material to the service design.""" pass + + @abstractmethod + def remove_material(self, **kwargs) -> dict: + """Remove material from the service design.""" + pass diff --git a/src/ansys/geometry/core/_grpc/_services/v0/materials.py b/src/ansys/geometry/core/_grpc/_services/v0/materials.py index df9da9b717..e71b4b1b02 100644 --- a/src/ansys/geometry/core/_grpc/_services/v0/materials.py +++ b/src/ansys/geometry/core/_grpc/_services/v0/materials.py @@ -62,3 +62,18 @@ def add_material(self, **kwargs) -> dict: # noqa: D102 # Convert the response to a dictionary return {} + + @protect_grpc + def remove_material(self, **kwargs) -> dict: # noqa: D102 + from ansys.api.geometry.v0.materials_pb2 import RemoveFromDocumentRequest + + # Create the request - assumes all inputs are valid and of the proper type + request = RemoveFromDocumentRequest( + request_data=[from_material_to_grpc_material(mat) for mat in kwargs["materials"]] + ) + + # Call the gRPC service + _ = self.stub.RemoveFromDocument(request=request) + + # Convert the response to a dictionary + return {} diff --git a/src/ansys/geometry/core/_grpc/_services/v1/materials.py b/src/ansys/geometry/core/_grpc/_services/v1/materials.py index 4b54d626b6..acf0f39b2c 100644 --- a/src/ansys/geometry/core/_grpc/_services/v1/materials.py +++ b/src/ansys/geometry/core/_grpc/_services/v1/materials.py @@ -50,3 +50,7 @@ def __init__(self, channel: grpc.Channel): # noqa: D102 @protect_grpc def add_material(self, **kwargs) -> dict: # noqa: D102 raise NotImplementedError + + @protect_grpc + def remove_material(self, **kwargs) -> dict: # noqa: D102 + raise NotImplementedError diff --git a/src/ansys/geometry/core/designer/design.py b/src/ansys/geometry/core/designer/design.py index 670c1e73e7..0136a54c50 100644 --- a/src/ansys/geometry/core/designer/design.py +++ b/src/ansys/geometry/core/designer/design.py @@ -227,6 +227,23 @@ def add_material(self, material: Material) -> None: self._materials.append(material) self._grpc_client.log.debug(f"Material {material.name} is successfully added to design.") + @check_input_types + @ensure_design_is_active + def remove_material(self, material: Material | list[Material]) -> None: + """Remove a material from the design. + + Parameters + ---------- + material : Material | list[Material] + Material or list of materials to remove. + """ + material = material if isinstance(material, list) else [material] + + self._grpc_client.services.materials.remove_material(materials=material) + for mat in material: + self._materials.remove(mat) + self._grpc_client.log.debug(f"Material {mat.name} is successfully removed from design.") + @check_input_types @ensure_design_is_active def save(self, file_location: Path | str, write_body_facets: bool = False) -> None: diff --git a/tests/integration/test_material.py b/tests/integration/test_material.py index 2329a38664..d2a5fee50e 100644 --- a/tests/integration/test_material.py +++ b/tests/integration/test_material.py @@ -84,3 +84,41 @@ def test_material_creation(modeler: Modeler): design.materials[0].properties[MaterialPropertyType.POISSON_RATIO].quantity == mat_prop_quantity ) + + +def test_material_removal(modeler: Modeler): + """Test the removal of a material from a design.""" + design = modeler.create_design("my_design") + + mat_name = "mat_1" + density = 1000 * UNITS.kg / (UNITS.m * UNITS.m * UNITS.m) + material = Material(mat_name, density) + design.add_material(material) + + assert design.materials[0].name == mat_name + + design.remove_material(material) + + assert len(design.materials) == 0 + + +def test_remove_multiple_materials(modeler: Modeler): + """Test the removal of multiple materials from a design.""" + design = modeler.create_design("my_design") + + mat_name_1 = "mat_1" + density_1 = 1000 * UNITS.kg / (UNITS.m * UNITS.m * UNITS.m) + material_1 = Material(mat_name_1, density_1) + design.add_material(material_1) + + mat_name_2 = "mat_2" + density_2 = 2000 * UNITS.kg / (UNITS.m * UNITS.m * UNITS.m) + material_2 = Material(mat_name_2, density_2) + design.add_material(material_2) + + assert design.materials[0].name == mat_name_1 + assert design.materials[1].name == mat_name_2 + + design.remove_material([material_1, material_2]) + + assert len(design.materials) == 0 \ No newline at end of file From 7cb4729821a3d6fd5279db5a2243fe3b9f7b10a6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 18:16:39 +0000 Subject: [PATCH 3/6] chore: auto fixes from pre-commit hooks --- src/ansys/geometry/core/_grpc/_services/v0/bodies.py | 6 ++---- src/ansys/geometry/core/_grpc/_services/v1/bodies.py | 2 +- src/ansys/geometry/core/designer/body.py | 6 +++--- tests/integration/test_design.py | 11 ++++------- tests/integration/test_material.py | 2 +- 5 files changed, 11 insertions(+), 16 deletions(-) diff --git a/src/ansys/geometry/core/_grpc/_services/v0/bodies.py b/src/ansys/geometry/core/_grpc/_services/v0/bodies.py index b585c73301..96c69c8cb2 100644 --- a/src/ansys/geometry/core/_grpc/_services/v0/bodies.py +++ b/src/ansys/geometry/core/_grpc/_services/v0/bodies.py @@ -452,7 +452,7 @@ def get_assigned_material(self, **kwargs) -> dict: # noqa: D102 # Return the response - formatted as a dictionary return {"material": from_grpc_material_to_material(resp)} - + @protect_grpc def remove_assigned_material(self, **kwargs) -> dict: # noqa: D102 from ansys.api.geometry.v0.bodies_pb2 import RemoveAssignedMaterialRequest @@ -464,9 +464,7 @@ def remove_assigned_material(self, **kwargs) -> dict: # noqa: D102 resp = self.stub.RemoveAssignedMaterial(request=request) # Return the response - formatted as a dictionary - return { - "successfully_removed": [id for id in resp.successfully_removed] - } + return {"successfully_removed": [id for id in resp.successfully_removed]} @protect_grpc def set_name(self, **kwargs) -> dict: # noqa: D102 diff --git a/src/ansys/geometry/core/_grpc/_services/v1/bodies.py b/src/ansys/geometry/core/_grpc/_services/v1/bodies.py index 85c2892c76..8b19767763 100644 --- a/src/ansys/geometry/core/_grpc/_services/v1/bodies.py +++ b/src/ansys/geometry/core/_grpc/_services/v1/bodies.py @@ -131,7 +131,7 @@ def set_assigned_material(self, **kwargs) -> dict: # noqa: D102 @protect_grpc def get_assigned_material(self, **kwargs) -> dict: # noqa: D102 raise NotImplementedError - + @protect_grpc def remove_assigned_material(self, **kwargs): # noqa: D102 raise NotImplementedError diff --git a/src/ansys/geometry/core/designer/body.py b/src/ansys/geometry/core/designer/body.py index 09bbeb6e48..bcfd24794e 100644 --- a/src/ansys/geometry/core/designer/body.py +++ b/src/ansys/geometry/core/designer/body.py @@ -337,7 +337,7 @@ def get_assigned_material(self) -> Material: Material assigned to the body. """ return - + @abstractmethod def remove_assigned_material(self) -> None: """Remove the material assigned to the body.""" @@ -1065,7 +1065,7 @@ def get_assigned_material(self) -> Material: # noqa: D102 self._grpc_client.log.debug(f"Retrieving assigned material for body {self.id}.") response = self._grpc_client.services.bodies.get_assigned_material(id=self.id) return response.get("material") - + def remove_assigned_material(self) -> None: # noqa: D102 self._grpc_client.log.debug(f"Removing assigned material for body {self.id}.") self._grpc_client.services.bodies.remove_assigned_material(ids=[self.id]) @@ -1601,7 +1601,7 @@ def assign_material(self, material: Material) -> None: # noqa: D102 @ensure_design_is_active def get_assigned_material(self) -> Material: # noqa: D102 return self._template.get_assigned_material() - + @ensure_design_is_active def remove_assigned_material(self): # noqa: D102 self._template.remove_assigned_material() diff --git a/tests/integration/test_design.py b/tests/integration/test_design.py index e002556fb7..b325c3110b 100644 --- a/tests/integration/test_design.py +++ b/tests/integration/test_design.py @@ -31,13 +31,10 @@ import pytest from ansys.geometry.core import Modeler -from ansys.geometry.core.connection import BackendType import ansys.geometry.core.connection.defaults as pygeom_defaults from ansys.geometry.core.designer import ( CurveType, - DesignFileFormat, MidSurfaceOffsetType, - SharedTopologyType, SurfaceType, ) from ansys.geometry.core.designer.body import CollisionType, FillStyle, MasterBody @@ -64,7 +61,6 @@ Circle, Cone, Cylinder, - Ellipse, Line, ParamUV, Sphere, @@ -331,7 +327,7 @@ def test_remove_material_from_body(modeler: Modeler): body = design.extrude_sketch("CircleBody", sketch, Quantity(10, UNITS.mm)) # Create and assign a material - density = Quantity(7850, UNITS.kg / (UNITS.m ** 3)) + density = Quantity(7850, UNITS.kg / (UNITS.m**3)) material = Material( "Steel", density, @@ -348,7 +344,8 @@ def test_remove_material_from_body(modeler: Modeler): assert body.material.name == "" assert len(body.material.properties) == 1 assert body.material.properties[MaterialPropertyType.DENSITY].quantity == Quantity( - 0, UNITS.kg / (UNITS.m ** 3)) + 0, UNITS.kg / (UNITS.m**3) + ) def test_face_to_body_creation(modeler: Modeler): @@ -3040,4 +3037,4 @@ def check_list_equality(lines, expected_lines): "|---(comp) Nested_1_Nested_1_Component_1", " |---(body) nested_1_nested_1_comp_1_circle", ] - assert check_list_equality(lines, ref) is True \ No newline at end of file + assert check_list_equality(lines, ref) is True diff --git a/tests/integration/test_material.py b/tests/integration/test_material.py index d2a5fee50e..c363f7aa68 100644 --- a/tests/integration/test_material.py +++ b/tests/integration/test_material.py @@ -121,4 +121,4 @@ def test_remove_multiple_materials(modeler: Modeler): design.remove_material([material_1, material_2]) - assert len(design.materials) == 0 \ No newline at end of file + assert len(design.materials) == 0 From b101b4f0a37bfa5a7f1d9784706ccd3a1c2a0746 Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Mon, 18 Aug 2025 18:17:47 +0000 Subject: [PATCH 4/6] chore: adding changelog file 2180.added.md [dependabot-skip] --- doc/changelog.d/2180.added.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changelog.d/2180.added.md diff --git a/doc/changelog.d/2180.added.md b/doc/changelog.d/2180.added.md new file mode 100644 index 0000000000..5dc61f7eba --- /dev/null +++ b/doc/changelog.d/2180.added.md @@ -0,0 +1 @@ +Remove materials From 767ee00005d53f2096bf1a119502d2cb5b6fce74 Mon Sep 17 00:00:00 2001 From: Jacob Kerstetter Date: Tue, 19 Aug 2025 10:54:32 -0400 Subject: [PATCH 5/6] resolving review comments --- src/ansys/geometry/core/designer/body.py | 1 + src/ansys/geometry/core/designer/design.py | 1 + tests/integration/test_design.py | 3100 ++++++++++++-------- 3 files changed, 1854 insertions(+), 1248 deletions(-) diff --git a/src/ansys/geometry/core/designer/body.py b/src/ansys/geometry/core/designer/body.py index bcfd24794e..6d6c6422b9 100644 --- a/src/ansys/geometry/core/designer/body.py +++ b/src/ansys/geometry/core/designer/body.py @@ -1066,6 +1066,7 @@ def get_assigned_material(self) -> Material: # noqa: D102 response = self._grpc_client.services.bodies.get_assigned_material(id=self.id) return response.get("material") + @min_backend_version(26, 1, 0) def remove_assigned_material(self) -> None: # noqa: D102 self._grpc_client.log.debug(f"Removing assigned material for body {self.id}.") self._grpc_client.services.bodies.remove_assigned_material(ids=[self.id]) diff --git a/src/ansys/geometry/core/designer/design.py b/src/ansys/geometry/core/designer/design.py index 0136a54c50..172f3d21af 100644 --- a/src/ansys/geometry/core/designer/design.py +++ b/src/ansys/geometry/core/designer/design.py @@ -227,6 +227,7 @@ def add_material(self, material: Material) -> None: self._materials.append(material) self._grpc_client.log.debug(f"Material {material.name} is successfully added to design.") + @min_backend_version(26, 1, 0) @check_input_types @ensure_design_is_active def remove_material(self, material: Material | list[Material]) -> None: diff --git a/tests/integration/test_design.py b/tests/integration/test_design.py index b325c3110b..a8266e7c01 100644 --- a/tests/integration/test_design.py +++ b/tests/integration/test_design.py @@ -31,10 +31,13 @@ import pytest from ansys.geometry.core import Modeler +from ansys.geometry.core.connection import BackendType import ansys.geometry.core.connection.defaults as pygeom_defaults from ansys.geometry.core.designer import ( CurveType, + DesignFileFormat, MidSurfaceOffsetType, + SharedTopologyType, SurfaceType, ) from ansys.geometry.core.designer.body import CollisionType, FillStyle, MasterBody @@ -61,6 +64,7 @@ Circle, Cone, Cylinder, + Ellipse, Line, ParamUV, Sphere, @@ -878,1493 +882,1382 @@ def test_delete_body_component(modeler: Modeler): _ = comp_2.add_component("Nested_1_Component_2") # Create the bodies - _ = comp_3.extrude_sketch(name="comp_3_circle", sketch=sketch, distance=distance) - _ = nested_2_comp_1.extrude_sketch( + body_1 = comp_3.extrude_sketch(name="comp_3_circle", sketch=sketch, distance=distance) + body_2 = nested_2_comp_1.extrude_sketch( name="nested_2_comp_1_circle", sketch=sketch, distance=distance ) _ = nested_1_nested_1_comp_1.extrude_sketch( name="nested_1_nested_1_comp_1_circle", sketch=sketch, distance=distance ) - # Create beams (in design) - circle_profile_1 = design.add_beam_circular_profile( - "CircleProfile1", Quantity(10, UNITS.mm), Point3D([0, 0, 0]), UNITVECTOR3D_X, UNITVECTOR3D_Y - ) - _ = design.create_beam( - Point3D([9, 99, 999], UNITS.mm), Point3D([8, 88, 888], UNITS.mm), circle_profile_1 - ) - - # Test the tree print - by default - ################################## - lines = design.tree_print(return_list=True) - ref = [ - ">>> Tree print view of component 'TreePrintComponent'", - "", - "Location", - "--------", - "Root component (Design)", - "", - "Subtree", - "-------", - "(comp) TreePrintComponent", - "|---(beam) 0:", - "|---(comp) Component_1", - ": |---(comp) Nested_1_Component_1", - ": : |---(comp) Nested_1_Nested_1_Component_1", - ": : |---(body) nested_1_nested_1_comp_1_circle", - ": |---(comp) Nested_2_Component_1", - ": |---(body) nested_2_comp_1_circle", - "|---(comp) Component_2", - ": |---(comp) Nested_1_Component_2", - "|---(comp) Component_3", - " |---(body) comp_3_circle", - ] - assert check_list_equality(lines, ref) is True - - # Test - request depth 1, and show only components - ################################################## - lines = design.tree_print( - return_list=True, depth=1, consider_bodies=False, consider_beams=False - ) - ref = [ - ">>> Tree print view of component 'TreePrintComponent'", - "", - "Location", - "--------", - "Root component (Design)", - "", - "Subtree", - "-------", - "(comp) TreePrintComponent", - "|---(comp) Component_1", - "|---(comp) Component_2", - "|---(comp) Component_3", - ] - assert check_list_equality(lines, ref) is True + # Let's start by doing something impossible - trying to delete body_1 from comp_1 + comp_1.delete_body(body_1) + + # Check that all the underlying objects are still alive + assert comp_1.is_alive + assert comp_1.components[0].is_alive + assert comp_1.components[0].components[0].is_alive + assert comp_1.components[0].components[0].bodies[0].is_alive + assert comp_1.components[1].is_alive + assert comp_1.components[1].bodies[0].is_alive + assert comp_2.is_alive + assert comp_2.components[0].is_alive + assert comp_3.is_alive + assert comp_3.bodies[0].is_alive + + # Do the same checks but calling them from the design object + assert design.is_alive + assert design.components[0].is_alive + assert design.components[0].components[0].is_alive + assert design.components[0].components[0].components[0].is_alive + assert design.components[0].components[0].components[0].bodies[0].is_alive + assert design.components[0].components[1].is_alive + assert design.components[0].components[1].bodies[0].is_alive + assert design.components[1].is_alive + assert design.components[1].components[0].is_alive + assert design.components[2].is_alive + assert design.components[2].bodies[0].is_alive + + # Let's do another impossible thing - trying to delete comp_3 from comp_1 + comp_1.delete_component(comp_3) + + # Check that all the underlying objects are still alive + assert comp_1.is_alive + assert comp_1.components[0].is_alive + assert comp_1.components[0].components[0].is_alive + assert comp_1.components[0].components[0].bodies[0].is_alive + assert comp_1.components[1].is_alive + assert comp_1.components[1].bodies[0].is_alive + assert comp_2.is_alive + assert comp_2.components[0].is_alive + assert comp_3.is_alive + assert comp_3.bodies[0].is_alive + + # Do the same checks but calling them from the design object + assert design.is_alive + assert design.components[0].is_alive + assert design.components[0].components[0].is_alive + assert design.components[0].components[0].components[0].is_alive + assert design.components[0].components[0].components[0].bodies[0].is_alive + assert design.components[0].components[1].is_alive + assert design.components[0].components[1].bodies[0].is_alive + assert design.components[1].is_alive + assert design.components[1].components[0].is_alive + assert design.components[2].is_alive + assert design.components[2].bodies[0].is_alive + + # Let's delete now the entire comp_2 component + comp_2.delete_component(comp_2) + + # Check that all the underlying objects are still alive except for comp_2 + assert comp_1.is_alive + assert comp_1.components[0].is_alive + assert comp_1.components[0].components[0].is_alive + assert comp_1.components[0].components[0].bodies[0].is_alive + assert comp_1.components[1].is_alive + assert comp_1.components[1].bodies[0].is_alive + assert not comp_2.is_alive + assert not comp_2.components[0].is_alive + assert comp_3.is_alive + assert comp_3.bodies[0].is_alive + + # Do the same checks but calling them from the design object + assert design.is_alive + assert design.components[0].is_alive + assert design.components[0].components[0].is_alive + assert design.components[0].components[0].components[0].is_alive + assert design.components[0].components[0].components[0].bodies[0].is_alive + assert design.components[0].components[1].is_alive + assert design.components[0].components[1].bodies[0].is_alive + assert not design.components[1].is_alive + assert not design.components[1].components[0].is_alive + assert design.components[2].is_alive + assert design.components[2].bodies[0].is_alive + + # Let's delete now the body_2 object + design.delete_body(body_2) + + # Check that all the underlying objects are still alive except for comp_2 and body_2 + assert comp_1.is_alive + assert comp_1.components[0].is_alive + assert comp_1.components[0].components[0].is_alive + assert comp_1.components[0].components[0].bodies[0].is_alive + assert comp_1.components[1].is_alive + assert not body_2.is_alive + assert not comp_2.is_alive + assert not comp_2.components[0].is_alive + assert comp_3.is_alive + + # Do the same checks but calling them from the design object + assert design.is_alive + assert design.components[0].is_alive + assert design.components[0].components[0].is_alive + assert design.components[0].components[0].components[0].is_alive + assert design.components[0].components[0].components[0].bodies[0].is_alive + assert design.components[0].components[1].is_alive + assert not design.components[1].is_alive + assert not design.components[1].components[0].is_alive + assert design.components[2].is_alive + assert design.components[2].bodies[0].is_alive + + # Finally, let's delete the most complex one - comp_1 + design.delete_component(comp_1) + + # Check that all the underlying objects are still alive except for comp_2, body_2 and comp_1 + assert not comp_1.is_alive + assert not comp_1.components[0].is_alive + assert not comp_1.components[0].components[0].is_alive + assert not comp_1.components[1].is_alive + assert not comp_2.is_alive + assert not comp_2.components[0].is_alive + assert comp_3.is_alive + assert comp_3.bodies[0].is_alive + + # Do the same checks but calling them from the design object + assert design.is_alive + assert not design.components[0].is_alive + assert not design.components[0].components[0].is_alive + assert not design.components[0].components[0].components[0].is_alive + assert not design.components[0].components[1].is_alive + assert not design.components[1].is_alive + assert not design.components[1].components[0].is_alive + assert design.components[2].is_alive + assert design.components[2].bodies[0].is_alive + + # Finally, let's delete the entire design + design.delete_component(comp_3) + + # Check everything is dead + assert design.is_alive + assert not design.components[0].is_alive + assert not design.components[0].components[0].is_alive + assert not design.components[0].components[0].components[0].is_alive + assert not design.components[0].components[1].is_alive + assert not design.components[1].is_alive + assert not design.components[1].components[0].is_alive + assert not design.components[2].is_alive + + # Try deleting the Design object itself - this is forbidden + with pytest.raises(ValueError, match="The design itself cannot be deleted."): + design.delete_component(design) + + # Let's try out the representation methods + design_str = str(design) + assert "ansys.geometry.core.designer.Design" in design_str + assert "Name : Deletion_Test" in design_str + assert "N Bodies : 0" in design_str + assert "N Components : 0" in design_str + assert "N Coordinate Systems : 0" in design_str + assert "N Named Selections : 0" in design_str + assert "N Materials : 0" in design_str + assert "N Beam Profiles : 0" in design_str + assert "N Design Points : 0" in design_str + + comp_1_str = str(comp_1) + assert "ansys.geometry.core.designer.Component" in comp_1_str + assert "Name : Component_1" in comp_1_str + assert "Exists : False" in comp_1_str + assert "Parent component : Deletion_Test" in comp_1_str + assert "N Bodies : 0" in comp_1_str + assert "N Beams : 0" in comp_1_str + assert "N Components : 0" in comp_1_str + assert "N Design Points : 0" in comp_1_str + assert "N Coordinate Systems : 0" in comp_1_str + + body_1_str = str(body_1) + assert "ansys.geometry.core.designer.Body" in body_1_str + assert "Name : comp_3_circle" in body_1_str + assert "Exists : False" in body_1_str + assert "Surface body : False" in body_1_str + assert "Parent component : Component_3" in body_1_str + assert "Color : None" in body_1_str + + +def test_shared_topology(modeler: Modeler): + """Test for checking the correct setting of shared topology on the server. - # Test - request depth 2, indent 1 (which will default to 2) - # and sort the components alphabetically - ############################################################ - lines = design.tree_print(return_list=True, depth=2, indent=1, sort_keys=True) - ref = [ - ">>> Tree print view of component 'TreePrintComponent'", - "", - "Location", - "--------", - "Root component (Design)", - "", - "Subtree", - "-------", - "(comp) TreePrintComponent", - "|-(beam) 0:", - "|-(comp) Component_1", - ": |-(comp) Nested_1_Component_1", - ": |-(comp) Nested_2_Component_1", - "|-(comp) Component_2", - ": |-(comp) Nested_1_Component_2", - "|-(comp) Component_3", - " |-(body) comp_3_circle", - ] - assert check_list_equality(lines, ref) is True + Notes + ----- + Requires storing scdocx file and checking manually (for now). + """ + # Create your design on the server side + design = modeler.create_design("SharedTopology_Test") - # Test - request from Nested_1_Component_1 - ########################################## - lines = nested_1_comp_1.tree_print(return_list=True) - ref = [ - ">>> Tree print view of component 'Nested_1_Component_1'", - "", - "Location", - "--------", - "TreePrintComponent > Component_1 > Nested_1_Component_1", - "", - "Subtree", - "-------", - "(comp) Nested_1_Component_1", - "|---(comp) Nested_1_Nested_1_Component_1", - " |---(body) nested_1_nested_1_comp_1_circle", - ] - assert check_list_equality(lines, ref) is True + # Create a Sketch object and draw a circle (all client side) + sketch = Sketch() + sketch.circle(Point2D([-30, -30], UNITS.mm), Quantity(10, UNITS.mm)) + distance = Quantity(30, UNITS.mm) + # Create a component + comp_1 = design.add_component("Component_1") + comp_1.extrude_sketch(name="Body_1", sketch=sketch, distance=distance) -def test_surface_body_creation(modeler: Modeler): - """Test surface body creation from trimmed surfaces.""" - design = modeler.create_design("Design1") + # Now that the component is created, let's try to assign a SharedTopology + assert comp_1.shared_topology is None - # half sphere - surface = Sphere([0, 0, 0], 1) - trimmed_surface = surface.trim(BoxUV(Interval(0, np.pi * 2), Interval(0, np.pi / 2))) - body = design.create_body_from_surface("sphere", trimmed_surface) - assert len(design.bodies) == 1 - assert body.is_surface - assert body.faces[0].area.m == pytest.approx(np.pi * 2) + # Set the shared topology + comp_1.set_shared_topology(SharedTopologyType.SHARETYPE_SHARE) + assert comp_1.shared_topology == SharedTopologyType.SHARETYPE_SHARE - # cylinder - surface = Cylinder([0, 0, 0], 1) - trimmed_surface = surface.trim(BoxUV(Interval(0, np.pi * 2), Interval(0, 1))) - body = design.create_body_from_surface("cylinder", trimmed_surface) + # Try to assign it to the entire design + assert design.shared_topology is None + with pytest.raises(ValueError, match="The design itself cannot have a shared topology."): + design.set_shared_topology(SharedTopologyType.SHARETYPE_NONE) - assert len(design.bodies) == 2 - assert body.is_surface - assert body.faces[0].area.m == pytest.approx(np.pi * 2) - # cone - surface = Cone([0, 0, 0], 1, np.pi / 4) - trimmed_surface = surface.trim(BoxUV(Interval(0, np.pi * 2), Interval(surface.apex.z.m, 0))) - body = design.create_body_from_surface("cone", trimmed_surface) +def test_single_body_translation(modeler: Modeler): + """Test for verifying the correct translation of a ``Body``. - assert len(design.bodies) == 3 - assert body.is_surface - assert body.faces[0].area.m == pytest.approx(4.44288293816) + Notes + ----- + Requires storing scdocx file and checking manually (for now). + """ + # Create your design on the server side + design = modeler.create_design("SingleBodyTranslation_Test") - # half torus - surface = Torus([0, 0, 0], 2, 1) - trimmed_surface = surface.trim(BoxUV(Interval(0, np.pi), Interval(0, np.pi * 2))) - body = design.create_body_from_surface("torus", trimmed_surface) + # Create 2 Sketch objects and draw a circle and a polygon (all client side) + sketch_1 = Sketch() + sketch_1.circle(Point2D([10, 10], UNITS.mm), Quantity(10, UNITS.mm)) + sketch_2 = Sketch() + sketch_2.polygon(Point2D([-30, -30], UNITS.mm), Quantity(10, UNITS.mm), sides=5) - assert len(design.bodies) == 4 - assert body.is_surface - assert body.faces[0].area.m == pytest.approx(39.4784176044) + # Build 2 independent components and bodies + circle_comp = design.add_component("CircleComponent") + body_circle_comp = circle_comp.extrude_sketch("Circle", sketch_1, Quantity(50, UNITS.mm)) + polygon_comp = design.add_component("PolygonComponent") + body_polygon_comp = polygon_comp.extrude_sketch("Polygon", sketch_2, Quantity(30, UNITS.mm)) - # SOLID BODIES + body_circle_comp.translate(UnitVector3D([1, 0, 0]), Distance(50, UNITS.mm)) + body_polygon_comp.translate(UnitVector3D([-1, 1, -1]), Quantity(88, UNITS.mm)) + body_polygon_comp.translate(UnitVector3D([-1, 1, -1]), 101) - # sphere - surface = Sphere([0, 0, 0], 1) - trimmed_surface = surface.trim(BoxUV(Interval(0, np.pi * 2), Interval(-np.pi / 2, np.pi / 2))) - body = design.create_body_from_surface("sphere_solid", trimmed_surface) - assert len(design.bodies) == 5 - assert not body.is_surface - assert body.faces[0].area.m == pytest.approx(np.pi * 4) - # torus - surface = Torus([0, 0, 0], 2, 1) - trimmed_surface = surface.trim(BoxUV(Interval(0, np.pi * 2), Interval(0, np.pi * 2))) - body = design.create_body_from_surface("torus_solid", trimmed_surface) +def test_bodies_translation(modeler: Modeler): + """Test for verifying the correct translation of list of ``Body``. - assert len(design.bodies) == 6 - assert not body.is_surface - assert body.faces[0].area.m == pytest.approx(39.4784176044 * 2) + Notes + ----- + Requires storing scdocx file and checking manually (for now). + """ + # Create your design on the server side + design = modeler.create_design("MultipleBodyTranslation_Test") + # Create 2 Sketch objects and draw a circle and a polygon (all client side) + sketch_1 = Sketch() + sketch_1.circle(Point2D([10, 10], UNITS.mm), Quantity(10, UNITS.mm)) + sketch_2 = Sketch() + sketch_2.polygon(Point2D([-30, -30], UNITS.mm), Quantity(10, UNITS.mm), sides=5) -def test_create_surface_from_nurbs_sketch(modeler: Modeler): - """Test creating a surface from a NURBS sketch.""" - design = modeler.create_design("NURBS_Sketch_Surface") + # Build 2 independent components and bodies + circle_comp = design.add_component("CircleComponent") + body_circle_comp = circle_comp.extrude_sketch("Circle", sketch_1, Quantity(50, UNITS.mm)) + polygon_comp = design.add_component("PolygonComponent") + body_polygon_comp = polygon_comp.extrude_sketch("Polygon", sketch_2, Quantity(30, UNITS.mm)) - # Create a NURBS sketch - sketch = Sketch() - sketch.nurbs_from_2d_points( - points=[ - Point2D([0, 0]), - Point2D([1, 0]), - Point2D([1, 1]), - Point2D([0, 1]), - ], - tag="nurbs_sketch", + design.translate_bodies( + [body_circle_comp, body_polygon_comp], UnitVector3D([1, 0, 0]), Distance(48, UNITS.mm) ) - sketch.segment( - start=Point2D([0, -1]), - end=Point2D([0, 2]), - tag="segment_1", + design.translate_bodies( + [body_circle_comp, body_polygon_comp], UnitVector3D([0, -1, 1]), Quantity(88, UNITS.mm) ) + design.translate_bodies([body_circle_comp, body_polygon_comp], UnitVector3D([0, -1, 1]), 101) - # Create a surface from the NURBS sketch - surface_body = design.create_surface( - name="nurbs_surface", - sketch=sketch, + # Try translating a body that does not belong to this component - no error thrown, + # but no operation performed either. + circle_comp.translate_bodies( + [body_polygon_comp], UnitVector3D([0, -1, 1]), Quantity(88, UNITS.mm) ) - assert len(design.bodies) == 1 - assert surface_body.is_surface - assert surface_body.faces[0].area.m > 0 +def test_body_rotation(modeler: Modeler): + """Test for verifying the correct rotation of a ``Body``.""" + # Create your design on the server side + design = modeler.create_design("BodyRotation_Test") -def test_design_parameters(modeler: Modeler): - """Test the design parameter's functionality.""" - design = modeler.open_file(FILES_DIR / "blockswithparameters.dsco") - test_parameters = design.parameters + body = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1) - # Verify the initial parameters - assert len(test_parameters) == 2 - assert test_parameters[0].name == "p1" - assert abs(test_parameters[0].dimension_value - 0.00010872999999999981) < 1e-8 - assert test_parameters[0].dimension_type == ParameterType.DIMENSIONTYPE_AREA + original_vertices = [] + for edge in body.edges: + original_vertices.extend([edge.shape.start, edge.shape.end]) - assert test_parameters[1].name == "p2" - assert abs(test_parameters[1].dimension_value - 0.0002552758322160813) < 1e-8 - assert test_parameters[1].dimension_type == ParameterType.DIMENSIONTYPE_AREA + body.rotate(Point3D([0, 0, 0]), UnitVector3D([0, 0, 1]), np.pi / 4) - # Update the second parameter and verify the status - test_parameters[1].dimension_value = 0.0006 - status = design.set_parameter(test_parameters[1]) - assert status == ParameterUpdateStatus.SUCCESS + new_vertices = [] + for edge in body.edges: + new_vertices.extend([edge.shape.start, edge.shape.end]) - # Attempt to update the first parameter and expect a constrained status - test_parameters[0].dimension_value = 0.0006 - status = design.set_parameter(test_parameters[0]) - assert status == ParameterUpdateStatus.CONSTRAINED_PARAMETERS + # Make sure no vertices are in the same position as in before rotation + for old_vertex, new_vertex in zip(original_vertices, new_vertices): + assert not np.allclose(old_vertex, new_vertex) - test_parameters[0].name = "NewName" - assert test_parameters[0].name == "NewName" - test_parameters[0].dimension_type = ParameterType.DIMENSIONTYPE_AREA - assert test_parameters[0].dimension_type == ParameterType.DIMENSIONTYPE_AREA +def test_download_file(modeler: Modeler, tmp_path_factory: pytest.TempPathFactory): + """Test for downloading a design in multiple modes and verifying the + correct download. + """ + # Create your design on the server side + design = modeler.create_design("MultipleBodyTranslation_Test") + # Create a Sketch object and draw a circle + sketch = Sketch() + sketch.circle(Point2D([10, 10], UNITS.mm), Quantity(10, UNITS.mm)) -def test_cached_bodies(modeler: Modeler): - """Test that bodies are cached correctly. + # Extrude the sketch + design.extrude_sketch(name="MyCylinder", sketch=sketch, distance=Quantity(50, UNITS.mm)) - Whenever a new body is created, modified etc. we should make sure that the cache is updated. - """ - design = modeler.create_design("ModelingDemo") + # Download the design + file = tmp_path_factory.mktemp("scdoc_files_download") / "dummy_folder" / "cylinder.scdocx" + design.download(file) - # Define a sketch - origin = Point3D([0, 0, 10]) - plane = Plane(origin, direction_x=[1, 0, 0], direction_y=[0, 1, 0]) + # Check that the file exists + assert file.exists() - # Create a sketch - sketch_box = Sketch(plane) - sketch_box.box(Point2D([20, 20]), 30 * UNITS.m, 30 * UNITS.m) + # Check that we can also save it (even if it is not accessible on the server) + if BackendType.is_linux_service(modeler.client.backend_type): + file_save = "/tmp/cylinder-temp.scdocx" + else: + file_save = tmp_path_factory.mktemp("scdoc_files_save") / "cylinder.scdocx" - sketch_cylinder = Sketch(plane) - sketch_cylinder.circle(Point2D([20, 20]), 5 * UNITS.m) + design.save(file_location=file_save) - design.extrude_sketch(name="BoxBody", sketch=sketch_box, distance=Distance(30, unit=UNITS.m)) - design.extrude_sketch( - name="CylinderBody", - sketch=sketch_cylinder, - distance=Distance(60, unit=UNITS.m), - ) + # Check for other exports - Windows backend... + if not BackendType.is_core_service(modeler.client.backend_type): + binary_parasolid_file = tmp_path_factory.mktemp("scdoc_files_download") / "cylinder.x_b" + text_parasolid_file = tmp_path_factory.mktemp("scdoc_files_download") / "cylinder.x_t" - my_bodies = design.bodies - my_bodies_2 = design.bodies + # Windows-only HOOPS exports for now + step_file = tmp_path_factory.mktemp("scdoc_files_download") / "cylinder.stp" + design.download(step_file, format=DesignFileFormat.STEP) + assert step_file.exists() - # We should make sure that the object memory addresses are the same - for body1, body2 in zip(my_bodies, my_bodies_2): - assert body1 is body2 # We are comparing the memory addresses - assert id(body1) == id(body2) + iges_file = tmp_path_factory.mktemp("scdoc_files_download") / "cylinder.igs" + design.download(iges_file, format=DesignFileFormat.IGES) + assert iges_file.exists() - design.extrude_sketch( - name="CylinderBody2", - sketch=sketch_cylinder, - distance=Distance(20, unit=UNITS.m), - direction="-", - ) - my_bodies_3 = design.bodies + # Linux backend... + else: + binary_parasolid_file = tmp_path_factory.mktemp("scdoc_files_download") / "cylinder.xmt_bin" + text_parasolid_file = tmp_path_factory.mktemp("scdoc_files_download") / "cylinder.xmt_txt" - for body1, body3 in zip(my_bodies, my_bodies_3): - assert body1 is not body3 - assert id(body1) != id(body3) + # FMD + fmd_file = tmp_path_factory.mktemp("scdoc_files_download") / "cylinder.fmd" + design.download(fmd_file, format=DesignFileFormat.FMD) + assert fmd_file.exists() + # PMDB + pmdb_file = tmp_path_factory.mktemp("scdoc_files_download") / "cylinder.pmdb" -def test_extrude_sketch_with_cut_request(modeler: Modeler): - """Test the cut argument when performing a sketch extrusion. + design.download(binary_parasolid_file, format=DesignFileFormat.PARASOLID_BIN) + design.download(text_parasolid_file, format=DesignFileFormat.PARASOLID_TEXT) + design.download(pmdb_file, format=DesignFileFormat.PMDB) - This method mimics a cut operation. + assert binary_parasolid_file.exists() + assert text_parasolid_file.exists() + assert pmdb_file.exists() - Behind the scenes, a subtraction operation is performed on the bodies. After extruding the - sketch, the resulting body should be a cut body. - """ - # Define a sketch - origin = Point3D([0, 0, 10]) - plane = Plane(origin, direction_x=[1, 0, 0], direction_y=[0, 1, 0]) - # Create a sketch - sketch_box = Sketch(plane) - sketch_box.box(Point2D([20, 20]), 30 * UNITS.m, 30 * UNITS.m) +def test_upload_file(modeler: Modeler, tmp_path_factory: pytest.TempPathFactory): + """Test uploading a file to the server.""" + file = tmp_path_factory.mktemp("test_design") / "upload_example.scdocx" + file_size = 1024 - sketch_cylinder = Sketch(plane) - sketch_cylinder.circle(Point2D([20, 20]), 5 * UNITS.m) + # Write random bytes + with file.open(mode="wb") as fout: + fout.write(os.urandom(file_size)) - # Create a design - design = modeler.create_design("ExtrudeSketchWithCut") + assert file.exists() - box_body = design.extrude_sketch( - name="BoxBody", sketch=sketch_box, distance=Distance(30, unit=UNITS.m) - ) - design.extrude_sketch( - name="CylinderBody", sketch=sketch_cylinder, distance=Distance(60, unit=UNITS.m), cut=True - ) + # Upload file + path_on_server = modeler._upload_file(file) + assert path_on_server is not None - # Verify there is only one body - assert len(design.bodies) == 1 - # Verify the volume of the resulting body is less than the volume of the box - assert design.bodies[0].volume.m < box_body.volume.m +def test_stream_upload_file(tmp_path_factory: pytest.TempPathFactory): + """Test uploading a file to the server.""" + # Define a new maximum message length + import ansys.geometry.core.connection.defaults as pygeom_defaults + old_value = pygeom_defaults.MAX_MESSAGE_LENGTH + try: + # Set the new maximum message length + pygeom_defaults.MAX_MESSAGE_LENGTH = 1024**2 # 1 MB -def test_extrude_sketch_with_cut_request_no_collision(modeler: Modeler): - """Test the cut argument when performing a sketch extrusion (with no collision). + file = tmp_path_factory.mktemp("test_design") / "upload_stream_example.scdocx" + file_size = 5 * 1024**2 # stream five messages - This method mimics an unsuccessful cut operation. + # Write random bytes + with file.open(mode="wb") as fout: + fout.write(os.urandom(file_size)) + assert file.exists() - The sketch extrusion should not result in a cut body since there is no collision between the - original body and the extruded body. - """ - # Define a sketch - origin = Point3D([0, 0, 10]) - plane = Plane(origin, direction_x=[1, 0, 0], direction_y=[0, 1, 0]) + # Upload file - necessary to import the Modeler class and create an instance + from ansys.geometry.core import Modeler - # Create a sketch - sketch_box = Sketch(plane) - sketch_box.box(Point2D([20, 20]), 30 * UNITS.m, 30 * UNITS.m) + modeler = Modeler() + path_on_server = modeler._upload_file_stream(file) + assert path_on_server is not None + finally: + pygeom_defaults.MAX_MESSAGE_LENGTH = old_value - sketch_cylinder = Sketch(plane) - sketch_cylinder.circle(Point2D([100, 100]), 5 * UNITS.m) - # Create a design - design = modeler.create_design("ExtrudeSketchWithCutNoCollision") +def test_slot_extrusion(modeler: Modeler): + """Test the extrusion of a slot.""" + # Create your design on the server side + design = modeler.create_design("ExtrudeSlot") - box_body = design.extrude_sketch( - name="BoxBody", sketch=sketch_box, distance=Distance(30, unit=UNITS.m) - ) - design.extrude_sketch( - name="CylinderBody", sketch=sketch_cylinder, distance=Distance(60, unit=UNITS.m), cut=True - ) + # Create a Sketch object and draw a slot + sketch = Sketch() + sketch.slot(Point2D([10, 10], UNITS.mm), Quantity(10, UNITS.mm), Quantity(5, UNITS.mm)) - # Verify there is only one body... the cut operation should delete it - assert len(design.bodies) == 1 + # Extrude the sketch + body = design.extrude_sketch(name="MySlot", sketch=sketch, distance=Distance(50, UNITS.mm)) - # Verify the volume of the resulting body is exactly the same - assert design.bodies[0].volume.m == box_body.volume.m + # A slot has 6 faces and 12 edges + assert len(body.faces) == 6 + assert len(body.edges) == 12 -def test_create_surface_body_from_trimmed_curves(modeler: Modeler): - design = modeler.create_design("surface") +def test_project_and_imprint_curves(modeler: Modeler): + """Test the projection of a set of curves on a body.""" + # Create your design on the server side + design = modeler.create_design("ExtrudeSlot") + comp = design.add_component("Comp1") - # pill shape - circle1 = Circle(Point3D([0, 0, 0]), 1).trim(Interval(0, np.pi)) - line1 = Line(Point3D([-1, 0, 0]), UnitVector3D([0, -1, 0])).trim(Interval(0, 1)) - circle2 = Circle(Point3D([0, -1, 0]), 1).trim(Interval(np.pi, np.pi * 2)) - line2 = Line(Point3D([1, 0, 0]), UnitVector3D([0, -1, 0])).trim(Interval(0, 1)) + # Create a Sketch object and draw a couple of slots + imprint_sketch = Sketch() + imprint_sketch.slot(Point2D([10, 10], UNITS.mm), Quantity(10, UNITS.mm), Quantity(5, UNITS.mm)) + imprint_sketch.slot(Point2D([50, 50], UNITS.mm), Quantity(10, UNITS.mm), Quantity(5, UNITS.mm)) - body = design.create_surface_from_trimmed_curves("body", [circle1, line1, line2, circle2]) - assert body.is_surface - assert body.faces[0].area.m == pytest.approx( - Quantity(2 + np.pi, UNITS.m**2).m, rel=1e-6, abs=1e-8 - ) + # Extrude the sketch + sketch = Sketch() + sketch.box(Point2D([0, 0], UNITS.mm), Quantity(150, UNITS.mm), Quantity(150, UNITS.mm)) + body = comp.extrude_sketch(name="MyBox", sketch=sketch, distance=Quantity(50, UNITS.mm)) + body_faces = body.faces - # create from edges (by getting their trimmed curves) - trimmed_curves_from_edges = [edge.shape for edge in body.edges] - body = design.create_surface_from_trimmed_curves("body2", trimmed_curves_from_edges) - assert body.is_surface - assert body.faces[0].area.m == pytest.approx( - Quantity(2 + np.pi, UNITS.m**2).m, rel=1e-6, abs=1e-8 - ) + body_copy = body.copy(design, "copy") + # Project the curves on the box + faces = body.project_curves(direction=UNITVECTOR3D_Z, sketch=imprint_sketch, closest_face=True) + assert len(faces) == 1 + # With the previous dir, the curves will be imprinted on the + # bottom face (closest one), i.e. the first one. + assert faces[0].id == body_faces[0].id -def test_shell_body(modeler: Modeler): - """Test shell command.""" - design = modeler.create_design("shell") - base = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1) + # If we now draw our curves on a higher plane, the upper face should be selected + imprint_sketch_2 = Sketch(plane=Plane(Point3D([0, 0, 50], UNITS.mm))) + imprint_sketch_2.slot( + Point2D([10, 10], UNITS.mm), Quantity(10, UNITS.mm), Quantity(5, UNITS.mm) + ) + imprint_sketch_2.slot( + Point2D([50, 50], UNITS.mm), Quantity(10, UNITS.mm), Quantity(5, UNITS.mm) + ) + faces = body.project_curves( + direction=UNITVECTOR3D_Z, sketch=imprint_sketch_2, closest_face=True + ) + assert len(faces) == 1 + # With the previous dir, the curves will be imprinted on the + # top face (closest one), i.e. the first one. + assert faces[0].id == body_faces[1].id - assert base.volume.m == pytest.approx(Quantity(1, UNITS.m**3).m, rel=1e-6, abs=1e-8) - assert len(base.faces) == 6 + # Now, let's try projecting only a single curve (i.e. one of the slots only) + faces = body.project_curves( + direction=UNITVECTOR3D_Z, sketch=imprint_sketch_2, closest_face=True, only_one_curve=True + ) + assert len(faces) == 1 + # With the previous dir, the curves will be imprinted on the + # top face (closest one), i.e. the first one. + assert faces[0].id == body_faces[1].id - # shell - success = base.shell_body(0.1) - assert success - assert base.volume.m == pytest.approx(Quantity(0.728, UNITS.m**3).m, rel=1e-6, abs=1e-8) - assert len(base.faces) == 12 + # Verify that the surface and curve types are of the correct type - related to PR + # https://github.com/ansys/pyansys-geometry/pull/1096 + assert isinstance(faces[0].surface_type, SurfaceType) - base = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1) - success = base.shell_body(-0.1) - assert success - assert base.volume.m == pytest.approx(Quantity(0.488, UNITS.m**3).m, rel=1e-6, abs=1e-8) - assert len(base.faces) == 12 + # Now once the previous curves have been projected, let's try imprinting our sketch + # + # It should generate two additional faces to our box = 6 + 2 + new_edges, new_faces = body.imprint_curves(faces=faces, sketch=imprint_sketch_2) + assert len(new_faces) == 2 + assert len(body.faces) == 8 -def test_shell_faces(modeler: Modeler): - """Test shell commands for a single face.""" - design = modeler.create_design("shell") - base = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1) + # Verify that the surface and curve types are of the correct type - related to PR + # https://github.com/ansys/pyansys-geometry/pull/1096 + assert isinstance(new_faces[0].surface_type, SurfaceType) + assert isinstance(new_edges[0].curve_type, CurveType) - assert base.volume.m == pytest.approx(Quantity(1, UNITS.m**3).m, rel=1e-6, abs=1e-8) - assert len(base.faces) == 6 + # Make sure we have occurrence faces, not master + assert faces[0].id not in [face.id for face in body._template.faces] + assert new_faces[0].id not in [face.id for face in body._template.faces] - # shell - success = base.remove_faces(base.faces[0], 0.1) - assert success - assert base.volume.m == pytest.approx(Quantity(0.584, UNITS.m**3).m, rel=1e-6, abs=1e-8) - assert len(base.faces) == 11 + faces = body_copy.imprint_projected_curves( + direction=UNITVECTOR3D_Z, sketch=imprint_sketch, closest_face=True + ) + assert len(faces) == 2 + assert len(body_copy.faces) == 8 -def test_shell_multiple_faces(modeler: Modeler): - """Test shell commands for multiple faces.""" - design = modeler.create_design("shell") - base = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1) +def test_imprint_trimmed_curves(modeler: Modeler): + """ + Test the imprinting of trimmed curves onto a specified face of a body. + """ + unit = DEFAULT_UNITS.LENGTH - assert base.volume.m == pytest.approx(Quantity(1, UNITS.m**3).m, rel=1e-6, abs=1e-8) - assert len(base.faces) == 6 + wx = 1 + wy = 1 + wz = 1 + design = modeler.create_design("test imprint") - # shell - success = base.remove_faces([base.faces[0], base.faces[2]], 0.1) - assert success - assert base.volume.m == pytest.approx(Quantity(0.452, UNITS.m**3).m, rel=1e-6, abs=1e-8) - assert len(base.faces) == 10 + # create box + start_at = Point3D([wx / 2, wy / 2, 0.0], unit=unit) + plane = Plane( + start_at, + UNITVECTOR3D_X, + UNITVECTOR3D_Y, + ) -def test_set_component_name(modeler: Modeler): - """Test the setting of component names.""" + box_plane = Sketch(plane) + box_plane.box(Point2D([0.0, 0.0], unit=unit), width=wx, height=wy) + box = design.extrude_sketch("box", box_plane, wz) - design = modeler.create_design("ComponentNameTest") - component = design.add_component("Component1") - assert component.name == "Component1" + assert len(box.faces) == 6 + assert len(box.edges) == 12 - component.name = "ChangedComponentName" - assert component.name == "ChangedComponentName" + # create cylinder + point = Point3D([0.5, 0.5, 0.5]) + ortho_1, ortho_2 = UNITVECTOR3D_X, UNITVECTOR3D_Y + plane = Plane(point, ortho_1, ortho_2) + sketch_cylinder = Sketch(plane) + sketch_cylinder.circle(Point2D([0.0, 0.0], unit=unit), radius=0.1) + cylinder = design.extrude_sketch("cylinder", sketch_cylinder, 0.5) + edges = cylinder.faces[1].edges + trimmed_curves = [edges[0].shape] + new_edges, new_faces = box.imprint_curves(faces=[box.faces[1]], trimmed_curves=trimmed_curves) -def test_get_face_bounding_box(modeler: Modeler): - """Test getting the bounding box of a face.""" - design = modeler.create_design("face_bounding_box") - body = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1) + # the new edge is coming from the circular top edge of the cylinder. + assert new_edges[0].start == new_edges[0].end + # verify that there is one new edge coming from the circle. + assert len(new_faces) == 1 + # verify that there is one new face coming from the circle. + assert len(new_edges) == 1 + # verify that there are 7 faces in total. + assert len(box.faces) == 7 + # verify that there are 14 edges in total. + assert len(box.edges) == 13 - bounding_box = body.faces[0].bounding_box - assert bounding_box.min_corner.x.m == bounding_box.min_corner.y.m == -0.5 - assert bounding_box.max_corner.x.m == 0.5 - assert bounding_box.max_corner.y.m == 0.5 - bounding_box = body.faces[1].bounding_box - assert bounding_box.min_corner.x.m == bounding_box.min_corner.y.m == -0.5 - assert bounding_box.max_corner.x.m == 0.5 - assert bounding_box.max_corner.y.m == -0.5 +def test_copy_body(modeler: Modeler): + """Test copying a body.""" + # Create your design on the server side + design = modeler.create_design("Design") + sketch_1 = Sketch().circle(Point2D([10, 10], UNITS.mm), Quantity(10, UNITS.mm)) + body = design.extrude_sketch("Original", sketch_1, Distance(1, UNITS.mm)) -def test_get_edge_bounding_box(modeler: Modeler): - """Test getting the bounding box of an edge.""" - design = modeler.create_design("edge_bounding_box") - body = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1) + # Copy body at same design level + copy = body.copy(design, "Copy") + assert len(design.bodies) == 2 + assert design.bodies[-1].id == copy.id - # Edge 0 goes from (-0.5, -0.5, 1) to (0.5, -0.5, 1) - bounding_box = body.edges[0].bounding_box - assert bounding_box.min_corner.x.m == bounding_box.min_corner.y.m == -0.5 - assert bounding_box.min_corner.z.m == 1 - assert bounding_box.max_corner.x.m == 0.5 - assert bounding_box.max_corner.y.m == -0.5 - assert bounding_box.max_corner.z.m == 1 + # Bodies should be distinct + assert body.id != copy.id + assert body != copy - # Test center - center = bounding_box.center - assert center.x.m == 0 - assert center.y.m == -0.5 - assert center.z.m == 1 + # Copy body into sub-component + comp1 = design.add_component("comp1") + copy2 = body.copy(comp1, "Subcopy") + assert len(comp1.bodies) == 1 + assert comp1.bodies[-1].id == copy2.id + # Bodies should be distinct + assert body.id != copy2.id + assert body != copy2 -def test_get_body_bounding_box(modeler: Modeler): - """Test getting the bounding box of a body.""" - design = modeler.create_design("body_bounding_box") - body = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1) + # Copy a copy + comp2 = comp1.add_component("comp2") + copy3 = copy2.copy(comp2, "Copy3") + assert len(comp2.bodies) == 1 + assert comp2.bodies[-1].id == copy3.id - bounding_box = body.bounding_box - assert bounding_box.min_corner.x.m == bounding_box.min_corner.y.m == -0.5 - assert bounding_box.min_corner.z.m == 0 - assert bounding_box.max_corner.x.m == bounding_box.max_corner.y.m == 0.5 - assert bounding_box.max_corner.z.m == 1 + # Bodies should be distinct + assert copy2.id != copy3.id + assert copy2 != copy3 - # Test center - center = bounding_box.center - assert center.x.m == 0 - assert center.y.m == 0 - assert center.z.m == 0.5 + # Ensure deleting original doesn't affect the copies + design.delete_body(body) + assert not body.is_alive + assert copy.is_alive -def test_extrude_faces_failure_log_to_file(modeler: Modeler): - """Test that the failure to extrude faces logs the correct message to a file.""" - # Create a design and a body for testing - design = modeler.create_design("test_design") - body = design.extrude_sketch("test_body", Sketch().box(Point2D([0, 0]), 1, 1), 1) +def test_beams(modeler: Modeler): + """Test beam creation.""" + # Create your design on the server side + design = modeler.create_design("BeamCreation") - # Call the method with invalid parameters to trigger failure - result = modeler.geometry_commands.extrude_faces( - faces=[body.faces[0]], - distance=-10.0, # Invalid distance to trigger failure - direction=UnitVector3D([0, 0, 1]), + circle_profile_1 = design.add_beam_circular_profile( + "CircleProfile1", Quantity(10, UNITS.mm), Point3D([0, 0, 0]), UNITVECTOR3D_X, UNITVECTOR3D_Y ) - # Assert the result is an empty list - assert result == [] - result = modeler.geometry_commands.extrude_faces_up_to( - faces=[body.faces[0]], - up_to_selection=body.faces[0], # Using the same face as target to trigger failure - direction=UnitVector3D([0, 0, 1]), - seed_point=Point3D([0, 0, 0]), - ) - # Assert the result is an empty list - assert result == [] + assert circle_profile_1.id is not None + assert circle_profile_1.center == Point3D([0, 0, 0]) + assert circle_profile_1.radius.value.m_as(DEFAULT_UNITS.LENGTH) == 0.01 + assert circle_profile_1.direction_x == UNITVECTOR3D_X + assert circle_profile_1.direction_y == UNITVECTOR3D_Y + circle_profile_2 = design.add_beam_circular_profile( + "CircleProfile2", + Distance(20, UNITS.mm), + Point3D([10, 20, 30], UNITS.mm), + UnitVector3D([1, 1, 1]), + UnitVector3D([0, -1, 1]), + ) -def test_extrude_edges_missing_parameters(modeler: Modeler): - """Test that extrude_edges raises a ValueError when required parameters are missing.""" - # Create a design and body for testing - design = modeler.create_design("test_design") - body = design.extrude_sketch("test_body", Sketch().box(Point2D([0, 0]), 1, 1), 1) + assert circle_profile_2.id is not None + assert circle_profile_2.id is not circle_profile_1.id - # Test case: Missing both `from_face` and `from_point`/`direction` - with pytest.raises( - ValueError, - match="To extrude edges, either a face or a direction and point must be provided.", - ): - modeler.geometry_commands.extrude_edges( - edges=[body.edges[0]], # Using the first edge of the body - distance=10.0, - from_face=None, - from_point=None, - direction=None, + with pytest.raises(ValueError, match="Radius must be a real positive value."): + design.add_beam_circular_profile( + "InvalidProfileRadius", + Quantity(-10, UNITS.mm), + Point3D([0, 0, 0]), + UNITVECTOR3D_X, + UNITVECTOR3D_Y, ) + with pytest.raises(ValueError, match="Direction X and direction Y must be perpendicular."): + design.add_beam_circular_profile( + "InvalidUnitVectorAlignment", + Quantity(10, UNITS.mm), + Point3D([0, 0, 0]), + UNITVECTOR3D_X, + UnitVector3D([-1, -1, -1]), + ) -def test_import_component_named_selections(modeler: Modeler): - """Test importing named selections from an inserted design component.""" - # This file had a component inserted into it that has named selections that we need to import - design = modeler.open_file(Path(FILES_DIR, "import_component_groups.scdocx")) - component = design.components[0] - - assert len(design.named_selections) == 0 - component.import_named_selections() - assert len(design.named_selections) == 3 - + # Create a beam at the root component level + beam_1 = design.create_beam( + Point3D([9, 99, 999], UNITS.mm), Point3D([8, 88, 888], UNITS.mm), circle_profile_1 + ) -def test_component_make_independent(modeler: Modeler): - """Test making components independent.""" + assert beam_1.id is not None + assert beam_1.start == Point3D([9, 99, 999], UNITS.mm) + assert beam_1.end == Point3D([8, 88, 888], UNITS.mm) + assert beam_1.profile == circle_profile_1 + assert beam_1.parent_component.id == design.id + assert beam_1.is_alive + assert len(design.beams) == 1 + assert design.beams[0] == beam_1 - design = modeler.open_file(Path(FILES_DIR, "cars.scdocx")) - face = next((ns for ns in design.named_selections if ns.name == "to_pull"), None).faces[0] - comp = next( - (ns for ns in design.named_selections if ns.name == "make_independent"), None - ).components[0] + beam_1_str = str(beam_1) + assert "ansys.geometry.core.designer.Beam" in beam_1_str + assert " Exists : True" in beam_1_str + assert " Start : [0.009" in beam_1_str + assert " End : [0.008" in beam_1_str + assert " Parent component : BeamCreation" in beam_1_str + assert " Beam Profile info" in beam_1_str + assert " -----------------" in beam_1_str + assert "ansys.geometry.core.designer.BeamCircularProfile " in beam_1_str + assert " Name : CircleProfile1" in beam_1_str + assert " Radius : 10.0 millimeter" in beam_1_str + assert " Center : [0.0,0.0,0.0] in meters" in beam_1_str + assert " Direction x : [1.0,0.0,0.0]" in beam_1_str + assert " Direction y : [0.0,1.0,0.0]" in beam_1_str - comp.make_independent() + # Now, let's create two beams at a nested component, with the same profile + nested_component = design.add_component("NestedComponent") + beam_2 = nested_component.create_beam( + Point3D([7, 77, 777], UNITS.mm), Point3D([6, 66, 666], UNITS.mm), circle_profile_2 + ) + beam_3 = nested_component.create_beam( + Point3D([8, 88, 888], UNITS.mm), Point3D([7, 77, 777], UNITS.mm), circle_profile_2 + ) - assert Accuracy.length_is_equal(comp.bodies[0].volume.m, face.body.volume.m) + assert beam_2.id is not None + assert beam_2.profile == circle_profile_2 + assert beam_2.parent_component.id == nested_component.id + assert beam_2.is_alive + assert beam_3.id is not None + assert beam_3.profile == circle_profile_2 + assert beam_3.parent_component.id == nested_component.id + assert beam_3.is_alive + assert beam_2.id != beam_3.id + assert len(nested_component.beams) == 2 + assert nested_component.beams[0] == beam_2 + assert nested_component.beams[1] == beam_3 - modeler.geometry_commands.extrude_faces(face, 1) - comp = design.components[0].components[-1].components[-1] # stale from update-design-in-place + # Once the beams are created, let's try deleting it. + # For example, we shouldn't be able to delete beam_1 from the nested component. + nested_component.delete_beam(beam_1) - assert not Accuracy.length_is_equal(comp.bodies[0].volume.m, face.body.volume.m) + assert beam_2.is_alive + assert nested_component.beams[0].is_alive + assert beam_3.is_alive + assert nested_component.beams[1].is_alive + assert beam_1.is_alive + assert design.beams[0].is_alive + # Let's try deleting one of the beams from the nested component + nested_component.delete_beam(beam_2) + assert not beam_2.is_alive + assert not nested_component.beams[0].is_alive + assert beam_3.is_alive + assert nested_component.beams[1].is_alive + assert beam_1.is_alive + assert design.beams[0].is_alive -def test_write_body_facets_on_save(modeler: Modeler, tmp_path_factory: pytest.TempPathFactory): - design = modeler.open_file(Path(FILES_DIR, "cars.scdocx")) + # Now, let's try deleting it from the design directly - this should be possible + design.delete_beam(beam_3) + assert not beam_2.is_alive + assert not nested_component.beams[0].is_alive + assert not beam_3.is_alive + assert not nested_component.beams[1].is_alive + assert beam_1.is_alive + assert design.beams[0].is_alive - # First file without body facets - filepath_no_facets = tmp_path_factory.mktemp("test_design") / "cars_no_facets.scdocx" - design.download(filepath_no_facets) + # Finally, let's delete the beam from the root component + design.delete_beam(beam_1) + assert not beam_2.is_alive + assert not nested_component.beams[0].is_alive + assert not beam_3.is_alive + assert not nested_component.beams[1].is_alive + assert not beam_1.is_alive + assert not design.beams[0].is_alive - # Second file with body facets - filepath_with_facets = tmp_path_factory.mktemp("test_design") / "cars_with_facets.scdocx" - design.download(filepath_with_facets, write_body_facets=True) + # Now, let's try deleting the beam profiles! + assert len(design.beam_profiles) == 2 + design.delete_beam_profile("MyInventedBeamProfile") + assert len(design.beam_profiles) == 2 + design.delete_beam_profile(circle_profile_1) + assert len(design.beam_profiles) == 1 + design.delete_beam_profile(circle_profile_2) + assert len(design.beam_profiles) == 0 - # Compare file sizes - size_no_facets = filepath_no_facets.stat().st_size - size_with_facets = filepath_with_facets.stat().st_size - assert size_with_facets > size_no_facets +def test_midsurface_properties(modeler: Modeler): + """Test mid-surface properties assignment.""" + # Create your design on the server side + design = modeler.create_design("MidSurfaceProperties") - # Ensure facets.bin and renderlist.xml files exist - with zipfile.ZipFile(filepath_with_facets, "r") as zip_ref: - namelist = set(zip_ref.namelist()) + # Create a Sketch object and draw a slot + sketch = Sketch() + sketch.slot(Point2D([10, 10], UNITS.mm), Quantity(10, UNITS.mm), Quantity(5, UNITS.mm)) - expected_files = { - "SpaceClaim/Graphics/facets.bin", - "SpaceClaim/Graphics/renderlist.xml", - } + # Create an actual body from the slot, and translate it + slot_body = design.extrude_sketch("MySlot", sketch, Quantity(10, UNITS.mm)) + slot_body.translate(UNITVECTOR3D_X, Quantity(40, UNITS.mm)) - missing = expected_files - namelist - assert not missing + # Create a surface body as well + slot_surf = design.create_surface("MySlotSurface", sketch) + + surf_repr = str(slot_surf) + assert "ansys.geometry.core.designer.Body" in surf_repr + assert "Name : MySlotSurface" in surf_repr + assert "Exists : True" in surf_repr + assert "Parent component : MidSurfaceProperties" in surf_repr + assert "Surface body : True" in surf_repr + assert "Surface thickness : None" in surf_repr + assert "Surface offset : None" in surf_repr + assert f"Color : {DEFAULT_COLOR}" in surf_repr + # Let's assign a thickness to both bodies + design.add_midsurface_thickness( + thickness=Quantity(10, UNITS.mm), + bodies=[slot_body, slot_surf], + ) -def test_upload_file(modeler: Modeler, tmp_path_factory: pytest.TempPathFactory): - """Test uploading a file to the server.""" - file = tmp_path_factory.mktemp("test_design") / "upload_example.scdocx" - file_size = 1024 + # Let's also assign a mid-surface offset to both bodies + design.add_midsurface_offset( + offset_type=MidSurfaceOffsetType.TOP, bodies=[slot_body, slot_surf] + ) - # Write random bytes - with file.open(mode="wb") as fout: - fout.write(os.urandom(file_size)) + # Let's check the values now + assert slot_body.surface_thickness is None + assert slot_body.surface_offset is None + assert slot_surf.surface_thickness == Quantity(10, UNITS.mm) + assert slot_surf.surface_offset == MidSurfaceOffsetType.TOP - assert file.exists() + # Let's check that the design-stored values are also updated + assert design.bodies[0].surface_thickness is None + assert design.bodies[0].surface_offset is None + assert design.bodies[1].surface_thickness == Quantity(10, UNITS.mm) + assert design.bodies[1].surface_offset == MidSurfaceOffsetType.TOP - # Upload file - path_on_server = modeler._upload_file(file) - assert path_on_server is not None + surf_repr = str(slot_surf) + assert "ansys.geometry.core.designer.Body" in surf_repr + assert "Name : MySlotSurface" in surf_repr + assert "Exists : True" in surf_repr + assert "Parent component : MidSurfaceProperties" in surf_repr + assert "Surface body : True" in surf_repr + assert "Surface thickness : 10 millimeter" in surf_repr + assert "Surface offset : MidSurfaceOffsetType.TOP" in surf_repr + assert f"Color : {DEFAULT_COLOR}" in surf_repr + # Let's try reassigning values directly to slot_body - this shouldn't do anything + slot_body.add_midsurface_thickness(Quantity(10, UNITS.mm)) + slot_body.add_midsurface_offset(MidSurfaceOffsetType.TOP) -def test_stream_upload_file(tmp_path_factory: pytest.TempPathFactory): - """Test uploading a file to the server.""" - # Define a new maximum message length - import ansys.geometry.core.connection.defaults as pygeom_defaults + body_repr = str(slot_body) + assert "ansys.geometry.core.designer.Body" in body_repr + assert "Name : MySlot" in body_repr + assert "Exists : True" in body_repr + assert "Parent component : MidSurfaceProperties" in body_repr + assert "Surface body : False" in body_repr + assert f"Color : {DEFAULT_COLOR}" in surf_repr + assert slot_body.surface_thickness is None + assert slot_body.surface_offset is None - old_value = pygeom_defaults.MAX_MESSAGE_LENGTH + # Let's try reassigning values directly to slot_surf - this should work + # TODO : at the moment the server does not allow to reassign - put in try/catch block + # https://github.com/ansys/pyansys-geometry/issues/1146 try: - # Set the new maximum message length - pygeom_defaults.MAX_MESSAGE_LENGTH = 1024**2 # 1 MB + slot_surf.add_midsurface_thickness(Quantity(30, UNITS.mm)) + slot_surf.add_midsurface_offset(MidSurfaceOffsetType.BOTTOM) - file = tmp_path_factory.mktemp("test_design") / "upload_stream_example.scdocx" - file_size = 5 * 1024**2 # stream five messages + surf_repr = str(slot_surf) + assert "ansys.geometry.core.designer.Body" in surf_repr + assert "Name : MySlotSurface" in surf_repr + assert "Exists : True" in surf_repr + assert "Parent component : MidSurfaceProperties" in surf_repr + assert "Surface body : True" in surf_repr + assert "Surface thickness : 30 millimeter" in surf_repr + assert "Surface offset : MidSurfaceOffsetType.BOTTOM" in surf_repr + assert f"Color : {DEFAULT_COLOR}" in surf_repr + except GeometryExitedError: + pass - # Write random bytes - with file.open(mode="wb") as fout: - fout.write(os.urandom(file_size)) - assert file.exists() + # Let's create a new surface body and assign them from body methods directly + slot_surf2 = design.create_surface("MySlotSurface2", sketch) - # Upload file - necessary to import the Modeler class and create an instance - from ansys.geometry.core import Modeler + slot_surf2.add_midsurface_thickness(Quantity(30, UNITS.mm)) + slot_surf2.add_midsurface_offset(MidSurfaceOffsetType.BOTTOM) - modeler = Modeler() - path_on_server = modeler._upload_file_stream(file) - assert path_on_server is not None - finally: - pygeom_defaults.MAX_MESSAGE_LENGTH = old_value + surf_repr = str(slot_surf2) + assert "ansys.geometry.core.designer.Body" in surf_repr + assert "Name : MySlotSurface2" in surf_repr + assert "Exists : True" in surf_repr + assert "Parent component : MidSurfaceProperties" in surf_repr + assert "Surface body : True" in surf_repr + assert "Surface thickness : 30 millimeter" in surf_repr + assert "Surface offset : MidSurfaceOffsetType.BOTTOM" in surf_repr + assert f"Color : {DEFAULT_COLOR}" in surf_repr -def test_slot_extrusion(modeler: Modeler): - """Test the extrusion of a slot.""" +def test_design_points(modeler: Modeler): + """Test for verifying the ``DesignPoints``""" # Create your design on the server side - design = modeler.create_design("ExtrudeSlot") + design = modeler.create_design("DesignPoints") + point = Point3D([6, 66, 666], UNITS.mm) + design_points_1 = design.add_design_point("FirstPointSet", point) - # Create a Sketch object and draw a slot - sketch = Sketch() - sketch.slot(Point2D([10, 10], UNITS.mm), Quantity(10, UNITS.mm), Quantity(5, UNITS.mm)) + # Check the design points + assert len(design.design_points) == 1 + assert design_points_1.id is not None + assert design_points_1.name == "FirstPointSet" + assert design_points_1.value == point - # Extrude the sketch - body = design.extrude_sketch(name="MySlot", sketch=sketch, distance=Distance(50, UNITS.mm)) + # Create another set of design points + point_set_2 = [Point3D([10, 10, 10], UNITS.m), Point3D([20, 20, 20], UNITS.m)] + design_points_2 = design.add_design_points("SecondPointSet", point_set_2) - # A slot has 6 faces and 12 edges - assert len(body.faces) == 6 - assert len(body.edges) == 12 + assert len(design.design_points) == 3 + + nested_component = design.add_component("NestedComponent") + design_point_3 = nested_component.add_design_point("Nested", Point3D([7, 77, 777], UNITS.mm)) + assert design_point_3.id is not None + assert design_point_3.value == Point3D([7, 77, 777], UNITS.mm) + assert design_point_3.parent_component.id == nested_component.id + assert len(nested_component.design_points) == 1 + assert nested_component.design_points[0] == design_point_3 -def test_project_and_imprint_curves(modeler: Modeler): - """Test the projection of a set of curves on a body.""" - # Create your design on the server side - design = modeler.create_design("ExtrudeSlot") - comp = design.add_component("Comp1") + design_point_1_str = str(design_points_1) + assert "ansys.geometry.core.designer.DesignPoint" in design_point_1_str + assert " Name : FirstPointSet" in design_point_1_str + assert " Design Point : [0.006 0.066 0.666]" in design_point_1_str - # Create a Sketch object and draw a couple of slots - imprint_sketch = Sketch() - imprint_sketch.slot(Point2D([10, 10], UNITS.mm), Quantity(10, UNITS.mm), Quantity(5, UNITS.mm)) - imprint_sketch.slot(Point2D([50, 50], UNITS.mm), Quantity(10, UNITS.mm), Quantity(5, UNITS.mm)) + design_point_2_str = str(design_points_2) + assert "ansys.geometry.core.designer.DesignPoint" in design_point_2_str + assert " Name : SecondPointSet" in design_point_2_str + assert " Design Point : [10. 10. 10.]" in design_point_2_str + assert "ansys.geometry.core.designer.DesignPoint" in design_point_2_str + assert " Name : SecondPointSet" in design_point_2_str + assert " Design Point : [20. 20. 20.]" in design_point_2_str - # Extrude the sketch - sketch = Sketch() - sketch.box(Point2D([0, 0], UNITS.mm), Quantity(150, UNITS.mm), Quantity(150, UNITS.mm)) - body = comp.extrude_sketch(name="MyBox", sketch=sketch, distance=Quantity(50, UNITS.mm)) - body_faces = body.faces + # SKIPPING IF GRAPHICS REQUIRED + if are_graphics_available(): + # make sure it can create polydata + pd = design_points_1._to_polydata() - body_copy = body.copy(design, "copy") + import pyvista as pv - # Project the curves on the box - faces = body.project_curves(direction=UNITVECTOR3D_Z, sketch=imprint_sketch, closest_face=True) - assert len(faces) == 1 - # With the previous dir, the curves will be imprinted on the - # bottom face (closest one), i.e. the first one. - assert faces[0].id == body_faces[0].id + assert isinstance(pd, pv.PolyData) - # If we now draw our curves on a higher plane, the upper face should be selected - imprint_sketch_2 = Sketch(plane=Plane(Point3D([0, 0, 50], UNITS.mm))) - imprint_sketch_2.slot( - Point2D([10, 10], UNITS.mm), Quantity(10, UNITS.mm), Quantity(5, UNITS.mm) + +def test_named_selections_beams(modeler: Modeler): + """Test for verifying the correct creation of ``NamedSelection`` with + beams. + """ + # Create your design on the server side + design = modeler.create_design("NamedSelectionBeams_Test") + + # Test creating a named selection out of beams + circle_profile_1 = design.add_beam_circular_profile( + "CircleProfile1", Quantity(10, UNITS.mm), Point3D([0, 0, 0]), UNITVECTOR3D_X, UNITVECTOR3D_Y ) - imprint_sketch_2.slot( - Point2D([50, 50], UNITS.mm), Quantity(10, UNITS.mm), Quantity(5, UNITS.mm) + beam_1 = design.create_beam( + Point3D([9, 99, 999], UNITS.mm), Point3D([8, 88, 888], UNITS.mm), circle_profile_1 ) - faces = body.project_curves( - direction=UNITVECTOR3D_Z, sketch=imprint_sketch_2, closest_face=True - ) - assert len(faces) == 1 - # With the previous dir, the curves will be imprinted on the - # top face (closest one), i.e. the first one. - assert faces[0].id == body_faces[1].id - - # Now, let's try projecting only a single curve (i.e. one of the slots only) - faces = body.project_curves( - direction=UNITVECTOR3D_Z, sketch=imprint_sketch_2, closest_face=True, only_one_curve=True - ) - assert len(faces) == 1 - # With the previous dir, the curves will be imprinted on the - # top face (closest one), i.e. the first one. - assert faces[0].id == body_faces[1].id - - # Verify that the surface and curve types are of the correct type - related to PR - # https://github.com/ansys/pyansys-geometry/pull/1096 - assert isinstance(faces[0].surface_type, SurfaceType) - - # Now once the previous curves have been projected, let's try imprinting our sketch - # - # It should generate two additional faces to our box = 6 + 2 - new_edges, new_faces = body.imprint_curves(faces=faces, sketch=imprint_sketch_2) - - assert len(new_faces) == 2 - assert len(body.faces) == 8 - - # Verify that the surface and curve types are of the correct type - related to PR - # https://github.com/ansys/pyansys-geometry/pull/1096 - assert isinstance(new_faces[0].surface_type, SurfaceType) - assert isinstance(new_edges[0].curve_type, CurveType) - - # Make sure we have occurrence faces, not master - assert faces[0].id not in [face.id for face in body._template.faces] - assert new_faces[0].id not in [face.id for face in body._template.faces] + ns_beams = design.create_named_selection("CircleProfile", beams=[beam_1]) + assert len(design.named_selections) == 1 + assert design.named_selections[0].name == "CircleProfile" - faces = body_copy.imprint_projected_curves( - direction=UNITVECTOR3D_Z, sketch=imprint_sketch, closest_face=True - ) - assert len(faces) == 2 - assert len(body_copy.faces) == 8 + # Try deleting this named selection + design.delete_named_selection(ns_beams) + assert len(design.named_selections) == 0 -def test_imprint_trimmed_curves(modeler: Modeler): - """ - Test the imprinting of trimmed curves onto a specified face of a body. +def test_named_selections_design_points(modeler: Modeler): + """Test for verifying the correct creation of ``NamedSelection`` with + design points. """ - unit = DEFAULT_UNITS.LENGTH + # Create your design on the server side + design = modeler.create_design("NamedSelectionDesignPoints_Test") - wx = 1 - wy = 1 - wz = 1 - design = modeler.create_design("test imprint") + # Test creating a named selection out of design_points + point_set_1 = Point3D([10, 10, 0], UNITS.m) + design_points_1 = design.add_design_point("FirstPointSet", point_set_1) + ns_despoint = design.create_named_selection("FirstPointSet", design_points=[design_points_1]) + assert len(design.named_selections) == 1 + assert design.named_selections[0].name == "FirstPointSet" - # create box - start_at = Point3D([wx / 2, wy / 2, 0.0], unit=unit) + # Try deleting this named selection + design.delete_named_selection(ns_despoint) + assert len(design.named_selections) == 0 - plane = Plane( - start_at, - UNITVECTOR3D_X, - UNITVECTOR3D_Y, - ) - box_plane = Sketch(plane) - box_plane.box(Point2D([0.0, 0.0], unit=unit), width=wx, height=wy) - box = design.extrude_sketch("box", box_plane, wz) +def test_named_selections_components(modeler: Modeler): + """Test for verifying the correct creation of ``NamedSelection`` with + components. + """ + # Create your design on the server side + design = modeler.create_design("NamedSelectionComponents_Test") - assert len(box.faces) == 6 - assert len(box.edges) == 12 + # Test creating a named selection out of components + comp1 = design.add_component("Comp1") + comp2 = design.add_component("Comp2") + ns_components = design.create_named_selection("Components", components=[comp1, comp2]) + assert len(design.named_selections) == 1 + assert design.named_selections[0].name == "Components" - # create cylinder - point = Point3D([0.5, 0.5, 0.5]) - ortho_1, ortho_2 = UNITVECTOR3D_X, UNITVECTOR3D_Y - plane = Plane(point, ortho_1, ortho_2) - sketch_cylinder = Sketch(plane) - sketch_cylinder.circle(Point2D([0.0, 0.0], unit=unit), radius=0.1) - cylinder = design.extrude_sketch("cylinder", sketch_cylinder, 0.5) + # Fetch the component from the named selection + assert ns_components.components[0].id == comp1.id + assert ns_components.components[1].id == comp2.id - edges = cylinder.faces[1].edges - trimmed_curves = [edges[0].shape] - new_edges, new_faces = box.imprint_curves(faces=[box.faces[1]], trimmed_curves=trimmed_curves) + # Try deleting this named selection + design.delete_named_selection(ns_components) + assert len(design.named_selections) == 0 - # the new edge is coming from the circular top edge of the cylinder. - assert new_edges[0].start == new_edges[0].end - # verify that there is one new edge coming from the circle. - assert len(new_faces) == 1 - # verify that there is one new face coming from the circle. - assert len(new_edges) == 1 - # verify that there are 7 faces in total. - assert len(box.faces) == 7 - # verify that there are 14 edges in total. - assert len(box.edges) == 13 +def test_component_instances(modeler: Modeler): + """Test creation of ``Component`` instances and the effects this has.""" + design_name = "ComponentInstance_Test" + design = modeler.create_design(design_name) -def test_copy_body(modeler: Modeler): - """Test copying a body.""" - # Create your design on the server side - design = modeler.create_design("Design") + # Create a car + car1 = design.add_component("Car1") + comp1 = car1.add_component("A") + comp2 = car1.add_component("B") + wheel1 = comp2.add_component("Wheel1") - sketch_1 = Sketch().circle(Point2D([10, 10], UNITS.mm), Quantity(10, UNITS.mm)) - body = design.extrude_sketch("Original", sketch_1, Distance(1, UNITS.mm)) + # Create car base frame + sketch = Sketch().box(Point2D([5, 10]), 10, 20) + comp2.extrude_sketch("Base", sketch, 5) - # Copy body at same design level - copy = body.copy(design, "Copy") - assert len(design.bodies) == 2 - assert design.bodies[-1].id == copy.id + # Create first wheel + sketch = Sketch(Plane(direction_x=Vector3D([0, 1, 0]), direction_y=Vector3D([0, 0, 1]))) + sketch.circle(Point2D([0, 0]), 5) + wheel1.extrude_sketch("Wheel", sketch, -5) - # Bodies should be distinct - assert body.id != copy.id - assert body != copy + # Create 3 other wheels and move them into position + rotation_origin = Point3D([0, 0, 0]) + rotation_direction = UnitVector3D([0, 0, 1]) - # Copy body into sub-component - comp1 = design.add_component("comp1") - copy2 = body.copy(comp1, "Subcopy") - assert len(comp1.bodies) == 1 - assert comp1.bodies[-1].id == copy2.id + wheel2 = comp2.add_component("Wheel2", wheel1) + wheel2.modify_placement(Vector3D([0, 20, 0])) - # Bodies should be distinct - assert body.id != copy2.id - assert body != copy2 + wheel3 = comp2.add_component("Wheel3", wheel1) + wheel3.modify_placement(Vector3D([10, 0, 0]), rotation_origin, rotation_direction, np.pi) - # Copy a copy - comp2 = comp1.add_component("comp2") - copy3 = copy2.copy(comp2, "Copy3") - assert len(comp2.bodies) == 1 - assert comp2.bodies[-1].id == copy3.id + wheel4 = comp2.add_component("Wheel4", wheel1) + wheel4.modify_placement(Vector3D([10, 20, 0]), rotation_origin, rotation_direction, np.pi) - # Bodies should be distinct - assert copy2.id != copy3.id - assert copy2 != copy3 + # Assert all components have unique IDs + comp_ids = [wheel1.id, wheel2.id, wheel3.id, wheel4.id] + assert len(comp_ids) == len(set(comp_ids)) - # Ensure deleting original doesn't affect the copies - design.delete_body(body) - assert not body.is_alive - assert copy.is_alive + # Assert all bodies have unique IDs + body_ids = [wheel1.bodies[0].id, wheel2.bodies[0].id, wheel3.bodies[0].id, wheel4.bodies[0].id] + assert len(body_ids) == len(set(body_ids)) + # Assert all instances have unique MasterComponents + comp_templates = [wheel2._master_component, wheel3._master_component, wheel4._master_component] + assert len(comp_templates) == len(set(comp_templates)) -def test_beams(modeler: Modeler): - """Test beam creation.""" - # Create your design on the server side - design = modeler.create_design("BeamCreation") + # Assert all instances have the same Part + comp_parts = [ + wheel2._master_component.part, + wheel3._master_component.part, + wheel4._master_component.part, + ] + assert len(set(comp_parts)) == 1 - circle_profile_1 = design.add_beam_circular_profile( - "CircleProfile1", Quantity(10, UNITS.mm), Point3D([0, 0, 0]), UNITVECTOR3D_X, UNITVECTOR3D_Y - ) + assert wheel1.get_world_transform() == IDENTITY_MATRIX44 + assert wheel2.get_world_transform() != IDENTITY_MATRIX44 - assert circle_profile_1.id is not None - assert circle_profile_1.center == Point3D([0, 0, 0]) - assert circle_profile_1.radius.value.m_as(DEFAULT_UNITS.LENGTH) == 0.01 - assert circle_profile_1.direction_x == UNITVECTOR3D_X - assert circle_profile_1.direction_y == UNITVECTOR3D_Y + # Create 2nd car + car2 = design.add_component("Car2", car1) + car2.modify_placement(Vector3D([30, 0, 0])) - circle_profile_2 = design.add_beam_circular_profile( - "CircleProfile2", - Distance(20, UNITS.mm), - Point3D([10, 20, 30], UNITS.mm), - UnitVector3D([1, 1, 1]), - UnitVector3D([0, -1, 1]), - ) + # Create top of car - applies to BOTH cars + sketch = Sketch(Plane(Point3D([0, 5, 5]))).box(Point2D([5, 2.5]), 10, 5) + comp1.extrude_sketch("Top", sketch, 5) - assert circle_profile_2.id is not None - assert circle_profile_2.id is not circle_profile_1.id + # Show the body also got added to Car2, and they are distinct, but + # not independent + assert car1.components[0].bodies[0].id != car2.components[0].bodies[0].id - with pytest.raises(ValueError, match="Radius must be a real positive value."): - design.add_beam_circular_profile( - "InvalidProfileRadius", - Quantity(-10, UNITS.mm), - Point3D([0, 0, 0]), - UNITVECTOR3D_X, - UNITVECTOR3D_Y, - ) + # If monikers were formatted properly, you should be able to use them + assert len(car2.components[1].components[1].bodies[0].faces) > 0 - with pytest.raises(ValueError, match="Direction X and direction Y must be perpendicular."): - design.add_beam_circular_profile( - "InvalidUnitVectorAlignment", - Quantity(10, UNITS.mm), - Point3D([0, 0, 0]), - UNITVECTOR3D_X, - UnitVector3D([-1, -1, -1]), - ) - # Create a beam at the root component level - beam_1 = design.create_beam( - Point3D([9, 99, 999], UNITS.mm), Point3D([8, 88, 888], UNITS.mm), circle_profile_1 - ) +def test_boolean_body_operations(modeler: Modeler): + """ + Test cases: - assert beam_1.id is not None - assert beam_1.start == Point3D([9, 99, 999], UNITS.mm) - assert beam_1.end == Point3D([8, 88, 888], UNITS.mm) - assert beam_1.profile == circle_profile_1 - assert beam_1.parent_component.id == design.id - assert beam_1.is_alive - assert len(design.beams) == 1 - assert design.beams[0] == beam_1 + 1) master/master + a) intersect + i) normal + x) identity + y) transform + ii) empty failure + b) subtract + i) normal + x) identity + y) transform + ii) empty failure + iii) disjoint + c) unite + i) normal + x) identity + y) transform + ii) disjoint + 2) instance/instance + a) intersect + i) normal + x) identity + y) transform + ii) empty failure + b) subtract + i) normal + x) identity + y) transform + ii) empty failure + c) unite + i) normal + x) identity + y) transform + """ + design = modeler.create_design("TestBooleanOperations") - beam_1_str = str(beam_1) - assert "ansys.geometry.core.designer.Beam" in beam_1_str - assert " Exists : True" in beam_1_str - assert " Start : [0.009" in beam_1_str - assert " End : [0.008" in beam_1_str - assert " Parent component : BeamCreation" in beam_1_str - assert " Beam Profile info" in beam_1_str - assert " -----------------" in beam_1_str - assert "ansys.geometry.core.designer.BeamCircularProfile " in beam_1_str - assert " Name : CircleProfile1" in beam_1_str - assert " Radius : 10.0 millimeter" in beam_1_str - assert " Center : [0.0,0.0,0.0] in meters" in beam_1_str - assert " Direction x : [1.0,0.0,0.0]" in beam_1_str - assert " Direction y : [0.0,1.0,0.0]" in beam_1_str + comp1 = design.add_component("Comp1") + comp2 = design.add_component("Comp2") + comp3 = design.add_component("Comp3") - # Now, let's create two beams at a nested component, with the same profile - nested_component = design.add_component("NestedComponent") - beam_2 = nested_component.create_beam( - Point3D([7, 77, 777], UNITS.mm), Point3D([6, 66, 666], UNITS.mm), circle_profile_2 - ) - beam_3 = nested_component.create_beam( - Point3D([8, 88, 888], UNITS.mm), Point3D([7, 77, 777], UNITS.mm), circle_profile_2 - ) + body1 = comp1.extrude_sketch("Body1", Sketch().box(Point2D([0, 0]), 1, 1), 1) + body2 = comp2.extrude_sketch("Body2", Sketch().box(Point2D([0.5, 0]), 1, 1), 1) + body3 = comp3.extrude_sketch("Body3", Sketch().box(Point2D([5, 0]), 1, 1), 1) - assert beam_2.id is not None - assert beam_2.profile == circle_profile_2 - assert beam_2.parent_component.id == nested_component.id - assert beam_2.is_alive - assert beam_3.id is not None - assert beam_3.profile == circle_profile_2 - assert beam_3.parent_component.id == nested_component.id - assert beam_3.is_alive - assert beam_2.id != beam_3.id - assert len(nested_component.beams) == 2 - assert nested_component.beams[0] == beam_2 - assert nested_component.beams[1] == beam_3 + # 1.a.i.x + copy1 = body1.copy(comp1, "Copy1") + copy2 = body2.copy(comp2, "Copy2") + copy1.intersect(copy2) - # Once the beams are created, let's try deleting it. - # For example, we shouldn't be able to delete beam_1 from the nested component. - nested_component.delete_beam(beam_1) + assert not copy2.is_alive + assert body2.is_alive + assert Accuracy.length_is_equal(copy1.volume.m, 0.5) - assert beam_2.is_alive - assert nested_component.beams[0].is_alive - assert beam_3.is_alive - assert nested_component.beams[1].is_alive - assert beam_1.is_alive - assert design.beams[0].is_alive + # 1.a.i.y + copy1 = body1.copy(comp1, "Copy1") + copy2 = body2.copy(comp2, "Copy2") + copy2.translate(UnitVector3D([1, 0, 0]), 0.25) + copy1.intersect(copy2) - # Let's try deleting one of the beams from the nested component - nested_component.delete_beam(beam_2) - assert not beam_2.is_alive - assert not nested_component.beams[0].is_alive - assert beam_3.is_alive - assert nested_component.beams[1].is_alive - assert beam_1.is_alive - assert design.beams[0].is_alive + assert not copy2.is_alive + assert Accuracy.length_is_equal(copy1.volume.m, 0.25) - # Now, let's try deleting it from the design directly - this should be possible - design.delete_beam(beam_3) - assert not beam_2.is_alive - assert not nested_component.beams[0].is_alive - assert not beam_3.is_alive - assert not nested_component.beams[1].is_alive - assert beam_1.is_alive - assert design.beams[0].is_alive + # 1.a.ii + copy1 = body1.copy(comp1, "Copy1") + copy3 = body3.copy(comp3, "Copy3") + with pytest.raises(ValueError, match="bodies do not intersect"): + copy1.intersect(copy3) - # Finally, let's delete the beam from the root component - design.delete_beam(beam_1) - assert not beam_2.is_alive - assert not nested_component.beams[0].is_alive - assert not beam_3.is_alive - assert not nested_component.beams[1].is_alive - assert not beam_1.is_alive - assert not design.beams[0].is_alive + assert copy1.is_alive + assert copy3.is_alive - # Now, let's try deleting the beam profiles! - assert len(design.beam_profiles) == 2 - design.delete_beam_profile("MyInventedBeamProfile") - assert len(design.beam_profiles) == 2 - design.delete_beam_profile(circle_profile_1) - assert len(design.beam_profiles) == 1 - design.delete_beam_profile(circle_profile_2) - assert len(design.beam_profiles) == 0 + # 1.b.i.x + copy1 = body1.copy(comp1, "Copy1") + copy2 = body2.copy(comp2, "Copy2") + copy1.subtract(copy2) + assert not copy2.is_alive + assert body2.is_alive + assert Accuracy.length_is_equal(copy1.volume.m, 0.5) -def test_midsurface_properties(modeler: Modeler): - """Test mid-surface properties assignment.""" - # Create your design on the server side - design = modeler.create_design("MidSurfaceProperties") + # 1.b.i.y + copy1 = body1.copy(comp1, "Copy1") + copy2 = body2.copy(comp2, "Copy2") + copy2.translate(UnitVector3D([1, 0, 0]), 0.25) + copy1.subtract(copy2) - # Create a Sketch object and draw a slot - sketch = Sketch() - sketch.slot(Point2D([10, 10], UNITS.mm), Quantity(10, UNITS.mm), Quantity(5, UNITS.mm)) + assert not copy2.is_alive + assert Accuracy.length_is_equal(copy1.volume.m, 0.75) - # Create an actual body from the slot, and translate it - slot_body = design.extrude_sketch("MySlot", sketch, Quantity(10, UNITS.mm)) - slot_body.translate(UNITVECTOR3D_X, Quantity(40, UNITS.mm)) + # 1.b.ii + copy1 = body1.copy(comp1, "Copy1") + copy1a = body1.copy(comp1, "Copy1a") + with pytest.raises(ValueError): + copy1.subtract(copy1a) - # Create a surface body as well - slot_surf = design.create_surface("MySlotSurface", sketch) + assert copy1.is_alive + assert copy1a.is_alive - surf_repr = str(slot_surf) - assert "ansys.geometry.core.designer.Body" in surf_repr - assert "Name : MySlotSurface" in surf_repr - assert "Exists : True" in surf_repr - assert "Parent component : MidSurfaceProperties" in surf_repr - assert "Surface body : True" in surf_repr - assert "Surface thickness : None" in surf_repr - assert "Surface offset : None" in surf_repr - assert f"Color : {DEFAULT_COLOR}" in surf_repr + # 1.b.iii + copy1 = body1.copy(comp1, "Copy1") + copy3 = body3.copy(comp3, "Copy3") + copy1.subtract(copy3) - # Let's assign a thickness to both bodies - design.add_midsurface_thickness( - thickness=Quantity(10, UNITS.mm), - bodies=[slot_body, slot_surf], - ) + assert Accuracy.length_is_equal(copy1.volume.m, 1) + assert copy1.volume + assert not copy3.is_alive - # Let's also assign a mid-surface offset to both bodies - design.add_midsurface_offset( - offset_type=MidSurfaceOffsetType.TOP, bodies=[slot_body, slot_surf] - ) + # 1.c.i.x + copy1 = body1.copy(comp1, "Copy1") + copy2 = body2.copy(comp2, "Copy2") + copy1.unite(copy2) - # Let's check the values now - assert slot_body.surface_thickness is None - assert slot_body.surface_offset is None - assert slot_surf.surface_thickness == Quantity(10, UNITS.mm) - assert slot_surf.surface_offset == MidSurfaceOffsetType.TOP + assert not copy2.is_alive + assert body2.is_alive + assert Accuracy.length_is_equal(copy1.volume.m, 1.5) - # Let's check that the design-stored values are also updated - assert design.bodies[0].surface_thickness is None - assert design.bodies[0].surface_offset is None - assert design.bodies[1].surface_thickness == Quantity(10, UNITS.mm) - assert design.bodies[1].surface_offset == MidSurfaceOffsetType.TOP + # 1.c.i.y + copy1 = body1.copy(comp1, "Copy1") + copy2 = body2.copy(comp2, "Copy2") + copy2.translate(UnitVector3D([1, 0, 0]), 0.25) + copy1.unite(copy2) - surf_repr = str(slot_surf) - assert "ansys.geometry.core.designer.Body" in surf_repr - assert "Name : MySlotSurface" in surf_repr - assert "Exists : True" in surf_repr - assert "Parent component : MidSurfaceProperties" in surf_repr - assert "Surface body : True" in surf_repr - assert "Surface thickness : 10 millimeter" in surf_repr - assert "Surface offset : MidSurfaceOffsetType.TOP" in surf_repr - assert f"Color : {DEFAULT_COLOR}" in surf_repr + assert not copy2.is_alive + assert Accuracy.length_is_equal(copy1.volume.m, 1.75) - # Let's try reassigning values directly to slot_body - this shouldn't do anything - slot_body.add_midsurface_thickness(Quantity(10, UNITS.mm)) - slot_body.add_midsurface_offset(MidSurfaceOffsetType.TOP) + # 1.c.ii + copy1 = body1.copy(comp1, "Copy1") + copy3 = body3.copy(comp3, "Copy3") + copy1.unite(copy3) - body_repr = str(slot_body) - assert "ansys.geometry.core.designer.Body" in body_repr - assert "Name : MySlot" in body_repr - assert "Exists : True" in body_repr - assert "Parent component : MidSurfaceProperties" in body_repr - assert "Surface body : False" in body_repr - assert f"Color : {DEFAULT_COLOR}" in surf_repr - assert slot_body.surface_thickness is None - assert slot_body.surface_offset is None + assert not copy3.is_alive + assert body3.is_alive + assert Accuracy.length_is_equal(copy1.volume.m, 1) - # Let's try reassigning values directly to slot_surf - this should work - # TODO : at the moment the server does not allow to reassign - put in try/catch block - # https://github.com/ansys/pyansys-geometry/issues/1146 - try: - slot_surf.add_midsurface_thickness(Quantity(30, UNITS.mm)) - slot_surf.add_midsurface_offset(MidSurfaceOffsetType.BOTTOM) + # Test instance/instance + comp1_i = design.add_component("Comp1_i", comp1) + comp2_i = design.add_component("Comp2_i", comp2) + comp3_i = design.add_component("Comp3_i", comp3) - surf_repr = str(slot_surf) - assert "ansys.geometry.core.designer.Body" in surf_repr - assert "Name : MySlotSurface" in surf_repr - assert "Exists : True" in surf_repr - assert "Parent component : MidSurfaceProperties" in surf_repr - assert "Surface body : True" in surf_repr - assert "Surface thickness : 30 millimeter" in surf_repr - assert "Surface offset : MidSurfaceOffsetType.BOTTOM" in surf_repr - assert f"Color : {DEFAULT_COLOR}" in surf_repr - except GeometryExitedError: - pass - - # Let's create a new surface body and assign them from body methods directly - slot_surf2 = design.create_surface("MySlotSurface2", sketch) - - slot_surf2.add_midsurface_thickness(Quantity(30, UNITS.mm)) - slot_surf2.add_midsurface_offset(MidSurfaceOffsetType.BOTTOM) + comp1_i.modify_placement( + Vector3D([52, 61, -43]), Point3D([-4, 26, 66]), UnitVector3D([-21, 20, 87]), np.pi / 4 + ) + comp2_i.modify_placement( + Vector3D([52, 61, -43]), Point3D([-4, 26, 66]), UnitVector3D([-21, 20, 87]), np.pi / 4 + ) + comp3_i.modify_placement( + Vector3D([52, 61, -43]), Point3D([-4, 26, 66]), UnitVector3D([-21, 20, 87]), np.pi / 4 + ) - surf_repr = str(slot_surf2) - assert "ansys.geometry.core.designer.Body" in surf_repr - assert "Name : MySlotSurface2" in surf_repr - assert "Exists : True" in surf_repr - assert "Parent component : MidSurfaceProperties" in surf_repr - assert "Surface body : True" in surf_repr - assert "Surface thickness : 30 millimeter" in surf_repr - assert "Surface offset : MidSurfaceOffsetType.BOTTOM" in surf_repr - assert f"Color : {DEFAULT_COLOR}" in surf_repr + body1 = comp1_i.bodies[0] + body2 = comp2_i.bodies[0] + body3 = comp3_i.bodies[0] + # 2.a.i.x + copy1 = body1.copy(comp1_i, "Copy1") + copy2 = body2.copy(comp2_i, "Copy2") + copy1.intersect(copy2) -def test_design_points(modeler: Modeler): - """Test for verifying the ``DesignPoints``""" - # Create your design on the server side - design = modeler.create_design("DesignPoints") - point = Point3D([6, 66, 666], UNITS.mm) - design_points_1 = design.add_design_point("FirstPointSet", point) + assert not copy2.is_alive + assert body2.is_alive + assert Accuracy.length_is_equal(copy1.volume.m, 0.5) - # Check the design points - assert len(design.design_points) == 1 - assert design_points_1.id is not None - assert design_points_1.name == "FirstPointSet" - assert design_points_1.value == point + # 2.a.i.y + copy1 = body1.copy(comp1_i, "Copy1") + copy2 = body2.copy(comp2_i, "Copy2") + copy2.translate(UnitVector3D([1, 0, 0]), 0.25) + copy1.intersect(copy2) - # Create another set of design points - point_set_2 = [Point3D([10, 10, 10], UNITS.m), Point3D([20, 20, 20], UNITS.m)] - design_points_2 = design.add_design_points("SecondPointSet", point_set_2) + assert not copy2.is_alive + assert Accuracy.length_is_equal(copy1.volume.m, 0.25) - assert len(design.design_points) == 3 + # 2.a.ii + copy1 = body1.copy(comp1_i, "Copy1") + copy3 = body3.copy(comp3_i, "Copy3") + with pytest.raises(ValueError, match="bodies do not intersect"): + copy1.intersect(copy3) - nested_component = design.add_component("NestedComponent") - design_point_3 = nested_component.add_design_point("Nested", Point3D([7, 77, 777], UNITS.mm)) + assert copy1.is_alive + assert copy3.is_alive - assert design_point_3.id is not None - assert design_point_3.value == Point3D([7, 77, 777], UNITS.mm) - assert design_point_3.parent_component.id == nested_component.id - assert len(nested_component.design_points) == 1 - assert nested_component.design_points[0] == design_point_3 + # 2.b.i.x + copy1 = body1.copy(comp1_i, "Copy1") + copy2 = body2.copy(comp2_i, "Copy2") + copy1.subtract(copy2) - design_point_1_str = str(design_points_1) - assert "ansys.geometry.core.designer.DesignPoint" in design_point_1_str - assert " Name : FirstPointSet" in design_point_1_str - assert " Design Point : [0.006 0.066 0.666]" in design_point_1_str + assert not copy2.is_alive + assert body2.is_alive + assert Accuracy.length_is_equal(copy1.volume.m, 0.5) - design_point_2_str = str(design_points_2) - assert "ansys.geometry.core.designer.DesignPoint" in design_point_2_str - assert " Name : SecondPointSet" in design_point_2_str - assert " Design Point : [10. 10. 10.]" in design_point_2_str - assert "ansys.geometry.core.designer.DesignPoint" in design_point_2_str - assert " Name : SecondPointSet" in design_point_2_str - assert " Design Point : [20. 20. 20.]" in design_point_2_str + # 2.b.i.y + copy1 = body1.copy(comp1_i, "Copy1") + copy2 = body2.copy(comp2_i, "Copy2") + copy2.translate(UnitVector3D([1, 0, 0]), 0.25) + copy1.subtract(copy2) - # SKIPPING IF GRAPHICS REQUIRED - if are_graphics_available(): - # make sure it can create polydata - pd = design_points_1._to_polydata() + assert not copy2.is_alive + assert Accuracy.length_is_equal(copy1.volume.m, 0.75) - import pyvista as pv + # 2.b.ii + copy1 = body1.copy(comp1_i, "Copy1") + copy1a = body1.copy(comp1_i, "Copy1a") + with pytest.raises(ValueError): + copy1.subtract(copy1a) - assert isinstance(pd, pv.PolyData) + assert copy1.is_alive + assert copy1a.is_alive + # 2.b.iii + copy1 = body1.copy(comp1_i, "Copy1") + copy3 = body3.copy(comp3_i, "Copy3") + copy1.subtract(copy3) -def test_named_selections_beams(modeler: Modeler): - """Test for verifying the correct creation of ``NamedSelection`` with - beams. - """ - # Create your design on the server side - design = modeler.create_design("NamedSelectionBeams_Test") + assert Accuracy.length_is_equal(copy1.volume.m, 1) + assert copy1.volume + assert not copy3.is_alive - # Test creating a named selection out of beams - circle_profile_1 = design.add_beam_circular_profile( - "CircleProfile1", Quantity(10, UNITS.mm), Point3D([0, 0, 0]), UNITVECTOR3D_X, UNITVECTOR3D_Y - ) - beam_1 = design.create_beam( - Point3D([9, 99, 999], UNITS.mm), Point3D([8, 88, 888], UNITS.mm), circle_profile_1 - ) - ns_beams = design.create_named_selection("CircleProfile", beams=[beam_1]) - assert len(design.named_selections) == 1 - assert design.named_selections[0].name == "CircleProfile" + # 2.c.i.x + copy1 = body1.copy(comp1_i, "Copy1") + copy2 = body2.copy(comp2_i, "Copy2") + copy1.unite(copy2) - # Try deleting this named selection - design.delete_named_selection(ns_beams) - assert len(design.named_selections) == 0 + assert not copy2.is_alive + assert body2.is_alive + assert Accuracy.length_is_equal(copy1.volume.m, 1.5) + # 2.c.i.y + copy1 = body1.copy(comp1_i, "Copy1") + copy2 = body2.copy(comp2_i, "Copy2") + copy2.translate(UnitVector3D([1, 0, 0]), 0.25) + copy1.unite(copy2) -def test_named_selections_design_points(modeler: Modeler): - """Test for verifying the correct creation of ``NamedSelection`` with - design points. - """ - # Create your design on the server side - design = modeler.create_design("NamedSelectionDesignPoints_Test") + assert not copy2.is_alive + assert Accuracy.length_is_equal(copy1.volume.m, 1.75) - # Test creating a named selection out of design_points - point_set_1 = Point3D([10, 10, 0], UNITS.m) - design_points_1 = design.add_design_point("FirstPointSet", point_set_1) - ns_despoint = design.create_named_selection("FirstPointSet", design_points=[design_points_1]) - assert len(design.named_selections) == 1 - assert design.named_selections[0].name == "FirstPointSet" + # 2.c.ii + copy1 = body1.copy(comp1_i, "Copy1") + copy3 = body3.copy(comp3_i, "Copy3") + copy1.unite(copy3) - # Try deleting this named selection - design.delete_named_selection(ns_despoint) - assert len(design.named_selections) == 0 + assert not copy3.is_alive + assert body3.is_alive + assert Accuracy.length_is_equal(copy1.volume.m, 1) -def test_named_selections_components(modeler: Modeler): - """Test for verifying the correct creation of ``NamedSelection`` with - components. - """ - # Create your design on the server side - design = modeler.create_design("NamedSelectionComponents_Test") +def test_multiple_bodies_boolean_operations(modeler: Modeler): + """Test boolean operations with multiple bodies.""" + design = modeler.create_design("TestBooleanOperationsMultipleBodies") - # Test creating a named selection out of components comp1 = design.add_component("Comp1") comp2 = design.add_component("Comp2") - ns_components = design.create_named_selection("Components", components=[comp1, comp2]) - assert len(design.named_selections) == 1 - assert design.named_selections[0].name == "Components" + comp3 = design.add_component("Comp3") - # Fetch the component from the named selection - assert ns_components.components[0].id == comp1.id - assert ns_components.components[1].id == comp2.id + body1 = comp1.extrude_sketch("Body1", Sketch().box(Point2D([0, 0]), 1, 1), 1) + body2 = comp2.extrude_sketch("Body2", Sketch().box(Point2D([0.5, 0]), 1, 1), 1) + body3 = comp3.extrude_sketch("Body3", Sketch().box(Point2D([5, 0]), 1, 1), 1) - # Try deleting this named selection - design.delete_named_selection(ns_components) - assert len(design.named_selections) == 0 + ################# Check subtract operation ################# + copy1_sub = body1.copy(comp1, "Copy1_subtract") + copy2_sub = body2.copy(comp2, "Copy2_subtract") + copy3_sub = body3.copy(comp3, "Copy3_subtract") + copy1_sub.subtract([copy2_sub, copy3_sub]) + assert not copy2_sub.is_alive + assert not copy3_sub.is_alive + assert body2.is_alive + assert body3.is_alive + assert len(comp1.bodies) == 2 + assert len(comp2.bodies) == 1 + assert len(comp3.bodies) == 1 -def test_component_instances(modeler: Modeler): - """Test creation of ``Component`` instances and the effects this has.""" - design_name = "ComponentInstance_Test" - design = modeler.create_design(design_name) + # Cleanup previous subtest + comp1.delete_body(copy1_sub) + assert len(comp1.bodies) == 1 - # Create a car - car1 = design.add_component("Car1") - comp1 = car1.add_component("A") - comp2 = car1.add_component("B") - wheel1 = comp2.add_component("Wheel1") + ################# Check unite operation ################# + copy1_uni = body1.copy(comp1, "Copy1_unite") + copy2_uni = body2.copy(comp2, "Copy2_unite") + copy3_uni = body3.copy(comp3, "Copy3_unite") + copy1_uni.unite([copy2_uni, copy3_uni]) - # Create car base frame - sketch = Sketch().box(Point2D([5, 10]), 10, 20) - comp2.extrude_sketch("Base", sketch, 5) + assert not copy2_uni.is_alive + assert not copy3_uni.is_alive + assert body2.is_alive + assert body3.is_alive + assert len(comp1.bodies) == 2 + assert len(comp2.bodies) == 1 + assert len(comp3.bodies) == 1 - # Create first wheel - sketch = Sketch(Plane(direction_x=Vector3D([0, 1, 0]), direction_y=Vector3D([0, 0, 1]))) - sketch.circle(Point2D([0, 0]), 5) - wheel1.extrude_sketch("Wheel", sketch, -5) + # Cleanup previous subtest + comp1.delete_body(copy1_uni) + assert len(comp1.bodies) == 1 - # Create 3 other wheels and move them into position - rotation_origin = Point3D([0, 0, 0]) - rotation_direction = UnitVector3D([0, 0, 1]) + ################# Check intersect operation ################# + copy1_int = body1.copy(comp1, "Copy1_intersect") + copy2_int = body2.copy(comp2, "Copy2_intersect") + copy3_int = body3.copy(comp3, "Copy3_intersect") # Body 3 does not intersect them + copy1_int.intersect([copy2_int]) - wheel2 = comp2.add_component("Wheel2", wheel1) - wheel2.modify_placement(Vector3D([0, 20, 0])) + assert not copy2_int.is_alive + assert copy3_int.is_alive + assert body2.is_alive + assert body3.is_alive + assert len(comp1.bodies) == 2 + assert len(comp2.bodies) == 1 + assert len(comp3.bodies) == 2 - wheel3 = comp2.add_component("Wheel3", wheel1) - wheel3.modify_placement(Vector3D([10, 0, 0]), rotation_origin, rotation_direction, np.pi) + # Cleanup previous subtest + comp1.delete_body(copy1_int) + comp3.delete_body(copy3_int) + assert len(comp1.bodies) == 1 + assert len(comp3.bodies) == 1 - wheel4 = comp2.add_component("Wheel4", wheel1) - wheel4.modify_placement(Vector3D([10, 20, 0]), rotation_origin, rotation_direction, np.pi) - # Assert all components have unique IDs - comp_ids = [wheel1.id, wheel2.id, wheel3.id, wheel4.id] - assert len(comp_ids) == len(set(comp_ids)) +def test_bool_operations_with_keep_other(modeler: Modeler): + """Test boolean operations with keep other option.""" + # Create the design and bodies + design = modeler.create_design("TestBooleanOperationsWithKeepOther") - # Assert all bodies have unique IDs - body_ids = [wheel1.bodies[0].id, wheel2.bodies[0].id, wheel3.bodies[0].id, wheel4.bodies[0].id] - assert len(body_ids) == len(set(body_ids)) + comp1 = design.add_component("Comp1") + comp2 = design.add_component("Comp2") + comp3 = design.add_component("Comp3") - # Assert all instances have unique MasterComponents - comp_templates = [wheel2._master_component, wheel3._master_component, wheel4._master_component] - assert len(comp_templates) == len(set(comp_templates)) - - # Assert all instances have the same Part - comp_parts = [ - wheel2._master_component.part, - wheel3._master_component.part, - wheel4._master_component.part, - ] - assert len(set(comp_parts)) == 1 - - assert wheel1.get_world_transform() == IDENTITY_MATRIX44 - assert wheel2.get_world_transform() != IDENTITY_MATRIX44 - - # Create 2nd car - car2 = design.add_component("Car2", car1) - car2.modify_placement(Vector3D([30, 0, 0])) - - # Create top of car - applies to BOTH cars - sketch = Sketch(Plane(Point3D([0, 5, 5]))).box(Point2D([5, 2.5]), 10, 5) - comp1.extrude_sketch("Top", sketch, 5) - - # Show the body also got added to Car2, and they are distinct, but - # not independent - assert car1.components[0].bodies[0].id != car2.components[0].bodies[0].id - - # If monikers were formatted properly, you should be able to use them - assert len(car2.components[1].components[1].bodies[0].faces) > 0 - - -def test_boolean_body_operations(modeler: Modeler): - """ - Test cases: - - 1) master/master - a) intersect - i) normal - x) identity - y) transform - ii) empty failure - b) subtract - i) normal - x) identity - y) transform - ii) empty failure - iii) disjoint - c) unite - i) normal - x) identity - y) transform - ii) disjoint - 2) instance/instance - a) intersect - i) normal - x) identity - y) transform - ii) empty failure - b) subtract - i) normal - x) identity - y) transform - ii) empty failure - c) unite - i) normal - x) identity - y) transform - """ - design = modeler.create_design("TestBooleanOperations") - - comp1 = design.add_component("Comp1") - comp2 = design.add_component("Comp2") - comp3 = design.add_component("Comp3") - - body1 = comp1.extrude_sketch("Body1", Sketch().box(Point2D([0, 0]), 1, 1), 1) - body2 = comp2.extrude_sketch("Body2", Sketch().box(Point2D([0.5, 0]), 1, 1), 1) - body3 = comp3.extrude_sketch("Body3", Sketch().box(Point2D([5, 0]), 1, 1), 1) - - # 1.a.i.x - copy1 = body1.copy(comp1, "Copy1") - copy2 = body2.copy(comp2, "Copy2") - copy1.intersect(copy2) - - assert not copy2.is_alive - assert body2.is_alive - assert Accuracy.length_is_equal(copy1.volume.m, 0.5) - - # 1.a.i.y - copy1 = body1.copy(comp1, "Copy1") - copy2 = body2.copy(comp2, "Copy2") - copy2.translate(UnitVector3D([1, 0, 0]), 0.25) - copy1.intersect(copy2) - - assert not copy2.is_alive - assert Accuracy.length_is_equal(copy1.volume.m, 0.25) - - # 1.a.ii - copy1 = body1.copy(comp1, "Copy1") - copy3 = body3.copy(comp3, "Copy3") - with pytest.raises(ValueError, match="bodies do not intersect"): - copy1.intersect(copy3) - - assert copy1.is_alive - assert copy3.is_alive - - # 1.b.i.x - copy1 = body1.copy(comp1, "Copy1") - copy2 = body2.copy(comp2, "Copy2") - copy1.subtract(copy2) - - assert not copy2.is_alive - assert body2.is_alive - assert Accuracy.length_is_equal(copy1.volume.m, 0.5) - - # 1.b.i.y - copy1 = body1.copy(comp1, "Copy1") - copy2 = body2.copy(comp2, "Copy2") - copy2.translate(UnitVector3D([1, 0, 0]), 0.25) - copy1.subtract(copy2) - - assert not copy2.is_alive - assert Accuracy.length_is_equal(copy1.volume.m, 0.75) - - # 1.b.ii - copy1 = body1.copy(comp1, "Copy1") - copy1a = body1.copy(comp1, "Copy1a") - with pytest.raises(ValueError): - copy1.subtract(copy1a) - - assert copy1.is_alive - assert copy1a.is_alive - - # 1.b.iii - copy1 = body1.copy(comp1, "Copy1") - copy3 = body3.copy(comp3, "Copy3") - copy1.subtract(copy3) - - assert Accuracy.length_is_equal(copy1.volume.m, 1) - assert copy1.volume - assert not copy3.is_alive - - # 1.c.i.x - copy1 = body1.copy(comp1, "Copy1") - copy2 = body2.copy(comp2, "Copy2") - copy1.unite(copy2) - - assert not copy2.is_alive - assert body2.is_alive - assert Accuracy.length_is_equal(copy1.volume.m, 1.5) - - # 1.c.i.y - copy1 = body1.copy(comp1, "Copy1") - copy2 = body2.copy(comp2, "Copy2") - copy2.translate(UnitVector3D([1, 0, 0]), 0.25) - copy1.unite(copy2) - - assert not copy2.is_alive - assert Accuracy.length_is_equal(copy1.volume.m, 1.75) - - # 1.c.ii - copy1 = body1.copy(comp1, "Copy1") - copy3 = body3.copy(comp3, "Copy3") - copy1.unite(copy3) - - assert not copy3.is_alive - assert body3.is_alive - assert Accuracy.length_is_equal(copy1.volume.m, 1) - - -def test_multiple_bodies_boolean_operations(modeler: Modeler): - """Test boolean operations with multiple bodies.""" - design = modeler.create_design("TestBooleanOperationsMultipleBodies") - - comp1 = design.add_component("Comp1") - comp2 = design.add_component("Comp2") - comp3 = design.add_component("Comp3") - - body1 = comp1.extrude_sketch("Body1", Sketch().box(Point2D([0, 0]), 1, 1), 1) - body2 = comp2.extrude_sketch("Body2", Sketch().box(Point2D([0.5, 0]), 1, 1), 1) - body3 = comp3.extrude_sketch("Body3", Sketch().box(Point2D([5, 0]), 1, 1), 1) - - ################# Check subtract operation ################# - copy1_sub = body1.copy(comp1, "Copy1_subtract") - copy2_sub = body2.copy(comp2, "Copy2_subtract") - copy3_sub = body3.copy(comp3, "Copy3_subtract") - copy1_sub.subtract([copy2_sub, copy3_sub]) - - assert not copy2_sub.is_alive - assert not copy3_sub.is_alive - assert body2.is_alive - assert body3.is_alive - assert len(comp1.bodies) == 2 - assert len(comp2.bodies) == 1 - assert len(comp3.bodies) == 1 - - # Cleanup previous subtest - comp1.delete_body(copy1_sub) - assert len(comp1.bodies) == 1 - - ################# Check unite operation ################# - copy1_uni = body1.copy(comp1, "Copy1_unite") - copy2_uni = body2.copy(comp2, "Copy2_unite") - copy3_uni = body3.copy(comp3, "Copy3_unite") - copy1_uni.unite([copy2_uni, copy3_uni]) - - assert not copy2_uni.is_alive - assert not copy3_uni.is_alive - assert body2.is_alive - assert body3 is not None and body3.is_alive - assert len(comp1.bodies) == 2 - assert len(comp2.bodies) == 1 - assert len(comp3.bodies) == 1 - - # Cleanup previous subtest - comp1.delete_body(copy1_uni) - assert len(comp1.bodies) == 1 - - ################# Check intersect operation ################# - copy1_int = body1.copy(comp1, "Copy1_intersect") - copy2_int = body2.copy(comp2, "Copy2_intersect") - copy3_int = body3.copy(comp3, "Copy3_intersect") # Body 3 does not intersect them - copy1_int.intersect([copy2_int]) - - assert not copy2_int.is_alive - assert copy3_int.is_alive - assert body2.is_alive - assert body3.is_alive - assert len(comp1.bodies) == 2 - assert len(comp2.bodies) == 1 - assert len(comp3.bodies) == 2 - - # Cleanup previous subtest - comp1.delete_body(copy1_int) - comp3.delete_body(copy3_int) - assert len(comp1.bodies) == 1 - assert len(comp3.bodies) == 1 - - -def test_bool_operations_with_keep_other(modeler: Modeler): - """Test boolean operations with keep other option.""" - # Create the design and bodies - design = modeler.create_design("TestBooleanOperationsWithKeepOther") - - comp1 = design.add_component("Comp1") - comp2 = design.add_component("Comp2") - comp3 = design.add_component("Comp3") - - body1 = comp1.extrude_sketch("Body1", Sketch().box(Point2D([0, 0]), 1, 1), 1) - body2 = comp2.extrude_sketch("Body2", Sketch().box(Point2D([0.5, 0]), 1, 1), 1) - body3 = comp3.extrude_sketch("Body3", Sketch().box(Point2D([5, 0]), 1, 1), 1) + body1 = comp1.extrude_sketch("Body1", Sketch().box(Point2D([0, 0]), 1, 1), 1) + body2 = comp2.extrude_sketch("Body2", Sketch().box(Point2D([0.5, 0]), 1, 1), 1) + body3 = comp3.extrude_sketch("Body3", Sketch().box(Point2D([5, 0]), 1, 1), 1) # ---- Verify subtract operation ---- body1.subtract([body2, body3], keep_other=True) @@ -2657,19 +2550,122 @@ def test_body_mapping(modeler: Modeler): .segment_to_point(Point2D([1, 1])), 1, ) - top = design.extrude_sketch( - "top", Sketch(Plane(Point3D([0, 0, 1]))).box(Point2D([0.5, 0.5]), 0.1, 0.1), 0.1 - ) - body.unite(top) - # Mirror across YZ plane - copy1 = body.copy(body.parent_component, "box2") - copy1.mirror(Plane(Point3D([2, 0, 0]), UnitVector3D([0, 0, 1]), UnitVector3D([0, 1, 0]))) + # Test 1: identity mapping - everything should be the same + copy = body.copy(body.parent_component, "copy") + copy.map(Frame(Point3D([0, 0, 0]), UnitVector3D([1, 0, 0]), UnitVector3D([0, 1, 0]))) - # results from SpaceClaim - expected_vertices = [ - Point3D([5.0, -1.0, 1.0]), - Point3D([5.0, -1.0, 0.0]), + vertices = [] + for edge in body.edges: + vertices.extend([edge.shape.start, edge.shape.end]) + + copy_vertices = [] + for edge in copy.edges: + copy_vertices.extend([edge.shape.start, edge.shape.end]) + + assert np.allclose(vertices, copy_vertices) + + # Test 2: mirror the body - flips only the x direction + copy = body.copy(body.parent_component, "copy") + copy.map(Frame(Point3D([-4, 0, 1]), UnitVector3D([-1, 0, 0]), UnitVector3D([0, 1, 0]))) + + copy_vertices = [] + for edge in copy.edges: + copy_vertices.extend([edge.shape.start, edge.shape.end]) + + # expected vertices from confirmed mirror + expected_vertices = [ + Point3D([-3.0, -1.0, 0.0]), + Point3D([-5.0, -1.0, 0.0]), + Point3D([-3.0, -1.0, 1.0]), + Point3D([-3.0, -1.0, 0.0]), + Point3D([-4.0, 0.5, 0.0]), + Point3D([-3.0, -1.0, 0.0]), + Point3D([-4.0, 0.5, 1.0]), + Point3D([-4.0, 0.5, 0.0]), + Point3D([-3.0, 1.0, 0.0]), + Point3D([-4.0, 0.5, 0.0]), + Point3D([-3.0, 1.0, 1.0]), + Point3D([-3.0, 1.0, 0.0]), + Point3D([-5.0, 1.0, 0.0]), + Point3D([-3.0, 1.0, 0.0]), + Point3D([-5.0, 1.0, 1.0]), + Point3D([-5.0, 1.0, 0.0]), + Point3D([-5.0, -1.0, 0.0]), + Point3D([-5.0, 1.0, 0.0]), + Point3D([-5.0, -1.0, 1.0]), + Point3D([-5.0, -1.0, 0.0]), + Point3D([-3.0, -1.0, 1.0]), + Point3D([-5.0, -1.0, 1.0]), + Point3D([-4.0, 0.5, 1.0]), + Point3D([-3.0, -1.0, 1.0]), + Point3D([-3.0, 1.0, 1.0]), + Point3D([-4.0, 0.5, 1.0]), + Point3D([-5.0, 1.0, 1.0]), + Point3D([-3.0, 1.0, 1.0]), + Point3D([-5.0, -1.0, 1.0]), + Point3D([-5.0, 1.0, 1.0]), + ] + + assert np.allclose(expected_vertices, copy_vertices) + + # Test 3: rotate body 180 degrees - flip x and y direction + map_copy = body.copy(body.parent_component, "copy") + map_copy.map(Frame(Point3D([0, 0, 0]), UnitVector3D([-1, 0, 0]), UnitVector3D([0, -1, 0]))) + + rotate_copy = body.copy(body.parent_component, "copy") + rotate_copy.rotate(Point3D([0, 0, 0]), UnitVector3D([0, 0, 1]), np.pi) + + map_vertices = [] + for edge in map_copy.edges: + map_vertices.extend([edge.shape.start, edge.shape.end]) + + rotate_vertices = [] + for edge in rotate_copy.edges: + rotate_vertices.extend([edge.shape.start, edge.shape.end]) + + assert np.allclose(map_vertices, rotate_vertices) + + +def test_sphere_creation(modeler: Modeler): + """Test the creation of a sphere body with a given radius.""" + design = modeler.create_design("Spheretest") + center_point = Point3D([10, 10, 10], UNITS.m) + radius = Distance(1, UNITS.m) + spherebody = design.create_sphere("testspherebody", center_point, radius) + assert spherebody.name == "testspherebody" + assert len(spherebody.faces) == 1 + assert round(spherebody.volume._magnitude, 3) == round(4.1887902, 3) + + +def test_body_mirror(modeler: Modeler): + """Test the mirroring of a body.""" + design = modeler.create_design("Design1") + + # Create shape with no lines of symmetry in any axis + body = design.extrude_sketch( + "box", + Sketch() + .segment(Point2D([1, 1]), Point2D([-1, 1])) + .segment_to_point(Point2D([0, 0.5])) + .segment_to_point(Point2D([-1, -1])) + .segment_to_point(Point2D([1, -1])) + .segment_to_point(Point2D([1, 1])), + 1, + ) + top = design.extrude_sketch( + "top", Sketch(Plane(Point3D([0, 0, 1]))).box(Point2D([0.5, 0.5]), 0.1, 0.1), 0.1 + ) + body.unite(top) + + # Mirror across YZ plane + copy1 = body.copy(body.parent_component, "box2") + copy1.mirror(Plane(Point3D([2, 0, 0]), UnitVector3D([0, 0, 1]), UnitVector3D([0, 1, 0]))) + + # results from SpaceClaim + expected_vertices = [ + Point3D([5.0, -1.0, 1.0]), + Point3D([5.0, -1.0, 0.0]), Point3D([4.0, 0.5, 1.0]), Point3D([4.0, 0.5, 0.0]), Point3D([5.0, 1.0, 1.0]), @@ -2776,7 +2772,6 @@ def test_sweep_sketch(modeler: Modeler): # create a circle on the XY-plane centered at (0, 0, 0) with radius 5 path = [Circle(Point3D([0, 0, 0]), path_radius).trim(Interval(0, 2 * np.pi))] - # create the donut body body = design_sketch.sweep_sketch("donutsweep", profile, path) assert body.is_surface is False @@ -2797,6 +2792,78 @@ def test_sweep_sketch(modeler: Modeler): assert Accuracy.length_is_equal(body.volume.m, 394.7841760435743) +def test_sweep_chain(modeler: Modeler): + """Test revolving a semi-elliptical profile around a circular axis to make + a bowl. + """ + design_chain = modeler.create_design("bowl") + + radius = 10 + + # create quarter-ellipse profile with major radius = 10, minor radius = 5 + profile = [ + Ellipse( + Point3D([0, 0, radius / 2]), radius, radius / 2, reference=[1, 0, 0], axis=[0, 1, 0] + ).trim(Interval(0, np.pi / 2)) + ] + + # create circle on the plane parallel to the XY-plane but moved up by 5 units with radius 10 + path = [Circle(Point3D([0, 0, radius / 2]), radius).trim(Interval(0, 2 * np.pi))] + + # create the bowl body + body = design_chain.sweep_chain("bowlsweep", path, profile) + + assert body.is_surface is True + + # check edges + assert len(body.edges) == 1 + + # check length of edge + # compute expected circumference (circle with radius 10) + expected_edge_cirumference = 2 * np.pi * 10 + assert body.edges[0].length.m == pytest.approx(expected_edge_cirumference) + + # check faces + assert len(body.faces) == 1 + + # check area of face + # compute expected area (half a spheroid) + minor_rad = radius / 2 + e_squared = 1 - (minor_rad**2 / radius**2) + e = np.sqrt(e_squared) + expected_face_area = ( + 2 * np.pi * radius**2 + (minor_rad**2 / e) * np.pi * np.log((1 + e) / (1 - e)) + ) / 2 + assert body.faces[0].area.m == pytest.approx(expected_face_area) + + # check volume of body + # expected is 0 since it's not a closed surface + assert body.volume.m == 0 + + +def test_create_body_from_loft_profile(modeler: Modeler): + """Test the ``create_body_from_loft_profile()`` method to create a vase + shape. + """ + design_sketch = modeler.create_design("loftprofile") + + profile1 = Circle(origin=[0, 0, 0], radius=8).trim(Interval(0, 2 * np.pi)) + profile2 = Circle(origin=[0, 0, 10], radius=10).trim(Interval(0, 2 * np.pi)) + profile3 = Circle(origin=[0, 0, 20], radius=5).trim(Interval(0, 2 * np.pi)) + + # Call the method + result = design_sketch.create_body_from_loft_profile( + "vase", [[profile1], [profile2], [profile3]], False, False + ) + + # Assert that the resulting body has only one face. + assert len(result.faces) == 1 + + # check volume of body + # expected is 0 since it's not a closed surface + assert result.volume.m == 0 + + def test_revolve_sketch(modeler: Modeler): """Test revolving a circular profile for a quarter donut.""" # Initialize the donut sketch design @@ -2920,7 +2987,6 @@ def check_list_equality(lines, expected_lines): # Now, only "comp_3", "nested_2_comp_1" and "nested_1_nested_1_comp_1" # will have a body associated. # - # # Create the components comp_1 = design.add_component("Component_1") @@ -3038,3 +3104,541 @@ def check_list_equality(lines, expected_lines): " |---(body) nested_1_nested_1_comp_1_circle", ] assert check_list_equality(lines, ref) is True + + +def test_surface_body_creation(modeler: Modeler): + """Test surface body creation from trimmed surfaces.""" + design = modeler.create_design("Design1") + + # half sphere + surface = Sphere([0, 0, 0], 1) + trimmed_surface = surface.trim(BoxUV(Interval(0, np.pi * 2), Interval(0, np.pi / 2))) + body = design.create_body_from_surface("sphere", trimmed_surface) + assert len(design.bodies) == 1 + assert body.is_surface + assert body.faces[0].area.m == pytest.approx(np.pi * 2) + + # cylinder + surface = Cylinder([0, 0, 0], 1) + trimmed_surface = surface.trim(BoxUV(Interval(0, np.pi * 2), Interval(0, 1))) + body = design.create_body_from_surface("cylinder", trimmed_surface) + + assert len(design.bodies) == 2 + assert body.is_surface + assert body.faces[0].area.m == pytest.approx(np.pi * 2) + + # cone + surface = Cone([0, 0, 0], 1, np.pi / 4) + trimmed_surface = surface.trim(BoxUV(Interval(0, np.pi * 2), Interval(surface.apex.z.m, 0))) + body = design.create_body_from_surface("cone", trimmed_surface) + + assert len(design.bodies) == 3 + assert body.is_surface + assert body.faces[0].area.m == pytest.approx(4.44288293816) + + # half torus + surface = Torus([0, 0, 0], 2, 1) + trimmed_surface = surface.trim(BoxUV(Interval(0, np.pi), Interval(0, np.pi * 2))) + body = design.create_body_from_surface("torus", trimmed_surface) + + assert len(design.bodies) == 4 + assert body.is_surface + assert body.faces[0].area.m == pytest.approx(39.4784176044) + + # SOLID BODIES + + # sphere + surface = Sphere([0, 0, 0], 1) + trimmed_surface = surface.trim(BoxUV(Interval(0, np.pi * 2), Interval(-np.pi / 2, np.pi / 2))) + body = design.create_body_from_surface("sphere_solid", trimmed_surface) + assert len(design.bodies) == 5 + assert not body.is_surface + assert body.faces[0].area.m == pytest.approx(np.pi * 4) + + # torus + surface = Torus([0, 0, 0], 2, 1) + trimmed_surface = surface.trim(BoxUV(Interval(0, np.pi * 2), Interval(0, np.pi * 2))) + body = design.create_body_from_surface("torus_solid", trimmed_surface) + + assert len(design.bodies) == 6 + assert not body.is_surface + assert body.faces[0].area.m == pytest.approx(39.4784176044 * 2) + + +def test_create_surface_from_nurbs_sketch(modeler: Modeler): + """Test creating a surface from a NURBS sketch.""" + design = modeler.create_design("NURBS_Sketch_Surface") + + # Create a NURBS sketch + sketch = Sketch() + sketch.nurbs_from_2d_points( + points=[ + Point2D([0, 0]), + Point2D([1, 0]), + Point2D([1, 1]), + Point2D([0, 1]), + ], + tag="nurbs_sketch", + ) + sketch.segment( + start=Point2D([0, -1]), + end=Point2D([0, 2]), + tag="segment_1", + ) + + # Create a surface from the NURBS sketch + surface_body = design.create_surface( + name="nurbs_surface", + sketch=sketch, + ) + + assert len(design.bodies) == 1 + assert surface_body.is_surface + assert surface_body.faces[0].area.m > 0 + + +def test_design_parameters(modeler: Modeler): + """Test the design parameter's functionality.""" + design = modeler.open_file(FILES_DIR / "blockswithparameters.dsco") + test_parameters = design.parameters + + # Verify the initial parameters + assert len(test_parameters) == 2 + assert test_parameters[0].name == "p1" + assert abs(test_parameters[0].dimension_value - 0.00010872999999999981) < 1e-8 + assert test_parameters[0].dimension_type == ParameterType.DIMENSIONTYPE_AREA + + assert test_parameters[1].name == "p2" + assert abs(test_parameters[1].dimension_value - 0.0002552758322160813) < 1e-8 + assert test_parameters[1].dimension_type == ParameterType.DIMENSIONTYPE_AREA + + # Update the second parameter and verify the status + test_parameters[1].dimension_value = 0.0006 + status = design.set_parameter(test_parameters[1]) + assert status == ParameterUpdateStatus.SUCCESS + + # Attempt to update the first parameter and expect a constrained status + test_parameters[0].dimension_value = 0.0006 + status = design.set_parameter(test_parameters[0]) + assert status == ParameterUpdateStatus.CONSTRAINED_PARAMETERS + + test_parameters[0].name = "NewName" + assert test_parameters[0].name == "NewName" + + test_parameters[0].dimension_type = ParameterType.DIMENSIONTYPE_AREA + assert test_parameters[0].dimension_type == ParameterType.DIMENSIONTYPE_AREA + + +def test_cached_bodies(modeler: Modeler): + """Test that bodies are cached correctly. + + Whenever a new body is created, modified etc. we should make sure that the cache is updated. + """ + design = modeler.create_design("ModelingDemo") + + # Define a sketch + origin = Point3D([0, 0, 10]) + plane = Plane(origin, direction_x=[1, 0, 0], direction_y=[0, 1, 0]) + + # Create a sketch + sketch_box = Sketch(plane) + sketch_box.box(Point2D([20, 20]), 30 * UNITS.m, 30 * UNITS.m) + + sketch_cylinder = Sketch(plane) + sketch_cylinder.circle(Point2D([20, 20]), 5 * UNITS.m) + + design.extrude_sketch(name="BoxBody", sketch=sketch_box, distance=Distance(30, unit=UNITS.m)) + design.extrude_sketch( + name="CylinderBody", + sketch=sketch_cylinder, + distance=Distance(60, unit=UNITS.m), + ) + + my_bodies = design.bodies + my_bodies_2 = design.bodies + + # We should make sure that the object memory addresses are the same + for body1, body2 in zip(my_bodies, my_bodies_2): + assert body1 is body2 # We are comparing the memory addresses + assert id(body1) == id(body2) + + design.extrude_sketch( + name="CylinderBody2", + sketch=sketch_cylinder, + distance=Distance(20, unit=UNITS.m), + direction="-", + ) + my_bodies_3 = design.bodies + + for body1, body3 in zip(my_bodies, my_bodies_3): + assert body1 is not body3 + assert id(body1) != id(body3) + + +def test_extrude_sketch_with_cut_request(modeler: Modeler): + """Test the cut argument when performing a sketch extrusion. + + This method mimics a cut operation. + + Behind the scenes, a subtraction operation is performed on the bodies. After extruding the + sketch, the resulting body should be a cut body. + """ + # Define a sketch + origin = Point3D([0, 0, 10]) + plane = Plane(origin, direction_x=[1, 0, 0], direction_y=[0, 1, 0]) + + # Create a sketch + sketch_box = Sketch(plane) + sketch_box.box(Point2D([20, 20]), 30 * UNITS.m, 30 * UNITS.m) + + sketch_cylinder = Sketch(plane) + sketch_cylinder.circle(Point2D([20, 20]), 5 * UNITS.m) + + # Create a design + design = modeler.create_design("ExtrudeSketchWithCut") + + box_body = design.extrude_sketch( + name="BoxBody", sketch=sketch_box, distance=Distance(30, unit=UNITS.m) + ) + volume_box = box_body.volume + + design.extrude_sketch( + name="CylinderBody", sketch=sketch_cylinder, distance=Distance(60, unit=UNITS.m), cut=True + ) + + # Verify there is only one body + assert len(design.bodies) == 1 + + # Verify the volume of the resulting body is less than the volume of the box + assert design.bodies[0].volume < volume_box + + +def test_extrude_sketch_with_cut_request_no_collision(modeler: Modeler): + """Test the cut argument when performing a sketch extrusion (with no collision). + + This method mimics an unsuccessful cut operation. + + The sketch extrusion should not result in a cut body since there is no collision between the + original body and the extruded body. + """ + # Define a sketch + origin = Point3D([0, 0, 10]) + plane = Plane(origin, direction_x=[1, 0, 0], direction_y=[0, 1, 0]) + + # Create a sketch + sketch_box = Sketch(plane) + sketch_box.box(Point2D([20, 20]), 30 * UNITS.m, 30 * UNITS.m) + + sketch_cylinder = Sketch(plane) + sketch_cylinder.circle(Point2D([100, 100]), 5 * UNITS.m) + + # Create a design + design = modeler.create_design("ExtrudeSketchWithCutNoCollision") + + box_body = design.extrude_sketch( + name="BoxBody", sketch=sketch_box, distance=Distance(30, unit=UNITS.m) + ) + volume_box = box_body.volume + + design.extrude_sketch( + name="CylinderBody", sketch=sketch_cylinder, distance=Distance(60, unit=UNITS.m), cut=True + ) + + # Verify there is only one body... the cut operation should delete it + assert len(design.bodies) == 1 + + # Verify the volume of the resulting body is exactly the same + assert design.bodies[0].volume == volume_box + + +def test_create_surface_body_from_trimmed_curves(modeler: Modeler): + design = modeler.create_design("surface") + + # pill shape + circle1 = Circle(Point3D([0, 0, 0]), 1).trim(Interval(0, np.pi)) + line1 = Line(Point3D([-1, 0, 0]), UnitVector3D([0, -1, 0])).trim(Interval(0, 1)) + circle2 = Circle(Point3D([0, -1, 0]), 1).trim(Interval(np.pi, np.pi * 2)) + line2 = Line(Point3D([1, 0, 0]), UnitVector3D([0, -1, 0])).trim(Interval(0, 1)) + + body = design.create_surface_from_trimmed_curves("body", [circle1, line1, line2, circle2]) + assert body.is_surface + assert body.faces[0].area.m == pytest.approx( + Quantity(2 + np.pi, UNITS.m**2).m, rel=1e-6, abs=1e-8 + ) + + # create from edges (by getting their trimmed curves) + trimmed_curves_from_edges = [edge.shape for edge in body.edges] + body = design.create_surface_from_trimmed_curves("body2", trimmed_curves_from_edges) + assert body.is_surface + assert body.faces[0].area.m == pytest.approx( + Quantity(2 + np.pi, UNITS.m**2).m, rel=1e-6, abs=1e-8 + ) + + +def test_shell_body(modeler: Modeler): + """Test shell command.""" + design = modeler.create_design("shell") + base = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1) + + assert base.volume.m == pytest.approx(Quantity(1, UNITS.m**3).m, rel=1e-6, abs=1e-8) + assert len(base.faces) == 6 + + # shell + success = base.shell_body(0.1) + assert success + assert base.volume.m == pytest.approx(Quantity(0.728, UNITS.m**3).m, rel=1e-6, abs=1e-8) + assert len(base.faces) == 12 + + base = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1) + success = base.shell_body(-0.1) + assert success + assert base.volume.m == pytest.approx(Quantity(0.488, UNITS.m**3).m, rel=1e-6, abs=1e-8) + assert len(base.faces) == 12 + + +def test_shell_faces(modeler: Modeler): + """Test shell commands for a single face.""" + design = modeler.create_design("shell") + base = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1) + + assert base.volume.m == pytest.approx(Quantity(1, UNITS.m**3).m, rel=1e-6, abs=1e-8) + assert len(base.faces) == 6 + + # shell + success = base.remove_faces(base.faces[0], 0.1) + assert success + assert base.volume.m == pytest.approx(Quantity(0.584, UNITS.m**3).m, rel=1e-6, abs=1e-8) + assert len(base.faces) == 11 + + +def test_shell_multiple_faces(modeler: Modeler): + """Test shell commands for multiple faces.""" + design = modeler.create_design("shell") + base = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1) + + assert base.volume.m == pytest.approx(Quantity(1, UNITS.m**3).m, rel=1e-6, abs=1e-8) + assert len(base.faces) == 6 + + # shell + success = base.remove_faces([base.faces[0], base.faces[2]], 0.1) + assert success + assert base.volume.m == pytest.approx(Quantity(0.452, UNITS.m**3).m, rel=1e-6, abs=1e-8) + assert len(base.faces) == 10 + + +def test_set_face_color(modeler: Modeler): + """Test the getting and setting of face colors.""" + + design = modeler.create_design("FaceColorTest") + box = design.extrude_sketch("Body1", Sketch().box(Point2D([0, 0]), 1, 1), 1) + faces = box.faces + assert len(faces) == 6 + + # Default body color is if it is not set on server side. + assert faces[0].color == DEFAULT_COLOR + + # Set the color of the body using hex code. + faces[0].color = "#0000ffff" + assert faces[0].color == "#0000ffff" + + faces[1].color = "#ffc000ff" + assert faces[1].color == "#ffc000ff" + + # Set the color of the body using color name. + faces[2].set_color("green") + assert faces[2].color == "#008000ff" + + # Set the color of the body using RGB values between (0,1) as floats. + faces[0].set_color((1.0, 0.0, 0.0)) + assert faces[0].color == "#ff0000ff" + + # Set the color of the body using RGB values between (0,255) as integers). + faces[1].set_color((0, 255, 0)) + assert faces[1].color == "#00ff00ff" + + # Assigning color object directly + blue_color = mcolors.to_rgba("#0000FF") + faces[2].color = blue_color + assert faces[2].color == "#0000ffff" + + # Assign a color with opacity + faces[3].color = (255, 0, 0, 80) + assert faces[3].color == "#ff000050" + + # Test setting the opacity separately + faces[3].opacity = 0.8 + assert faces[3].color == "#ff0000cc" + + # Try setting the opacity to an invalid value + with pytest.raises( + ValueError, match="Invalid color value: Opacity value must be between 0 and 1." + ): + faces[3].opacity = 255 + + +def test_set_component_name(modeler: Modeler): + """Test the setting of component names.""" + + design = modeler.create_design("ComponentNameTest") + component = design.add_component("Component1") + assert component.name == "Component1" + + component.name = "ChangedComponentName" + assert component.name == "ChangedComponentName" + + +def test_get_face_bounding_box(modeler: Modeler): + """Test getting the bounding box of a face.""" + design = modeler.create_design("face_bounding_box") + body = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1) + + bounding_box = body.faces[0].bounding_box + assert bounding_box.min_corner.x.m == bounding_box.min_corner.y.m == -0.5 + assert bounding_box.max_corner.x.m == bounding_box.max_corner.y.m == 0.5 + + bounding_box = body.faces[1].bounding_box + assert bounding_box.min_corner.x.m == bounding_box.min_corner.y.m == -0.5 + assert bounding_box.max_corner.x.m == bounding_box.max_corner.y.m == 0.5 + + +def test_get_edge_bounding_box(modeler: Modeler): + """Test getting the bounding box of an edge.""" + design = modeler.create_design("edge_bounding_box") + body = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1) + + # Edge 0 goes from (-0.5, -0.5, 1) to (0.5, -0.5, 1) + bounding_box = body.edges[0].bounding_box + assert bounding_box.min_corner.x.m == bounding_box.min_corner.y.m == -0.5 + assert bounding_box.min_corner.z.m == 1 + assert bounding_box.max_corner.x.m == 0.5 + assert bounding_box.max_corner.y.m == -0.5 + assert bounding_box.max_corner.z.m == 1 + + # Test center + center = bounding_box.center + assert center.x.m == 0 + assert center.y.m == -0.5 + assert center.z.m == 1 + + +def test_get_body_bounding_box(modeler: Modeler): + """Test getting the bounding box of a body.""" + design = modeler.create_design("body_bounding_box") + body = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1) + + bounding_box = body.bounding_box + assert bounding_box.min_corner.x.m == bounding_box.min_corner.y.m == -0.5 + assert bounding_box.min_corner.z.m == 0 + assert bounding_box.max_corner.x.m == bounding_box.max_corner.y.m == 0.5 + assert bounding_box.max_corner.z.m == 1 + + # Test center + center = bounding_box.center + assert center.x.m == 0 + assert center.y.m == 0 + assert center.z.m == 0.5 + + +def test_extrude_faces_failure_log_to_file(modeler: Modeler): + """Test that the failure to extrude faces logs the correct message to a file.""" + # Create a design and body for testing + design = modeler.create_design("test_design") + body = design.extrude_sketch("test_body", Sketch().box(Point2D([0, 0]), 1, 1), 1) + + # Call the method with invalid parameters to trigger failure + result = modeler.geometry_commands.extrude_faces( + faces=[body.faces[0]], + distance=-10.0, # Invalid distance to trigger failure + direction=UnitVector3D([0, 0, 1]), + ) + # Assert the result is an empty list + assert result == [] + + result = modeler.geometry_commands.extrude_faces_up_to( + faces=[body.faces[0]], + up_to_selection=body.faces[0], # Using the same face as target to trigger failure + direction=UnitVector3D([0, 0, 1]), + seed_point=Point3D([0, 0, 0]), + ) + # Assert the result is an empty list + assert result == [] + + +def test_extrude_edges_missing_parameters(modeler: Modeler): + """Test that extrude_edges raises a ValueError when required parameters are missing.""" + # Create a design and body for testing + design = modeler.create_design("test_design") + body = design.extrude_sketch("test_body", Sketch().box(Point2D([0, 0]), 1, 1), 1) + + # Test case: Missing both `from_face` and `from_point`/`direction` + with pytest.raises( + ValueError, + match="To extrude edges, either a face or a direction and point must be provided.", + ): + modeler.geometry_commands.extrude_edges( + edges=[body.edges[0]], # Using the first edge of the body + distance=10.0, + from_face=None, + from_point=None, + direction=None, + ) + + +def test_import_component_named_selections(modeler: Modeler): + """Test importing named selections from an inserted design component.""" + # This file had a component inserted into it that has named selections that we need to import + design = modeler.open_file(Path(FILES_DIR, "import_component_groups.scdocx")) + component = design.components[0] + + assert len(design.named_selections) == 0 + component.import_named_selections() + assert len(design.named_selections) == 3 + + +def test_component_make_independent(modeler: Modeler): + """Test making components independent.""" + + design = modeler.open_file(Path(FILES_DIR, "cars.scdocx")) + face = next((ns for ns in design.named_selections if ns.name == "to_pull"), None).faces[0] + comp = next( + (ns for ns in design.named_selections if ns.name == "make_independent"), None + ).components[0] + + comp.make_independent() + + assert Accuracy.length_is_equal(comp.bodies[0].volume.m, face.body.volume.m) + + modeler.geometry_commands.extrude_faces(face, 1) + comp = design.components[0].components[-1].components[-1] # stale from update-design-in-place + + assert not Accuracy.length_is_equal(comp.bodies[0].volume.m, face.body.volume.m) + + +def test_write_body_facets_on_save(modeler: Modeler, tmp_path_factory: pytest.TempPathFactory): + design = modeler.open_file(Path(FILES_DIR, "cars.scdocx")) + + # First file without body facets + filepath_no_facets = tmp_path_factory.mktemp("test_design") / "cars_no_facets.scdocx" + design.download(filepath_no_facets) + + # Second file with body facets + filepath_with_facets = tmp_path_factory.mktemp("test_design") / "cars_with_facets.scdocx" + design.download(filepath_with_facets, write_body_facets=True) + + # Compare file sizes + size_no_facets = filepath_no_facets.stat().st_size + size_with_facets = filepath_with_facets.stat().st_size + + assert size_with_facets > size_no_facets + + # Ensure facets.bin and renderlist.xml files exist + with zipfile.ZipFile(filepath_with_facets, "r") as zip_ref: + namelist = set(zip_ref.namelist()) + + expected_files = { + "SpaceClaim/Graphics/facets.bin", + "SpaceClaim/Graphics/renderlist.xml", + } + + missing = expected_files - namelist + assert not missing \ No newline at end of file From f03eaccd8a8ca530f8ac7f697b8df49954ca402c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 14:55:11 +0000 Subject: [PATCH 6/6] chore: auto fixes from pre-commit hooks --- tests/integration/test_design.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/test_design.py b/tests/integration/test_design.py index a8266e7c01..9c56cb3957 100644 --- a/tests/integration/test_design.py +++ b/tests/integration/test_design.py @@ -3641,4 +3641,4 @@ def test_write_body_facets_on_save(modeler: Modeler, tmp_path_factory: pytest.Te } missing = expected_files - namelist - assert not missing \ No newline at end of file + assert not missing