def test_can_retrieve_list_of_vertices_for_each_face(nexus_wrapper): # Reverse process of test_can_record_list_of_vertices_for_each_face component = add_component_to_file(nexus_wrapper) shape = OFFGeometryNoNexus( [QVector3D(0.0, 0.0, 1.0), QVector3D(0.0, 1.0, 0.0), QVector3D(0.0, 0.0, 0.0)], [[0, 1, 2]], ) component.set_off_shape(shape) test_input_flat_list_of_vertex_indices = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] # Define there are three faces, the difference in starting index indicates there are three vertices # in the first face (triangle), four in the second (square), and five in the third face (pentagon) test_input_start_index_of_each_face = [0, 3, 7] expected_output_vertex_indices_split_by_face = [ [0, 1, 2], [3, 4, 5, 6], [7, 8, 9, 10, 11], ] nexus_shape, _ = component.shape nexus_wrapper.set_field_value( nexus_shape.group, "winding_order", test_input_flat_list_of_vertex_indices ) nexus_wrapper.set_field_value( nexus_shape.group, "faces", test_input_start_index_of_each_face ) assert nexus_shape.faces == expected_output_vertex_indices_split_by_face
def _load_stl_geometry( file: StringIO, mult_factor: float, geometry: OFFGeometry = OFFGeometryNoNexus() ) -> OFFGeometry: """ Loads geometry from an STL file into an OFFGeometry instance. :param file: The file containing an STL geometry. :param mult_factor: The multiplication factor for unit conversion. :param geometry: The optional OFFGeometry to load the STL data into. If not provided, a new instance will be returned. :return: An OFFGeometry instance containing that file's geometry. """ mesh_data = mesh.Mesh.from_file("", fh=file, calculate_normals=False) # numpy-stl loads numbers as python decimals, not floats, which aren't valid in json geometry.vertices = [ QVector3D( float(corner[0]) * mult_factor, float(corner[1]) * mult_factor, float(corner[2]) * mult_factor, ) for triangle in mesh_data.vectors for corner in triangle ] geometry.faces = [[i * 3, (i * 3) + 1, (i * 3) + 2] for i in range(len(mesh_data.vectors))] logging.info("STL loaded") return geometry
def test_can_set_off_geometry_properties(nexus_wrapper): component = add_component_to_file(nexus_wrapper) vertices = [ QVector3D(0.0, 0.0, 1.0), QVector3D(0.0, 1.0, 0.0), QVector3D(0.0, 0.0, 0.0), QVector3D(0.0, 1.0, 1.0), ] faces = [[0, 1, 2, 3]] shape = OFFGeometryNoNexus(vertices, faces) component.set_off_shape(shape) nexus_shape, _ = component.shape vertex_2_x = 0.5 vertex_2_y = -0.5 vertex_2_z = 0 new_vertices = [ QVector3D(-0.5, -0.5, 0), QVector3D(0, 0.5, 0), QVector3D(vertex_2_x, vertex_2_y, vertex_2_z), ] triangle = [0, 1, 2] new_faces = [triangle] nexus_shape.vertices = new_vertices nexus_shape.faces = new_faces assert nexus_shape.faces == new_faces assert nexus_shape.vertices[2].x() == approx(vertex_2_x) assert nexus_shape.vertices[2].y() == approx(vertex_2_y) assert nexus_shape.vertices[2].z() == approx(vertex_2_z)
def test_can_record_list_of_vertices_for_each_face(nexus_wrapper): # Reverse process of test_can_retrieve_list_of_vertices_for_each_face component = add_component_to_file(nexus_wrapper) shape = OFFGeometryNoNexus( [QVector3D(0.0, 0.0, 1.0), QVector3D(0.0, 1.0, 0.0), QVector3D(0.0, 0.0, 0.0)], [[0, 1, 2]], ) component.set_off_shape(shape) nexus_shape, _ = component.shape test_input_vertex_indices_split_by_face = [ [0, 1, 2], [3, 4, 5, 6], [7, 8, 9, 10, 11], ] record_faces_in_file( nexus_wrapper, nexus_shape.group, test_input_vertex_indices_split_by_face ) expected_output_flat_list_of_vertex_indices = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] expected_output_start_index_of_each_face = [0, 3, 7] flat_list_of_vertex_indices = nexus_shape.group["winding_order"][...].tolist() start_index_of_each_face = nexus_shape.group["faces"][...].tolist() assert flat_list_of_vertex_indices == expected_output_flat_list_of_vertex_indices assert start_index_of_each_face == expected_output_start_index_of_each_face
def load_geometry_from_file_object( file: StringIO, extension: str, units: str, geometry: OFFGeometry = OFFGeometryNoNexus(), ) -> OFFGeometry: """ Loads geometry from a file object into an OFFGeometry instance Supported file types are OFF and STL. :param file: The file object to load the geometry from. :param units: A unit of length in the form of a string. Used to determine the multiplication factor. :param geometry: The optional OFFGeometry to load the geometry data into. If not provided, a new instance will be returned. :return: An OFFGeometry instance containing that file's geometry, or an empty instance if filename's extension is unsupported. """ mult_factor = calculate_unit_conversion_factor(units, METRES) if extension == ".off": _load_off_geometry(file, mult_factor, geometry) elif extension == ".stl": _load_stl_geometry(file, mult_factor, geometry) else: geometry.faces = [] geometry.vertices = [] logging.error("geometry file extension not supported") return geometry
def test_can_get_off_geometry_properties(nexus_wrapper): component = add_component_to_file(nexus_wrapper) vertex_3_x = 0.0 vertex_3_y = 1.0 vertex_3_z = 1.0 vertices = [ QVector3D(0, 0, 1), QVector3D(0, 1, 0), QVector3D(0, 0, 0), QVector3D(vertex_3_x, vertex_3_y, vertex_3_z), ] faces = [[0, 1, 2, 3]] shape = OFFGeometryNoNexus(vertices, faces) component.set_off_shape(shape) nexus_shape, _ = component.shape assert isinstance(nexus_shape, OFFGeometryNexus) assert nexus_shape.faces == faces assert nexus_shape.vertices[3].x() == approx(vertex_3_x) assert nexus_shape.vertices[3].y() == approx(vertex_3_y) assert nexus_shape.vertices[3].z() == approx(vertex_3_z)
def test_GIVEN_pixel_grid_WHEN_setting_off_geometry_shape_THEN_off_geometry_is_not_called_with_pixel_data( component, ): pixel_grid = PixelGrid() off_geometry = OFFGeometryNoNexus(vertices=[], faces=[]) units = "m" filename = "somefile.off" with patch( "nexus_constructor.component.component.OFFGeometryNexus" ) as mock_off_geometry_constructor: component.set_off_shape( loaded_geometry=off_geometry, units=units, filename=filename, pixel_data=pixel_grid, ) mock_off_geometry_constructor.assert_called_once_with( component.file, component.group[PIXEL_SHAPE_GROUP_NAME], units, filename, None, )
def load_geometry( filename: str, units: str, geometry: OFFGeometry = OFFGeometryNoNexus()) -> OFFGeometry: """ Loads geometry from a file into an OFFGeometry instance Supported file types are OFF and STL. :param filename: The name of the file to open. :param units: A unit of length in the form of a string. Used to determine the multiplication factor. :param geometry: The optional OFFGeometry to load the geometry data into. If not provided, a new instance will be returned. :return: An OFFGeometry instance containing that file's geometry, or an empty instance if filename's extension is unsupported. """ extension = filename[filename.rfind("."):].lower() try: with open(filename) as file: return load_geometry_from_file_object(file, extension, units, geometry) except UnicodeDecodeError: # Try again in case the file is in binary. At least one of these should work when a user selects a file because # GeometryFileValidator inspects the file beforehand to check that it's valid. with open(filename, "rb") as file: return load_geometry_from_file_object(file, extension, units, geometry)
def create_disk_chopper_geometry(self) -> OFFGeometryNoNexus: """ Create the string that stores all the information needed in the OFF file. """ self.convert_chopper_details_to_off() # Add the point information to the string vertices = [point.point_to_qvector3d() for point in self.points] return OFFGeometryNoNexus(vertices, self.faces)
def test_GIVEN_off_properties_WHEN_setting_off_geometry_shape_THEN_shape_group_has_class_nxoff_geometry( component, ): off_geometry = OFFGeometryNoNexus(vertices=[], faces=[]) with patch("nexus_constructor.component.component.OFFGeometryNexus"): component.set_off_shape(loaded_geometry=off_geometry) assert ( component.group[SHAPE_GROUP_NAME].attrs["NX_class"] == OFF_GEOMETRY_NEXUS_NAME )
def get_dummy_OFF(): # A square with a triangle on the side original_vertices = [ QVector3D(0, 0, 0), QVector3D(0, 1, 0), QVector3D(1, 1, 0), QVector3D(1, 0, 0), QVector3D(1.5, 0.5, 0), ] original_faces = [[0, 1, 2, 3], [2, 3, 4]] return OFFGeometryNoNexus(vertices=original_vertices, faces=original_faces)
def test_GIVEN_geometry_WHEN_creating_off_mesh_THEN_geometry_contains_original_geometry( ): off_output = OFFGeometryNoNexus( vertices=[QVector3D(0, 0, 0), QVector3D(0, 1, 0), QVector3D(1, 1, 0)], faces=[[0, 1, 2]], ) off_mesh = OffMesh(off_output, None) assert off_mesh.geometry().vertex_count == VERTICES_IN_TRIANGLE
def test_GIVEN_a_triangle_WHEN_creating_off_geometry_with_no_pixel_data_THEN_vertex_count_equals_3( ): off_geometry = OFFGeometryNoNexus( vertices=[QVector3D(0, 0, 0), QVector3D(0, 1, 0), QVector3D(1, 1, 0)], faces=[[0, 1, 2]], ) qt_geometry = QtOFFGeometry(off_geometry, None) assert qt_geometry.vertex_count == 3
def test_GIVEN_faces_WHEN_calling_winding_order_on_OFF_THEN_order_is_correct(): vertices = [ QVector3D(0, 0, 1), QVector3D(0, 1, 0), QVector3D(0, 0, 0), QVector3D(0, 1, 1), ] faces = [[0, 1, 2, 3]] geom = OFFGeometryNoNexus(vertices, faces) expected = [point for face in faces for point in face] assert expected == geom.winding_order
def test_GIVEN_off_geometry_WHEN_calling_off_geometry_on_offGeometry_THEN_original_geometry_is_returned(): vertices = [ QVector3D(0, 0, 1), QVector3D(0, 1, 0), QVector3D(0, 0, 0), QVector3D(0, 1, 1), ] faces = [[0, 1, 2, 3]] geom = OFFGeometryNoNexus(vertices, faces) assert geom.faces == faces assert geom.vertices == vertices assert geom.off_geometry == geom
def test_GIVEN_faces_WHEN_calling_winding_order_indices_on_OFF_THEN_order_is_correct(): vertices = [ QVector3D(0, 0, 1), QVector3D(0, 1, 0), QVector3D(0, 0, 0), QVector3D(0, 1, 1), ] faces = [[0, 1, 2, 3]] geom = OFFGeometryNoNexus(vertices, faces) expected = [0] # only one face assert expected == geom.winding_order_indices
def test_can_override_existing_shape(nexus_wrapper): component = add_component_to_file(nexus_wrapper, "some_field", 42, "component_name") component.set_cylinder_shape() cylinder, _ = component.shape assert isinstance( cylinder, CylindricalGeometry ), "Expect shape to initially be a cylinder" vertices = [QVector3D(-0.5, -0.5, 0), QVector3D(0, 0.5, 0), QVector3D(0.5, -0.5, 0)] faces = [[0, 1, 2]] input_mesh = OFFGeometryNoNexus(vertices, faces) component.set_off_shape(input_mesh) output_mesh, _ = component.shape assert isinstance(output_mesh, OFFGeometryNexus), "Expect shape to now be a mesh"
def test_GIVEN_component_with_off_shape_information_WHEN_duplicating_component_THEN_shape_information_is_stored_in_nexus_file( ): wrapper = NexusWrapper("test_duplicate_off_shape") instrument = Instrument(wrapper, NX_CLASS_DEFINITIONS) first_component_name = "component1" first_component_nx_class = "NXdetector" description = "desc" first_component = instrument.create_component(first_component_name, first_component_nx_class, description) vertices = [ QVector3D(-0.5, -0.5, 0.5), QVector3D(0.5, -0.5, 0.5), QVector3D(-0.5, 0.5, 0.5), QVector3D(0.5, 0.5, 0.5), QVector3D(-0.5, 0.5, -0.5), QVector3D(0.5, 0.5, -0.5), QVector3D(-0.5, -0.5, -0.5), QVector3D(0.5, -0.5, -0.5), ] faces = [ [0, 1, 3, 2], [2, 3, 5, 4], [4, 5, 7, 6], [6, 7, 1, 0], [1, 7, 5, 3], [6, 0, 2, 4], ] first_component.set_off_shape( OFFGeometryNoNexus(vertices=vertices, faces=faces)) tree_model = ComponentTreeModel(instrument) first_component_index = tree_model.index(0, 0, QModelIndex()) tree_model.duplicate_node(first_component_index) assert tree_model.rowCount(QModelIndex()) == 3 second_component_index = tree_model.index(2, 0, QModelIndex()) second_component = second_component_index.internalPointer() second_shape, _ = second_component.shape assert second_shape.vertices == vertices assert second_shape.faces == faces
def test_can_get_cad_file_units_from_model_when_already_in_model(nexus_wrapper): component = add_component_to_file(nexus_wrapper, "some_field", 42, "component_name") vertices = [ QVector3D(-0.5, -0.5, 0), QVector3D(0, 0.5, 0), QVector3D(0.5, -0.05, 0), ] triangle = [0, 1, 2] faces = [triangle] input_mesh = OFFGeometryNoNexus(vertices, faces) component.set_off_shape(input_mesh) units = "m" component.group["shape"]["cad_file_units"] = units output_mesh, _ = component.shape assert isinstance(output_mesh, OFFGeometryNexus) assert output_mesh.units == units assert output_mesh.group["cad_file_units"][()] == units
def test_setting_cad_path_through_shape_persists_in_file(nexus_wrapper): component = add_component_to_file(nexus_wrapper, "some_field", 42, "component_name") vertex_2_x = 0.5 vertex_2_y = -0.5 vertex_2_z = 0 vertices = [ QVector3D(-0.5, -0.5, 0), QVector3D(0, 0.5, 0), QVector3D(vertex_2_x, vertex_2_y, vertex_2_z), ] triangle = [0, 1, 2] faces = [triangle] input_mesh = OFFGeometryNoNexus(vertices, faces) component.set_off_shape(input_mesh) filepath = "/home/asdf/teapot.off" output_mesh, _ = component.shape output_mesh.file_path = filepath assert component.group["shape"]["cad_file_path"][()] == filepath
def test_no_cad_path_returns_none_when_getting_cad_path_from_off_geometry( nexus_wrapper, ): component = add_component_to_file(nexus_wrapper, "some_field", 42, "component_name") vertex_2_x = 0.5 vertex_2_y = -0.5 vertex_2_z = 0 vertices = [ QVector3D(-0.5, -0.5, 0), QVector3D(0, 0.5, 0), QVector3D(vertex_2_x, vertex_2_y, vertex_2_z), ] triangle = [0, 1, 2] faces = [triangle] input_mesh = OFFGeometryNoNexus(vertices, faces) component.set_off_shape(input_mesh) output_mesh, _ = component.shape assert output_mesh.file_path is None
def generate_geometry_model( self, component: Component, pixel_data: PixelData = None ): """ Generates a geometry model depending on the type of geometry selected and the current values of the line edits that apply to the particular geometry type. :return: The generated model. """ if self.CylinderRadioButton.isChecked(): component.set_cylinder_shape( QVector3D( self.cylinderXLineEdit.value(), self.cylinderYLineEdit.value(), self.cylinderZLineEdit.value(), ), self.cylinderHeightLineEdit.value(), self.cylinderRadiusLineEdit.value(), self.unitsLineEdit.text(), pixel_data=pixel_data, ) elif self.meshRadioButton.isChecked(): mesh_geometry = OFFGeometryNoNexus() geometry_model = load_geometry( self.cad_file_name, self.unitsLineEdit.text(), mesh_geometry ) # Units have already been used during loading the file, but we store them and file name # so we can repopulate their fields in the edit component window geometry_model.units = self.unitsLineEdit.text() geometry_model.file_path = self.cad_file_name component.set_off_shape( geometry_model, units=self.unitsLineEdit.text(), filename=self.fileLineEdit.text(), pixel_data=pixel_data, )
def test_can_add_mesh_shape_to_and_component_and_get_the_same_shape_back(nexus_wrapper): component = add_component_to_file(nexus_wrapper, "some_field", 42, "component_name") # Our test input mesh is a single triangle vertex_2_x = 0.5 vertex_2_y = -0.5 vertex_2_z = 0 vertices = [ QVector3D(-0.5, -0.5, 0), QVector3D(0, 0.5, 0), QVector3D(vertex_2_x, vertex_2_y, vertex_2_z), ] triangle = [0, 1, 2] faces = [triangle] input_mesh = OFFGeometryNoNexus(vertices, faces) component.set_off_shape(input_mesh) output_mesh, _ = component.shape assert isinstance(output_mesh, OFFGeometryNexus) assert output_mesh.faces[0] == triangle assert output_mesh.vertices[2].x() == approx(vertex_2_x) assert output_mesh.vertices[2].y() == approx(vertex_2_y) assert output_mesh.vertices[2].z() == approx(vertex_2_z)
def _load_off_geometry( file: StringIO, mult_factor: float, geometry: OFFGeometry = OFFGeometryNoNexus() ) -> OFFGeometry: """ Loads geometry from an OFF file into an OFFGeometry instance. :param file: The file containing an OFF geometry. :param mult_factor: The multiplication factor for unit conversion. :param geometry: The optional OFFGeometry to load the OFF data into. If not provided, a new instance will be returned. :return: An OFFGeometry instance containing that file's geometry. """ vertices, faces = parse_off_file(file) geometry.vertices = [ QVector3D(x * mult_factor, y * mult_factor, z * mult_factor) for x, y, z in (vertex for vertex in vertices) ] geometry.faces = [face.tolist()[1:] for face in faces] logging.info("OFF loaded") return geometry
def test_GIVEN_nothing_WHEN_constructing_OFFGeometry_THEN_geometry_str_is_OFF(): geom = OFFGeometryNoNexus() assert geom.geometry_str == "OFF"
def test_GIVEN_off_file_containing_geometry_WHEN_loading_geometry_to_file_THEN_vertices_and_faces_loaded_are_the_same_as_the_file( ): model = OFFGeometryNoNexus() model.units = "m" off_file = ("OFF\n" "# cube.off\n" "# A cube\n" "8 6 0\n" "-0.500000 -0.500000 0.500000\n" "0.500000 -0.500000 0.500000\n" "-0.500000 0.500000 0.500000\n" "0.500000 0.500000 0.500000\n" "-0.500000 0.500000 -0.500000\n" "0.500000 0.500000 -0.500000\n" "-0.500000 -0.500000 -0.500000\n" "0.500000 -0.500000 -0.500000\n" "4 0 1 3 2\n" "4 2 3 5 4\n" "4 4 5 7 6\n" "4 6 7 1 0\n" "4 1 7 5 3\n" "4 6 0 2 4\n") load_geometry_from_file_object(StringIO(off_file), ".off", model.units, model) assert model.vertices == [ QVector3D(-0.5, -0.5, 0.5), QVector3D(0.5, -0.5, 0.5), QVector3D(-0.5, 0.5, 0.5), QVector3D(0.5, 0.5, 0.5), QVector3D(-0.5, 0.5, -0.5), QVector3D(0.5, 0.5, -0.5), QVector3D(-0.5, -0.5, -0.5), QVector3D(0.5, -0.5, -0.5), ] assert model.faces == [ [0, 1, 3, 2], [2, 3, 5, 4], [4, 5, 7, 6], [6, 7, 1, 0], [1, 7, 5, 3], [6, 0, 2, 4], ] assert model.winding_order == [ 0, 1, 3, 2, 2, 3, 5, 4, 4, 5, 7, 6, 6, 7, 1, 0, 1, 7, 5, 3, 6, 0, 2, 4, ] assert model.winding_order_indices == [0, 4, 8, 12, 16, 20]