├── 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 | 
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 |  | 
18 |  | 
19 |  | 
20 |  | 
21 |  | 
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 | 
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 | 
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 | 
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 | 
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_())
--------------------------------------------------------------------------------