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_remove_from_beginning_2(nexus_wrapper): component1 = add_component_to_file(nexus_wrapper, "field", 42, "component1") rot1 = component1.add_rotation(QVector3D(1.0, 0.0, 0.0), 90.0) rot2 = component1.add_rotation(QVector3D(1.0, 0.0, 0.0), 90.0) component1.depends_on = rot1 rot1.depends_on = rot2 assert len(rot2.get_dependents()) == 1 rot1.remove_from_dependee_chain() assert len(rot2.get_dependents()) == 1 assert rot2.get_dependents()[0] == component1 assert component1.depends_on == rot2
def showAll(self): self.rootEntity = Qt3DCore.QEntity() self.materialSquare = Qt3DExtras.QPhongMaterial(self.rootEntity) self.materialSphere = Qt3DExtras.QPhongMaterial(self.rootEntity) # self.material = Qt3DExtras.QPhongMaterial(self.rootEntity) with open('local2.csv', 'r') as file: reader = csv.reader(file) for row in reader: if row[0] == 's': self.sphereEntity = Qt3DCore.QEntity(self.rootEntity) self.sphereMesh = Qt3DExtras.QSphereMesh() self.name = row[1] self.materialSphere.setAmbient( QColor(int(row[2]), int(row[3]), int(row[4]), int(row[5]))) self.sphereMesh.setRadius(int(row[6])) self.QTransformSphere = Qt3DCore.QTransform() self.sphereEntity.addComponent(self.sphereMesh) self.sphereEntity.addComponent(self.materialSphere) self.sphereEntity.addComponent(self.QTransformSphere) sphereVector3D = QVector3D() sphereVector3D.setX(int(row[7])) sphereVector3D.setY(int(row[8])) sphereVector3D.setZ(int(row[9])) self.QTransformSphere.setTranslation(sphereVector3D) else: self.squareEntity = Qt3DCore.QEntity(self.rootEntity) self.squareMesh = Qt3DExtras.QCuboidMesh() self.name = row[1] self.materialSquare.setAmbient( QColor(int(row[2]), int(row[3]), int(row[4]), int(row[5]))) self.squareMesh.setXExtent(int(row[6])) self.squareMesh.setYExtent(int(row[7])) self.squareMesh.setZExtent(int(row[8])) self.QTransformSquare = Qt3DCore.QTransform() self.squareEntity.addComponent(self.squareMesh) self.squareEntity.addComponent(self.materialSquare) self.squareEntity.addComponent(self.QTransformSquare) squareVector3D = QVector3D() squareVector3D.setX(int(row[9])) squareVector3D.setY(int(row[10])) squareVector3D.setZ(int(row[11])) self.QTransformSquare.setTranslation(squareVector3D) self.QTransformSquare.setRotationX(int(row[12])) self.QTransformSquare.setRotationY(int(row[13])) self.QTransformSquare.setRotationZ(int(row[14]))
def test_has_link_2(nexus_wrapper): component1 = add_component_to_file(nexus_wrapper, "field", 42, "component1") rot1 = component1.add_rotation(QVector3D(1.0, 0.0, 0.0), 90.0) component1.depends_on = rot1 component2 = add_component_to_file(nexus_wrapper, "field", 42, "component2") rot2 = component2.add_rotation(QVector3D(1.0, 0.0, 0.0), 90.0) component2.depends_on = rot2 rot1.depends_on = rot2 new_component = Component(component1.file, component1.group) assert new_component.transforms.has_link
def calculateModel(self): translation = QMatrix4x4() translation.translate(self.translationVec) scale = QMatrix4x4() scale.scale(self.scaleVec) rotation = QMatrix4x4() rotation.rotate(self.rotationVec.y(), QVector3D(0, 1, 0)) rotation.rotate(self.rotationVec.x(), QVector3D(1, 0, 0)) rotation.rotate(self.rotationVec.z(), QVector3D(0, 0, 1)) return translation * rotation * scale
def __init__(self): self.__modelMatrix = QMatrix4x4() self.__vertices = vertices self.__indices = indices self.__shader = None self.__vertexArray = None self.__indexBuffer = None self.__vertexBuffer = None self.__translate = QVector3D() self.__scale = QVector3D(1.0, 0.0, 1.0) self.initObject()
def test_GIVEN_a_square_WHEN_creating_vertex_buffer_THEN_length_is_correct(): vertices = [ QVector3D(0, 0, 0), QVector3D(1, 0, 0), QVector3D(0, 1, 0), QVector3D(1, 1, 0), ] faces = [[0, 1, 2, 3]] vertex_buffer = create_vertex_buffer(vertices, faces) assert (len(list(vertex_buffer)) == TRIANGLES_IN_SQUARE * VERTICES_IN_TRIANGLE * POINTS_IN_VERTEX)
def test_GIVEN_cylinder_with_height_of_zero_WHEN_getting_axis_direction_THEN_default_value_is_returned( component, nexus_wrapper ): axis_x = 1.0 axis_y = 0.0 axis_z = 0.0 axis = QVector3D(axis_x, axis_y, axis_z) height = 0 radius = 37.0 component.set_cylinder_shape(axis, height, radius) assert component.shape[0].axis_direction == QVector3D(0, 0, 1)
def set_beam_transform(cylinder_transform, neutron_animation_distance): """ Configures the transform for the beam cylinder by giving it a matrix. The matrix will turn the cylinder sideways and then move it "backwards" in the z-direction by 20 units so that it ends at the location of the sample. :param cylinder_transform: A QTransform object. :param neutron_animation_distance: The distance that the neutron travels during its animation. """ cylinder_matrix = QMatrix4x4() cylinder_matrix.rotate(90, QVector3D(1, 0, 0)) cylinder_matrix.translate( QVector3D(0, neutron_animation_distance * 0.5, 0)) cylinder_transform.setMatrix(cylinder_matrix)
def set_parameters_from_dict(self, parameters): self.set_unit_of_measurement(parameters['unit_of_measurements']) position = QVector3D(parameters['position'][0], parameters['position'][1], parameters['position'][2]) self.set_position(position) rotation = QVector3D(parameters['rotation'][0], parameters['rotation'][1], parameters['rotation'][2]) self.set_rotation(rotation) scale = QVector3D(parameters['scale'][0], parameters['scale'][1], parameters['scale'][2]) self.set_scale(scale)
def load_text_stl(filename, swap_yz=False, test=True): fp = open(filename, 'r') number_of_triangles = 0 normals_list = [] vertices_list = [] is_bbox_defined = False bbox_min = None bbox_max = None try: for line in fp.readlines(): words = line.split() if len(words) > 0: if words[0] == 'facet': number_of_triangles += 1 v = [float(words[2]), float(words[3]), float(words[4])] if swap_yz: v = [-v[0], v[2], v[1]] normals_list.append(v) normals_list.append(v) normals_list.append(v) if words[0] == 'vertex': v = [float(words[1]), float(words[2]), float(words[3])] if swap_yz: v = [-v[0], v[2], v[1]] vertices_list.append(v) q_v = QVector3D(v[0], v[1], v[2]) if is_bbox_defined: min_temp = np.minimum(bbox_min.toTuple(), v) max_temp = np.maximum(bbox_max.toTuple(), v) bbox_min = QVector3D(min_temp[0], min_temp[1], min_temp[2]) bbox_max = QVector3D(max_temp[0], max_temp[1], max_temp[2]) else: bbox_max = q_v bbox_min = q_v is_bbox_defined = True except Exception as e: fp.close() return False, vertices_list, normals_list, bbox_min, bbox_max fp.close() # bbox recentering around origin bbox_center = 0.5 * (bbox_min + bbox_max) for idx in range(len(vertices_list)): vertices_list[idx] = vertices_list[idx] - np.array( bbox_center.toTuple()) bbox_min = bbox_min - bbox_center bbox_max = bbox_max - bbox_center vertices_list = np.array(vertices_list, dtype=np.float32).ravel() normals_list = np.array(normals_list, dtype=np.float32).ravel() return True, vertices_list, normals_list, bbox_min, bbox_max
def __init__(self): super(Window, self).__init__() # Default set-up self.rootEntity = Kuesa.SceneEntity() self.rootEntity.addComponent(DefaultEnvMap(self.rootEntity)) self.camera().setPosition(QVector3D(5, 1.5, 5)) self.camera().setViewCenter(QVector3D(0, 0.5, 0)) self.camera().setUpVector(QVector3D(0, 1, 0)) self.camera().setAspectRatio(16. / 9.) self.camController = Qt3DExtras.QOrbitCameraController(self.rootEntity) self.camController.setCamera(self.camera()) self.fg = Kuesa.ForwardRenderer() self.fg.setCamera(self.camera()) self.fg.setClearColor("white") self.setActiveFrameGraph(self.fg) # Load a glTF model self.gltfImporter = Kuesa.GLTF2Importer(self.rootEntity) self.gltfImporter.setSceneEntity(self.rootEntity) self.gltfImporter.setSource(assetsUrl() + "/models/duck/Duck.glb") self.gltfImporter.statusChanged.connect(self.importerLoaded) # Skybox creation envmap_root = assetsUrl() + "/envmaps/pink_sunrise" envmap_name = "pink_sunrise" + ("_16f" if platform.system() == "Darwin" else "") + "_radiance" self.skybox = Kuesa.Skybox(self.rootEntity) self.skybox.setBaseName(envmap_root + "/" + envmap_name) self.skybox.setExtension(".dds") # Creation of a few post-processing effects self.blurFx = Kuesa.GaussianBlurEffect() self.blurFx.setBlurPassCount(8) self.dofFx = Kuesa.DepthOfFieldEffect() self.dofFx.setFocusRange(3.1) self.dofFx.setRadius(21.) self.dofFx.setFocusDistance(6.6) self.threshFx = Kuesa.ThresholdEffect() self.threshFx.setThreshold(.1) # self.fg.addPostProcessingEffect(self.blurFx) # self.fg.addPostProcessingEffect(self.dofFx) self.fg.addPostProcessingEffect(self.threshFx) self.setRootEntity(self.rootEntity)
def test_deleting_a_transformation_which_the_component_indirectly_depends_on_is_not_allowed( nexus_wrapper, ): component = add_component_to_file(nexus_wrapper, "some_field", 42, "component_name") first_transform = component.add_rotation(QVector3D(1.0, 0.0, 0.0), 90.0) second_transform = component.add_translation( QVector3D(1.0, 0.0, 0.0), depends_on=first_transform ) component.depends_on = second_transform with pytest.raises(DependencyError): assert component.remove_transformation( first_transform ), "Expected not to be allowed to delete the transform as the component indirectly depends on it"
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 make_rotation_matrix(cls, axis: str, angle: float): "make rotation matrix with given axis" ax = axis.lower() mat = QMatrix4x4() mat.setToIdentity() if ax == "x": mat.rotate(angle, QVector3D(1.0, 0.0, 0.0)) elif ax == "y": mat.rotate(angle, QVector3D(0.0, 1.0, 0.0)) elif ax == "z": mat.rotate(angle, QVector3D(0.0, 0.0, 1.0)) else: raise ValueError("Unknown axis: " + axis + ". x, y, z available") return mat
def test_does_not_link_back_4(nexus_wrapper): component1 = add_component_to_file(nexus_wrapper, "field", 42, "component1") rot2 = component1.add_rotation(QVector3D(1.0, 0.0, 0.0), 90.0) component1.depends_on = rot2 component2 = add_component_to_file(nexus_wrapper, "field", 42, "component2") component3 = add_component_to_file(nexus_wrapper, "field", 42, "component3") rot1 = component3.add_rotation(QVector3D(1.0, 0.0, 0.0), 90.0) component3.depends_on = rot1 component2.transforms.link.linked_component = component3 assert not links_back_to_component(component1, component2)
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 extrude(self, x1, y1, x2, y2): n = QVector3D.normal(QVector3D(0, 0, -0.1), QVector3D(x2 - x1, y2 - y1, 0)) self.add(QVector3D(x1, y1, 0.05), n) self.add(QVector3D(x1, y1, -0.05), n) self.add(QVector3D(x2, y2, 0.05), n) self.add(QVector3D(x2, y2, -0.05), n) self.add(QVector3D(x2, y2, 0.05), n) self.add(QVector3D(x1, y1, -0.05), n)
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 update_vectors(self): "override base class" yawRadian = math.radians(self.yaw) yawCos = math.cos(yawRadian) pitchRadian = math.radians(self.pitch) pitchCos = math.cos(pitchRadian) frontX = yawCos * pitchCos frontY = math.sin(pitchRadian) frontZ = math.sin(yawRadian) * pitchCos self.front = QVector3D(frontX, frontY, frontZ) self.front.normalize() self.right = QVector3D.crossProduct(self.front, self.worldUp) self.right.normalize() self.up = QVector3D.crossProduct(self.right, self.front) self.up.normalize()
def __init__(self): "" super().__init__() # Camera attributes self.front = QVector3D(0.0, 0.0, -0.5) self.worldUp = QVector3D(0.0, 1.0, 0.0) # Euler Angles for rotation self.yaw = -90.0 self.pitch = 0.0 # camera options self.movementSpeed = 2.5 self.movementSensitivity = 0.00001 self.zoom = 45.0
def updateCameraVectors(self): "Update the camera vectors and compute a new front" yawRadian = np.radians(self.yaw) yawCos = np.cos(yawRadian) pitchRadian = np.radians(self.pitch) pitchCos = np.cos(pitchRadian) frontX = yawCos * pitchCos frontY = np.sin(pitchRadian) frontZ = np.sin(yawRadian) * pitchCos self.front = QVector3D(frontX, frontY, frontZ) self.front.normalize() self.right = QVector3D.crossProduct(self.front, self.worldUp) self.right.normalize() self.up = QVector3D.crossProduct(self.right, self.front) self.up.normalize()
def test_remove_from_beginning_3(nexus_wrapper): component1 = add_component_to_file(nexus_wrapper, "field", 42, "component1") component2 = add_component_to_file(nexus_wrapper, "field", 42, "component2") rot1 = component1.add_rotation(QVector3D(1.0, 0.0, 0.0), 90.0) rot2 = component2.add_rotation(QVector3D(1.0, 0.0, 0.0), 90.0) component1.depends_on = rot1 component2.depends_on = rot2 rot1.depends_on = rot2 assert len(rot2.dependents) == 2 rot1.remove_from_dependee_chain() assert len(rot2.dependents) == 2 assert component2 in rot2.dependents assert component1 in rot2.dependents assert component1.depends_on == rot2 assert component1.transforms.link.linked_component == component2
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 __init__(self, root_entity, main_camera): """ A class that houses the Qt3D items (entities, transformations, etc) related to the gnomon (or axis indicator). The gnomon/axis indicator is an object that appears in the bottom right-hand corner of the instrument view that shows the direction of the x, y, and z axes. :param root_entity: The root entity for the gnomon. :param main_camera: The main component view camera. """ self.gnomon_root_entity = root_entity self.gnomon_cylinder_length = 4 self.main_camera = main_camera self.gnomon_camera = self.create_gnomon_camera(main_camera) self.x_text_transformation = Qt3DCore.QTransform() self.y_text_transformation = Qt3DCore.QTransform() self.z_text_transformation = Qt3DCore.QTransform() # Set the text translation value to be the length of the cylinder plus some extra space so that it doesn't # overlap with the cylinder or the cones. text_translation = self.gnomon_cylinder_length * 1.3 # The text translation value calculated above is used in addition to some "extra" values in order to make the # text placement look good and appear centered next to the cone point. This extra values were found via trial # and error and will likely have to be figured out again if you decide to change the font/size/height/etc of # the text. self.x_text_vector = QVector3D(text_translation, -0.5, 0) self.y_text_vector = QVector3D(-0.4, text_translation, 0) self.z_text_vector = QVector3D(-0.5, -0.5, text_translation) diffuse_color = QColor("grey") self.x_material = create_material(AxisColors.X.value, diffuse_color, root_entity, remove_shininess=True) self.y_material = create_material(AxisColors.Y.value, diffuse_color, root_entity, remove_shininess=True) self.z_material = create_material(AxisColors.Z.value, diffuse_color, root_entity, remove_shininess=True) self.num_neutrons = 9 self.neutron_animation_length = self.gnomon_cylinder_length * 1.5
def test_GIVEN_off_shape_json_WHEN_reading_shape_THEN_geometry_object_has_expected_properties( off_shape_reader, off_shape_json, mock_component ): children = off_shape_json[CommonKeys.CHILDREN] name = off_shape_json[CommonKeys.NAME] vertices_dataset = off_shape_reader._get_shape_dataset_from_list( CommonAttrs.VERTICES, children ) vertices = list( map( lambda vertex: QVector3D(*vertex), vertices_dataset[NodeType.CONFIG][CommonKeys.VALUES], ) ) faces = off_shape_reader._get_shape_dataset_from_list("faces", children)[ NodeType.CONFIG ][CommonKeys.VALUES] units = _find_attribute_from_list_or_dict( CommonAttrs.UNITS, vertices_dataset[CommonKeys.ATTRIBUTES] ) winding_order = off_shape_reader._get_shape_dataset_from_list( "winding_order", children )[NodeType.CONFIG][CommonKeys.VALUES] off_shape_reader.add_shape_to_component() shape = mock_component[SHAPE_GROUP_NAME] assert isinstance(shape, OFFGeometryNexus) assert shape.nx_class == OFF_GEOMETRY_NX_CLASS assert shape.name == name assert shape.units == units assert shape.get_field_value("faces") == faces assert shape.vertices == vertices assert shape.winding_order == winding_order
def test_GIVEN_all_information_present_in_json_with_stream_WHEN_attempting_to_create_translation_THEN_create_transform_is_called( transformation_reader, transformation_with_stream_json): transform_json = transformation_with_stream_json["children"][0] transform_json["config"]["name"] = name = "TranslationName" transform_json["attributes"][0][ "values"] = transformation_type = "translation" transform_json["attributes"][1]["values"] = units = "mm" transform_json["attributes"][2]["values"] = vector = [ 1.0, 2.0, 3.0, ] depends_on = None values = _create_transformation_datastream_group(transform_json, name) transformation_reader._create_transformations( transformation_with_stream_json["children"]) transformation_reader.parent_component._create_and_add_transform.assert_called_once_with( name=name, transformation_type=TRANSFORMATION_MAP[transformation_type], angle_or_magnitude=0.0, units=units, vector=QVector3D(*vector), depends_on=depends_on, values=values, )
def _create_transformation_vectors_for_pixel_offsets( self, ) -> Optional[List[QVector3D]]: """ Construct a transformation (as a QVector3D) for each pixel offset """ try: units = self.get_field_attribute(X_PIXEL_OFFSET, CommonAttrs.UNITS) unit_conversion_factor = calculate_unit_conversion_factor( units, METRES) x_offsets = self.get_field_value( X_PIXEL_OFFSET) * unit_conversion_factor y_offsets = self.get_field_value( Y_PIXEL_OFFSET) * unit_conversion_factor except AttributeError: logging.info( "In pixel_shape_component expected to find x_pixel_offset and y_pixel_offset datasets" ) return None try: z_offsets = self.get_field_value(Z_PIXEL_OFFSET) except AttributeError: z_offsets = np.zeros_like(x_offsets) if not isinstance(x_offsets, list): x_offsets = x_offsets.flatten() if not isinstance(y_offsets, list): y_offsets = y_offsets.flatten() if not isinstance(z_offsets, list): z_offsets = z_offsets.flatten() # offsets datasets can be 2D to match dimensionality of detector, so flatten to 1D return [ QVector3D(x, y, z) for x, y, z in zip(x_offsets, y_offsets, z_offsets) ]
def set_cylinder_shape( self, axis_direction: QVector3D = QVector3D(0.0, 0.0, 1.0), height: float = 1.0, radius: float = 1.0, units: Union[str, bytes] = "m", pixel_data=None, ) -> Optional[CylindricalGeometry]: if validate_nonzero_qvector(axis_direction): return None self.remove_shape() shape_group = _get_shape_group_for_pixel_data(pixel_data) geometry = CylindricalGeometry(shape_group) geometry.nx_class = CYLINDRICAL_GEOMETRY_NX_CLASS vertices = CylindricalGeometry.calculate_vertices( axis_direction, height, radius) geometry.set_field_value(CommonAttrs.VERTICES, vertices, ValueTypes.FLOAT) # # Specify 0th vertex is base centre, 1st is base edge, 2nd is top centre geometry.set_field_value(CYLINDERS, np.array([0, 1, 2]), ValueTypes.INT) geometry[CommonAttrs.VERTICES].attributes.set_attribute_value( CommonAttrs.UNITS, units) if isinstance(pixel_data, PixelMapping): geometry.detector_number = get_detector_number_from_pixel_mapping( pixel_data) self[shape_group] = geometry return geometry
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 quad(self, x1, y1, x2, y2, x3, y3, x4, y4): n = QVector3D.normal(QVector3D(x4 - x1, y4 - y1, 0), QVector3D(x2 - x1, y2 - y1, 0)) self.add(QVector3D(x1, y1, -0.05), n) self.add(QVector3D(x4, y4, -0.05), n) self.add(QVector3D(x2, y2, -0.05), n) self.add(QVector3D(x3, y3, -0.05), n) self.add(QVector3D(x2, y2, -0.05), n) self.add(QVector3D(x4, y4, -0.05), n) n = QVector3D.normal(QVector3D(x1 - x4, y1 - y4, 0), QVector3D(x2 - x4, y2 - y4, 0)) self.add(QVector3D(x4, y4, 0.05), n) self.add(QVector3D(x1, y1, 0.05), n) self.add(QVector3D(x2, y2, 0.05), n) self.add(QVector3D(x2, y2, 0.05), n) self.add(QVector3D(x3, y3, 0.05), n) self.add(QVector3D(x4, y4, 0.05), n)