├── src ├── __init__.py ├── core.py └── TPMSgen_GUI.ui ├── .gitignore ├── requirements_python_3_9.txt ├── requirements_python_3_10.txt ├── LICENSE ├── README.md ├── TPMSgen_CLI.py └── TPMSgen_GUI.py /src/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Directories 2 | *pycache* 3 | dist/ 4 | build/ 5 | Other\ Files/ 6 | Releases/ 7 | 8 | # Files 9 | .DS_Store 10 | -------------------------------------------------------------------------------- /requirements_python_3_9.txt: -------------------------------------------------------------------------------- 1 | vtk==9.1.0 2 | numpy==1.22.2 3 | pyvista==0.33.2 4 | scikit_image==0.19.3 5 | trimesh==3.9.10 6 | PyQt5==5.15.7 7 | -------------------------------------------------------------------------------- /requirements_python_3_10.txt: -------------------------------------------------------------------------------- 1 | vtk==9.2.2 2 | numpy==1.22.2 3 | pyvista==0.34.2 4 | scikit_image==0.19.3 5 | trimesh==3.9.10 6 | PyQt5==5.15.7 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Albert Forés Garriga, Héctor García De la Torre and Ricard Lado Roigé 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TPMSgen 2 | 3 | **Triply Periodic Minimal Surfaces**, also known as TPMS, are a class of mathematical surfaces that are periodic in all three spatial dimensions. They are known for their unique geometric properties, such as a lack of local extrema and a high degree of symmetry. These surfaces have a wide range of applications, including in architecture, engineering, and materials science. They have been studied extensively by mathematicians and have been found to have many interesting properties, such as the existence of an infinite number of distinct TPMS. They are also related to other mathematical structures such as soap films, minimal surfaces, and constant mean curvature surfaces. 4 | 5 | [**TPMSgen**](https://github.com/albertforesg/TPMSgen) is a powerful program based on **Python** that allows users to easily design and generate Triply Periodic Minimal Surface (TPMS) geometries. It features a user-friendly interface with multiple **design parameters** (TPMS typology, specimen dimensions, unit cell size…) that makes it simple to generate their corresponding 3D model employing their mathematical equations. In addition, the program offers the possibility to export the 3D model in the **.STL** file format, which can be later used for fabrication with additive manufacturing technologies or in finite element simulation studies. This makes [**TPMSgen**](https://github.com/albertforesg/TPMSgen) a versatile tool for architects, engineers, and material scientists who are interested in exploring the unique properties of TPMS and their potential applications. 6 | 7 | ![TPMSgen](https://user-images.githubusercontent.com/81706331/212754604-6bf67f0f-b447-4496-8e0a-cb3c199b3c98.png) 8 | 9 | --- 10 | 11 | ## Built-in TPMS designs 12 | 13 | The current library of [**TPMSgen**](https://github.com/albertforesg/TPMSgen) features a total of 10 distinct TPMS typologies, including 5 skeletal and 5 shell morphologies. These typologies provide users with a wide range of options for designing and exploring different TPMS geometries, and the ability to choose between skeletal and shell structures allows for even greater flexibility in their designs. 14 | 15 | | Shell-TPMS Unit Cell Designs | Skeletal-TPMS Unit Cell Designs | 16 | :-------------------------:|:-------------------------: 17 | ![Gyroid](https://user-images.githubusercontent.com/81706331/212520075-61342071-74e8-4a0c-abcf-6d7c4e0b2b0f.png) | ![Schoen Gyroid](https://user-images.githubusercontent.com/81706331/212520098-fa5b5f22-cd61-4911-96df-fa7c7154e5f3.png) 18 | ![Diamond](https://user-images.githubusercontent.com/81706331/212520079-ea284516-671c-4ec8-8540-510663d1fe84.png) | ![Schwarz Diamond](https://user-images.githubusercontent.com/81706331/212520101-3db607d9-cc28-4036-9d74-31e0583dd23d.png) 19 | ![Lidinoid](https://user-images.githubusercontent.com/81706331/212520087-77b587e0-c96f-4ed0-846e-e0ff68a5f94c.png) | ![Schwarz Primitive (pinched)](https://user-images.githubusercontent.com/81706331/212520106-87890470-5ee5-4857-b95c-2b7d2bcd28e5.png) 20 | ![Split-P](https://user-images.githubusercontent.com/81706331/212520090-54eccde5-42f4-4c08-9005-09529f2ea752.png) | ![Schwarz Primitive](https://user-images.githubusercontent.com/81706331/212520110-a109381d-d538-4e91-8528-9b3cc7bc4d04.png) 21 | ![Schwarz](https://user-images.githubusercontent.com/81706331/212520094-0863ce69-5f61-41ea-a1cc-f3939d13902a.png) | ![Body Diagonals with Nodes](https://user-images.githubusercontent.com/81706331/212520113-e11ff779-645f-43e4-ad5d-1994a2a10e17.png) 22 | 23 | ### Equations of Shell-TPMS designs: 24 | 25 | #### a) Gyroid: 26 | 27 | $\sin(x) \cdot \cos(y) + \sin(y) \cdot \cos(z) + \sin(z) \cdot \cos(x) = 0$ 28 | 29 | #### b) Diamond 30 | 31 | $\sin(x) \cdot \sin(y) \cdot \sin(z) + \sin(x) \cdot \cos(y) \cdot \cos(z) + \cos(x) \cdot \sin(y) \cdot \cos(z) + \cos(x) \cdot \cos(y) \cdot \sin(z) = 0$ 32 | 33 | #### c) Lidinoid: 34 | 35 | $\sin(2x) \cdot \cos(y) \cdot \sin(z) + \sin(x) \cdot \sin(2y) \cdot \cos(z) + \cos(x) \cdot \sin(y) \cdot \sin(2z) -$ 36 | $\quad - \cos(2x) \cdot \cos(2y) - \cos(2y) \cdot \cos(2z) - \cos(2z) \cdot \cos(2x) + 0.3 = 0$ 37 | 38 | #### d) Split-P: 39 | 40 | $1.1 \cdot \left[ \sin(2x) \cdot \cos(y) \cdot \sin(z) + \sin(x) \cdot \sin(2y) \cdot \cos(z) + \cos(x) \cdot \sin(y) \cdot \sin(2z) \right] - $ 41 | $\quad - 0.2 \cdot \left[ \cos(2x) \cdot \cos(2y) + \cos(2y) \cdot \cos(2z) + \cos(2z) \cdot \cos(2x) \right] - 0.4 \cdot \left[ \cos(2x) + \cos(2y) + \cos(2z) \right] = 0$ 42 | 43 | #### e) Schwarz: 44 | 45 | $\cos(x) + \cos(y) + \cos(z) = 0$ 46 | 47 | 48 | ### Equations of Skeletal-TPMS desgins: 49 | 50 | #### f) Schoen Gyroid: 51 | 52 | $\sin(x) \cdot \cos(y) + \sin(y) \cdot \cos(z) + \sin(z) \cdot \cos(x) - C = 0$ 53 | 54 | #### g) Schwarz Diamond: 55 | 56 | $\cos(x) \cdot \cos(y) \cdot \cos(z) + \sin(x) \cdot \sin(y) \cdot \sin(z) - C = 0$ 57 | 58 | #### h) Schwarz Primitive (pinched): 59 | 60 | $\cos(x) + \cos(y) + \cos(z) - C = 0$ 61 | 62 | #### i) Schwarz Primitive: 63 | 64 | $\cos(x) + \cos(y) + \cos(z) - C = 0$ 65 | 66 | #### j) Body Diagonals with Nodes: 67 | 68 | $2 \cdot \left[ \cos(x) \cdot \cos(y) + \cos(y) \cdot \cos(z) + \cos(z) \cdot \cos(x) \right] - \left[ \cos(2x) + \cos(2y) + \cos(2z) \right] - C = 0$ 69 | 70 | --- 71 | 72 | ## Quick Start 73 | 74 | A [**standalone release v1.0.0**](https://github.com/albertforesg/TPMSgen/releases/tag/v1.0.0) has already been published and it is the simpliest way to execute the [**TPMSgen**](https://github.com/albertforesg/TPMSgen) application. 75 | 76 | - Operating Systems: 77 | - Windows 78 | - MacOS 79 | - Linux 80 | - Hardware: 4GB of RAM or more 81 | 82 | Some of the operations that [**TPMSgen**](https://github.com/albertforesg/TPMSgen) employs to create the mesh of the desired TPMS designs also require that [Blender](https://www.blender.org/download/) (version 3.4.1+) software is installed in your computer. You can follow the available [documentation](https://www.blender.org/support/) for troubleshooting during its installation. 83 | 84 | --- 85 | 86 | ## Run TPMSgen using local Python Interpreter 87 | 88 | If the compiled files of the release do not work properly on your system of choice, you can solve it by running the GUI version or execute the CLI version of [**TPMSgen**](https://github.com/albertforesg/TPMSgen) directly from your Python interpreter. 89 | 90 | - Python version: 3.9 or 3.10 91 | - Required libraries: 92 | - numpy 93 | - pyvista 94 | - scikit_image 95 | - skimage 96 | - trimesh 97 | - vtk 98 | - PyQt5 (only for GUI version) 99 | 100 | ### Installing prerequisites for Python 3.9: 101 | 102 | The `requirements_python_3_9.txt` file lists all the Python libraries that [**TPMSgen**](https://github.com/albertforesg/TPMSgen) depends on. If needed, they can be easily installed by running the following code: 103 | 104 | ```bash 105 | pip install -r requirements_python_3_9.txt 106 | ``` 107 | 108 | ### Installing prerequisites for Python 3.10: 109 | 110 | The `requirements_python_3_10.txt` file lists all the Python libraries that [**TPMSgen**](https://github.com/albertforesg/TPMSgen) depends on. If needed, they can be easily installed by running the following code: 111 | 112 | ```bash 113 | pip install -r requirements_python_3_10.txt 114 | ``` 115 | 116 | ### Running TPMSgen application from terminal: 117 | 118 | Once prerequisites have been installed, you can run the following code to open the GUI version of [**TPMSgen**](https://github.com/albertforesg/TPMSgen) from your Python interpreter: 119 | 120 | ```bash 121 | python TPMSgen_GUI.py 122 | ``` 123 | 124 | Furthermore, if your system does not satisfy some of the requisites to run that version of the code, you can always execute the CLI version of the application: 125 | 126 | ```bash 127 | python TPMSgen_CLI.py 128 | ``` 129 | 130 | --- 131 | 132 | ## Interface preview / Help 133 | 134 | Let's take a closer look at the user interface of [**TPMSgen**](https://github.com/albertforesg/TPMSgen) through a series of screenshots. The following pictures will provide a visual representation of the program and the process to generate the mesh of the desired TPMS design and export it into **.STL** format. In particular, you will be able to see its layout, the different buttons and design parameters provided in the main menu, as well as the rest of the program's features. 135 | 136 | ### Main menu: 137 | 138 | This is the main menu of [**TPMSgen**](https://github.com/albertforesg/TPMSgen). Here, the user can easily select one of the 10 popular TPMS typologies included in its library. Then, the rest of the design parameters (*C value* -only for Skeletal designs- and *thickness* -only for Shell designs-, *specimen dimensions*, *unit cell size*, *unit cell origin*…) can be set. Moreover, the appropiate mesh density can be selected by modifying the *unit cell mesh resolution* parameter. 139 | 140 | ![Main menu](https://user-images.githubusercontent.com/81706331/212754634-4941e974-dad7-46d9-b9f7-b23d93c61147.png) 141 | 142 | ### Vertices generation: 143 | 144 | When all the design parameters are properly set, the vertices of the sample can be generated by clicking on the **Plot TPMS equation** button. The following plot will be displayed, and the user can check the shape of the generated equation according to the choosen preferences. 145 | 146 | ![Vertices](https://user-images.githubusercontent.com/81706331/212511031-57cb2c76-9377-42b5-8ea9-aafbf7140e48.png) 147 | 148 | ### Face normals inspection: 149 | 150 | When the desired shape is obtained, the normals' direction of the faces of the mesh must be carefully inspected in order to obtain a proper watertight **.STL**. To do so, click on the **Check face normals** button, and a new figure will appear. Depending on the typology of the choosen TPMS (Shell o Skeletal), check that the plotted normals are orientated according to the instructions that are displayed in the bottom-right corner of the main menu. If not, flip them by just by clicking on the **Flip face normals** button. 151 | 152 | ![Face normals](https://user-images.githubusercontent.com/81706331/212511074-81564c47-6f31-48ff-862f-7ebbc986c726.png) 153 | 154 | ### Mesh preview: 155 | 156 | After checking the orientation of the face nomals, it is now time to generate the final watertight mesh by clicking on the **Generate mesh** button. Depending on the selected design parameters, this process may take a few minutes. When the calculation finishes, the message in the bottom-right corner will be updated indicating the quality of the achieved mesh. If the process doesn't succeed in obtaining the expected watertight mesh, try increasing the unit cell mesh resolution value or flipping the face normals. 157 | 158 | In all cases, the result can be plotted and exported into **.STL** file format by clicking on the **View mesh** and **Export STL file** buttons, respectivelly. 159 | 160 | ![Mesh](https://user-images.githubusercontent.com/81706331/212511172-339de4fe-e169-4aa8-9791-4215f01efe70.png) 161 | 162 | ## Please, cite this pubication as such 163 | 164 | To correctly cite the application it is necessary to refer to the Github repository and the paper. 165 | 166 | A. Forés-Garriga, G. Gómez-Gras, and M. A. Pérez. Mechanical performance of additively manufactured three-dimensional lightweight cellular solids: experimental and numerical analysis, Materials & Design (2023) - Accepted 17th January 2023 167 | 168 | ```bibtex 169 | @article{fores_2023, 170 | title = {Mechanical performance of additively manufactured three-dimensional lightweight cellular solids: experimental and numerical analysis.}, 171 | doi = {}, 172 | author = {A. For{\'{e}}s-Garriga, G. G{\'{o}}mez-Gras, P{\'{e}}rez, Marco A.}, 173 | journal = {Materials & Design}, 174 | year = {2023}, 175 | note = { (Accepted 17th January 2023) } 176 | } 177 | ``` 178 | 179 | A. Forés-Garriga, H. García de la Torre, R. Lado-Roigé, G. Gómez-Gras, and M. A. Pérez. Triply Periodic Minimal Surfaces Generator - TPMSgen, 2023. URL: [https://github.com/albertforesg/TPMSgen](https://github.com/albertforesg/TPMSgen). 180 | 181 | ```bibtex 182 | @software{TPMSgen, 183 | author = {{A. For{\'{e}}s-Garriga, H. Garc{\'{i}}a de la Torre, R. Lado-Roig{\'{e}}, G. G{\'{o}}mez- Gras, and M. A. P{\'{e}}rez.}}, 184 | title = {Triply Periodic Minimal Surfaces Generator - TPMSgen}, 185 | url = {https://github.com/albertforesg/TPMSgen}, 186 | version = {1.0}, 187 | year = {2023}, 188 | } 189 | ``` 190 | --- 191 | 192 | ## Repository contributors 193 | 194 | - Albert Forés Garriga [^1] 195 | 196 | - Héctor García de la Torre [^1] 197 | 198 | - Ricard Lado Roigé [^1] 199 | 200 | [^1]: Applied Mechanics and Advanced Manufactuing Research Group (GAM). IQS School of Engineering - Universitat Ramon llull (Barcelona, Spain) 201 | 202 | ## Acknowledgements 203 | 204 | This work has been supported by the **Ministry of Science, Innovation and Universities** through the project **New Developments in Lightweight Composite Sandwich Panels with 3D Printed Cores (3DPC) - RTI2018-099754-A-I00** and **Development of Gyroid-Type Cellular Metallic Microstructures by FFF to Optimize Structural Components with High Added-Value (GRIM) - PID2021-123876OB-I00**. 205 | -------------------------------------------------------------------------------- /src/core.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import os 3 | import trimesh 4 | 5 | import numpy as np 6 | import pyvista as pv 7 | 8 | from skimage import measure 9 | 10 | # MAIN FUNCTIONS 11 | # Plot TPMS equation: 12 | def fn_plot_tpms_eq(tpms_type, tpms_design, sizes, cell_sizes, origin, unit_cell_mesh_resolution, c, thickness, mesh): 13 | # Generation of the meshgrid: 14 | tols = [0, 0, 0] 15 | X, Y, Z, tols, spacing = generate_meshgrid(0, sizes, cell_sizes, unit_cell_mesh_resolution) 16 | 17 | # Generate TPMS: 18 | F, t = tpms_library(X, Y, Z, c, tpms_design, cell_sizes, origin) 19 | 20 | # Mesh TPMS: 21 | if tpms_type == 'Shell': 22 | mesh, vertices = mesh_shell(F, t, thickness, sizes, mesh, tols, spacing) 23 | else: 24 | mesh, vertices = mesh_skeletal(F, sizes, mesh, tols, spacing) 25 | 26 | # Colour TPMS vertices: 27 | color = [] 28 | for vert in vertices: 29 | color.append(vert[0] * vert[1] * vert[2]) 30 | color = np.array(color) 31 | 32 | # Plot TPMS vertices: 33 | plotter1 = pv.Plotter(window_size = [1400, 1600]) 34 | _ = plotter1.add_title('Close this window to continue', font_size = 10) 35 | _ = plotter1.add_mesh(vertices, scalars = color, cmap = 'jet') 36 | _ = plotter1.remove_scalar_bar() 37 | _ = plotter1.show_grid() 38 | plotter1.show() 39 | 40 | return mesh, vertices 41 | 42 | # Check face normals: 43 | def fn_check_face_normals(mesh, silent = False): 44 | # Calculate face centroids and normals: 45 | mesh_pv = pv.wrap(mesh) 46 | cent = mesh_pv.cell_centers().points 47 | direction = mesh_pv.cell_normals 48 | 49 | # Update output message: 50 | if not silent: 51 | print('\nCheck if face normals are pointing OUT of the mesh') 52 | 53 | # Plot TPMS face normals: 54 | plotter2 = pv.Plotter(window_size = [1400, 1600]) 55 | _ = plotter2.add_title('Close this window to continue', font_size = 10) 56 | _ = plotter2.add_mesh(mesh, color = True, show_edges = True) 57 | _ = plotter2.add_arrows(cent, direction, mag = 1) 58 | _ = plotter2.remove_scalar_bar() 59 | _ = plotter2.show_grid() 60 | plotter2.show() 61 | 62 | return mesh 63 | 64 | # Check face normals: 65 | def fn_flip_face_normals(mesh, silent = False): 66 | # Flip mesh: 67 | mesh = pv.wrap(mesh) 68 | mesh.flip_normals() 69 | mesh = mesh_conversion(mesh) 70 | 71 | if not silent: 72 | print('Face normals were flipped. Now face normals should be pointing OUT of the mesh') 73 | 74 | # Calculate face centroids and normals: 75 | mesh_pv = pv.wrap(mesh) 76 | cent = mesh_pv.cell_centers().points 77 | direction = mesh_pv.cell_normals 78 | 79 | # Plot TPMS face normals: 80 | plotter3 = pv.Plotter(window_size = [1400, 1600]) 81 | _ = plotter3.add_title('Close this window to continue', font_size = 10) 82 | _ = plotter3.add_mesh(mesh, color = True, show_edges = True) 83 | _ = plotter3.add_arrows(cent, direction, mag = 1) 84 | _ = plotter3.remove_scalar_bar() 85 | _ = plotter3.show_grid() 86 | plotter3.show() 87 | 88 | return mesh 89 | 90 | # Generate mesh: 91 | def fn_generate_mesh(tpms_type, tpms_design, c, thickness, sizes, cell_sizes, origin, unit_cell_mesh_resolution, mesh, flip_face_normals, silent = False): 92 | is_watertight = False 93 | k = int(5 / 100 * unit_cell_mesh_resolution) 94 | k_max = int(45 / 100 * unit_cell_mesh_resolution) 95 | k_increment = int(5 / 100 * unit_cell_mesh_resolution) 96 | iterative_mesh = copy.deepcopy(mesh) 97 | 98 | # Mesh generation iterative process: 99 | if tpms_type == 'Shell': 100 | # Generate bounding box: 101 | shell_bounding_box = trimesh.creation.box(extents = (sizes[0], sizes[1], sizes[2]), transform = None) 102 | while not is_watertight and k <= k_max: 103 | # Generation of the meshgrid: 104 | X, Y, Z, tols, spacing = generate_meshgrid(k, sizes, cell_sizes, unit_cell_mesh_resolution) 105 | 106 | # Generate TPMS for intersection: 107 | F, t = tpms_library(X, Y, Z, c, tpms_design, cell_sizes, origin) 108 | 109 | # Mesh TPMS for intersection: 110 | iterative_mesh, _ = mesh_shell(F, t, thickness, sizes, iterative_mesh, tols, spacing) 111 | 112 | # Check face normals orientation: 113 | if flip_face_normals: 114 | iterative_mesh = pv.wrap(iterative_mesh) 115 | iterative_mesh.flip_normals() 116 | iterative_mesh = mesh_conversion(iterative_mesh) 117 | 118 | # Calculate intercection: 119 | iterative_mesh = trimesh.boolean.intersection((iterative_mesh, shell_bounding_box), engine = 'blender') 120 | 121 | # Check obtained results 122 | k += k_increment 123 | is_watertight = iterative_mesh.is_watertight 124 | if not iterative_mesh.is_watertight: 125 | iterative_mesh.fill_holes() 126 | is_watertight = iterative_mesh.is_watertight 127 | else: 128 | # Generate bounding box: 129 | bounding_box_1 = trimesh.creation.box(extents = (2 * sizes[0], 2 * sizes[1], 2 * sizes[2]), transform = None) 130 | bounding_box_2 = trimesh.creation.box(extents = (sizes[0], sizes[1], sizes[2]), transform = None) 131 | bounding_box = trimesh.boolean.difference((bounding_box_1, bounding_box_2), engine = 'blender') 132 | 133 | del bounding_box_1, bounding_box_2 134 | 135 | while not is_watertight and k <= k_max: 136 | # Generation of the meshgrid: 137 | X, Y, Z, tols, spacing = generate_meshgrid(k, sizes, cell_sizes, unit_cell_mesh_resolution) 138 | 139 | # Generate TPMS for intersection: 140 | F, t = tpms_library(X, Y, Z, c, tpms_design, cell_sizes, origin) 141 | 142 | # Mesh TPMS for intersection: 143 | iterative_mesh, _ = mesh_skeletal(F, sizes, iterative_mesh, tols, spacing) 144 | 145 | # Check face normals orientation: 146 | if flip_face_normals: 147 | iterative_mesh = pv.wrap(iterative_mesh) 148 | iterative_mesh.flip_normals() 149 | iterative_mesh = mesh_conversion(iterative_mesh) 150 | 151 | # Calculate intercection: 152 | iterative_mesh = trimesh.boolean.difference((iterative_mesh, bounding_box), engine = 'blender') 153 | 154 | # Check obtained results 155 | k += k_increment 156 | is_watertight = iterative_mesh.is_watertight 157 | if not iterative_mesh.is_watertight: 158 | iterative_mesh.fill_holes() 159 | is_watertight = iterative_mesh.is_watertight 160 | 161 | # Update output message: 162 | if not silent: 163 | if is_watertight: 164 | print('Mesh is generated!') 165 | print('The obtained mesh is watertight. If the opposite solution was desired, try using the opposite face normals direction.') 166 | else: 167 | print('Mesh is generated:') 168 | print('Cannot obtain a watertight mesh. Try increasing unit cell mesh resolution. Please, check results carefully and treat them to solve this issue.') 169 | 170 | # Plot generated mesh: 171 | plotter4 = pv.Plotter(window_size = [1400, 1600]) 172 | _ = plotter4.add_title('Generated mesh can be exported into STL format', font_size = 10) 173 | _ = plotter4.add_mesh(iterative_mesh, color = True, show_edges = True) 174 | _ = plotter4.show_grid() 175 | plotter4.show() 176 | 177 | return iterative_mesh 178 | 179 | # Export mesh: 180 | def fn_export_stl_file(iterative_mesh, file_name, directory_path, silent = False): 181 | export = trimesh.exchange.stl.export_stl_ascii(iterative_mesh) 182 | 183 | file_path = os.path.join(directory_path, file_name + '.stl') 184 | with open(file_path, 'w') as file: 185 | file.write(export) 186 | 187 | if not silent: 188 | print('\nMesh exported as .STL into ' + file_path) 189 | 190 | # SUPLEMENTARY FUNCTIONS: 191 | # Generate meshgrid 192 | def generate_meshgrid(k, sizes, cell_sizes, unit_cell_mesh_resolution): 193 | tol_x = k * cell_sizes[0] / unit_cell_mesh_resolution 194 | tol_y = k * cell_sizes[1] / unit_cell_mesh_resolution 195 | tol_z = k * cell_sizes[2] / unit_cell_mesh_resolution 196 | tols = [tol_x, tol_y, tol_z] 197 | 198 | xl = np.linspace(-sizes[0]/2 - tols[0], sizes[0]/2 + tols[0], int(sizes[0] / cell_sizes[0]) * unit_cell_mesh_resolution + 2 * k + 1) 199 | yl = np.linspace(-sizes[1]/2 - tols[1], sizes[1]/2 + tols[1], int(sizes[1] / cell_sizes[1]) * unit_cell_mesh_resolution + 2 * k + 1) 200 | zl = np.linspace(-sizes[2]/2 - tols[2], sizes[2]/2 + tols[2], int(sizes[2] / cell_sizes[2]) * unit_cell_mesh_resolution + 2 * k + 1) 201 | spacing = [xl, yl, zl] 202 | 203 | Y, X, Z = np.meshgrid(yl, xl, zl) 204 | 205 | return X, Y, Z, tols, spacing 206 | 207 | # Mesh conversion 208 | def mesh_conversion(mesh_pv): 209 | faces_as_array = mesh_pv.faces.reshape((mesh_pv.n_faces, 4))[:, 1:] 210 | mesh = trimesh.Trimesh(mesh_pv.points, faces_as_array) 211 | 212 | return mesh 213 | 214 | # Mesh Shell 215 | def mesh_shell(F, t, thickness, sizes, mesh, tols, spacing): 216 | vertices_positive, faces_positive, vertex_normals_positive, _ = measure.marching_cubes(F, thickness * t, spacing = [np.diff(spacing[0])[0], np.diff(spacing[1])[0], np.diff(spacing[2])[0]]) 217 | vertices_negative, faces_negative, vertex_normals_negative, _ = measure.marching_cubes(F, -thickness * t, spacing = [np.diff(spacing[0])[0], np.diff(spacing[1])[0], np.diff(spacing[2])[0]]) 218 | 219 | for i, vert in enumerate(vertices_positive): 220 | vertices_positive[i, 0] = vert[0] - sizes[0]/2 - tols[0] 221 | vertices_positive[i, 1] = vert[1] - sizes[1]/2 - tols[1] 222 | vertices_positive[i, 2] = vert[2] - sizes[2]/2 - tols[2] 223 | 224 | for i, vert in enumerate(vertices_negative): 225 | vertices_negative[i, 0] = vert[0] - sizes[0]/2 - tols[0] 226 | vertices_negative[i, 1] = vert[1] - sizes[1]/2 - tols[1] 227 | vertices_negative[i, 2] = vert[2] - sizes[2]/2 - tols[2] 228 | 229 | vertices = np.concatenate((vertices_positive, vertices_negative)) 230 | 231 | mesh_1 = trimesh.Trimesh(vertices = vertices_positive, faces = faces_positive, vertex_normals = vertex_normals_positive) 232 | mesh_2 = trimesh.Trimesh(vertices = vertices_negative, faces = faces_negative, vertex_normals = vertex_normals_negative) 233 | mesh_2 = pv.wrap(mesh_2) 234 | mesh_2.flip_normals() 235 | mesh_2 = mesh_conversion(mesh_2) 236 | 237 | mesh = trimesh.util.concatenate((mesh_1, mesh_2)) 238 | 239 | del vertices_positive, faces_positive, vertex_normals_positive, vertices_negative, faces_negative, vertex_normals_negative, mesh_1, mesh_2 240 | 241 | return mesh, vertices 242 | 243 | # Mesh Skeletal 244 | def mesh_skeletal(F, sizes, mesh, tols, spacing): 245 | vertices, faces, _, _ = measure.marching_cubes(F, 0, spacing = [np.diff(spacing[0])[0], np.diff(spacing[1])[0], np.diff(spacing[2])[0]]) 246 | for i, vert in enumerate(vertices): 247 | vertices[i, 0] = vert[0] - sizes[0]/2 - tols[0] 248 | vertices[i, 1] = vert[1] - sizes[1]/2 - tols[1] 249 | vertices[i, 2] = vert[2] - sizes[2]/2 - tols[2] 250 | 251 | mesh = trimesh.Trimesh(vertices = vertices, faces = faces) 252 | 253 | del faces 254 | 255 | return mesh, vertices 256 | 257 | # TPMS library 258 | def tpms_library(X, Y, Z, c, tpms_design, cell_sizes, origin, silent = False): 259 | w_x = 1 / cell_sizes[0] * 2 * np.pi 260 | w_y = 1 / cell_sizes[1] * 2 * np.pi 261 | w_z = 1 / cell_sizes[2] * 2 * np.pi 262 | 263 | if tpms_design == 'Skeletal-TPMS Schoen gyroid' or tpms_design == 'Shell-TPMS Gyroid': 264 | F = (np.cos(w_x * (X + origin[0])) * np.sin(w_y * (Y + origin[1])) + np.cos(w_y * (Y + origin[1])) * np.sin(w_z * (Z + origin[2])) + np.cos(w_z * (Z + origin[2])) * np.sin(w_x * (X + origin[0])) - c) # J 265 | t = 0.125 266 | 267 | elif tpms_design == 'Skeletal-TPMS Schwarz diamond': 268 | F = (np.cos(w_x * (X + origin[0])) * np.cos(w_y * (Y + origin[1])) * np.cos(w_z * (Z + origin[2])) + np.sin(w_x * (X + origin[0])) * np.sin(w_y * (Y + origin[1])) * np.sin(w_z * (Z + origin[2])) - c) # K 269 | t = 0 270 | 271 | elif tpms_design == 'Skeletal-TPMS Schwarz primitive (pinched)' or tpms_design == 'Skeletal-TPMS Schwarz primitive': 272 | F = (np.cos(w_x * (X + origin[0])) + np.cos(w_y * (Y + origin[1])) + np.cos(w_z * (Z + origin[2])) - c) # M, N 273 | t = 0 274 | 275 | elif tpms_design == 'Skeletal-TPMS Body diagonals with nodes': 276 | F = (2 * (np.cos(w_x * ((X + origin[0]))) * np.cos(w_y * (Y + origin[1])) + np.cos(w_y * (Y + origin[1])) * np.cos(w_z * (Z + origin[2])) + np.cos(w_z * (Z + origin[2])) * np.cos(w_x * (X + origin[0]))) - (np.cos(2 * w_x * (X + origin[0])) + np.cos(2 * w_y * (Y + origin[1])) + np.cos(2 * w_z * (Z + origin[2]))) - c) # O 277 | t = 0 278 | 279 | elif tpms_design == 'Shell-TPMS Diamond': 280 | F = (np.sin(w_x * (X + origin[0])) * np.sin(w_y * (Y + origin[1])) * np.sin(w_z * (Z + origin[2])) + np.sin(w_x * (X + origin[0])) * np.cos(w_y * (Y + origin[1])) * np.cos(w_z * (Z + origin[2])) + np.cos(w_x * (X + origin[0])) * np.sin(w_y * (Y + origin[1])) * np.cos(w_z * (Z + origin[2])) + np.cos(w_x * (X + origin[0])) * np.cos(w_y * (Y + origin[1])) * np.sin(w_z * (Z + origin[2])) - c) # Q 281 | t = 0.115 282 | 283 | elif tpms_design == 'Shell-TPMS Lidinoid': 284 | F = (np.sin(2 * w_x * (X + origin[0])) * np.cos(w_y * (Y + origin[1])) * np.sin(w_z * (Z + origin[2])) + np.sin(w_x * (X + origin[0])) * np.sin(2 * w_y * (Y + origin[1])) * np.cos(w_z * (Z + origin[2])) + np.cos(w_x * (X + origin[0])) * np.sin(w_y * (Y + origin[1])) * np.sin(2 * w_z * (Z + origin[2])) - np.cos(2 * w_x * (X + origin[0])) * np.cos(2 * w_y * (Y + origin[1])) - np.cos(2 * w_y * (Y + origin[1])) * np.cos(2 * w_z * (Z + origin[2])) - np.cos(2 * w_z * (Z + origin[2])) * np.cos(2 * w_x * (X + origin[0])) + 0.3 - c) 285 | t = 0.37 286 | 287 | elif tpms_design == 'Shell-TPMS Split-P': 288 | F = (1.1 * (np.sin(2 * w_x * (X + origin[0])) * np.cos(w_y * (Y + origin[1])) * np.sin(w_z * (Z + origin[2])) + np.sin(w_x * (X + origin[0])) * np.sin(2 * w_y * (Y + origin[1])) * np.cos(w_z * (Z + origin[2])) + np.cos(w_x * (X + origin[0])) * np.sin(w_y * (Y + origin[1])) * np.sin(2 * w_z * (Z + origin[2]))) - 0.2 * (np.cos(2 * w_x * (X + origin[0])) * np.cos(2 * w_y * (Y + origin[1])) + np.cos(2 * w_y * (Y + origin[1])) * np.cos(2 * w_z * (Z + origin[2])) + np.cos(2 * w_z * (Z + origin[2])) * np.cos(2 * w_x * (X + origin[0]))) - 0.4 * (np.cos(2 * w_x * (X + origin[0])) + np.cos(2 * w_y * (Y + origin[1])) + np.cos(2 * w_z * (Z + origin[2]))) - c) 289 | t = 0.19 290 | 291 | elif tpms_design == 'Shell-TPMS Schwarz': 292 | F = (np.cos(w_x * (X + origin[0])) + np.cos(w_y * (Y + origin[1])) + np.cos(w_z * (Z + origin[2])) - c) 293 | t = 0.0875 294 | 295 | else: 296 | if not silent: 297 | print('Design not found in library') 298 | F = 0 299 | t = 0 300 | 301 | return F, t -------------------------------------------------------------------------------- /TPMSgen_CLI.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from src import core 4 | 5 | if __name__ == "__main__": 6 | active_session = True 7 | while active_session: 8 | # Selection of TPMS typology: 9 | input_validator = False 10 | while not input_validator: 11 | print('\n[SH] for Shell') 12 | print('[SK] for Skeletal') 13 | tpms_type = input('Select TPMS typology: ') 14 | if tpms_type == 'SH': 15 | tpms_type = 'Shell' 16 | input_validator = True 17 | elif tpms_type == 'SK': 18 | tpms_type = 'Skeletal' 19 | input_validator = True 20 | else: 21 | print('Invalid input') 22 | 23 | if tpms_type == 'Shell': 24 | # Selection of Shell-TPMS design: 25 | input_validator = False 26 | while not input_validator: 27 | print('\n[1] for Gyroid') 28 | print('[2] for Diamond') 29 | print('[3] for Lidinoid') 30 | print('[4] for Split-P') 31 | print('[5] for Schwarz') 32 | tpms_design = input('Select Shell-TPMS design: ') 33 | if tpms_design == '1': 34 | tpms_design = 'Shell-TPMS Gyroid' 35 | input_validator = True 36 | elif tpms_design == '2': 37 | tpms_design = 'Shell-TPMS Diamond' 38 | input_validator = True 39 | elif tpms_design == '3': 40 | tpms_design = 'Shell-TPMS Lidinoid' 41 | input_validator = True 42 | elif tpms_design == '4': 43 | tpms_design = 'Shell-TPMS Split-P' 44 | input_validator = True 45 | elif tpms_design == '5': 46 | tpms_design = 'Shell-TPMS Schwarz' 47 | input_validator = True 48 | else: 49 | print('Invalid input') 50 | 51 | # Selection of thickness: 52 | input_validator = False 53 | while not input_validator: 54 | thickness = input('\nEnter thickness in mm (default = 3 mm): ') 55 | if not thickness: 56 | thickness = 3 57 | c = 0 58 | print(thickness) 59 | input_validator = True 60 | else: 61 | try: 62 | thickness = float(thickness) 63 | if thickness > 0: 64 | c = 0 65 | input_validator = True 66 | else: 67 | print('Invalid input') 68 | except ValueError: 69 | print('Invalid input') 70 | 71 | else: 72 | # Selection of Shell-TPMS design: 73 | input_validator = False 74 | while not input_validator: 75 | print('\n[1] for Schoen gyroid') 76 | print('[2] for Schwarz diamond') 77 | print('[3] for Schwarz primitive (pinched)') 78 | print('[4] for Schwarz primitive') 79 | print('[5] for Body diagonals with nodes') 80 | tpms_design = input('Select Skeletal-TPMS design: ') 81 | if tpms_design == '1': 82 | tpms_design = 'Skeletal-TPMS Schoen gyroid' 83 | input_validator = True 84 | elif tpms_design == '2': 85 | tpms_design = 'Skeletal-TPMS Schwarz diamond' 86 | input_validator = True 87 | elif tpms_design == '3': 88 | tpms_design = 'Skeletal-TPMS Schwarz primitive (pinched)' 89 | input_validator = True 90 | elif tpms_design == '4': 91 | tpms_design = 'Skeletal-TPMS Schwarz primitive' 92 | input_validator = True 93 | elif tpms_design == '5': 94 | tpms_design = 'Skeletal-TPMS Body diagonals with nodes' 95 | input_validator = True 96 | else: 97 | print('Invalid input') 98 | 99 | # Selection of C value: 100 | input_validator = False 101 | while not input_validator: 102 | c = input('\nEnter C value (default = 0.5): ') 103 | if not c: 104 | c = 0.5 105 | thickness = 0 106 | print(c) 107 | input_validator = True 108 | else: 109 | try: 110 | c = float(c) 111 | if c != 0: 112 | thickness = 0 113 | input_validator = True 114 | else: 115 | print('C value must be different from 0 in Skeletal-TPMS designs.') 116 | except ValueError: 117 | print('Invalid input') 118 | 119 | # Bounding box definition: 120 | print('\nEnter bounding box dimensions:') 121 | input_validator = False 122 | while not input_validator: 123 | x_size = input('Enter X bounding box dimension in mm (default = 40 mm): ') 124 | if not x_size: 125 | x_size = 40 126 | print(x_size) 127 | input_validator = True 128 | else: 129 | try: 130 | x_size = float(x_size) 131 | if x_size > 0: 132 | input_validator = True 133 | else: 134 | print('X bounding box dimension must be positive') 135 | except: 136 | print('Invalid input') 137 | input_validator = False 138 | while not input_validator: 139 | y_size = input('Enter Y bounding box dimension in mm (default = 40 mm): ') 140 | if not y_size: 141 | y_size = 40 142 | print(y_size) 143 | input_validator = True 144 | else: 145 | try: 146 | y_size = float(y_size) 147 | if y_size > 0: 148 | input_validator = True 149 | else: 150 | print('Y bounding box dimension must be positive') 151 | except: 152 | print('Invalid input') 153 | input_validator = False 154 | while not input_validator: 155 | z_size = input('Enter Z bounding box dimension in mm (default = 40 mm): ') 156 | if not z_size: 157 | z_size = 40 158 | print(z_size) 159 | input_validator = True 160 | else: 161 | try: 162 | z_size = float(z_size) 163 | if z_size > 0: 164 | input_validator = True 165 | else: 166 | print('Z bounding box dimension must be positive') 167 | except: 168 | print('Invalid input') 169 | sizes = [x_size, y_size, z_size] 170 | del x_size, y_size, z_size 171 | 172 | # Unit cell size definition: 173 | print('\nEnter unit cell dimensions:') 174 | input_validator = False 175 | while not input_validator: 176 | x_cell_size = input('Enter X unit cell dimension in mm (default = 40 mm): ') 177 | if not x_cell_size: 178 | x_cell_size = 40 179 | print(x_cell_size) 180 | input_validator = True 181 | else: 182 | try: 183 | x_cell_size = float(x_cell_size) 184 | if x_cell_size > 0: 185 | if x_cell_size <= sizes[0]: 186 | input_validator = True 187 | else: 188 | print('X unit cell dimension must be lower or equal than X bounding box dimension (' + str(sizes[0]) + ' mm)') 189 | else: 190 | print('X unit cell dimension must be positive and lower or equal than X bounding box dimension (' + str(sizes[0]) + ' mm)') 191 | except: 192 | print('Invalid input') 193 | input_validator = False 194 | while not input_validator: 195 | y_cell_size = input('Enter Y unit cell dimension in mm (default = 40 mm): ') 196 | if not y_cell_size: 197 | y_cell_size = 40 198 | print(y_cell_size) 199 | input_validator = True 200 | else: 201 | try: 202 | y_cell_size = float(y_cell_size) 203 | if y_cell_size > 0: 204 | if y_cell_size <= sizes[1]: 205 | input_validator = True 206 | else: 207 | print('Y unit cell dimension must be lower or equal than Y bounding box dimension (' + str(sizes[1]) + ' mm)') 208 | else: 209 | print('Y unit cell dimension must be positive and lower or equal than Y bounding box dimension (' + str(sizes[1]) + ' mm)') 210 | except: 211 | print('Invalid input') 212 | input_validator = False 213 | while not input_validator: 214 | z_cell_size = input('Enter Z unit cell dimension in mm (default = 40 mm): ') 215 | if not z_cell_size: 216 | z_cell_size = 40 217 | print(z_cell_size) 218 | input_validator = True 219 | else: 220 | try: 221 | z_cell_size = float(z_cell_size) 222 | if z_cell_size > 0: 223 | if z_cell_size <= sizes[2]: 224 | input_validator = True 225 | else: 226 | print('Z unit cell dimension must be lower or equal than Z bounding box dimension (' + str(sizes[2]) + ' mm)') 227 | else: 228 | print('Z unit cell dimension must be positive and lower or equal than Z bounding box dimension (' + str(sizes[2]) + ' mm)') 229 | except: 230 | print('Invalid input') 231 | cell_sizes = [x_cell_size, y_cell_size, z_cell_size] 232 | del x_cell_size, y_cell_size, z_cell_size 233 | 234 | # Unit cell origin definition: 235 | print('\nEnter unit cell origin:') 236 | input_validator = False 237 | while not input_validator: 238 | x_origin = input('Enter X origin in mm (default = 0 mm): ') 239 | if not x_origin: 240 | x_origin = 0 241 | print(x_origin) 242 | input_validator = True 243 | else: 244 | try: 245 | x_origin = float(x_origin) 246 | input_validator = True 247 | except: 248 | print('Invalid input') 249 | input_validator = False 250 | while not input_validator: 251 | y_origin = input('Enter Y origin in mm (default = 0 mm): ') 252 | if not y_origin: 253 | y_origin = 0 254 | print(y_origin) 255 | input_validator = True 256 | else: 257 | try: 258 | y_origin = float(y_origin) 259 | input_validator = True 260 | except: 261 | print('Invalid input') 262 | input_validator = False 263 | while not input_validator: 264 | z_origin = input('Enter Z origin in mm (default = 0 mm): ') 265 | if not z_origin: 266 | z_origin = 0 267 | print(z_origin) 268 | input_validator = True 269 | else: 270 | try: 271 | z_origin = float(z_origin) 272 | input_validator = True 273 | except: 274 | print('Invalid input') 275 | origin = [x_origin, y_origin, z_origin] 276 | del x_origin, y_origin, z_origin 277 | 278 | # Unit cell origin definition: 279 | input_validator = False 280 | while not input_validator: 281 | unit_cell_mesh_resolution = input('\nEnter unit cell mesh resolution (minimum = 20, default = 50): ') 282 | if not unit_cell_mesh_resolution: 283 | unit_cell_mesh_resolution = 50 284 | print(str(unit_cell_mesh_resolution) + '\n') 285 | input_validator = True 286 | else: 287 | try: 288 | unit_cell_mesh_resolution = int(unit_cell_mesh_resolution) 289 | if unit_cell_mesh_resolution >= 20: 290 | input_validator = True 291 | except: 292 | print('Invalid input') 293 | 294 | # Plot TPMS equation: 295 | mesh = None 296 | mesh, vertices = core.fn_plot_tpms_eq(tpms_type, tpms_design, sizes, cell_sizes, origin, unit_cell_mesh_resolution, c, thickness, mesh) 297 | 298 | # Check face normals: 299 | mesh = core.fn_check_face_normals(mesh) 300 | 301 | # Flip face normals: 302 | input_validator = False 303 | while not input_validator: 304 | flip_face_normals = input('Is it necessary to flip face normals? (y/N)') 305 | if not flip_face_normals: 306 | flip_face_normals = False 307 | input_validator = True 308 | else: 309 | if flip_face_normals != 'n' and flip_face_normals != 'N' and flip_face_normals != 'y' and flip_face_normals != 'Y': 310 | print('Invalid input') 311 | elif flip_face_normals == 'n' or flip_face_normals == 'N': 312 | flip_face_normals = False 313 | input_validator = True 314 | elif flip_face_normals == 'y' or flip_face_normals == 'Y': 315 | flip_face_normals = True 316 | mesh = core.fn_flip_face_normals(mesh) 317 | input_validator = True 318 | 319 | # Generate mesh 320 | print('\nMesh generation in progress ...') 321 | mesh = core.fn_generate_mesh(tpms_type, tpms_design, c, thickness, sizes, cell_sizes, origin, unit_cell_mesh_resolution, mesh, flip_face_normals) 322 | 323 | 324 | # Export mesh 325 | input_validator = False 326 | while not input_validator: 327 | export_stl = input('\nDo you want to export the generated mesh as .STL? (Y/n)') 328 | if not export_stl: 329 | export_stl = True 330 | input_validator = True 331 | else: 332 | if export_stl != 'n' and export_stl != 'N' and export_stl != 'y' and export_stl != 'Y': 333 | print('Invalid input') 334 | elif export_stl == 'n' or export_stl == 'N': 335 | export_stl = False 336 | input_validator = True 337 | elif export_stl == 'y' or export_stl == 'Y': 338 | export_stl = True 339 | input_validator = True 340 | if export_stl: 341 | file_name = input('Enter file name (without extension): ') 342 | directory_path = input('Enter directory path (default = root folder): ') 343 | if not directory_path: 344 | directory_path = os.getcwd() 345 | core.fn_export_stl_file(mesh, file_name, directory_path) 346 | 347 | # Create another design: 348 | input_validator = False 349 | while not input_validator: 350 | active_session = input('\nDo you want to create another design? (Y/n)') 351 | if not active_session: 352 | active_session = True 353 | input_validator = True 354 | else: 355 | if active_session != 'n' and active_session != 'N' and active_session != 'y' and active_session != 'Y': 356 | print('Invalid input') 357 | elif active_session == 'n' or active_session == 'N': 358 | active_session = False 359 | input_validator = True 360 | exit(0) 361 | elif active_session == 'y' or active_session == 'Y': 362 | active_session = True 363 | input_validator = True -------------------------------------------------------------------------------- /src/TPMSgen_GUI.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | tpms_generator_gui 4 | 5 | 6 | 7 | 0 8 | 0 9 | 839 10 | 530 11 | 12 | 13 | 14 | 15 | 0 16 | 0 17 | 18 | 19 | 20 | TPMSgen 21 | 22 | 23 | 24 | 25 | 0 26 | 0 27 | 28 | 29 | 30 | 31 | 32 | 10 33 | 10 34 | 821 35 | 481 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 0 50 | 0 51 | 52 | 53 | 54 | 55 | 200 56 | 0 57 | 58 | 59 | 60 | 61 | true 62 | 63 | 64 | 65 | ACTIONS 66 | 67 | 68 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 0 77 | 0 78 | 79 | 80 | 81 | 82 | 200 83 | 0 84 | 85 | 86 | 87 | Qt::Horizontal 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 0 96 | 0 97 | 98 | 99 | 100 | 101 | 200 102 | 0 103 | 104 | 105 | 106 | Plot TPMS equation 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 0 115 | 0 116 | 117 | 118 | 119 | 120 | 200 121 | 0 122 | 123 | 124 | 125 | Check face normals 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 0 134 | 0 135 | 136 | 137 | 138 | 139 | 200 140 | 0 141 | 142 | 143 | 144 | Flip face normals 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 0 153 | 0 154 | 155 | 156 | 157 | 158 | 200 159 | 0 160 | 161 | 162 | 163 | Generate mesh 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 0 172 | 0 173 | 174 | 175 | 176 | 177 | 200 178 | 0 179 | 180 | 181 | 182 | View mesh 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 0 191 | 0 192 | 193 | 194 | 195 | 196 | 200 197 | 0 198 | 199 | 200 | 201 | Export STL file 202 | 203 | 204 | 205 | 206 | 207 | 208 | Qt::Vertical 209 | 210 | 211 | 212 | 200 213 | 40 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 0 223 | 0 224 | 225 | 226 | 227 | 228 | 200 229 | 0 230 | 231 | 232 | 233 | 234 | true 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 200 247 | 0 248 | 249 | 250 | 251 | Qt::Horizontal 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 0 260 | 0 261 | 262 | 263 | 264 | 265 | 200 266 | 0 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | Qt::Vertical 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 0 290 | 0 291 | 292 | 293 | 294 | 295 | true 296 | 297 | 298 | 299 | TPMS TYPOLOGY SELECTOR 300 | 301 | 302 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 303 | 304 | 305 | 306 | 307 | 308 | 309 | Qt::Horizontal 310 | 311 | 312 | 313 | 314 | 315 | 316 | Designs library: 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 0 325 | 0 326 | 327 | 328 | 329 | 330 | 270 331 | 0 332 | 333 | 334 | 335 | 336 | 16777215 337 | 280 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | mm 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 0 356 | 0 357 | 358 | 359 | 360 | 361 | 362 | 363 | Qt::AlignCenter 364 | 365 | 366 | 367 | 368 | 369 | 370 | Thickness: 371 | 372 | 373 | Qt::AlignCenter 374 | 375 | 376 | 377 | 378 | 379 | 380 | C: 381 | 382 | 383 | Qt::AlignCenter 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 0 392 | 0 393 | 394 | 395 | 396 | Qt::AlignCenter 397 | 398 | 399 | 400 | 401 | 402 | 403 | Qt::Vertical 404 | 405 | 406 | QSizePolicy::Fixed 407 | 408 | 409 | 410 | 20 411 | 30 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | Qt::Vertical 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 0 434 | 0 435 | 436 | 437 | 438 | Qt::AlignCenter 439 | 440 | 441 | 442 | 443 | 444 | 445 | mm 446 | 447 | 448 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter 449 | 450 | 451 | 452 | 453 | 454 | 455 | Y: 456 | 457 | 458 | Qt::AlignCenter 459 | 460 | 461 | 462 | 463 | 464 | 465 | mm 466 | 467 | 468 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter 469 | 470 | 471 | 472 | 473 | 474 | 475 | Y: 476 | 477 | 478 | Qt::AlignCenter 479 | 480 | 481 | 482 | 483 | 484 | 485 | Unit cell dimensions: 486 | 487 | 488 | 489 | 490 | 491 | 492 | X: 493 | 494 | 495 | Qt::AlignCenter 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 0 504 | 0 505 | 506 | 507 | 508 | Qt::AlignCenter 509 | 510 | 511 | 512 | 513 | 514 | 515 | mm 516 | 517 | 518 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 0 527 | 0 528 | 529 | 530 | 531 | X: 532 | 533 | 534 | Qt::AlignCenter 535 | 536 | 537 | 538 | 539 | 540 | 541 | Unit cell origin: 542 | 543 | 544 | 545 | 546 | 547 | 548 | mm 549 | 550 | 551 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter 552 | 553 | 554 | 555 | 556 | 557 | 558 | Bounding box dimensions: 559 | 560 | 561 | 562 | 563 | 564 | 565 | X: 566 | 567 | 568 | Qt::AlignCenter 569 | 570 | 571 | 572 | 573 | 574 | 575 | mm 576 | 577 | 578 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter 579 | 580 | 581 | 582 | 583 | 584 | 585 | Z: 586 | 587 | 588 | Qt::AlignCenter 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 0 597 | 0 598 | 599 | 600 | 601 | Qt::AlignCenter 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 0 610 | 0 611 | 612 | 613 | 614 | Qt::AlignCenter 615 | 616 | 617 | 618 | 619 | 620 | 621 | mm 622 | 623 | 624 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 0 633 | 0 634 | 635 | 636 | 637 | Qt::AlignCenter 638 | 639 | 640 | 641 | 642 | 643 | 644 | mm 645 | 646 | 647 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | true 656 | 657 | 658 | 659 | DESIGN CONFIGURATOR 660 | 661 | 662 | 663 | 664 | 665 | 666 | mm 667 | 668 | 669 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 0 678 | 0 679 | 680 | 681 | 682 | Qt::AlignCenter 683 | 684 | 685 | 686 | 687 | 688 | 689 | Z: 690 | 691 | 692 | Qt::AlignCenter 693 | 694 | 695 | 696 | 697 | 698 | 699 | mm 700 | 701 | 702 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter 703 | 704 | 705 | 706 | 707 | 708 | 709 | Z: 710 | 711 | 712 | Qt::AlignCenter 713 | 714 | 715 | 716 | 717 | 718 | 719 | Qt::Horizontal 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 0 728 | 0 729 | 730 | 731 | 732 | Qt::AlignCenter 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 0 741 | 0 742 | 743 | 744 | 745 | Qt::AlignCenter 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | Unit cell mesh resolution: 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 0 763 | 0 764 | 765 | 766 | 767 | Qt::AlignCenter 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | Y: 777 | 778 | 779 | Qt::AlignCenter 780 | 781 | 782 | 783 | 784 | 785 | 786 | 787 | 0 788 | 0 789 | 790 | 791 | 792 | Qt::AlignCenter 793 | 794 | 795 | 796 | 797 | 798 | 799 | 800 | 801 | 802 | 803 | 804 | 805 | Qt::Horizontal 806 | 807 | 808 | 809 | 810 | 811 | 812 | 813 | 10 814 | false 815 | 816 | 817 | 818 | Developed by Albert Forés Garriga, Héctor Garcia de la Torre, Ricard Lado Roigé, Giovanni Gómez Gras and Marco A. Pérez Martínez 819 | 820 | 821 | 822 | 823 | 824 | 825 | 826 | 10 827 | false 828 | 829 | 830 | 831 | IQS School of Engineering, Universitat Ramon Llull (Barcelona, Spain) 832 | 833 | 834 | 835 | 836 | 837 | 838 | 839 | 840 | 841 | 842 | 843 | -------------------------------------------------------------------------------- /TPMSgen_GUI.py: -------------------------------------------------------------------------------- 1 | from src import core 2 | 3 | import os 4 | import sys 5 | import trimesh 6 | 7 | import numpy as np 8 | import pyvista as pv 9 | 10 | from PyQt5 import uic 11 | from PyQt5.QtCore import QFileInfo 12 | from PyQt5.QtWidgets import QMainWindow, QApplication, QMessageBox, QDesktopWidget, QFileDialog, QSizeGrip 13 | from skimage import measure 14 | 15 | # GUI menu functions: 16 | class gui_menu(QMainWindow): 17 | def __init__(self): 18 | super().__init__() 19 | 20 | uic.loadUi('TPMSgen_GUI.ui', self) 21 | 22 | # Initialize variables: 23 | self.initialize_variables() 24 | 25 | # Initialize visibilities: 26 | self.reset_visibility() 27 | 28 | # Load default values: 29 | self.load_default_values() 30 | 31 | # TPMS design selector changed: 32 | self.designs_library.currentRowChanged.connect(self.fn_tpms_selector_changed) 33 | 34 | # Design configurator changeed parameters: 35 | self.skeletal_c_input.textChanged.connect(self.reset_visibility) 36 | self.shell_thickness_input.textChanged.connect(self.reset_visibility) 37 | self.bounding_box_x_dim_input.textChanged.connect(self.reset_visibility) 38 | self.bounding_box_y_dim_input.textChanged.connect(self.reset_visibility) 39 | self.bounding_box_z_dim_input.textChanged.connect(self.reset_visibility) 40 | self.unit_cell_x_dim_input.textChanged.connect(self.reset_visibility) 41 | self.unit_cell_y_dim_input.textChanged.connect(self.reset_visibility) 42 | self.unit_cell_z_dim_input.textChanged.connect(self.reset_visibility) 43 | self.unit_cell_origin_x_input.textChanged.connect(self.reset_visibility) 44 | self.unit_cell_origin_y_input.textChanged.connect(self.reset_visibility) 45 | self.unit_cell_origin_z_input.textChanged.connect(self.reset_visibility) 46 | self.mesh_resolution_input.textChanged.connect(self.reset_visibility) 47 | 48 | # Plot TPMS equation button pressed: 49 | self.plot_tpms_eq_button.clicked.connect(self.fn_plot_tpms_eq) 50 | 51 | # Check normals button pressed: 52 | self.check_normals_button.clicked.connect(self.fn_check_face_normals) 53 | 54 | # Flip normals button pressed: 55 | self.flip_normals_button.clicked.connect(self.fn_flip_face_normals) 56 | 57 | # Generate mesh button pressed: 58 | self.generate_mesh_button.clicked.connect(self.fn_generate_mesh) 59 | 60 | # View mesh button pressed: 61 | self.view_mesh_button.clicked.connect(self.fn_view_mesh) 62 | 63 | # Export STL file button pressed: 64 | self.export_stl_file_button.clicked.connect(self.fn_export_stl_file) 65 | 66 | def fn_tpms_selector_changed(self): 67 | # Checking TPMS type: 68 | tpms_type_changed = False 69 | row = self.designs_library.currentRow() 70 | if row < 5: 71 | if self.tpms_type != 'Shell': 72 | tpms_type_changed = True 73 | self.tpms_type = 'Shell' 74 | else: 75 | if self.tpms_type != 'Skeletal': 76 | tpms_type_changed = True 77 | self.tpms_type = 'Skeletal' 78 | self.tpms_design = self.tpms_library_items[row] 79 | 80 | # Updating data fields: 81 | if self.tpms_type == 'Shell': 82 | self.skeletal_c_input.setEnabled(False) 83 | self.skeletal_c_input.setText('0') 84 | self.c = 0 85 | self.shell_thickness_input.setEnabled(True) 86 | if tpms_type_changed: 87 | self.shell_thickness_input.setText('3') 88 | self.thickness = 3 89 | 90 | if self.tpms_type == 'Skeletal': 91 | self.skeletal_c_input.setEnabled(True) 92 | if tpms_type_changed: 93 | self.skeletal_c_input.setText('0.5') 94 | self.c = 0.5 95 | self.shell_thickness_input.setEnabled(False) 96 | self.shell_thickness_input.setText('--') 97 | self.thickness = 0 98 | 99 | del tpms_type_changed 100 | 101 | # Reset visibilities: 102 | self.reset_visibility() 103 | 104 | def fn_plot_tpms_eq(self): 105 | # Check input data warnings: 106 | self.check_input_data() 107 | 108 | # Plotting vertices: 109 | if not self.are_warnings: 110 | # Generation of the meshgrid: 111 | self.sizes = [float(self.bounding_box_x_dim_input.text()), float(self.bounding_box_y_dim_input.text()), float(self.bounding_box_z_dim_input.text())] 112 | self.cell_sizes = [float(self.unit_cell_x_dim_input.text()), float(self.unit_cell_y_dim_input.text()), float(self.unit_cell_z_dim_input.text())] 113 | self.origin = [float(self.unit_cell_origin_x_input.text()), float(self.unit_cell_origin_y_input.text()), float(self.unit_cell_origin_z_input.text())] 114 | self.unit_cell_mesh_resolution = int(self.mesh_resolution_input.text()) 115 | 116 | # Get C value or thickness: 117 | if self.tpms_type == 'Shell': 118 | self.c = 0 119 | self.thickness = float(self.shell_thickness_input.text()) 120 | else: 121 | self.c = float(self.skeletal_c_input.text()) 122 | self.thickness = None 123 | 124 | # Update output message: 125 | self.message_output_label.setText('Next step:') 126 | self.message_output_label.setStyleSheet('color : black') 127 | self.message_output.setText('Inspect the generated TPMS\ndesign and check the orientation\nof its face normals.') 128 | 129 | # Plot TPMS equation: 130 | self.mesh, self.vertices = core.fn_plot_tpms_eq(self.tpms_type, self.tpms_design, self.sizes, self.cell_sizes, self.origin, self.unit_cell_mesh_resolution, self.c, self.thickness, self.mesh) 131 | 132 | # Activate check normals button: 133 | self.change_visibility(self.check_normals_button, True) 134 | 135 | def fn_check_face_normals(self): 136 | # Update output message: 137 | self.message_output_label.setText('Next step:') 138 | self.message_output_label.setStyleSheet('color : black') 139 | self.message_output.setText('Check if face normals are\npointing OUT of the mesh. If not,\nplease flip them. Then proceeed\nto generate mesh.') 140 | 141 | # Check face normals: 142 | self.mesh = core.fn_check_face_normals(self.mesh, True) 143 | 144 | # Activate check normals button: 145 | self.change_visibility(self.flip_normals_button, True) 146 | 147 | # Activate flip normals button: 148 | self.change_visibility(self.generate_mesh_button, True) 149 | 150 | def fn_flip_face_normals(self): 151 | # Flip face normals 152 | self.mesh = core.fn_flip_face_normals(self.mesh, True) 153 | if self.flip_face_normals: 154 | self.flip_face_normals = False 155 | else: 156 | self.flip_face_normals = True 157 | 158 | # Update output message: 159 | self.message_output_label.setText('Next step:') 160 | self.message_output_label.setStyleSheet('color : black') 161 | self.message_output.setText('Check if face normals are\npointing OUT to the mesh. If not,\nplease flip them. Then proceeed\nto generate mesh.') 162 | 163 | # Deactivate check normals button: 164 | self.change_visibility(self.view_mesh_button, False) 165 | 166 | # Deactivate check normals button: 167 | self.change_visibility(self.export_stl_file_button, False) 168 | 169 | def fn_generate_mesh(self): 170 | # Generate mesh: 171 | self.iterative_mesh = core.fn_generate_mesh(self.tpms_type, self.tpms_design, self.c, self.thickness, self.sizes, self.cell_sizes, self.origin, self.unit_cell_mesh_resolution, self.mesh, self.flip_face_normals, True) 172 | 173 | # Update output message: 174 | if self.iterative_mesh.is_watertight: 175 | self.message_output_label.setText('Mesh is generated:') 176 | self.message_output_label.setStyleSheet('color : green') 177 | self.message_output.setText('The obtained mesh is watertight.\nIf the opposite solution was\ndesired, try flipping face normals.') 178 | else: 179 | self.message_output_label.setText('Mesh is generated:') 180 | self.message_output_label.setStyleSheet('color : orange') 181 | self.message_output.setText('Cannot obtain a watertight mesh.\nTry increasing unit cell mesh\nresolution. Please, check results\ncarefully and treat them to solve\nthis issue.') 182 | 183 | # Activate check normals button: 184 | self.change_visibility(self.view_mesh_button, True) 185 | 186 | # Activate check normals button: 187 | self.change_visibility(self.export_stl_file_button, True) 188 | 189 | def fn_view_mesh(self): 190 | # Plot generated mesh: 191 | self.plotter = pv.Plotter(window_size = [1400, 1600]) 192 | _ = self.plotter.add_title('Generated mesh can be exported into STL format', font_size = 10) 193 | _ = self.plotter.add_mesh(self.iterative_mesh, color = True, show_edges = True) 194 | _ = self.plotter.show_grid() 195 | self.plotter.show() 196 | 197 | def fn_export_stl_file(self): 198 | file_path, _ = QFileDialog.getSaveFileName(self, 'Export STL', None, 'STL Files (.stl);;All Files ()') 199 | 200 | if file_path != '': 201 | suffix = QFileInfo(file_path).suffix() 202 | if len(suffix) > 0 and suffix != '.stl': 203 | self.show_warning_messagebox('Incorrect file format for exporting the generated mesh.') 204 | else: 205 | file_path += '.stl' 206 | export = trimesh.exchange.stl.export_stl_ascii(self.iterative_mesh) 207 | with open(file_path, 'w') as file: 208 | file.write(export) 209 | 210 | def change_visibility(self, variable, state): 211 | variable.setEnabled(state) 212 | 213 | def check_input_data(self): 214 | # Warnings inspector: 215 | self.are_warnings = False 216 | 217 | # Checking thickness value: 218 | if not self.are_warnings: 219 | if self.tpms_type == 'Shell': 220 | try: 221 | float(self.shell_thickness_input.text()) 222 | if not (float(self.shell_thickness_input.text()) > 0): 223 | self.show_warning_messagebox('Please, check thickness value.') 224 | self.are_warnings = True 225 | except: 226 | self.show_warning_messagebox('Please, check thickness value.') 227 | self.are_warnings = True 228 | 229 | # Checking C value: 230 | if not self.are_warnings: 231 | if self.tpms_type == 'Skeletal': 232 | try: 233 | float(self.skeletal_c_input.text()) 234 | if not (float(self.skeletal_c_input.text()) != 0): 235 | self.show_warning_messagebox('Please, check C value.') 236 | self.are_warnings = True 237 | except: 238 | self.show_warning_messagebox('Please, check C value.') 239 | self.are_warnings = True 240 | 241 | # Checking bounding box dimensions: 242 | if not self.are_warnings: 243 | try: 244 | float(self.bounding_box_x_dim_input.text()) 245 | float(self.bounding_box_y_dim_input.text()) 246 | float(self.bounding_box_z_dim_input.text()) 247 | if not (float(self.bounding_box_x_dim_input.text()) > 0 and float(self.bounding_box_y_dim_input.text()) > 0 and float(self.bounding_box_z_dim_input.text()) > 0): 248 | self.show_warning_messagebox('Please, check bounnding box dimensions.') 249 | self.are_warnings = True 250 | except: 251 | self.show_warning_messagebox('Please, check bounnding box dimensions.') 252 | self.are_warnings = True 253 | 254 | # Checking unit cell dimensions: 255 | if not self.are_warnings: 256 | try: 257 | float(self.unit_cell_x_dim_input.text()) 258 | float(self.unit_cell_y_dim_input.text()) 259 | float(self.unit_cell_z_dim_input.text()) 260 | 261 | if not ((float(self.unit_cell_x_dim_input.text()) > 0 and float(self.unit_cell_x_dim_input.text()) <= float(self.bounding_box_x_dim_input.text())) and (float(self.unit_cell_y_dim_input.text()) > 0 and float(self.unit_cell_y_dim_input.text()) <= float(self.bounding_box_y_dim_input.text())) and (float(self.unit_cell_z_dim_input.text()) > 0 and float(self.unit_cell_z_dim_input.text()) <= float(self.bounding_box_z_dim_input.text()))): 262 | self.show_warning_messagebox('Please, check unit cell dimensions.') 263 | self.are_warnings = True 264 | except: 265 | self.show_warning_messagebox('Please, check unit cell dimensions.') 266 | self.are_warnings = True 267 | 268 | # Checking unit cell origin: 269 | if not self.are_warnings: 270 | try: 271 | float(self.unit_cell_origin_x_input.text()) 272 | float(self.unit_cell_origin_y_input.text()) 273 | float(self.unit_cell_origin_z_input.text()) 274 | except: 275 | self.show_warning_messagebox('Please, check unit cell origin.') 276 | self.are_warnings = True 277 | 278 | # Checking unit cell mesh resolution: 279 | if not self.are_warnings: 280 | try: 281 | int(self.mesh_resolution_input.text()) 282 | if not (int(self.mesh_resolution_input.text()) >= 20): 283 | self.show_warning_messagebox('Please, check unit cell mesh resolution value (minimum 20, type int.).') 284 | self.are_warnings = True 285 | except: 286 | self.show_warning_messagebox('Please, check unit cell mesh resolution value (minimum 20, type int.).') 287 | self.are_warnings = True 288 | 289 | def generate_meshgrid(self, k): 290 | tol_x = k * self.cell_sizes[0] / self.unit_cell_mesh_resolution 291 | tol_y = k * self.cell_sizes[1] / self.unit_cell_mesh_resolution 292 | tol_z = k * self.cell_sizes[2] / self.unit_cell_mesh_resolution 293 | tols = [tol_x, tol_y, tol_z] 294 | 295 | xl = np.linspace(-self.sizes[0]/2 - tols[0], self.sizes[0]/2 + tols[0], int(self.sizes[0] / self.cell_sizes[0]) * self.unit_cell_mesh_resolution + 2 * k + 1) 296 | yl = np.linspace(-self.sizes[1]/2 - tols[1], self.sizes[1]/2 + tols[1], int(self.sizes[1] / self.cell_sizes[1]) * self.unit_cell_mesh_resolution + 2 * k + 1) 297 | zl = np.linspace(-self.sizes[2]/2 - tols[2], self.sizes[2]/2 + tols[2], int(self.sizes[2] / self.cell_sizes[2]) * self.unit_cell_mesh_resolution + 2 * k + 1) 298 | spacing = [xl, yl, zl] 299 | 300 | Y, X, Z = np.meshgrid(yl, xl, zl) 301 | 302 | return X, Y, Z, tols, spacing 303 | 304 | def initialize_variables(self): 305 | self.are_warnings = None 306 | self.c = None 307 | self.cell_sizes = None 308 | self.flip_face_normals = False 309 | self.iterative_mesh = None 310 | self.mesh = None 311 | self.origin = None 312 | self.sizes = None 313 | self.t = None 314 | self.thickness = None 315 | self.tpms_library_items = None 316 | self.tpms_type = None 317 | self.tpms_design = None 318 | self.unit_cell_mesh_resolution = None 319 | self.vertices = None 320 | 321 | def load_default_values(self): 322 | # Initialize Skeletal-TPMS designs library: 323 | self.tpms_library_items = [ 324 | 'Shell-TPMS Gyroid', 325 | 'Shell-TPMS Diamond', 326 | 'Shell-TPMS Lidinoid', 327 | 'Shell-TPMS Split-P', 328 | 'Shell-TPMS Schwarz', 329 | 'Skeletal-TPMS Schoen gyroid', 330 | 'Skeletal-TPMS Schwarz diamond', 331 | 'Skeletal-TPMS Schwarz primitive (pinched)', 332 | 'Skeletal-TPMS Schwarz primitive', 333 | 'Skeletal-TPMS Body diagonals with nodes' 334 | ] 335 | self.designs_library.addItems(self.tpms_library_items) 336 | self.designs_library.setCurrentRow(0) 337 | self.tpms_type = 'Shell' 338 | self.tpms_design = self.tpms_library_items[0] 339 | 340 | self.bounding_box_x_dim_input.setText('40') 341 | self.bounding_box_y_dim_input.setText('40') 342 | self.bounding_box_z_dim_input.setText('40') 343 | 344 | self.unit_cell_x_dim_input.setText('40') 345 | self.unit_cell_y_dim_input.setText('40') 346 | self.unit_cell_z_dim_input.setText('40') 347 | 348 | self.unit_cell_origin_x_input.setText('0') 349 | self.unit_cell_origin_y_input.setText('0') 350 | self.unit_cell_origin_z_input.setText('0') 351 | 352 | self.mesh_resolution_input.setText('50') 353 | 354 | self.skeletal_c_input.setText('0') 355 | self.skeletal_c_input.setEnabled(False) 356 | 357 | self.shell_thickness_input.setText('3') 358 | 359 | # Update output message: 360 | self.message_output_label.setText('Start:') 361 | self.message_output_label.setStyleSheet('color : black') 362 | self.message_output.setText('Set your design parameters and\nplot the equation of the choosen \nTPMS typology.') 363 | 364 | def mesh_conversion(self, mesh_pv): 365 | faces_as_array = mesh_pv.faces.reshape((mesh_pv.n_faces, 4))[:, 1:] 366 | mesh = trimesh.Trimesh(mesh_pv.points, faces_as_array) 367 | 368 | return mesh 369 | 370 | def mesh_shell(self, F, mesh, tols, spacing): 371 | vertices_positive, faces_positive, vertex_normals_positive, _ = measure.marching_cubes(F, self.thickness * self.t, spacing = [np.diff(spacing[0])[0], np.diff(spacing[1])[0], np.diff(spacing[2])[0]]) 372 | vertices_negative, faces_negative, vertex_normals_negative, _ = measure.marching_cubes(F, -self.thickness * self.t, spacing = [np.diff(spacing[0])[0], np.diff(spacing[1])[0], np.diff(spacing[2])[0]]) 373 | 374 | for i, vert in enumerate(vertices_positive): 375 | vertices_positive[i, 0] = vert[0] - self.sizes[0]/2 - tols[0] 376 | vertices_positive[i, 1] = vert[1] - self.sizes[1]/2 - tols[1] 377 | vertices_positive[i, 2] = vert[2] - self.sizes[2]/2 - tols[2] 378 | 379 | for i, vert in enumerate(vertices_negative): 380 | vertices_negative[i, 0] = vert[0] - self.sizes[0]/2 - tols[0] 381 | vertices_negative[i, 1] = vert[1] - self.sizes[1]/2 - tols[1] 382 | vertices_negative[i, 2] = vert[2] - self.sizes[2]/2 - tols[2] 383 | 384 | vertices = np.concatenate((vertices_positive, vertices_negative)) 385 | 386 | mesh_1 = trimesh.Trimesh(vertices = vertices_positive, faces = faces_positive, vertex_normals = vertex_normals_positive) 387 | mesh_2 = trimesh.Trimesh(vertices = vertices_negative, faces = faces_negative, vertex_normals = vertex_normals_negative) 388 | mesh_2 = pv.wrap(mesh_2) 389 | mesh_2.flip_normals() 390 | mesh_2 = self.mesh_conversion(mesh_2) 391 | 392 | mesh = trimesh.util.concatenate((mesh_1, mesh_2)) 393 | 394 | del vertices_positive, faces_positive, vertex_normals_positive, vertices_negative, faces_negative, vertex_normals_negative, mesh_1, mesh_2 395 | 396 | return mesh, vertices 397 | 398 | def mesh_skeletal(self, F, mesh, tols, spacing): 399 | vertices, faces, _, _ = measure.marching_cubes(F, 0, spacing = [np.diff(spacing[0])[0], np.diff(spacing[1])[0], np.diff(spacing[2])[0]]) 400 | for i, vert in enumerate(vertices): 401 | vertices[i, 0] = vert[0] - self.sizes[0]/2 - tols[0] 402 | vertices[i, 1] = vert[1] - self.sizes[1]/2 - tols[1] 403 | vertices[i, 2] = vert[2] - self.sizes[2]/2 - tols[2] 404 | 405 | mesh = trimesh.Trimesh(vertices = vertices, faces = faces) 406 | 407 | del faces 408 | 409 | return mesh, vertices 410 | 411 | def reset_visibility(self): 412 | self.check_normals_button.setEnabled(False) 413 | self.flip_normals_button.setEnabled(False) 414 | self.generate_mesh_button.setEnabled(False) 415 | self.view_mesh_button.setEnabled(False) 416 | self.export_stl_file_button.setEnabled(False) 417 | 418 | # Update output message: 419 | self.message_output_label.setText('Start:') 420 | self.message_output_label.setStyleSheet('color : black') 421 | self.message_output.setText('Set your design parameters and\nplot the equation of the choosen \nTPMS typology.') 422 | 423 | def show_warning_messagebox(self, text): 424 | # Initialize message Box 425 | msg = QMessageBox() 426 | 427 | # Setting icon for message box 428 | msg.setIcon(QMessageBox.Warning) 429 | 430 | # Setting message for message Box 431 | msg.setText('Warning\t\t\t') 432 | 433 | # Setting message box window title 434 | msg.setWindowTitle('Warning') 435 | 436 | # Setting message box informative text 437 | msg.setInformativeText(text) 438 | 439 | # Declaring buttons on message Box 440 | msg.setStandardButtons(QMessageBox.Ok) 441 | 442 | # Show message box 443 | retval = msg.exec_() 444 | 445 | def tpms_library(self, X, Y, Z): 446 | w_x = 1 / self.cell_sizes[0] * 2 * np.pi 447 | w_y = 1 / self.cell_sizes[1] * 2 * np.pi 448 | w_z = 1 / self.cell_sizes[2] * 2 * np.pi 449 | 450 | if self.tpms_design == 'Skeletal-TPMS Schoen gyroid' or self.tpms_design == 'Shell-TPMS Gyroid': 451 | F = (np.cos(w_x * (X + self.origin[0])) * np.sin(w_y * (Y + self.origin[1])) + np.cos(w_y * (Y + self.origin[1])) * np.sin(w_z * (Z + self.origin[2])) + np.cos(w_z * (Z + self.origin[2])) * np.sin(w_x * (X + self.origin[0])) - self.c) # J 452 | self.t = 0.125 453 | 454 | elif self.tpms_design == 'Skeletal-TPMS Schwarz diamond': 455 | F = (np.cos(w_x * (X + self.origin[0])) * np.cos(w_y * (Y + self.origin[1])) * np.cos(w_z * (Z + self.origin[2])) + np.sin(w_x * (X + self.origin[0])) * np.sin(w_y * (Y + self.origin[1])) * np.sin(w_z * (Z + self.origin[2])) - self.c) # K 456 | 457 | elif self.tpms_design == 'Skeletal-TPMS Schwarz primitive (pinched)' or self.tpms_design == 'Skeletal-TPMS Schwarz primitive': 458 | F = (np.cos(w_x * (X + self.origin[0])) + np.cos(w_y * (Y + self.origin[1])) + np.cos(w_z * (Z + self.origin[2])) - self.c) # M, N 459 | 460 | elif self.tpms_design == 'Skeletal-TPMS Body diagonals with nodes': 461 | F = (2 * (np.cos(w_x * ((X + self.origin[0]))) * np.cos(w_y * (Y + self.origin[1])) + np.cos(w_y * (Y + self.origin[1])) * np.cos(w_z * (Z + self.origin[2])) + np.cos(w_z * (Z + self.origin[2])) * np.cos(w_x * (X + self.origin[0]))) - (np.cos(2 * w_x * (X + self.origin[0])) + np.cos(2 * w_y * (Y + self.origin[1])) + np.cos(2 * w_z * (Z + self.origin[2]))) - self.c) # O 462 | 463 | elif self.tpms_design == 'Shell-TPMS Diamond': 464 | F = (np.sin(w_x * (X + self.origin[0])) * np.sin(w_y * (Y + self.origin[1])) * np.sin(w_z * (Z + self.origin[2])) + np.sin(w_x * (X + self.origin[0])) * np.cos(w_y * (Y + self.origin[1])) * np.cos(w_z * (Z + self.origin[2])) + np.cos(w_x * (X + self.origin[0])) * np.sin(w_y * (Y + self.origin[1])) * np.cos(w_z * (Z + self.origin[2])) + np.cos(w_x * (X + self.origin[0])) * np.cos(w_y * (Y + self.origin[1])) * np.sin(w_z * (Z + self.origin[2])) - self.c) # Q 465 | self.t = 0.115 466 | 467 | elif self.tpms_design == 'Shell-TPMS Lidinoid': 468 | F = (np.sin(2 * w_x * (X + self.origin[0])) * np.cos(w_y * (Y + self.origin[1])) * np.sin(w_z * (Z + self.origin[2])) + np.sin(w_x * (X + self.origin[0])) * np.sin(2 * w_y * (Y + self.origin[1])) * np.cos(w_z * (Z + self.origin[2])) + np.cos(w_x * (X + self.origin[0])) * np.sin(w_y * (Y + self.origin[1])) * np.sin(2 * w_z * (Z + self.origin[2])) - np.cos(2 * w_x * (X + self.origin[0])) * np.cos(2 * w_y * (Y + self.origin[1])) - np.cos(2 * w_y * (Y + self.origin[1])) * np.cos(2 * w_z * (Z + self.origin[2])) - np.cos(2 * w_z * (Z + self.origin[2])) * np.cos(2 * w_x * (X + self.origin[0])) + 0.3 - self.c) 469 | self.t = 0.37 470 | 471 | elif self.tpms_design == 'Shell-TPMS Split-P': 472 | F = (1.1 * (np.sin(2 * w_x * (X + self.origin[0])) * np.cos(w_y * (Y + self.origin[1])) * np.sin(w_z * (Z + self.origin[2])) + np.sin(w_x * (X + self.origin[0])) * np.sin(2 * w_y * (Y + self.origin[1])) * np.cos(w_z * (Z + self.origin[2])) + np.cos(w_x * (X + self.origin[0])) * np.sin(w_y * (Y + self.origin[1])) * np.sin(2 * w_z * (Z + self.origin[2]))) - 0.2 * (np.cos(2 * w_x * (X + self.origin[0])) * np.cos(2 * w_y * (Y + self.origin[1])) + np.cos(2 * w_y * (Y + self.origin[1])) * np.cos(2 * w_z * (Z + self.origin[2])) + np.cos(2 * w_z * (Z + self.origin[2])) * np.cos(2 * w_x * (X + self.origin[0]))) - 0.4 * (np.cos(2 * w_x * (X + self.origin[0])) + np.cos(2 * w_y * (Y + self.origin[1])) + np.cos(2 * w_z * (Z + self.origin[2]))) - self.c) 473 | self.t = 0.19 474 | 475 | elif self.tpms_design == 'Shell-TPMS Schwarz': 476 | F = (np.cos(w_x * (X + self.origin[0])) + np.cos(w_y * (Y + self.origin[1])) + np.cos(w_z * (Z + self.origin[2])) - self.c) 477 | self.t = 0.0875 478 | 479 | else: 480 | print('Design not found in library') 481 | F = 0 482 | self.t = 0 483 | 484 | return F 485 | 486 | if __name__ == "__main__": 487 | # ui 488 | ui = ''' 489 | 490 | tpms_generator_gui 491 | 492 | 493 | 494 | 0 495 | 0 496 | 839 497 | 530 498 | 499 | 500 | 501 | 502 | 0 503 | 0 504 | 505 | 506 | 507 | TPMSgen 508 | 509 | 510 | 511 | 512 | 0 513 | 0 514 | 515 | 516 | 517 | 518 | 519 | 10 520 | 10 521 | 821 522 | 481 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 0 537 | 0 538 | 539 | 540 | 541 | 542 | 200 543 | 0 544 | 545 | 546 | 547 | 548 | true 549 | 550 | 551 | 552 | ACTIONS 553 | 554 | 555 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 0 564 | 0 565 | 566 | 567 | 568 | 569 | 200 570 | 0 571 | 572 | 573 | 574 | Qt::Horizontal 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 0 583 | 0 584 | 585 | 586 | 587 | 588 | 200 589 | 0 590 | 591 | 592 | 593 | Plot TPMS equation 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 0 602 | 0 603 | 604 | 605 | 606 | 607 | 200 608 | 0 609 | 610 | 611 | 612 | Check face normals 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 0 621 | 0 622 | 623 | 624 | 625 | 626 | 200 627 | 0 628 | 629 | 630 | 631 | Flip face normals 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 0 640 | 0 641 | 642 | 643 | 644 | 645 | 200 646 | 0 647 | 648 | 649 | 650 | Generate mesh 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 0 659 | 0 660 | 661 | 662 | 663 | 664 | 200 665 | 0 666 | 667 | 668 | 669 | View mesh 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 0 678 | 0 679 | 680 | 681 | 682 | 683 | 200 684 | 0 685 | 686 | 687 | 688 | Export STL file 689 | 690 | 691 | 692 | 693 | 694 | 695 | Qt::Vertical 696 | 697 | 698 | 699 | 200 700 | 40 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 0 710 | 0 711 | 712 | 713 | 714 | 715 | 200 716 | 0 717 | 718 | 719 | 720 | 721 | true 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 200 734 | 0 735 | 736 | 737 | 738 | Qt::Horizontal 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 0 747 | 0 748 | 749 | 750 | 751 | 752 | 200 753 | 0 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | 765 | 766 | Qt::Vertical 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 0 777 | 0 778 | 779 | 780 | 781 | 782 | true 783 | 784 | 785 | 786 | TPMS TYPOLOGY SELECTOR 787 | 788 | 789 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 790 | 791 | 792 | 793 | 794 | 795 | 796 | Qt::Horizontal 797 | 798 | 799 | 800 | 801 | 802 | 803 | Designs library: 804 | 805 | 806 | 807 | 808 | 809 | 810 | 811 | 0 812 | 0 813 | 814 | 815 | 816 | 817 | 270 818 | 0 819 | 820 | 821 | 822 | 823 | 16777215 824 | 280 825 | 826 | 827 | 828 | 829 | 830 | 831 | 832 | 833 | 834 | mm 835 | 836 | 837 | 838 | 839 | 840 | 841 | 842 | 0 843 | 0 844 | 845 | 846 | 847 | 848 | 849 | 850 | Qt::AlignCenter 851 | 852 | 853 | 854 | 855 | 856 | 857 | Thickness: 858 | 859 | 860 | Qt::AlignCenter 861 | 862 | 863 | 864 | 865 | 866 | 867 | C: 868 | 869 | 870 | Qt::AlignCenter 871 | 872 | 873 | 874 | 875 | 876 | 877 | 878 | 0 879 | 0 880 | 881 | 882 | 883 | Qt::AlignCenter 884 | 885 | 886 | 887 | 888 | 889 | 890 | Qt::Vertical 891 | 892 | 893 | QSizePolicy::Fixed 894 | 895 | 896 | 897 | 20 898 | 30 899 | 900 | 901 | 902 | 903 | 904 | 905 | 906 | 907 | 908 | 909 | 910 | Qt::Vertical 911 | 912 | 913 | 914 | 915 | 916 | 917 | 918 | 919 | 920 | 0 921 | 0 922 | 923 | 924 | 925 | Qt::AlignCenter 926 | 927 | 928 | 929 | 930 | 931 | 932 | mm 933 | 934 | 935 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter 936 | 937 | 938 | 939 | 940 | 941 | 942 | Y: 943 | 944 | 945 | Qt::AlignCenter 946 | 947 | 948 | 949 | 950 | 951 | 952 | mm 953 | 954 | 955 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter 956 | 957 | 958 | 959 | 960 | 961 | 962 | Y: 963 | 964 | 965 | Qt::AlignCenter 966 | 967 | 968 | 969 | 970 | 971 | 972 | Unit cell dimensions: 973 | 974 | 975 | 976 | 977 | 978 | 979 | X: 980 | 981 | 982 | Qt::AlignCenter 983 | 984 | 985 | 986 | 987 | 988 | 989 | 990 | 0 991 | 0 992 | 993 | 994 | 995 | Qt::AlignCenter 996 | 997 | 998 | 999 | 1000 | 1001 | 1002 | mm 1003 | 1004 | 1005 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter 1006 | 1007 | 1008 | 1009 | 1010 | 1011 | 1012 | 1013 | 0 1014 | 0 1015 | 1016 | 1017 | 1018 | X: 1019 | 1020 | 1021 | Qt::AlignCenter 1022 | 1023 | 1024 | 1025 | 1026 | 1027 | 1028 | Unit cell origin: 1029 | 1030 | 1031 | 1032 | 1033 | 1034 | 1035 | mm 1036 | 1037 | 1038 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter 1039 | 1040 | 1041 | 1042 | 1043 | 1044 | 1045 | Bounding box dimensions: 1046 | 1047 | 1048 | 1049 | 1050 | 1051 | 1052 | X: 1053 | 1054 | 1055 | Qt::AlignCenter 1056 | 1057 | 1058 | 1059 | 1060 | 1061 | 1062 | mm 1063 | 1064 | 1065 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter 1066 | 1067 | 1068 | 1069 | 1070 | 1071 | 1072 | Z: 1073 | 1074 | 1075 | Qt::AlignCenter 1076 | 1077 | 1078 | 1079 | 1080 | 1081 | 1082 | 1083 | 0 1084 | 0 1085 | 1086 | 1087 | 1088 | Qt::AlignCenter 1089 | 1090 | 1091 | 1092 | 1093 | 1094 | 1095 | 1096 | 0 1097 | 0 1098 | 1099 | 1100 | 1101 | Qt::AlignCenter 1102 | 1103 | 1104 | 1105 | 1106 | 1107 | 1108 | mm 1109 | 1110 | 1111 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter 1112 | 1113 | 1114 | 1115 | 1116 | 1117 | 1118 | 1119 | 0 1120 | 0 1121 | 1122 | 1123 | 1124 | Qt::AlignCenter 1125 | 1126 | 1127 | 1128 | 1129 | 1130 | 1131 | mm 1132 | 1133 | 1134 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter 1135 | 1136 | 1137 | 1138 | 1139 | 1140 | 1141 | 1142 | true 1143 | 1144 | 1145 | 1146 | DESIGN CONFIGURATOR 1147 | 1148 | 1149 | 1150 | 1151 | 1152 | 1153 | mm 1154 | 1155 | 1156 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter 1157 | 1158 | 1159 | 1160 | 1161 | 1162 | 1163 | 1164 | 0 1165 | 0 1166 | 1167 | 1168 | 1169 | Qt::AlignCenter 1170 | 1171 | 1172 | 1173 | 1174 | 1175 | 1176 | Z: 1177 | 1178 | 1179 | Qt::AlignCenter 1180 | 1181 | 1182 | 1183 | 1184 | 1185 | 1186 | mm 1187 | 1188 | 1189 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter 1190 | 1191 | 1192 | 1193 | 1194 | 1195 | 1196 | Z: 1197 | 1198 | 1199 | Qt::AlignCenter 1200 | 1201 | 1202 | 1203 | 1204 | 1205 | 1206 | Qt::Horizontal 1207 | 1208 | 1209 | 1210 | 1211 | 1212 | 1213 | 1214 | 0 1215 | 0 1216 | 1217 | 1218 | 1219 | Qt::AlignCenter 1220 | 1221 | 1222 | 1223 | 1224 | 1225 | 1226 | 1227 | 0 1228 | 0 1229 | 1230 | 1231 | 1232 | Qt::AlignCenter 1233 | 1234 | 1235 | 1236 | 1237 | 1238 | 1239 | 1240 | 1241 | Unit cell mesh resolution: 1242 | 1243 | 1244 | 1245 | 1246 | 1247 | 1248 | 1249 | 0 1250 | 0 1251 | 1252 | 1253 | 1254 | Qt::AlignCenter 1255 | 1256 | 1257 | 1258 | 1259 | 1260 | 1261 | 1262 | 1263 | Y: 1264 | 1265 | 1266 | Qt::AlignCenter 1267 | 1268 | 1269 | 1270 | 1271 | 1272 | 1273 | 1274 | 0 1275 | 0 1276 | 1277 | 1278 | 1279 | Qt::AlignCenter 1280 | 1281 | 1282 | 1283 | 1284 | 1285 | 1286 | 1287 | 1288 | 1289 | 1290 | 1291 | 1292 | Qt::Horizontal 1293 | 1294 | 1295 | 1296 | 1297 | 1298 | 1299 | 1300 | 10 1301 | false 1302 | 1303 | 1304 | 1305 | Developed by Albert Forés Garriga, Héctor Garcia de la Torre, Ricard Lado Roigé, Giovanni Gómez Gras and Marco A. Pérez Martínez 1306 | 1307 | 1308 | 1309 | 1310 | 1311 | 1312 | 1313 | 10 1314 | false 1315 | 1316 | 1317 | 1318 | IQS School of Engineering, Universitat Ramon Llull (Barcelona, Spain) 1319 | 1320 | 1321 | 1322 | 1323 | 1324 | 1325 | 1326 | 1327 | 1328 | 1329 | 1330 | ''' 1331 | 1332 | with open('TPMSgen_GUI.ui', 'w') as file: 1333 | file.write(ui) 1334 | 1335 | app = QApplication(sys.argv) 1336 | GUI = gui_menu() 1337 | GUI.move(800, int((QDesktopWidget().screenGeometry().height() - GUI.height()) / 2)) 1338 | GUI.show() 1339 | sys.exit(app.exec_()) --------------------------------------------------------------------------------