├── .gitattributes ├── LICENSE ├── MANIFEST.in ├── README.md ├── examples ├── CC0.txt ├── quatrefoil_voronoi.py └── shield.py ├── lgpl-2.1.txt ├── meshlabxml ├── __init__.py ├── clean.py ├── color_names.py ├── color_names.txt ├── compute.py ├── create.py ├── delete.py ├── files.py ├── layers.py ├── mlx.py ├── mp_func.py ├── normals.py ├── remesh.py ├── sampling.py ├── select.py ├── smooth.py ├── subdivide.py ├── texture.py ├── transfer.py ├── transform.py ├── util.py └── vert_color.py ├── models ├── bunny.txt ├── bunny_flat(1Z).ply └── bunny_raw(-1250Y).ply ├── setup.cfg ├── setup.py └── test ├── black.png ├── blue.png ├── cyan.png ├── green.png ├── magenta.png ├── red.png ├── white.png └── yellow.png /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto 3 | 4 | # Explicitly declare text files you want to always be normalized and converted 5 | # to native line endings on checkout. 6 | #*.c text 7 | #*.h text 8 | 9 | # Declare files that will always have CRLF line endings on checkout. 10 | #*.sln text eol=crlf 11 | 12 | # Denote all files that are truly binary and should not be modified. 13 | *.png binary 14 | *.jpg binary 15 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include lgpl-2.1.txt 2 | include README.md 3 | include examples/CC0.txt 4 | include examples/shield.py 5 | include examples/quatrefoil_voronoi.py 6 | include meshlabxml/color_names.txt 7 | include models/bunny.txt 8 | include models/bunny_raw(-1250Y).ply 9 | include models/bunny_flat(1Z).ply 10 | recursive-exclude test * 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![mlx_logo](https://user-images.githubusercontent.com/15272764/30234183-88b1588e-94c0-11e7-9cce-6252e3c39237.png) 2 | 3 | 4 | MLX, or **M**esh**L**ab**X**ML, is a Python (2.7 or 3) scripting interface to [MeshLab](http://www.meshlab.net/), the open source system for processing and editing 3D triangular meshes. 5 | 6 | Under the hood, MLX generates XML filter scripts that can then be executed headless with the meshlabserver executable or run in the MeshLab GUI. It can also parse some of MeshLab's output, such as the results of the measure_geometry and measure_topology functions. 7 | 8 | MLX is named after the .mlx file extension for MeshLab script files, however the name was already taken on PyPi (for an unrelated machine learning library), so it is formally registered under the longer name of MeshLabXML. 9 | 10 | ## Installation 11 | 12 | MLX can be installed via [PyPI](https://pypi.org/project/MeshLabXML/) and pip: 13 | 14 | pip install meshlabxml 15 | 16 | The released PyPI version may lag behind this git repository somewhat, so install from git if you want the latest and greatest. MLX may also be installed and run in other Python environments, such as [Blender](https://www.blender.org/). Note that Blender does not come with pip by default, however it can be easily installed using [get-pip](https://bootstrap.pypa.io/get-pip.py). 17 | 18 | ## Platforms & Versions 19 | 20 | *Platforms:* MLX should work anywhere that MeshLab will run, including Windows, Mac & Linux, although it is only routinely tested on 64 bit Windows. 21 | 22 | *Python:* MLX should work under Python 2.7 and 3.x, although it is only routinely tested on 64 bit >=3.5 23 | 24 | *MeshLab:* MLX is known to work on MeshLab versions 1.34BETA (64 bit Windows only) and 2016.12. As of the time of this writing, not all functions have been tested with 2016.12 yet, so please open an issue if you find a bug. 25 | 26 | *MLX version numbers:* PyPI releases are numbered by the year and month of release, e.g. 2017.9. A letter may be added on the end ("a", "b", "c", etc) if there is more than one release in a month. 27 | 28 | ## Filters & Functions 29 | 30 | MLX contains a fairly large subset of the filters available in MeshLab. Additional filters will be added over time, generally on an "as I need them" basis. If you need a filter that is not yet incorporated, please open an issue. 31 | 32 | Many of the functions below are a direct implementation of a MeshLab filter. Others are created from a combination of other functions, or implement new functionality using the [muparser](http://beltoforion.de/article.php?a=muparser) function filters. 33 | 34 | Documentation for most filters is available by using "help" within a Python shell, although there are many that still need to be documented. In addition, in many cases the documentation is taken directly from the MeshLab filter, which is not always sufficient to understand the function if you are not already familiar with how it works. 35 | 36 | *mlx* - functions to create and run scripts, determine inputs & outputs, etc. 37 | * FilterScript - Main class to create scripts 38 | * create_mlp 39 | * find_texture_files 40 | * default_output_mask 41 | * run 42 | 43 | *mlx.create* - functions that create a new mesh 44 | * grid 45 | * cube 46 | * cube_hires 47 | * cube_open_hires 48 | * cylinder 49 | * cylinder_open_hires 50 | * tube_hires 51 | * icosphere 52 | * half_sphere_hires 53 | * sphere_cap 54 | * plane_hires_edges 55 | * annulus 56 | * annulus_hires 57 | * torus 58 | 59 | *mlx.transform* - functions that transform, deform or morph mesh geometry 60 | * translate 61 | * translate2 62 | * rotate 63 | * rotate2 64 | * scale 65 | * scale2 66 | * freeze_matrix 67 | * function 68 | * function_cyl_co 69 | * wrap2cylinder 70 | * wrap2sphere 71 | * emboss_sphere 72 | * bend 73 | * deform2curve 74 | 75 | *mlx.select* - functions that work with selections 76 | * all 77 | * none 78 | * invert 79 | * border 80 | * grow 81 | * shrink 82 | * self_intersecting_face 83 | * nonmanifold_vert 84 | * nonmanifold_edge 85 | * small_parts 86 | * vert_quality 87 | * face_function 88 | * vert_function 89 | * cylindrical_vert 90 | * spherical_vert 91 | 92 | *mlx.delete* - functions that delete faces and/or vertices 93 | * nonmanifold_vert 94 | * nonmanifold_edge 95 | * small_parts 96 | * selected 97 | * faces_from_nonmanifold_edges 98 | * unreferenced_vert 99 | * duplicate_faces 100 | * duplicate_verts 101 | * zero_area_face 102 | 103 | *mlx.clean* - functions to clean and repair a mesh 104 | * merge_vert 105 | * close_holes 106 | * split_vert_on_nonmanifold_face 107 | * fix_folded_face 108 | * snap_mismatched_borders 109 | 110 | *mlx.layers* - functions that work with mesh layers 111 | * join 112 | * delete 113 | * rename 114 | * change 115 | * duplicate 116 | * split_parts 117 | 118 | *mlx.normals* - functions that work with normals 119 | * reorient 120 | * flip 121 | * fix 122 | * point_sets 123 | 124 | *mlx.remesh* - remeshing functions 125 | * simplify 126 | * uniform_resampling 127 | * hull 128 | * surface_poisson 129 | * surface_poisson_screened 130 | * curvature_flipping 131 | * voronoi 132 | 133 | *mlx.sampling* - sampling functions 134 | * hausdorff_distance 135 | * poisson_disk 136 | * mesh_element 137 | * clustered_vert 138 | 139 | *mlx.smooth* - smoothing functions 140 | * laplacian 141 | * hc_laplacian 142 | * taubin 143 | * twostep 144 | * depth 145 | 146 | *mlx.subdivide* - subdivision functions 147 | * loop 148 | * ls3loop 149 | * midpoint 150 | * butterfly 151 | * catmull_clark 152 | 153 | *mlx.texture* - functions that work with textures and UV mapping (parameterization) 154 | * flat_plane 155 | * per_triangle 156 | * voronoi 157 | * isometric 158 | * isometric_build_atlased_mesh 159 | * isometric_save 160 | * isometric_load 161 | * isometric_transfer 162 | * isometric_remesh 163 | * set_texture 164 | * project_rasters 165 | * param_texture_from_rasters 166 | * param_from_rasters 167 | 168 | *mlx.transfer* - functions to transfer attributes 169 | * tex2vc 170 | * vc2tex 171 | * fc2vc 172 | * vc2fc 173 | * mesh2fc 174 | * vert_attr_2_meshes 175 | * vert_attr2tex_2_meshes 176 | * tex2vc_2_meshes 177 | 178 | *mlx.compute* - functions that measure or perform a computation 179 | * section 180 | * measure_geometry 181 | * measure_topology 182 | * parse_geometry 183 | * parse_topology 184 | 185 | *mlx.vert_color* - functions that work with vertex colors 186 | * function 187 | * voronoi 188 | * cyclic_rainbow 189 | 190 | *mlx.mp_func* - functions to work with muparser filter functions, this is mostly a vector math library 191 | * muparser_ref 192 | * v_cross 193 | * v_dot 194 | * v_add 195 | * v_subtract 196 | * v_multiply 197 | * v_length 198 | * v_normalize 199 | * torus_knot 200 | * torus_knot_bbox 201 | * vert_attr 202 | * face_attr 203 | * vq_function 204 | * fq_function 205 | 206 | *mlx.files* - functions that operate directly on files, usually to measure them 207 | * measure_aabb 208 | * measure_section 209 | * measure_geometry 210 | * measure_topology 211 | * measure_all 212 | * measure_dimension 213 | 214 | 215 | ## Possible Workflow 216 | 217 | For production MLX can run completely headless, however while developing new scripts some visual feedback can be helpful. 218 | 219 | MLX does not have an integrated GUI like OpenSCAD or Blender, however you can simulate one by arranging several programs into a useful layout, such as shown below. This can be accomplished pretty easily in modern versions of Windows using the Windows key and the arrow keys. 220 | 221 | Generally you will want a text editor, a console and of course MeshLab itself. The general script development workflow may consist of the following steps 222 | 1. Write & edit script in text editor 223 | 2. Run script in console and view meshlabserver output 224 | 3. Load output in MeshLab. Use the "reload" button to reload the mesh after any changes are made. 225 | 4. Repeat ad nauseam 226 | 227 | ![workflow_2016 12](https://user-images.githubusercontent.com/15272764/30234194-9dd84f10-94c0-11e7-89a3-cd5d0acb598f.png) 228 | 229 | ## Examples 230 | 231 | Some simple examples are shown below. These assume that the meshlabserver executable is in your path. If it is not already in your path, you can add it to your path in your script using something similar to the following: 232 | 233 | import os 234 | 235 | meshlabserver_path = 'C:\\Program Files\\VCG\\MeshLab' 236 | os.environ['PATH'] = meshlabserver_path + os.pathsep + os.environ['PATH'] 237 | 238 | 239 | Example #1: Create an orange cube and apply some transformations 240 | 241 | import meshlabxml as mlx 242 | 243 | orange_cube = mlx.FilterScript(file_out='orange_cube.ply', ml_version='2016.12') 244 | mlx.create.cube(orange_cube, size=[3.0, 4.0, 5.0], center=True, color='orange') 245 | mlx.transform.rotate(orange_cube, axis='x', angle=45) 246 | mlx.transform.rotate(orange_cube, axis='y', angle=45) 247 | mlx.transform.translate(orange_cube, value=[0, 5.0, 0]) 248 | orange_cube.run_script() 249 | 250 | Output: 251 | 252 | ![orange_cube](https://user-images.githubusercontent.com/15272764/30234857-4214198c-94c7-11e7-80b4-e0d73ee60126.png) 253 | 254 | 255 | Example #2: Measure the built-in Stanford Bunny test model and print the results 256 | 257 | import meshlabxml as mlx 258 | 259 | aabb, geometry, topology = mlx.files.measure_all('bunny', ml_version='2016.12') 260 | 261 | Output: 262 | 263 | max = [103.817589, 87.032661, 191.203903] 264 | diagonal = 310.4472554416525 265 | size = [193.95133199999998, 149.001499, 191.203903] 266 | center = [6.841923000000001, 12.531911500000003, 95.6019515] 267 | min = [-90.133743, -61.968838, 0.0] 268 | volume_cm3 = 1486.804 269 | inertia_tensor = [[3170550016.0, -46370464.0, 987163904.0], [-46370464.0, 5072923136.0, -58690096.0], [987163904.0, -58690096.0, 3817212928.0]] 270 | area_cm2 = 901.506875 271 | center_of_mass = [1.747876, -2.226919, 64.788971] 272 | total_edge_length = 179819.484375 273 | volume_mm3 = 1486804.0 274 | principal_axes = [[0.809736, 0.001188, -0.586793], [0.581329, 0.134539, 0.802468], [-0.0799, 0.990908, -0.108251]] 275 | total_edge_length_incl_faux = 179819.484375 276 | area_mm2 = 90150.6875 277 | axis_momenta = [2455110656.0, 4522500608.0, 5083073024.0] 278 | genus = 0 279 | manifold = True 280 | non_manifold_E = 0 281 | vert_num = 32285 282 | boundry_edge_num = 0 283 | hole_num = 0 284 | unref_vert_num = 0 285 | edge_num = 96849 286 | face_num = 64566 287 | non_manifold_V = 0 288 | part_num = 1 289 | 290 | Check out the "examples" directory for more complex examples. 291 | 292 | ## Logo 293 | 294 | The MLX logo is a rainbow colored quatrefoil torus knot with Voronoi meshing. This model is created entirely using MLX; it's source code is included in the "examples" directory. This is a moderately complex script that should give you an idea of some of the things that MLX can do; it makes heavy use of the powerful muparser functions. 295 | 296 | 297 | ## Tips 298 | 299 | * MeshLabServer can be a bit unstable, especially on certain filters such as mlx.remesh.uniform_resampling. If you have many filters to run, it is better to break the project up into smaller scripts and run problematic filters or sequences independently. 300 | * It is not currently possible to measure a mesh and use the results in the same script; you will need to measure the mesh and input the results into another instance. For example, if you want to simplify a mesh based on a percentage of the number of faces, you would first need to measure the number of faces with mlx.compute.measure_topology, then input the results into mlx.remesh.simplify. 301 | 302 | 303 | ## Status 304 | 305 | MLX is still under heavy development and the API is not yet considered stable. Still, it is already quite useful, and is used for production purposes within our small company. 306 | 307 | ## License 308 | 309 | MLX is released under [LGPL version 2.1](http://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html) 310 | 311 | Example code is released into the public domain, except the quatrefoil logo, which is LGPL. 312 | 313 | Any included models (such as the Stanford Bunny) are released under their own licenses. 314 | 315 | Much of the documentation for the filter functions is taken directly from MeshLab, and is under the MeshLab license [GPL version 3](https://www.gnu.org/licenses/gpl-3.0-standalone.html) 316 | -------------------------------------------------------------------------------- /examples/CC0.txt: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /examples/quatrefoil_voronoi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | """ MLX logo and example script 3 | 4 | Demonstrates how to use MLX as a design tool for a moderately complex project. 5 | 6 | Units: mm 7 | 8 | License: 9 | Copyright (C) 2017 by Tim Ayres, 3DLirious@gmail.com 10 | 11 | This program is free software; you can redistribute it and/or 12 | modify it under the terms of the GNU Lesser General Public 13 | License as published by the Free Software Foundation; either 14 | version 2.1 of the License, or (at your option) any later version. 15 | 16 | This program is distributed in the hope that it will be useful, 17 | but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 19 | Lesser General Public License for more details. 20 | 21 | You should have received a copy of the GNU Lesser General Public 22 | License along with this program; if not, write to the Free Software 23 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 24 | """ 25 | 26 | import os 27 | import time 28 | import inspect 29 | import math 30 | 31 | import meshlabxml as mlx 32 | 33 | THIS_SCRIPTPATH = os.path.dirname( 34 | os.path.realpath(inspect.getsourcefile(lambda: 0))) 35 | 36 | 37 | def quatrefoil(): 38 | """ Rainbow colored voronoi quatrefoil (3,4) torus knot """ 39 | start_time = time.time() 40 | 41 | os.chdir(THIS_SCRIPTPATH) 42 | #ml_version = '1.3.4BETA' 43 | ml_version = '2016.12' 44 | 45 | # Add meshlabserver directory to OS PATH; omit this if it is already in 46 | # your PATH 47 | #meshlabserver_path = 'C:\\Program Files\\VCG\\MeshLab' 48 | #""" 49 | if ml_version == '1.3.4BETA': 50 | meshlabserver_path = 'C:\\Program Files\\VCG\\MeshLab' 51 | elif ml_version == '2016.12': 52 | meshlabserver_path = 'C:\\Program Files\\VCG\\MeshLab_2016_12' 53 | #""" 54 | os.environ['PATH'] = meshlabserver_path + os.pathsep + os.environ['PATH'] 55 | 56 | # Cross section parameters 57 | length = math.radians(360) 58 | tube_size = [10, 10, length] 59 | segments = [64, 64, 720*2] 60 | inner_radius = 2.0 61 | 62 | # Sinusoidal deformation parameters 63 | amplitude = 4.2 64 | freq = 4 65 | phase = 270 66 | center = 'r' 67 | start_pt = 0 68 | increment = 'z-{}'.format(start_pt) 69 | 70 | # Cyclic rainbow color parameters 71 | c_start_pt = 0 72 | c_freq = 5 73 | c_phase_shift = 0 #90 #300 74 | c_phase = (0 + c_phase_shift, 120 + c_phase_shift, 240 + c_phase_shift, 0) 75 | 76 | # Voronoi surface parameters 77 | holes = [2, 2, 44] # Number of holes in each axis; x are sides, y is outside 78 | web_thickness = 0.5 79 | solid_radius = 5.0 # If the mesh is smaller than this radius the holes will be closed 80 | faces_surface = 50000 81 | 82 | # Voronoi solid parameters 83 | voxel = 0.5 84 | thickness = 2.5 85 | faces_solid = 200000 86 | 87 | # Scaling parameters 88 | size = 75 # desired max size of the curve 89 | curve_max_size = 2*(1 + 1.5) # the 1.5 s/b inner_radius, but am keepng current scaling 90 | scale = (size-2*(thickness + amplitude) - tube_size[1])/curve_max_size 91 | 92 | # File names 93 | file_color = 'quatrefoil_color.ply' 94 | file_voronoi_surf = 'quatrefoil_voronoi_surf.ply' 95 | file_voronoi_solid = 'quatrefoil_voronoi_solid.ply' 96 | file_voronoi_color = 'quatrefoil_voronoi_final.ply' 97 | 98 | # Create FilterScript objects for each step in the process 99 | quatrefoil_color = mlx.FilterScript( 100 | file_in=None, file_out=file_color, ml_version=ml_version) 101 | quatrefoil_voronoi_surf = mlx.FilterScript( 102 | file_in=file_color, file_out=file_voronoi_surf, ml_version=ml_version) 103 | quatrefoil_voronoi_solid = mlx.FilterScript( 104 | file_in=file_voronoi_surf, file_out=file_voronoi_solid, 105 | ml_version=ml_version) 106 | quatrefoil_voronoi_color = mlx.FilterScript( 107 | file_in=[file_color, file_voronoi_solid], file_out=file_voronoi_color, 108 | ml_version=ml_version) 109 | 110 | 111 | print('\n Create colored quatrefoil curve ...') 112 | mlx.create.cube_open_hires( 113 | quatrefoil_color, size=tube_size, x_segments=segments[0], 114 | y_segments=segments[1], z_segments=segments[2], center=True) 115 | mlx.transform.translate(quatrefoil_color, [0, 0, length/2]) 116 | 117 | # Sinusoidal deformation 118 | r_func = '({a})*sin(({f})*({i}) + ({p})) + ({c})'.format( 119 | f=freq, i=increment, p=math.radians(phase), a=amplitude, c=center) 120 | mlx.transform.function_cyl_co( 121 | quatrefoil_color, r_func=r_func, theta_func='theta', z_func='z') 122 | 123 | # Save max radius in quality field so that we can save it with the file 124 | # for use in the next step 125 | max_radius = math.sqrt((tube_size[0]/2)**2+(tube_size[1]/2)**2) # at corners 126 | q_func = '({a})*sin(({f})*({i}) + ({p})) + ({c})'.format( 127 | f=freq, i=increment, p=math.radians(phase), a=amplitude, c=max_radius) 128 | mlx.mp_func.vq_function(quatrefoil_color, function=q_func) 129 | 130 | # Apply rainbow vertex colors 131 | mlx.vert_color.cyclic_rainbow( 132 | quatrefoil_color, direction='z', start_pt=c_start_pt, amplitude=255 / 2, 133 | center=255 / 2, freq=c_freq, phase=c_phase) 134 | 135 | # Deform mesh to quatrefoil curve. Merge vertices after, which 136 | # will weld the ends together so it becomes watertight 137 | quatrefoil_func = mlx.transform.deform2curve( 138 | quatrefoil_color, 139 | curve=mlx.mp_func.torus_knot('t', p=3, q=4, scale=scale, 140 | radius=inner_radius)) 141 | mlx.clean.merge_vert(quatrefoil_color, threshold=0.0001) 142 | 143 | # Run script 144 | mlx.layers.delete_lower(quatrefoil_color) 145 | quatrefoil_color.run_script(output_mask='-m vc vq') 146 | 147 | print('\n Create Voronoi surface ...') 148 | # Move quality value into radius attribute 149 | mlx.mp_func.vert_attr(quatrefoil_voronoi_surf, name='radius', function='q') 150 | 151 | # Create seed vertices 152 | # For grid style holes, we will create a mesh similar to the original 153 | # but with fewer vertices. 154 | mlx.create.cube_open_hires( 155 | quatrefoil_voronoi_surf, size=tube_size, x_segments=holes[0]+1, 156 | y_segments=holes[1]+1, z_segments=holes[2]+1, center=True) 157 | mlx.select.all(quatrefoil_voronoi_surf, vert=False) 158 | mlx.delete.selected(quatrefoil_voronoi_surf, vert=False) 159 | mlx.select.cylindrical_vert(quatrefoil_voronoi_surf, 160 | radius=max_radius-0.0001, inside=False) 161 | mlx.transform.translate(quatrefoil_voronoi_surf, [0, 0, 20]) 162 | mlx.delete.selected(quatrefoil_voronoi_surf, face=False) 163 | 164 | mlx.transform.function_cyl_co(quatrefoil_voronoi_surf, r_func=r_func, 165 | theta_func='theta', z_func='z') 166 | mlx.transform.vert_function( 167 | quatrefoil_voronoi_surf, x_func=quatrefoil_func[0], 168 | y_func=quatrefoil_func[1], z_func=quatrefoil_func[2]) 169 | 170 | mlx.layers.change(quatrefoil_voronoi_surf, 0) 171 | mlx.vert_color.voronoi(quatrefoil_voronoi_surf) 172 | 173 | if quatrefoil_voronoi_surf.ml_version == '1.3.4BETA': 174 | sel_func = '(q <= {}) or ((radius)<={})'.format(web_thickness, solid_radius) 175 | else: 176 | sel_func = '(q <= {}) || ((radius)<={})'.format(web_thickness, solid_radius) 177 | mlx.select.vert_function(quatrefoil_voronoi_surf, function=sel_func) 178 | #mlx.select.face_function(quatrefoil_voronoi_surf, function='(vsel0 && vsel1 && vsel2)') 179 | mlx.select.invert(quatrefoil_voronoi_surf, face=False) 180 | mlx.delete.selected(quatrefoil_voronoi_surf, face=False) 181 | 182 | mlx.smooth.laplacian(quatrefoil_voronoi_surf, iterations=3) 183 | mlx.remesh.simplify(quatrefoil_voronoi_surf, texture=False, faces=faces_surface) 184 | 185 | mlx.layers.delete_lower(quatrefoil_voronoi_surf) 186 | #quatrefoil_voronoi_surf.save_to_file('temp_script.mlx') 187 | quatrefoil_voronoi_surf.run_script(script_file=None, output_mask='-m vc vq') 188 | 189 | print('\n Solidify Voronoi surface ...') 190 | mlx.remesh.uniform_resampling(quatrefoil_voronoi_solid, voxel=voxel, 191 | offset=thickness/2, thicken=True) 192 | mlx.layers.delete_lower(quatrefoil_voronoi_solid) 193 | quatrefoil_voronoi_solid.run_script() 194 | 195 | print('\n Clean up & transfer color to final model ...') 196 | # Clean up from uniform mesh resamplng 197 | mlx.delete.small_parts(quatrefoil_voronoi_color) 198 | mlx.delete.unreferenced_vert(quatrefoil_voronoi_color) 199 | mlx.delete.faces_from_nonmanifold_edges(quatrefoil_voronoi_color) 200 | mlx.clean.split_vert_on_nonmanifold_face(quatrefoil_voronoi_color) 201 | mlx.clean.close_holes(quatrefoil_voronoi_color) 202 | 203 | # Simplify (to improve triangulation quality), refine, & smooth 204 | mlx.remesh.simplify(quatrefoil_voronoi_color, texture=False, faces=faces_solid) 205 | mlx.subdivide.ls3loop(quatrefoil_voronoi_color, iterations=1) 206 | mlx.smooth.laplacian(quatrefoil_voronoi_color, iterations=3) 207 | 208 | # Transfer colors from original curve 209 | mlx.transfer.vert_attr_2_meshes( 210 | quatrefoil_voronoi_color, source_mesh=0, target_mesh=1, color=True, 211 | max_distance=7) 212 | mlx.layers.delete_lower(quatrefoil_voronoi_color) 213 | quatrefoil_voronoi_color.run_script(script_file=None) 214 | print(' done! Took %.1f sec' % (time.time() - start_time)) 215 | 216 | return None 217 | 218 | if __name__ == '__main__': 219 | quatrefoil() 220 | -------------------------------------------------------------------------------- /examples/shield.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | """Example MeshLabXML script to create a heroic shield. 3 | 4 | MeshLab is typically used to process existing meshes; this script was 5 | created to demonstrate some of MeshLab's mesh creation features as well. 6 | It demonstrates combining python functionality (math and flow control) 7 | and MeshLab to create a parametric 3D model that's truly heroic! 8 | 9 | Note that the final model is composed of separate surfaces. It is not 10 | manifold and is e.g. not suitable for 3D printing; it's just a silly 11 | example. 12 | 13 | License: 14 | Written in 2016 by Tim Ayres 3DLirious@gmail.com 15 | 16 | To the extent possible under law, the author(s) have dedicated all 17 | copyright and related and neighboring rights to this software to the 18 | public domain worldwide. This software is distributed without any 19 | warranty. 20 | 21 | You should have received a copy of the CC0 Public Domain Dedication 22 | along with this software. If not, see 23 | . 24 | 25 | """ 26 | 27 | import os 28 | import math 29 | 30 | import meshlabxml as mlx 31 | 32 | # Add meshlabserver directory to OS PATH; omit this if it is already in 33 | # your PATH 34 | MESHLABSERVER_PATH = 'C:\\Program Files\\VCG\\MeshLab' 35 | os.environ['PATH'] += os.pathsep + MESHLABSERVER_PATH 36 | 37 | 38 | def main(): 39 | """Run main script""" 40 | # segments = number of segments to use for circles 41 | segments = 50 42 | # star_points = number of points (or sides) of the star 43 | star_points = 5 44 | # star_radius = radius of circle circumscribing the star 45 | star_radius = 2 46 | # ring_thickness = thickness of the colored rings 47 | ring_thickness = 1 48 | # sphere_radius = radius of sphere the shield will be deformed to 49 | sphere_radius = 2 * (star_radius + 3 * ring_thickness) 50 | 51 | # Star calculations: 52 | # Visually approximate a star by using multiple diamonds (i.e. scaled 53 | # squares) which overlap in the center. For the star calculations, 54 | # consider a central polygon with triangles attached to the edges, all 55 | # circumscribed by a circle. 56 | # polygon_radius = distance from center of circle to polygon edge midpoint 57 | polygon_radius = star_radius / \ 58 | (1 + math.tan(math.radians(180 / star_points)) / 59 | math.tan(math.radians(90 / star_points))) 60 | # width = 1/2 width of polygon edge/outer triangle bottom 61 | width = polygon_radius * math.tan(math.radians(180 / star_points)) 62 | # height = height of outer triangle 63 | height = width / math.tan(math.radians(90 / star_points)) 64 | 65 | shield = mlx.FilterScript(file_out="shield.ply") 66 | 67 | # Create the colored front of the shield using several concentric 68 | # annuluses; combine them together and subdivide so we have more vertices 69 | # to give a smoother deformation later. 70 | mlx.create.annulus(shield, radius=star_radius, cir_segments=segments, color='blue') 71 | mlx.create.annulus(shield, 72 | radius1=star_radius + ring_thickness, 73 | radius2=star_radius, 74 | cir_segments=segments, 75 | color='red') 76 | mlx.create.annulus(shield, 77 | radius1=star_radius + 2 * ring_thickness, 78 | radius2=star_radius + ring_thickness, 79 | cir_segments=segments, 80 | color='white') 81 | mlx.create.annulus(shield, 82 | radius1=star_radius + 3 * ring_thickness, 83 | radius2=star_radius + 2 * ring_thickness, 84 | cir_segments=segments, 85 | color='red') 86 | mlx.layers.join(shield) 87 | mlx.subdivide.midpoint(shield, iterations=2) 88 | 89 | # Create the inside surface of the shield & translate down slightly so it 90 | # doesn't overlap the front. 91 | mlx.create.annulus(shield, 92 | radius1=star_radius + 3 * ring_thickness, 93 | cir_segments=segments, 94 | color='silver') 95 | mlx.transform.rotate(shield, axis='y', angle=180) 96 | mlx.transform.translate(shield, value=[0, 0, -0.005]) 97 | mlx.subdivide.midpoint(shield, iterations=4) 98 | 99 | # Create a diamond for the center star. First create a plane, specifying 100 | # extra vertices to support the final deformation. The length from the 101 | # center of the plane to the corners should be 1 for ease of scaling, so 102 | # we use a side length of sqrt(2) (thanks Pythagoras!). Rotate the plane 103 | # by 45 degrees and scale it to stretch it out per the calculations above, 104 | # then translate it into place (including moving it up in z slightly so 105 | # that it doesn't overlap the shield front). 106 | mlx.create.grid(shield, 107 | size=math.sqrt(2), 108 | x_segments=10, 109 | y_segments=10, 110 | center=True, 111 | color='white') 112 | mlx.transform.rotate(shield, axis='z', angle=45) 113 | mlx.transform.scale(shield, value=[width, height, 1]) 114 | mlx.transform.translate(shield, value=[0, polygon_radius, 0.001]) 115 | 116 | # Duplicate the diamond and rotate the duplicates around, generating the 117 | # star. 118 | for _ in range(1, star_points): 119 | mlx.layers.duplicate(shield) 120 | mlx.transform.rotate(shield, axis='z', angle=360 / star_points) 121 | 122 | # Combine everything together and deform using a spherical function. 123 | mlx.layers.join(shield) 124 | mlx.transform.vert_function(shield, 125 | z_func='sqrt(%s-x^2-y^2)-%s+z' % 126 | (sphere_radius**2, sphere_radius)) 127 | 128 | # Run the script using meshlabserver and generate the model 129 | shield.run_script() 130 | return None 131 | 132 | if __name__ == '__main__': 133 | main() 134 | -------------------------------------------------------------------------------- /meshlabxml/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from .mlx import * 3 | 4 | from . import clean 5 | from . import compute 6 | from . import create 7 | from . import delete 8 | from . import files 9 | from . import layers 10 | from . import normals 11 | from . import remesh 12 | from . import sampling 13 | from . import select 14 | from . import smooth 15 | from . import subdivide 16 | from . import texture 17 | from . import transfer 18 | from . import transform 19 | from . import util 20 | from . import vert_color 21 | from . import mp_func 22 | 23 | #from .color_names import color_name 24 | -------------------------------------------------------------------------------- /meshlabxml/clean.py: -------------------------------------------------------------------------------- 1 | """MeshLabXML cleaning and repairing functions 2 | 3 | See select and delete modules for additional cleaning functions 4 | """ 5 | 6 | from . import util 7 | 8 | def merge_vert(script, threshold=0.0): 9 | """ Merge together all the vertices that are nearer than the specified 10 | threshold. Like a unify duplicate vertices but with some tolerance. 11 | 12 | Args: 13 | script: the FilterScript object or script filename to write 14 | the filter to. 15 | threshold (float): Merging distance. All the vertices that are closer 16 | than this threshold are merged together. Use very small values, 17 | default is zero. 18 | 19 | Layer stack: 20 | No impacts 21 | 22 | MeshLab versions: 23 | 2016.12 24 | 1.3.4BETA 25 | """ 26 | filter_xml = ''.join([ 27 | ' \n', 28 | ' \n', 35 | ' \n']) 36 | util.write_filter(script, filter_xml) 37 | return None 38 | 39 | 40 | def close_holes(script, hole_max_edge=30, selected=False, 41 | sel_new_face=True, self_intersection=True): 42 | """ Close holes smaller than a given threshold 43 | 44 | Args: 45 | script: the FilterScript object or script filename to write 46 | the filter to. 47 | hole_max_edge (int): The size is expressed as number of edges composing 48 | the hole boundary. 49 | selected (bool): Only the holes with at least one of the boundary faces 50 | selected are closed. 51 | sel_new_face (bool): After closing a hole the faces that have been 52 | created are left selected. Any previous selection is lost. Useful 53 | for example for smoothing or subdividing the newly created holes. 54 | self_intersection (bool): When closing an holes it tries to prevent the 55 | creation of faces that intersect faces adjacent to the boundary of 56 | the hole. It is an heuristic, non intersecting hole filling can be 57 | NP-complete. 58 | 59 | Layer stack: 60 | No impacts 61 | 62 | MeshLab versions: 63 | 2016.12 64 | 1.3.4BETA 65 | """ 66 | filter_xml = ''.join([ 67 | ' \n', 68 | ' \n', 73 | ' \n', 78 | ' \n', 83 | ' \n', 88 | ' \n']) 89 | util.write_filter(script, filter_xml) 90 | return None 91 | 92 | 93 | def split_vert_on_nonmanifold_face(script, vert_displacement_ratio=0.0): 94 | """ Split non-manifold vertices until it becomes two-manifold. 95 | 96 | Args: 97 | script: the FilterScript object or script filename to write 98 | the filter to. 99 | vert_displacement_ratio (float): When a vertex is split it is moved 100 | along the average vector going from its position to the centroid 101 | of the FF connected faces sharing it. 102 | 103 | Layer stack: 104 | No impacts 105 | 106 | MeshLab versions: 107 | 2016.12 108 | 1.3.4BETA 109 | """ 110 | if script.ml_version == '1.3.4BETA': 111 | filter_name = 'Split Vertexes Incident on Non Manifold Faces' 112 | else: 113 | filter_name = 'Repair non Manifold Vertices by splitting' 114 | filter_xml = ''.join([ 115 | ' \n'.format(filter_name), 116 | ' \n', 121 | ' \n']) 122 | util.write_filter(script, filter_xml) 123 | return None 124 | 125 | 126 | def fix_folded_face(script): 127 | """ Delete all the single folded faces. 128 | 129 | A face is considered folded if its normal is opposite to all the adjacent 130 | faces. It is removed by flipping it against the face f adjacent along the 131 | edge e such that the vertex opposite to e fall inside f. 132 | 133 | Args: 134 | script: the FilterScript object or script filename to write 135 | the filter to. 136 | 137 | Layer stack: 138 | No impacts 139 | 140 | MeshLab versions: 141 | 2016.12 142 | 1.3.4BETA 143 | """ 144 | filter_xml = ' \n' 145 | util.write_filter(script, filter_xml) 146 | return None 147 | 148 | 149 | def snap_mismatched_borders(script, edge_dist_ratio=0.01, unify_vert=True): 150 | """ Try to snap together adjacent borders that are slightly mismatched. 151 | 152 | This situation can happen on badly triangulated adjacent patches defined by 153 | high order surfaces. For each border vertex the filter snaps it onto the 154 | closest boundary edge only if it is closest of edge_legth*threshold. When 155 | vertex is snapped the corresponding face it split and a new vertex is 156 | created. 157 | 158 | Args: 159 | script: the FilterScript object or script filename to write 160 | the filter to. 161 | edge_dist_ratio (float): Collapse edge when the edge / distance ratio 162 | is greater than this value. E.g. for default value 1000 two 163 | straight border edges are collapsed if the central vertex dist from 164 | the straight line composed by the two edges less than a 1/1000 of 165 | the sum of the edges length. Larger values enforce that only 166 | vertexes very close to the line are removed. 167 | unify_vert (bool): If true the snap vertices are welded together. 168 | 169 | Layer stack: 170 | No impacts 171 | 172 | MeshLab versions: 173 | 2016.12 174 | 1.3.4BETA 175 | """ 176 | filter_xml = ''.join([ 177 | ' \n', 178 | ' \n', 183 | ' \n', 188 | ' \n']) 189 | util.write_filter(script, filter_xml) 190 | return None 191 | -------------------------------------------------------------------------------- /meshlabxml/color_names.py: -------------------------------------------------------------------------------- 1 | """ Dictionary of the 140 HTML Color Names defined in CSS & SVG 2 | https://en.wikipedia.org/wiki/Web_colors#X11_color_names 3 | Format: 4 | key = color_name (lowercase str) 5 | value = r (int), g (int), b (int), hex (str) 6 | """ 7 | 8 | color_name = { 9 | 'aliceblue': (240, 248, 255, 'f0f8ff'), 10 | 'antiquewhite': (250, 235, 215, 'faebd7'), 11 | 'aqua': (0, 255, 255, '00ffff'), 12 | 'aquamarine': (127, 255, 212, '7fffd4'), 13 | 'azure': (240, 255, 255, 'f0ffff'), 14 | 'beige': (245, 245, 220, 'f5f5dc'), 15 | 'bisque': (255, 228, 196, 'ffe4c4'), 16 | 'black': (0, 0, 0, '000000'), 17 | 'blanchedalmond': (255, 235, 205, 'ffebcd'), 18 | 'blue': (0, 0, 255, '0000ff'), 19 | 'blueviolet': (138, 43, 226, '8a2be2'), 20 | 'brown': (165, 42, 42, 'a52a2a'), 21 | 'burlywood': (222, 184, 135, 'deb887'), 22 | 'cadetblue': (95, 158, 160, '5f9ea0'), 23 | 'chartreuse': (127, 255, 0, '7fff00'), 24 | 'chocolate': (210, 105, 30, 'd2691e'), 25 | 'coral': (255, 127, 80, 'ff7f50'), 26 | 'cornflowerblue': (100, 149, 237, '6495ed'), 27 | 'cornsilk': (255, 248, 220, 'fff8dc'), 28 | 'crimson': (220, 20, 60, 'dc143c'), 29 | 'cyan': (0, 255, 255, '00ffff'), 30 | 'darkblue': (0, 0, 139, '00008b'), 31 | 'darkcyan': (0, 139, 139, '008b8b'), 32 | 'darkgoldenrod': (184, 134, 11, 'b8860b'), 33 | 'darkgray': (169, 169, 169, 'a9a9a9'), 34 | 'darkgreen': (0, 100, 0, '006400'), 35 | 'darkgrey': (169, 169, 169, 'a9a9a9'), 36 | 'darkkhaki': (189, 183, 107, 'bdb76b'), 37 | 'darkmagenta': (139, 0, 139, '8b008b'), 38 | 'darkolivegreen': (85, 107, 47, '556b2f'), 39 | 'darkorange': (255, 140, 0, 'ff8c00'), 40 | 'darkorchid': (153, 50, 204, '9932cc'), 41 | 'darkred': (139, 0, 0, '8b0000'), 42 | 'darksalmon': (233, 150, 122, 'e9967a'), 43 | 'darkseagreen': (143, 188, 143, '8fbc8f'), 44 | 'darkslateblue': (72, 61, 139, '483d8b'), 45 | 'darkslategray': (47, 79, 79, '2f4f4f'), 46 | 'darkslategrey': (47, 79, 79, '2f4f4f'), 47 | 'darkturquoise': (0, 206, 209, '00ced1'), 48 | 'darkviolet': (148, 0, 211, '9400d3'), 49 | 'deeppink': (255, 20, 147, 'ff1493'), 50 | 'deepskyblue': (0, 191, 255, '00bfff'), 51 | 'dimgray': (105, 105, 105, '696969'), 52 | 'dimgrey': (105, 105, 105, '696969'), 53 | 'dodgerblue': (30, 144, 255, '1e90ff'), 54 | 'firebrick': (178, 34, 34, 'b22222'), 55 | 'floralwhite': (255, 250, 240, 'fffaf0'), 56 | 'forestgreen': (34, 139, 34, '228b22'), 57 | 'fuchsia': (255, 0, 255, 'ff00ff'), 58 | 'gainsboro': (220, 220, 220, 'dcdcdc'), 59 | 'ghostwhite': (248, 248, 255, 'f8f8ff'), 60 | 'gold': (255, 215, 0, 'ffd700'), 61 | 'goldenrod': (218, 165, 32, 'daa520'), 62 | 'gray': (128, 128, 128, '808080'), 63 | 'green': (0, 128, 0, '008000'), 64 | 'greenyellow': (173, 255, 47, 'adff2f'), 65 | 'grey': (128, 128, 128, '808080'), 66 | 'honeydew': (240, 255, 240, 'f0fff0'), 67 | 'hotpink': (255, 105, 180, 'ff69b4'), 68 | 'indianred': (205, 92, 92, 'cd5c5c'), 69 | 'indigo': (75, 0, 130, '4b0082'), 70 | 'ivory': (255, 255, 240, 'fffff0'), 71 | 'khaki': (240, 230, 140, 'f0e68c'), 72 | 'lavender': (230, 230, 250, 'e6e6fa'), 73 | 'lavenderblush': (255, 240, 245, 'fff0f5'), 74 | 'lawngreen': (124, 252, 0, '7cfc00'), 75 | 'lemonchiffon': (255, 250, 205, 'fffacd'), 76 | 'lightblue': (173, 216, 230, 'add8e6'), 77 | 'lightcoral': (240, 128, 128, 'f08080'), 78 | 'lightcyan': (224, 255, 255, 'e0ffff'), 79 | 'lightgoldenrodyellow': (250, 250, 210, 'fafad2'), 80 | 'lightgray': (211, 211, 211, 'd3d3d3'), 81 | 'lightgreen': (144, 238, 144, '90ee90'), 82 | 'lightgrey': (211, 211, 211, 'd3d3d3'), 83 | 'lightpink': (255, 182, 193, 'ffb6c1'), 84 | 'lightsalmon': (255, 160, 122, 'ffa07a'), 85 | 'lightseagreen': (32, 178, 170, '20b2aa'), 86 | 'lightskyblue': (135, 206, 250, '87cefa'), 87 | 'lightslategray': (119, 136, 153, '778899'), 88 | 'lightslategrey': (119, 136, 153, '778899'), 89 | 'lightsteelblue': (176, 196, 222, 'b0c4de'), 90 | 'lightyellow': (255, 255, 224, 'ffffe0'), 91 | 'lime': (0, 255, 0, '00ff00'), 92 | 'limegreen': (50, 205, 50, '32cd32'), 93 | 'linen': (250, 240, 230, 'faf0e6'), 94 | 'magenta': (255, 0, 255, 'ff00ff'), 95 | 'maroon': (128, 0, 0, '800000'), 96 | 'mediumaquamarine': (102, 205, 170, '66cdaa'), 97 | 'mediumblue': (0, 0, 205, '0000cd'), 98 | 'mediumorchid': (186, 85, 211, 'ba55d3'), 99 | 'mediumpurple': (147, 112, 219, '9370db'), 100 | 'mediumseagreen': (60, 179, 113, '3cb371'), 101 | 'mediumslateblue': (123, 104, 238, '7b68ee'), 102 | 'mediumspringgreen': (0, 250, 154, '00fa9a'), 103 | 'mediumturquoise': (72, 209, 204, '48d1cc'), 104 | 'mediumvioletred': (199, 21, 133, 'c71585'), 105 | 'midnightblue': (25, 25, 112, '191970'), 106 | 'mintcream': (245, 255, 250, 'f5fffa'), 107 | 'mistyrose': (255, 228, 225, 'ffe4e1'), 108 | 'moccasin': (255, 228, 181, 'ffe4b5'), 109 | 'navajowhite': (255, 222, 173, 'ffdead'), 110 | 'navy': (0, 0, 128, '000080'), 111 | 'oldlace': (253, 245, 230, 'fdf5e6'), 112 | 'olive': (128, 128, 0, '808000'), 113 | 'olivedrab': (107, 142, 35, '6b8e23'), 114 | 'orange': (255, 165, 0, 'ffa500'), 115 | 'orangered': (255, 69, 0, 'ff4500'), 116 | 'orchid': (218, 112, 214, 'da70d6'), 117 | 'palegoldenrod': (238, 232, 170, 'eee8aa'), 118 | 'palegreen': (152, 251, 152, '98fb98'), 119 | 'paleturquoise': (175, 238, 238, 'afeeee'), 120 | 'palevioletred': (219, 112, 147, 'db7093'), 121 | 'papayawhip': (255, 239, 213, 'ffefd5'), 122 | 'peachpuff': (255, 218, 185, 'ffdab9'), 123 | 'peru': (205, 133, 63, 'cd853f'), 124 | 'pink': (255, 192, 203, 'ffc0cb'), 125 | 'plum': (221, 160, 221, 'dda0dd'), 126 | 'powderblue': (176, 224, 230, 'b0e0e6'), 127 | 'purple': (128, 0, 128, '800080'), 128 | 'red': (255, 0, 0, 'ff0000'), 129 | 'rosybrown': (188, 143, 143, 'bc8f8f'), 130 | 'royalblue': (65, 105, 225, '4169e1'), 131 | 'saddlebrown': (139, 69, 19, '8b4513'), 132 | 'salmon': (250, 128, 114, 'fa8072'), 133 | 'sandybrown': (244, 164, 96, 'f4a460'), 134 | 'seagreen': (46, 139, 87, '2e8b57'), 135 | 'seashell': (255, 245, 238, 'fff5ee'), 136 | 'sienna': (160, 82, 45, 'a0522d'), 137 | 'silver': (192, 192, 192, 'c0c0c0'), 138 | 'skyblue': (135, 206, 235, '87ceeb'), 139 | 'slateblue': (106, 90, 205, '6a5acd'), 140 | 'slategray': (112, 128, 144, '708090'), 141 | 'slategrey': (112, 128, 144, '708090'), 142 | 'snow': (255, 250, 250, 'fffafa'), 143 | 'springgreen': (0, 255, 127, '00ff7f'), 144 | 'steelblue': (70, 130, 180, '4682b4'), 145 | 'tan': (210, 180, 140, 'd2b48c'), 146 | 'teal': (0, 128, 128, '008080'), 147 | 'thistle': (216, 191, 216, 'd8bfd8'), 148 | 'tomato': (255, 99, 71, 'ff6347'), 149 | 'turquoise': (64, 224, 208, '40e0d0'), 150 | 'violet': (238, 130, 238, 'ee82ee'), 151 | 'wheat': (245, 222, 179, 'f5deb3'), 152 | 'white': (255, 255, 255, 'ffffff'), 153 | 'whitesmoke': (245, 245, 245, 'f5f5f5'), 154 | 'yellow': (255, 255, 0, 'ffff00'), 155 | 'yellowgreen': (154, 205, 50, '9acd32'), 156 | } 157 | 158 | -------------------------------------------------------------------------------- /meshlabxml/color_names.txt: -------------------------------------------------------------------------------- 1 | # The 140 HTML Color Names defined in CSS & SVG 2 | # https://en.wikipedia.org/wiki/Web_colors#X11_color_names 3 | color_name hex r g b 4 | aliceblue #f0f8ff 240 248 255 5 | antiquewhite #faebd7 250 235 215 6 | aqua #00ffff 0 255 255 7 | aquamarine #7fffd4 127 255 212 8 | azure #f0ffff 240 255 255 9 | beige #f5f5dc 245 245 220 10 | bisque #ffe4c4 255 228 196 11 | black #000000 0 0 0 12 | blanchedalmond #ffebcd 255 235 205 13 | blue #0000ff 0 0 255 14 | blueviolet #8a2be2 138 43 226 15 | brown #a52a2a 165 42 42 16 | burlywood #deb887 222 184 135 17 | cadetblue #5f9ea0 95 158 160 18 | chartreuse #7fff00 127 255 0 19 | chocolate #d2691e 210 105 30 20 | coral #ff7f50 255 127 80 21 | cornflowerblue #6495ed 100 149 237 22 | cornsilk #fff8dc 255 248 220 23 | crimson #dc143c 220 20 60 24 | cyan #00ffff 0 255 255 25 | darkblue #00008b 0 0 139 26 | darkcyan #008b8b 0 139 139 27 | darkgoldenrod #b8860b 184 134 11 28 | darkgray #a9a9a9 169 169 169 29 | darkgreen #006400 0 100 0 30 | darkgrey #a9a9a9 169 169 169 31 | darkkhaki #bdb76b 189 183 107 32 | darkmagenta #8b008b 139 0 139 33 | darkolivegreen #556b2f 85 107 47 34 | darkorange #ff8c00 255 140 0 35 | darkorchid #9932cc 153 50 204 36 | darkred #8b0000 139 0 0 37 | darksalmon #e9967a 233 150 122 38 | darkseagreen #8fbc8f 143 188 143 39 | darkslateblue #483d8b 72 61 139 40 | darkslategray #2f4f4f 47 79 79 41 | darkslategrey #2f4f4f 47 79 79 42 | darkturquoise #00ced1 0 206 209 43 | darkviolet #9400d3 148 0 211 44 | deeppink #ff1493 255 20 147 45 | deepskyblue #00bfff 0 191 255 46 | dimgray #696969 105 105 105 47 | dimgrey #696969 105 105 105 48 | dodgerblue #1e90ff 30 144 255 49 | firebrick #b22222 178 34 34 50 | floralwhite #fffaf0 255 250 240 51 | forestgreen #228b22 34 139 34 52 | fuchsia #ff00ff 255 0 255 53 | gainsboro #dcdcdc 220 220 220 54 | ghostwhite #f8f8ff 248 248 255 55 | gold #ffd700 255 215 0 56 | goldenrod #daa520 218 165 32 57 | gray #808080 128 128 128 58 | green #008000 0 128 0 59 | greenyellow #adff2f 173 255 47 60 | grey #808080 128 128 128 61 | honeydew #f0fff0 240 255 240 62 | hotpink #ff69b4 255 105 180 63 | indianred #cd5c5c 205 92 92 64 | indigo #4b0082 75 0 130 65 | ivory #fffff0 255 255 240 66 | khaki #f0e68c 240 230 140 67 | lavender #e6e6fa 230 230 250 68 | lavenderblush #fff0f5 255 240 245 69 | lawngreen #7cfc00 124 252 0 70 | lemonchiffon #fffacd 255 250 205 71 | lightblue #add8e6 173 216 230 72 | lightcoral #f08080 240 128 128 73 | lightcyan #e0ffff 224 255 255 74 | lightgoldenrodyellow #fafad2 250 250 210 75 | lightgray #d3d3d3 211 211 211 76 | lightgreen #90ee90 144 238 144 77 | lightgrey #d3d3d3 211 211 211 78 | lightpink #ffb6c1 255 182 193 79 | lightsalmon #ffa07a 255 160 122 80 | lightseagreen #20b2aa 32 178 170 81 | lightskyblue #87cefa 135 206 250 82 | lightslategray #778899 119 136 153 83 | lightslategrey #778899 119 136 153 84 | lightsteelblue #b0c4de 176 196 222 85 | lightyellow #ffffe0 255 255 224 86 | lime #00ff00 0 255 0 87 | limegreen #32cd32 50 205 50 88 | linen #faf0e6 250 240 230 89 | magenta #ff00ff 255 0 255 90 | maroon #800000 128 0 0 91 | mediumaquamarine #66cdaa 102 205 170 92 | mediumblue #0000cd 0 0 205 93 | mediumorchid #ba55d3 186 85 211 94 | mediumpurple #9370db 147 112 219 95 | mediumseagreen #3cb371 60 179 113 96 | mediumslateblue #7b68ee 123 104 238 97 | mediumspringgreen #00fa9a 0 250 154 98 | mediumturquoise #48d1cc 72 209 204 99 | mediumvioletred #c71585 199 21 133 100 | midnightblue #191970 25 25 112 101 | mintcream #f5fffa 245 255 250 102 | mistyrose #ffe4e1 255 228 225 103 | moccasin #ffe4b5 255 228 181 104 | navajowhite #ffdead 255 222 173 105 | navy #000080 0 0 128 106 | oldlace #fdf5e6 253 245 230 107 | olive #808000 128 128 0 108 | olivedrab #6b8e23 107 142 35 109 | orange #ffa500 255 165 0 110 | orangered #ff4500 255 69 0 111 | orchid #da70d6 218 112 214 112 | palegoldenrod #eee8aa 238 232 170 113 | palegreen #98fb98 152 251 152 114 | paleturquoise #afeeee 175 238 238 115 | palevioletred #db7093 219 112 147 116 | papayawhip #ffefd5 255 239 213 117 | peachpuff #ffdab9 255 218 185 118 | peru #cd853f 205 133 63 119 | pink #ffc0cb 255 192 203 120 | plum #dda0dd 221 160 221 121 | powderblue #b0e0e6 176 224 230 122 | purple #800080 128 0 128 123 | red #ff0000 255 0 0 124 | rosybrown #bc8f8f 188 143 143 125 | royalblue #4169e1 65 105 225 126 | saddlebrown #8b4513 139 69 19 127 | salmon #fa8072 250 128 114 128 | sandybrown #f4a460 244 164 96 129 | seagreen #2e8b57 46 139 87 130 | seashell #fff5ee 255 245 238 131 | sienna #a0522d 160 82 45 132 | silver #c0c0c0 192 192 192 133 | skyblue #87ceeb 135 206 235 134 | slateblue #6a5acd 106 90 205 135 | slategray #708090 112 128 144 136 | slategrey #708090 112 128 144 137 | snow #fffafa 255 250 250 138 | springgreen #00ff7f 0 255 127 139 | steelblue #4682b4 70 130 180 140 | tan #d2b48c 210 180 140 141 | teal #008080 0 128 128 142 | thistle #d8bfd8 216 191 216 143 | tomato #ff6347 255 99 71 144 | turquoise #40e0d0 64 224 208 145 | violet #ee82ee 238 130 238 146 | wheat #f5deb3 245 222 179 147 | white #ffffff 255 255 255 148 | whitesmoke #f5f5f5 245 245 245 149 | yellow #ffff00 255 255 0 150 | yellowgreen #9acd32 154 205 50 151 | -------------------------------------------------------------------------------- /meshlabxml/compute.py: -------------------------------------------------------------------------------- 1 | """ MeshLabXML measurement and computation functions """ 2 | 3 | #from . import mlx.FilterScript 4 | import meshlabxml as mlx 5 | import re 6 | from . import util 7 | 8 | ml_version = '2020.09' 9 | 10 | def section(script, axis='z', offset=0.0, surface=False, custom_axis=None, 11 | planeref=2, split_surface_with_section=False, ml_version=ml_version): 12 | """ Compute the polyline representing a planar section (a slice) of a mesh. 13 | 14 | If the resulting polyline is closed the result can be filled with a 15 | triangular mesh representing the section. 16 | 17 | Args: 18 | script: the mlx.FilterScript object or script filename to write 19 | the filter to. 20 | axis (str): The slicing plane is perpendicular to this axis. Accepted 21 | values are 'x', 'y', or 'z'; any other input will be interpreted 22 | as a custom axis (although using 'custom' is recommended 23 | for clarity). Upper or lowercase values are accepted. 24 | offset (float): Specify an offset of the cross-plane. The offset 25 | corresponds to the distance along 'axis' from the point specified 26 | in 'planeref'. 27 | surface (bool): If True, in addition to a layer with the section 28 | polyline, also a layer with a triangulated version of the section 29 | polyline will be created. This only works if the section polyline 30 | is closed. 31 | split_surface_with_section (bool): If selected, it will create two layers 32 | with the portion of the mesh under and over the section plane. It 33 | requires manifoldness of the mesh. 34 | custom_axis (3 component list or tuple): Specify a custom axis as 35 | a 3 component vector (x, y, z); this is ignored unless 'axis' is 36 | set to 'custom'. 37 | planeref (int): Specify the reference from which the planes are 38 | shifted. Valid values are: 39 | 0 - Bounding box center 40 | 1 - Bounding box min 41 | 2 - Origin (default) 42 | 43 | Layer stack: 44 | Creates a new layer '{label}_sect_{axis_name}_{offset}', where 45 | 'axis_name' is one of [X, Y, Z, custom] and 'offest' is 46 | truncated 'offset' 47 | If surface is True, create a new layer '{label}_sect_{axis}_{offset}_mesh' 48 | Current layer is changed to the last (newly created) layer 49 | 50 | MeshLab versions: 51 | 2020.09 52 | 2016.12 53 | 1.3.4BETA 54 | """ 55 | # Convert axis name into number 56 | if axis.lower() == 'x': 57 | axis_num = 0 58 | axis_name = 'X' 59 | elif axis.lower() == 'y': 60 | axis_num = 1 61 | axis_name = 'Y' 62 | elif axis.lower() == 'z': 63 | axis_num = 2 64 | axis_name = 'Z' 65 | else: # custom axis 66 | axis_num = 3 67 | axis_name = 'custom' 68 | if custom_axis is None: 69 | print('WARNING: a custom axis was selected, however', 70 | '"custom_axis" was not provided. Using default (Z).') 71 | if custom_axis is None: 72 | custom_axis = (0.0, 0.0, 1.0) 73 | 74 | filter_xml = ''.join([ 75 | ' \n', 76 | ' \n', 86 | ' \n', 92 | ' \n', 97 | ' \n', 106 | ' \n',]) 111 | if ml_version == '2020.09': 112 | filter_xml = ''.join([ 113 | filter_xml, 114 | ' \n']) 119 | filter_xml = ''.join([filter_xml, ' \n']) 120 | util.write_filter(script, filter_xml) 121 | if isinstance(script, mlx.FilterScript): 122 | current_layer_label = script.layer_stack[script.current_layer()] 123 | script.add_layer('{}_sect_{}_{}'.format(current_layer_label, axis_name, 124 | int(offset))) 125 | if surface: 126 | script.add_layer('{}_sect_{}_{}_filled'.format(current_layer_label, 127 | axis_name, int(offset))) 128 | if split_surface_with_section: 129 | script.add_layer('{}_sect_{}_{}_under'.format(current_layer_label, 130 | axis_name, int(offset))) 131 | script.add_layer('{}_sect_{}_{}_over'.format(current_layer_label, 132 | axis_name, int(offset))) 133 | return None 134 | 135 | 136 | def measure_geometry(script): 137 | """ Compute a set of geometric measures of a mesh/pointcloud. 138 | 139 | Bounding box extents and diagonal, principal axis, thin shell barycenter 140 | (mesh only), vertex barycenter and quality-weighted barycenter (pointcloud 141 | only), surface area (mesh only), volume (closed mesh) and Inertia tensor 142 | Matrix (closed mesh). 143 | 144 | Args: 145 | script: the mlx.FilterScript object or script filename to write 146 | the filter to. 147 | 148 | Layer stack: 149 | No impacts 150 | 151 | MeshLab versions: 152 | 2016.12 153 | 1.3.4BETA 154 | 155 | Bugs: 156 | Bounding box extents not computed correctly for some volumes 157 | """ 158 | if script.ml_version == '1.3.4BETA' or script.ml_version == '2016.12': 159 | filter_xml = ' \n' 160 | else: 161 | filter_xml = ' \n' 162 | util.write_filter(script, filter_xml) 163 | if isinstance(script, mlx.FilterScript): 164 | script.parse_geometry = True 165 | return None 166 | 167 | 168 | def measure_topology(script): 169 | """ Compute a set of topological measures over a mesh 170 | 171 | Args: 172 | script: the mlx.FilterScript object or script filename to write 173 | the filter to. 174 | 175 | Layer stack: 176 | No impacts 177 | 178 | MeshLab versions: 179 | 2016.12 180 | 1.3.4BETA 181 | """ 182 | if script.ml_version == '1.3.4BETA' or script.ml_version == '2016.12': 183 | filter_xml = ' \n' 184 | else: 185 | filter_xml = ' \n' 186 | util.write_filter(script, filter_xml) 187 | if isinstance(script, mlx.FilterScript): 188 | script.parse_topology = True 189 | return None 190 | 191 | 192 | def parse_geometry(ml_log, log=None, ml_version='2016.12', print_output=False): 193 | """Parse the ml_log file generated by the measure_geometry function. 194 | 195 | Warnings: Not all keys may exist if mesh is not watertight or manifold 196 | 197 | Args: 198 | ml_log (str): MeshLab log file to parse 199 | log (str): filename to log output 200 | """ 201 | # TODO: read more than one occurrence per file. Record in list. 202 | aabb = {} 203 | geometry = {'aabb':aabb} 204 | with open(ml_log) as fread: 205 | for line in fread: 206 | if 'Mesh Bounding Box min' in line: #2016.12 207 | geometry['aabb']['min'] = (line.split()[4:7]) 208 | geometry['aabb']['min'] = [util.to_float(val) for val in geometry['aabb']['min']] 209 | if 'Mesh Bounding Box max' in line: #2016.12 210 | geometry['aabb']['max'] = (line.split()[4:7]) 211 | geometry['aabb']['max'] = [util.to_float(val) for val in geometry['aabb']['max']] 212 | if 'Mesh Bounding Box Size' in line: #2016.12 213 | geometry['aabb']['size'] = (line.split()[4:7]) 214 | geometry['aabb']['size'] = [util.to_float(val) for val in geometry['aabb']['size']] 215 | if 'Mesh Bounding Box Diag' in line: #2016.12 216 | geometry['aabb']['diagonal'] = util.to_float(line.split()[4]) 217 | if 'Mesh Volume' in line: 218 | geometry['volume_mm3'] = util.to_float(line.split()[3]) 219 | geometry['volume_cm3'] = geometry['volume_mm3'] * 0.001 220 | if 'Mesh Surface' in line: 221 | if ml_version == '1.3.4BETA': 222 | geometry['area_mm2'] = util.to_float(line.split()[3]) 223 | else: 224 | geometry['area_mm2'] = util.to_float(line.split()[4]) 225 | geometry['area_cm2'] = geometry['area_mm2'] * 0.01 226 | if 'Mesh Total Len of' in line: 227 | if 'including faux edges' in line: 228 | geometry['total_edge_length_incl_faux'] = util.to_float( 229 | line.split()[7]) 230 | else: 231 | geometry['total_edge_length'] = util.to_float( 232 | line.split()[7]) 233 | if 'Thin shell barycenter' in line: 234 | geometry['barycenter'] = (line.split()[3:6]) 235 | geometry['barycenter'] = [util.to_float(val) for val in geometry['barycenter']] 236 | if 'Thin shell (faces) barycenter' in line: #2016.12 237 | geometry['barycenter'] = (line.split()[4:7]) 238 | geometry['barycenter'] = [util.to_float(val) for val in geometry['barycenter']] 239 | if 'Vertices barycenter' in line: #2016.12 240 | geometry['vert_barycenter'] = (line.split()[2:5]) 241 | geometry['vert_barycenter'] = [util.to_float(val) for val in geometry['vert_barycenter']] 242 | if 'Center of Mass' in line: 243 | geometry['center_of_mass'] = (line.split()[4:7]) 244 | geometry['center_of_mass'] = [util.to_float(val) for val in geometry['center_of_mass']] 245 | if 'Inertia Tensor' in line: 246 | geometry['inertia_tensor'] = [] 247 | for val in range(3): 248 | row = (next(fread, val).split()[1:4]) 249 | row = [util.to_float(b) for b in row] 250 | geometry['inertia_tensor'].append(row) 251 | if 'Principal axes' in line: 252 | geometry['principal_axes'] = [] 253 | for val in range(3): 254 | row = (next(fread, val).split()[1:4]) 255 | row = [util.to_float(b) for b in row] 256 | geometry['principal_axes'].append(row) 257 | if 'axis momenta' in line: 258 | geometry['axis_momenta'] = (next(fread).split()[1:4]) 259 | geometry['axis_momenta'] = [util.to_float(val) for val in geometry['axis_momenta']] 260 | break # stop after we find the first match 261 | geometry['aabb']['center'] = [geometry['aabb']['max'][0] - geometry['aabb']['size'][0]/2.0, geometry['aabb']['max'][1] - geometry['aabb']['size'][1]/2, geometry['aabb']['max'][2] - geometry['aabb']['size'][2]/2] 262 | 263 | for key, value in geometry.items(): 264 | if log is not None: 265 | log_file = open(log, 'a') 266 | log_file.write('{:27} = {}\n'.format(key, value)) 267 | log_file.close() 268 | elif print_output: 269 | print('{:27} = {}'.format(key, value)) 270 | return geometry 271 | 272 | 273 | def parse_topology(ml_log, log=None, ml_version='1.3.4BETA', print_output=False): 274 | """Parse the ml_log file generated by the measure_topology function. 275 | 276 | Args: 277 | ml_log (str): MeshLab log file to parse 278 | log (str): filename to log output 279 | 280 | Returns: 281 | dict: dictionary with the following keys: 282 | vert_num (int): number of vertices 283 | edge_num (int): number of edges 284 | face_num (int): number of faces 285 | unref_vert_num (int): number or unreferenced vertices 286 | boundry_edge_num (int): number of boundary edges 287 | part_num (int): number of parts (components) in the mesh. 288 | manifold (bool): True if mesh is two-manifold, otherwise false. 289 | non_manifold_edge (int): number of non_manifold edges. 290 | non_manifold_vert (int): number of non-manifold verices 291 | genus (int or str): genus of the mesh, either a number or 292 | 'undefined' if the mesh is non-manifold. 293 | holes (int or str): number of holes in the mesh, either a number 294 | or 'undefined' if the mesh is non-manifold. 295 | 296 | """ 297 | topology = {'manifold': True, 'non_manifold_E': 0, 'non_manifold_V': 0} 298 | with open(ml_log) as fread: 299 | for line in fread: 300 | if 'V:' in line: 301 | vert_edge_face = line.replace('V:', ' ').replace('E:', ' ').replace('F:', ' ').split() 302 | topology['vert_num'] = int(vert_edge_face[0]) 303 | topology['edge_num'] = int(vert_edge_face[1]) 304 | topology['face_num'] = int(vert_edge_face[2]) 305 | if 'Unreferenced Vertices' in line: 306 | topology['unref_vert_num'] = int(line.split()[2]) 307 | if 'Boundary Edges' in line: 308 | topology['boundry_edge_num'] = int(line.split()[2]) 309 | if 'Mesh is composed by' in line: 310 | topology['part_num'] = int(line.split()[4]) 311 | if 'non 2-manifold mesh' in line: 312 | topology['manifold'] = False 313 | if 'non two manifold edges' in line: 314 | topology['non_manifold_edge'] = int(line.split()[2]) 315 | if 'non two manifold vertexes' in line: 316 | topology['non_manifold_vert'] = int(line.split()[2]) 317 | if 'Genus is' in line: # undefined or int 318 | topology['genus'] = line.split()[2] 319 | if topology['genus'] != 'undefined': 320 | topology['genus'] = int(topology['genus']) 321 | if 'Mesh has' in line and 'holes' in line: # previously just searched on 'holes' but this collided with filenames 322 | topology['hole_num'] = line.split()[2] 323 | if topology['hole_num'] == 'a': 324 | topology['hole_num'] = 'undefined' 325 | else: 326 | topology['hole_num'] = int(topology['hole_num']) 327 | for key, value in topology.items(): 328 | if log is not None: 329 | log_file = open(log, 'a') 330 | log_file.write('{:16} = {}\n'.format(key, value)) 331 | log_file.close() 332 | elif print_output: 333 | print('{:16} = {}'.format(key, value)) 334 | 335 | return topology 336 | 337 | 338 | def parse_hausdorff(ml_log, log=None, print_output=False): 339 | """Parse the ml_log file generated by the hausdorff_distance function. 340 | 341 | Args: 342 | ml_log (str): MeshLab log file to parse 343 | log (str): filename to log output 344 | 345 | Returns: 346 | dict: dictionary with the following keys: 347 | number_points (int): number of points in mesh 348 | min_distance (float): minimum hausdorff distance 349 | max_distance (float): maximum hausdorff distance 350 | mean_distance (float): mean hausdorff distance 351 | rms_distance (float): root mean square distance 352 | 353 | """ 354 | hausdorff_distance = {"min_distance": 0.0, 355 | "max_distance": 0.0, 356 | "mean_distance": 0.0, 357 | "rms_distance": 0.0, 358 | "number_points": 0} 359 | with open(ml_log) as fread: 360 | result = fread.readlines() 361 | data = "" 362 | 363 | for idx, line in enumerate(result): 364 | m = re.match(r"\s*Sampled (\d+) pts.*", line) 365 | if m is not None: 366 | hausdorff_distance["number_points"] = int(m.group(1)) 367 | if 'Hausdorff Distance computed' in line: 368 | data = result[idx + 2] 369 | 370 | m = re.match(r"\D+(\d+\.*\d*)\D+(\d+\.*\d*)\D+(\d+\.*\d*)\D+(\d+\.*\d*)", data) 371 | hausdorff_distance["min_distance"] = float(m.group(1)) 372 | hausdorff_distance["max_distance"] = float(m.group(2)) 373 | hausdorff_distance["mean_distance"] = float(m.group(3)) 374 | hausdorff_distance["rms_distance"] = float(m.group(4)) 375 | for key, value in hausdorff_distance.items(): 376 | if log is not None: 377 | log_file = open(log, 'a') 378 | log_file.write('{:16} = {}\n'.format(key, value)) 379 | log_file.close() 380 | elif print_output: 381 | print('{:16} = {}'.format(key, value)) 382 | return hausdorff_distance 383 | -------------------------------------------------------------------------------- /meshlabxml/delete.py: -------------------------------------------------------------------------------- 1 | """ MeshLabXML deletion functions""" 2 | 3 | from . import util 4 | from . import select 5 | 6 | def nonmanifold_vert(script): 7 | """ Select & delete the non manifold vertices that do not belong to 8 | non manifold edges. 9 | 10 | For example two cones connected by their apex. Vertices incident on 11 | non manifold edges are ignored. 12 | 13 | Args: 14 | script: the FilterScript object or script filename to write 15 | the filter to. 16 | 17 | Layer stack: 18 | No impacts 19 | 20 | MeshLab versions: 21 | 2016.12 22 | 1.3.4BETA 23 | """ 24 | select.nonmanifold_vert(script) 25 | selected(script, face=False) 26 | return None 27 | 28 | 29 | def nonmanifold_edge(script): 30 | """ Select & delete the faces and the vertices incident on 31 | non manifold edges (e.g. edges where more than two faces are incident). 32 | 33 | Note that this function selects the components that are related to 34 | non manifold edges. The case of non manifold vertices is specifically 35 | managed by nonmanifold_vert. 36 | 37 | Args: 38 | script: the FilterScript object or script filename to write 39 | the filter to. 40 | 41 | Layer stack: 42 | No impacts 43 | 44 | MeshLab versions: 45 | 2016.12 46 | 1.3.4BETA 47 | """ 48 | select.nonmanifold_edge(script) 49 | selected(script, face=False) 50 | return None 51 | 52 | 53 | def small_parts(script, ratio=0.2, non_closed_only=False): 54 | """ Select & delete the small disconnected parts (components) of a mesh. 55 | 56 | Args: 57 | script: the FilterScript object or script filename to write 58 | the filter to. 59 | ratio (float): This ratio (between 0 and 1) defines the meaning of 60 | 'small' as the threshold ratio between the number of faces of the 61 | largest component and the other ones. A larger value will select 62 | more components. 63 | non_closed_only (bool): Select only non-closed components. 64 | 65 | Layer stack: 66 | No impacts 67 | 68 | MeshLab versions: 69 | 2016.12 70 | 1.3.4BETA 71 | """ 72 | select.small_parts(script, ratio, non_closed_only) 73 | selected(script) 74 | return None 75 | 76 | 77 | def selected(script, face=True, vert=True): 78 | """ Delete selected vertices and/or faces 79 | 80 | Note: if the mesh has no faces (e.g. a point cloud) you must 81 | set face=False, or the vertices will not be deleted 82 | 83 | Args: 84 | script: the FilterScript object or script filename to write 85 | the filter to. 86 | face (bool): if True the selected faces will be deleted. If vert 87 | is also True, then all the vertices surrounded by those faces will 88 | also be deleted. Note that if no faces are selected (only vertices) 89 | then this filter will not do anything. For example, if you want to 90 | delete a point cloud selection, you must set this to False. 91 | vert (bool): if True the selected vertices will be deleted. 92 | 93 | Layer stack: 94 | No impacts 95 | 96 | MeshLab versions: 97 | 2016.12 98 | 1.3.4BETA 99 | """ 100 | if face and vert: 101 | filter_xml = ' \n' 102 | elif face and not vert: 103 | filter_xml = ' \n' 104 | elif not face and vert: 105 | filter_xml = ' \n' 106 | util.write_filter(script, filter_xml) 107 | return None 108 | 109 | 110 | def faces_from_nonmanifold_edges(script): 111 | """ For each non manifold edge it iteratively deletes the smallest area 112 | face until it becomes two-manifold. 113 | 114 | Args: 115 | script: the FilterScript object or script filename to write 116 | the filter to. 117 | 118 | Layer stack: 119 | No impacts 120 | 121 | MeshLab versions: 122 | 2016.12 123 | 1.3.4BETA 124 | """ 125 | if script.ml_version == '1.3.4BETA': 126 | filter_xml = ' \n' 127 | else: 128 | filter_xml = ' \n' 129 | util.write_filter(script, filter_xml) 130 | #unreferenced_vert(script) 131 | return None 132 | 133 | 134 | def unreferenced_vert(script): 135 | """ Check for every vertex on the mesh: if it is NOT referenced by a face, 136 | removes it. 137 | 138 | Args: 139 | script: the FilterScript object or script filename to write 140 | the filter to. 141 | 142 | Layer stack: 143 | No impacts 144 | 145 | MeshLab versions: 146 | 2016.12 147 | 1.3.4BETA 148 | """ 149 | if script.ml_version == '1.3.4BETA': 150 | filter_xml = ' \n' 151 | else: 152 | filter_xml = ' \n' 153 | util.write_filter(script, filter_xml) 154 | return None 155 | 156 | 157 | def duplicate_faces(script): 158 | """ Delete all the duplicate faces. 159 | 160 | Two faces are considered equal if they are composed by the same set of 161 | vertices, regardless of the order of the vertices. 162 | 163 | Args: 164 | script: the FilterScript object or script filename to write 165 | the filter to. 166 | 167 | Layer stack: 168 | No impacts 169 | 170 | MeshLab versions: 171 | 2016.12 172 | 1.3.4BETA 173 | """ 174 | filter_xml = ' \n' 175 | util.write_filter(script, filter_xml) 176 | return None 177 | 178 | 179 | def duplicate_verts(script): 180 | """ "Check for every vertex on the mesh: if there are two vertices with 181 | the same coordinates they are merged into a single one. 182 | 183 | Args: 184 | script: the FilterScript object or script filename to write 185 | the filter to. 186 | 187 | Layer stack: 188 | No impacts 189 | 190 | MeshLab versions: 191 | 2016.12 192 | 1.3.4BETA 193 | """ 194 | if script.ml_version == '1.3.4BETA': 195 | filter_xml = ' \n' 196 | else: 197 | filter_xml = ' \n' 198 | 199 | util.write_filter(script, filter_xml) 200 | return None 201 | 202 | 203 | def zero_area_face(script): 204 | """ Remove null faces (the ones with area equal to zero) 205 | 206 | Args: 207 | script: the FilterScript object or script filename to write 208 | the filter to. 209 | 210 | Layer stack: 211 | No impacts 212 | 213 | MeshLab versions: 214 | 2016.12 215 | 1.3.4BETA 216 | """ 217 | filter_xml = ' \n' 218 | util.write_filter(script, filter_xml) 219 | return None 220 | -------------------------------------------------------------------------------- /meshlabxml/files.py: -------------------------------------------------------------------------------- 1 | """ MeshLabXML functions that operate on mesh files """ 2 | 3 | import os 4 | import sys 5 | import math 6 | 7 | import meshlabxml as mlx 8 | from . import run 9 | from . import util 10 | from . import compute 11 | from . import transform 12 | from . import layers 13 | 14 | #ml_version = '1.3.4BETA' 15 | #ml_version = '2016.12' 16 | ml_version = '2020.09' 17 | 18 | def measure_aabb(fbasename=None, log=None, coord_system='CARTESIAN'): 19 | """ Measure the axis aligned bounding box (aabb) of a mesh 20 | in multiple coordinate systems. 21 | 22 | Args: 23 | fbasename (str): filename of input model 24 | log (str): filename of log file 25 | coord_system (enum in ['CARTESIAN', 'CYLINDRICAL'] 26 | Coordinate system to use: 27 | 'CARTESIAN': lists contain [x, y, z] 28 | 'CYLINDRICAL': lists contain [r, theta, z] 29 | Returns: 30 | dict: dictionary with the following aabb properties 31 | min (3 element list): minimum values 32 | max (3 element list): maximum values 33 | center (3 element list): the center point 34 | size (3 element list): size of the aabb in each coordinate (max-min) 35 | diagonal (float): the diagonal of the aabb 36 | """ 37 | # TODO: add center point, spherical coordinate system 38 | fext = os.path.splitext(fbasename)[1][1:].strip().lower() 39 | if fext != 'xyz': 40 | fin = 'TEMP3D_aabb.xyz' 41 | run(log=log, file_in=fbasename, file_out=fin, script=None) 42 | else: 43 | fin = fbasename 44 | fread = open(fin, 'r') 45 | aabb = {'min': [999999.0, 999999.0, 999999.0], 'max': [-999999.0, -999999.0, -999999.0]} 46 | for line in fread: 47 | x_co, y_co, z_co = line.split() 48 | x_co = util.to_float(x_co) 49 | y_co = util.to_float(y_co) 50 | z_co = util.to_float(z_co) 51 | if coord_system == 'CARTESIAN': 52 | if x_co < aabb['min'][0]: 53 | aabb['min'][0] = x_co 54 | if y_co < aabb['min'][1]: 55 | aabb['min'][1] = y_co 56 | if z_co < aabb['min'][2]: 57 | aabb['min'][2] = z_co 58 | if x_co > aabb['max'][0]: 59 | aabb['max'][0] = x_co 60 | if y_co > aabb['max'][1]: 61 | aabb['max'][1] = y_co 62 | if z_co > aabb['max'][2]: 63 | aabb['max'][2] = z_co 64 | elif coord_system == 'CYLINDRICAL': 65 | radius = math.sqrt(x_co**2 + y_co**2) 66 | theta = math.degrees(math.atan2(y_co, x_co)) 67 | if radius < aabb['min'][0]: 68 | aabb['min'][0] = radius 69 | if theta < aabb['min'][1]: 70 | aabb['min'][1] = theta 71 | if z_co < aabb['min'][2]: 72 | aabb['min'][2] = z_co 73 | if radius > aabb['max'][0]: 74 | aabb['max'][0] = radius 75 | if theta > aabb['max'][1]: 76 | aabb['max'][1] = theta 77 | if z_co > aabb['max'][2]: 78 | aabb['max'][2] = z_co 79 | fread.close() 80 | try: 81 | aabb['center'] = [(aabb['max'][0] + aabb['min'][0]) / 2, 82 | (aabb['max'][1] + aabb['min'][1]) / 2, 83 | (aabb['max'][2] + aabb['min'][2]) / 2] 84 | aabb['size'] = [aabb['max'][0] - aabb['min'][0], aabb['max'][1] - aabb['min'][1], 85 | aabb['max'][2] - aabb['min'][2]] 86 | aabb['diagonal'] = math.sqrt( 87 | aabb['size'][0]**2 + 88 | aabb['size'][1]**2 + 89 | aabb['size'][2]**2) 90 | except UnboundLocalError: 91 | print('Error: aabb input file does not contain valid data. Exiting ...') 92 | sys.exit(1) 93 | for key, value in aabb.items(): 94 | if log is None: 95 | print('{:10} = {}'.format(key, value)) 96 | else: 97 | log_file = open(log, 'a') 98 | log_file.write('{:10} = {}\n'.format(key, value)) 99 | log_file.close() 100 | """ 101 | if log is not None: 102 | log_file = open(log, 'a') 103 | #log_file.write('***Axis Aligned Bounding Results for file "%s":\n' % fbasename) 104 | log_file.write('min = %s\n' % aabb['min']) 105 | log_file.write('max = %s\n' % aabb['max']) 106 | log_file.write('center = %s\n' % aabb['center']) 107 | log_file.write('size = %s\n' % aabb['size']) 108 | log_file.write('diagonal = %s\n\n' % aabb['diagonal']) 109 | log_file.close() 110 | # print(aabb) 111 | """ 112 | return aabb 113 | 114 | 115 | def measure_section(fbasename=None, log=None, axis='z', offset=0.0, 116 | rotate_x_angle=None, ml_version=ml_version): 117 | """Measure a cross section of a mesh 118 | 119 | Perform a plane cut in one of the major axes (X, Y, Z). If you want to cut on 120 | a different plane you will need to rotate the model in place, perform the cut, 121 | and rotate it back. 122 | 123 | Args: 124 | fbasename (str): filename of input model 125 | log (str): filename of log file 126 | axis (str): axis perpendicular to the cutting plane, e.g. specify "z" to cut 127 | parallel to the XY plane. 128 | offset (float): amount to offset the cutting plane from the origin 129 | rotate_x_angle (float): degrees to rotate about the X axis. Useful for correcting "Up" direction: 90 to rotate Y to Z, and -90 to rotate Z to Y. 130 | 131 | Returns: 132 | dict: dictionary with the following keys for the aabb of the section: 133 | min (list): list of the x, y & z minimum values 134 | max (list): list of the x, y & z maximum values 135 | center (list): the x, y & z coordinates of the center of the aabb 136 | size (list): list of the x, y & z sizes (max - min) 137 | diagonal (float): the diagonal of the aabb 138 | """ 139 | ml_script1_file = 'TEMP3D_measure_section.mlx' 140 | file_out = 'TEMP3D_sect_aabb.xyz' 141 | 142 | ml_script1 = mlx.FilterScript(file_in=fbasename, file_out=file_out, ml_version=ml_version) 143 | if rotate_x_angle is not None: 144 | transform.rotate(ml_script1, axis='x', angle=rotate_x_angle) 145 | compute.section(ml_script1, axis=axis, offset=offset) 146 | layers.delete_lower(ml_script1) 147 | ml_script1.save_to_file(ml_script1_file) 148 | ml_script1.run_script(log=log, script_file=ml_script1_file) 149 | aabb = measure_aabb(file_out, log) 150 | return aabb 151 | 152 | 153 | def polylinesort(fbasename=None, log=None): 154 | """Sort separate line segments in obj format into a continuous polyline or polylines. 155 | NOT FINISHED; DO NOT USE 156 | 157 | Also measures the length of each polyline 158 | 159 | Return polyline and polylineMeta (lengths) 160 | 161 | """ 162 | fext = os.path.splitext(fbasename)[1][1:].strip().lower() 163 | if fext != 'obj': 164 | print('Input file must be obj. Exiting ...') 165 | sys.exit(1) 166 | fread = open(fbasename, 'r') 167 | first = True 168 | polyline_vertices = [] 169 | line_segments = [] 170 | for line in fread: 171 | element, x_co, y_co, z_co = line.split() 172 | if element == 'v': 173 | polyline_vertices.append( 174 | [util.to_float(x_co), util.to_float(y_co), util.to_float(z_co)]) 175 | elif element == 'l': 176 | p1 = x_co 177 | p2 = y_co 178 | line_segments.append([int(p1), int(p2)]) 179 | 180 | fread.close() 181 | if log is not None: 182 | log_file = open(log, 'a') 183 | #log_file.write('***Axis Aligned Bounding Results for file "%s":\n' % fbasename) 184 | """log_file.write('min = %s\n' % aabb['min']) 185 | log_file.write('max = %s\n' % aabb['max']) 186 | log_file.write('center = %s\n' % aabb['center']) 187 | log_file.write('size = %s\n' % aabb['size']) 188 | log_file.write('diagonal = %s\n' % aabb['diagonal'])""" 189 | log_file.close() 190 | # print(aabb) 191 | return None 192 | 193 | 194 | def measure_geometry(fbasename=None, log=None, ml_version=ml_version): 195 | """Measures mesh geometry, including aabb""" 196 | ml_script1_file = 'TEMP3D_measure_geometry.mlx' 197 | if ml_version == '1.3.4BETA': 198 | file_out = 'TEMP3D_aabb.xyz' 199 | else: 200 | file_out = None 201 | 202 | ml_script1 = mlx.FilterScript(file_in=fbasename, file_out=file_out, ml_version=ml_version) 203 | compute.measure_geometry(ml_script1) 204 | ml_script1.save_to_file(ml_script1_file) 205 | ml_script1.run_script(log=log, script_file=ml_script1_file) 206 | geometry = ml_script1.geometry 207 | 208 | if ml_version == '1.3.4BETA': 209 | if log is not None: 210 | log_file = open(log, 'a') 211 | log_file.write( 212 | '***Axis Aligned Bounding Results for file "%s":\n' % 213 | fbasename) 214 | log_file.close() 215 | aabb = measure_aabb(file_out, log) 216 | else: 217 | aabb = geometry['aabb'] 218 | return aabb, geometry 219 | 220 | 221 | def measure_topology(fbasename=None, log=None, ml_version=ml_version): 222 | """Measures mesh topology 223 | 224 | Args: 225 | fbasename (str): input filename. 226 | log (str): filename to log output 227 | 228 | Returns: 229 | dict: dictionary with the following keys: 230 | vert_num (int): number of vertices 231 | edge_num (int): number of edges 232 | face_num (int): number of faces 233 | unref_vert_num (int): number or unreferenced vertices 234 | boundry_edge_num (int): number of boundary edges 235 | part_num (int): number of parts (components) in the mesh. 236 | manifold (bool): True if mesh is two-manifold, otherwise false. 237 | non_manifold_edge (int): number of non_manifold edges. 238 | non_manifold_vert (int): number of non-manifold verices 239 | genus (int or str): genus of the mesh, either a number or 240 | 'undefined' if the mesh is non-manifold. 241 | holes (int or str): number of holes in the mesh, either a number 242 | or 'undefined' if the mesh is non-manifold. 243 | 244 | """ 245 | ml_script1_file = 'TEMP3D_measure_topology.mlx' 246 | ml_script1 = mlx.FilterScript(file_in=fbasename, ml_version=ml_version) 247 | compute.measure_topology(ml_script1) 248 | ml_script1.save_to_file(ml_script1_file) 249 | ml_script1.run_script(log=log, script_file=ml_script1_file) 250 | topology = ml_script1.topology 251 | return topology 252 | 253 | 254 | def measure_all(fbasename=None, log=None, ml_version=ml_version): 255 | """Measures mesh geometry, aabb and topology.""" 256 | ml_script1_file = 'TEMP3D_measure_gAndT.mlx' 257 | if ml_version == '1.3.4BETA': 258 | file_out = 'TEMP3D_aabb.xyz' 259 | else: 260 | file_out = None 261 | 262 | ml_script1 = mlx.FilterScript(file_in=fbasename, file_out=file_out, ml_version=ml_version) 263 | compute.measure_geometry(ml_script1) 264 | compute.measure_topology(ml_script1) 265 | ml_script1.save_to_file(ml_script1_file) 266 | ml_script1.run_script(log=log, script_file=ml_script1_file) 267 | geometry = ml_script1.geometry 268 | topology = ml_script1.topology 269 | 270 | if ml_version == '1.3.4BETA': 271 | if log is not None: 272 | log_file = open(log, 'a') 273 | log_file.write( 274 | '***Axis Aligned Bounding Results for file "%s":\n' % 275 | fbasename) 276 | log_file.close() 277 | aabb = measure_aabb(file_out, log) 278 | else: 279 | aabb = geometry['aabb'] 280 | return aabb, geometry, topology 281 | 282 | 283 | def measure_dimension(fbasename=None, log=None, axis1=None, offset1=0.0, 284 | axis2=None, offset2=0.0, ml_version=ml_version): 285 | """Measure a dimension of a mesh""" 286 | axis1 = axis1.lower() 287 | axis2 = axis2.lower() 288 | ml_script1_file = 'TEMP3D_measure_dimension.mlx' 289 | file_out = 'TEMP3D_measure_dimension.xyz' 290 | 291 | ml_script1 = mlx.FilterScript(file_in=fbasename, file_out=file_out, ml_version=ml_version) 292 | compute.section(ml_script1, axis1, offset1, surface=True) 293 | compute.section(ml_script1, axis2, offset2, surface=False) 294 | layers.delete_lower(ml_script1) 295 | ml_script1.save_to_file(ml_script1_file) 296 | ml_script1.run_script(log=log, script_file=ml_script1_file) 297 | 298 | for val in ('x', 'y', 'z'): 299 | if val not in (axis1, axis2): 300 | axis = val 301 | # ord: Get number that represents letter in ASCII 302 | # Here we find the offset from 'x' to determine the list reference 303 | # i.e. 0 for x, 1 for y, 2 for z 304 | axis_num = ord(axis) - ord('x') 305 | aabb = measure_aabb(file_out, log) 306 | dimension = {'min': aabb['min'][axis_num], 'max': aabb['max'][axis_num], 307 | 'length': aabb['size'][axis_num], 'axis': axis} 308 | if log is None: 309 | print('\nFor file "%s"' % fbasename) 310 | print('Dimension parallel to %s with %s=%s & %s=%s:' % (axis, axis1, offset1, 311 | axis2, offset2)) 312 | print(' Min = %s, Max = %s, Total length = %s' % (dimension['min'], 313 | dimension['max'], dimension['length'])) 314 | else: 315 | log_file = open(log, 'a') 316 | log_file.write('\nFor file "%s"\n' % fbasename) 317 | log_file.write('Dimension parallel to %s with %s=%s & %s=%s:\n' % (axis, axis1, offset1, 318 | axis2, offset2)) 319 | log_file.write('min = %s\n' % dimension['min']) 320 | log_file.write('max = %s\n' % dimension['max']) 321 | log_file.write('Total length = %s\n' % dimension['length']) 322 | log_file.close() 323 | return dimension 324 | -------------------------------------------------------------------------------- /meshlabxml/layers.py: -------------------------------------------------------------------------------- 1 | """ MeshLabXML layer functions """ 2 | 3 | #from . import mlx.FilterScript 4 | #from meshlabxml import mlx.FilterScript 5 | import meshlabxml as mlx 6 | from . import util 7 | 8 | def join(script, merge_visible=True, merge_vert=False, delete_layer=True, 9 | keep_unreferenced_vert=False): 10 | """ Flatten all or only the visible layers into a single new mesh. 11 | 12 | Transformations are preserved. Existing layers can be optionally 13 | deleted. 14 | 15 | Args: 16 | script: the mlx.FilterScript object or script filename to write 17 | the filter to. 18 | merge_visible (bool): merge only visible layers 19 | merge_vert (bool): merge the vertices that are duplicated among 20 | different layers. Very useful when the layers are spliced portions 21 | of a single big mesh. 22 | delete_layer (bool): delete all the merged layers. If all layers are 23 | visible only a single layer will remain after the invocation of 24 | this filter. 25 | keep_unreferenced_vert (bool): Do not discard unreferenced vertices 26 | from source layers. Necessary for point-only layers. 27 | 28 | Layer stack: 29 | Creates a new layer "Merged Mesh" 30 | Changes current layer to the new layer 31 | Optionally deletes all other layers 32 | 33 | MeshLab versions: 34 | 2016.12 35 | 1.3.4BETA 36 | 37 | Bugs: 38 | UV textures: not currently preserved, however will be in a future 39 | release. https://github.com/cnr-isti-vclab/meshlab/issues/128 40 | merge_visible: it is not currently possible to change the layer 41 | visibility from meshlabserver, however this will be possible 42 | in the future https://github.com/cnr-isti-vclab/meshlab/issues/123 43 | """ 44 | filter_xml = ''.join([ 45 | ' \n', 46 | ' \n', 51 | ' \n', 56 | ' \n', 61 | ' \n', 66 | ' \n']) 67 | util.write_filter(script, filter_xml) 68 | if isinstance(script, mlx.FilterScript): 69 | script.add_layer('Merged Mesh') 70 | if delete_layer: 71 | # As it is not yet possible to change the layer visibility, all 72 | # layers will be deleted. This will be updated once layer 73 | # visibility is tracked. 74 | for i in range(script.last_layer()): 75 | script.del_layer(0) 76 | return None 77 | 78 | 79 | def delete(script, layer_num=None): 80 | """ Delete layer 81 | 82 | Args: 83 | script: the mlx.FilterScript object or script filename to write 84 | the filter to. 85 | layer_num (int): the number of the layer to delete. Default is the 86 | current layer. Not supported on the file base API. 87 | 88 | Layer stack: 89 | Deletes a layer 90 | will change current layer if deleted layer is lower in the stack 91 | 92 | MeshLab versions: 93 | 2016.12 94 | 1.3.4BETA 95 | """ 96 | filter_xml = ' \n' 97 | if isinstance(script, mlx.FilterScript): 98 | if (layer_num is None) or (layer_num == script.current_layer()): 99 | util.write_filter(script, filter_xml) 100 | script.del_layer(script.current_layer()) 101 | else: 102 | cur_layer = script.current_layer() 103 | change(script, layer_num) 104 | util.write_filter(script, filter_xml) 105 | if layer_num < script.current_layer(): 106 | change(script, cur_layer - 1) 107 | else: 108 | change(script, cur_layer) 109 | script.del_layer(layer_num) 110 | else: 111 | util.write_filter(script, filter_xml) 112 | return None 113 | 114 | 115 | def rename(script, label='blank', layer_num=None): 116 | """ Rename layer label 117 | 118 | Can be useful for outputting mlp files, as the output file names use 119 | the labels. 120 | 121 | Args: 122 | script: the mlx.FilterScript object or script filename to write 123 | the filter to. 124 | label (str): new label for the mesh layer 125 | layer_num (int): layer number to rename. Default is the 126 | current layer. Not supported on the file base API. 127 | 128 | Layer stack: 129 | Renames a layer 130 | 131 | MeshLab versions: 132 | 2016.12 133 | 1.3.4BETA 134 | """ 135 | filter_xml = ''.join([ 136 | ' \n', 137 | ' \n', 142 | ' \n']) 143 | if isinstance(script, mlx.FilterScript): 144 | if (layer_num is None) or (layer_num == script.current_layer()): 145 | util.write_filter(script, filter_xml) 146 | script.layer_stack[script.current_layer()] = label 147 | else: 148 | cur_layer = script.current_layer() 149 | change(script, layer_num) 150 | util.write_filter(script, filter_xml) 151 | change(script, cur_layer) 152 | script.layer_stack[layer_num] = label 153 | else: 154 | util.write_filter(script, filter_xml) 155 | return None 156 | 157 | 158 | def change(script, layer_num=None): 159 | """ Change the current layer by specifying the new layer number. 160 | 161 | Args: 162 | script: the mlx.FilterScript object or script filename to write 163 | the filter to. 164 | layer_num (int): the number of the layer to change to. Default is the 165 | last layer if script is a mlx.FilterScript object; if script is a 166 | filename the default is the first layer. 167 | 168 | Layer stack: 169 | Modifies current layer 170 | 171 | MeshLab versions: 172 | 2016.12 173 | 1.3.4BETA 174 | """ 175 | if layer_num is None: 176 | if isinstance(script, mlx.FilterScript): 177 | layer_num = script.last_layer() 178 | else: 179 | layer_num = 0 180 | if script.ml_version == '1.3.4BETA' or script.ml_version == '2016.12': 181 | filter_xml = ''.join([ 182 | ' \n', 183 | ' \n', 188 | ' \n']) 189 | else: 190 | filter_xml = ''.join([ 191 | ' \n', 192 | ' \n', 197 | ' \n']) 198 | util.write_filter(script, filter_xml) 199 | if isinstance(script, mlx.FilterScript): 200 | script.set_current_layer(layer_num) 201 | #script.layer_stack[len(self.layer_stack) - 1] = layer_num 202 | return None 203 | 204 | 205 | def duplicate(script, layer_num=None): 206 | """ Duplicate a layer. 207 | 208 | New layer label is '*_copy'. 209 | 210 | Args: 211 | script: the mlx.FilterScript object or script filename to write 212 | the filter to. 213 | layer_num (int): layer number to duplicate. Default is the 214 | current layer. Not supported on the file base API. 215 | 216 | Layer stack: 217 | Creates a new layer 218 | Changes current layer to the new layer 219 | 220 | MeshLab versions: 221 | 2016.12 222 | 1.3.4BETA 223 | """ 224 | filter_xml = ' \n' 225 | if isinstance(script, mlx.FilterScript): 226 | if (layer_num is None) or (layer_num == script.current_layer()): 227 | util.write_filter(script, filter_xml) 228 | script.add_layer('{}_copy'.format(script.layer_stack[script.current_layer()]), True) 229 | else: 230 | change(script, layer_num) 231 | util.write_filter(script, filter_xml) 232 | script.add_layer('{}_copy'.format(script.layer_stack[layer_num]), True) 233 | else: 234 | util.write_filter(script, filter_xml) 235 | return None 236 | 237 | 238 | def split_parts(script, part_num=None, layer_num=None): 239 | """ Split current layer into many layers, one for each part (connected 240 | component) 241 | 242 | Mesh is split so that the largest part is the lowest named layer "CC 0" 243 | and the smallest part is the highest numbered "CC" layer. 244 | 245 | Args: 246 | script: the mlx.FilterScript object or script filename to write 247 | the filter to. 248 | part_num (int): the number of parts in the model. This is needed in 249 | order to properly create and manage the layer stack. Can be found 250 | with mlx.compute.measure_topology. 251 | layer_num (int): the number of the layer to split. Default is the 252 | current layer. Not supported on the file base API. 253 | 254 | Layer stack: 255 | Creates a new layer for each part named "CC 0", "CC 1", etc. 256 | Changes current layer to the last new layer 257 | 258 | MeshLab versions: 259 | 2016.12 260 | 1.3.4BETA 261 | 262 | Bugs: 263 | UV textures: not currently preserved, however will be in a future 264 | release. https://github.com/cnr-isti-vclab/meshlab/issues/127 265 | """ 266 | filter_xml = ' \n' 267 | if isinstance(script, mlx.FilterScript): 268 | if (layer_num is not None) and (layer_num != script.current_layer()): 269 | change(script, layer_num) 270 | util.write_filter(script, filter_xml) 271 | if part_num is not None: 272 | for i in range(part_num): 273 | script.add_layer('CC {}'.format(i), True) 274 | else: 275 | script.add_layer('CC 0', True) 276 | print('Warning: the number of parts was not provided and cannot', 277 | 'be determined automatically. The layer stack is likely', 278 | 'incorrect!') 279 | else: 280 | util.write_filter(script, filter_xml) 281 | return None 282 | 283 | def delete_lower(script, layer_num=None): 284 | """ Delete all layers below the specified one. 285 | 286 | Useful for MeshLab ver 2016.12, which will only output layer 0. 287 | """ 288 | if layer_num is None: 289 | layer_num = script.current_layer() 290 | if layer_num != 0: 291 | change(script, 0) 292 | for i in range(layer_num): 293 | delete(script, 0) 294 | return None 295 | -------------------------------------------------------------------------------- /meshlabxml/mp_func.py: -------------------------------------------------------------------------------- 1 | """ MeshLabXML muparser functions """ 2 | 3 | import math 4 | import re 5 | 6 | from . import FilterScript 7 | from . import util 8 | 9 | 10 | def muparser_ref(): 11 | """Reference documentation for muparser. 12 | 13 | muparser is used by many internal MeshLab filters, specifically those 14 | where you can control parameters via a mathematical expression. Examples: 15 | transform.function 16 | select.vert_function 17 | vert_color.function 18 | 19 | The valid variables that can be used in an expression are given in the 20 | documentation for the individual functions. Generally, it's possible to use 21 | the following per-vertex variables in the expression: 22 | 23 | Variables (per vertex): 24 | x, y, z (coordinates) 25 | nx, ny, nz (normal) 26 | r, g, b, a (color) 27 | q (quality) 28 | rad 29 | vi (vertex index) 30 | vtu, vtv (texture coordinates) 31 | ti (texture index) 32 | vsel (is the vertex selected? 1 yes, 0 no) 33 | and all custom vertex attributes already defined by user. 34 | 35 | Below is a list of the predefined operators and functions within muparser. 36 | 37 | muparser homepage: http://beltoforion.de/article.php?a=muparser 38 | ml_version='1.3.4Beta' muparser version: 1.3.2 39 | ml_version='2016.12' muparser version: 2.2.5 40 | 41 | Built-in functions: 42 | Name Argc. Explanation 43 | ---------------------------- 44 | sin 1 sine function 45 | cos 1 cosine function 46 | tan 1 tangens function 47 | asin 1 arcus sine function 48 | acos 1 arcus cosine function 49 | atan 1 arcus tangens function 50 | atan2 2 atan2(y, x) 51 | sinh 1 hyperbolic sine function 52 | cosh 1 hyperbolic cosine 53 | tanh 1 hyperbolic tangens function 54 | asinh 1 hyperbolic arcus sine function 55 | acosh 1 hyperbolic arcus tangens function 56 | atanh 1 hyperbolic arcur tangens function 57 | log2 1 logarithm to the base 2 58 | log10 1 logarithm to the base 10 59 | log 1 logarithm to the base 10 60 | ln 1 logarithm to base e (2.71828...) 61 | exp 1 e raised to the power of x 62 | sqrt 1 square root of a value 63 | sign 1 sign function -1 if x<0; 1 if x>0 64 | rint 1 round to nearest integer 65 | abs 1 absolute value 66 | min var. min of all arguments 67 | max var. max of all arguments 68 | sum var. sum of all arguments 69 | avg var. mean value of all arguments 70 | 71 | Built-in binary operators 72 | Operator Description Priority 73 | ----------------------------------------------- 74 | = assignment -1 75 | && logical and* 1 76 | || logical or* 2 77 | <= less or equal 4 78 | >= greater or equal 4 79 | != not equal 4 80 | == equal 4 81 | > greater than 4 82 | < less than 4 83 | + addition 5 84 | - subtraction 5 85 | * multiplication 6 86 | / division 6 87 | ^ raise x to the power of y 7 88 | 89 | Built-in ternary operator 90 | ?: (Test ? Then_value : Otherwise_value)* 91 | 92 | *: Some operators in older muparser (ml_version='1.3.4Beta') are different: 93 | and logical and 1 94 | or logical or 2 95 | if ternary operator: if(Test, Then_value, Otherwise_value) 96 | atan2 not included; use mp_atan2 below 97 | """ 98 | pass 99 | 100 | 101 | def mp_atan2(y, x): 102 | """muparser atan2 function 103 | 104 | Implements an atan2(y,x) function for older muparser versions (<2.1.0); 105 | atan2 was added as a built-in function in muparser 2.1.0 106 | 107 | Args: 108 | y (str): y argument of the atan2(y,x) function 109 | x (str): x argument of the atan2(y,x) function 110 | 111 | Returns: 112 | A muparser string that calculates atan2(y,x) 113 | """ 114 | return 'if((x)>0, atan((y)/(x)), if(((x)<0) and ((y)>=0), atan((y)/(x))+pi, if(((x)<0) and ((y)<0), atan((y)/(x))-pi, if(((x)==0) and ((y)>0), pi/2, if(((x)==0) and ((y)<0), -pi/2, 0)))))'.replace( 115 | 'pi', str(math.pi)).replace('y', y).replace('x', x) 116 | 117 | 118 | def modulo(a,b): 119 | """ Modulo operator 120 | Example: modulo(angle,2*pi) to limit an angle to within 2pi radians 121 | """ 122 | return '(a < 0) ? b - (a - (b * rint(a/b))) : a - (b * rint(a/b))'.replace('a', a).replace('b', b) 123 | 124 | 125 | def v_cross(u, v): 126 | """muparser cross product function 127 | 128 | Compute the cross product of two 3x1 vectors 129 | 130 | Args: 131 | u (list or tuple of 3 strings): first vector 132 | v (list or tuple of 3 strings): second vector 133 | Returns: 134 | A list containing a muparser string of the cross product 135 | """ 136 | """ 137 | i = u[1]*v[2] - u[2]*v[1] 138 | j = u[2]*v[0] - u[0]*v[2] 139 | k = u[0]*v[1] - u[1]*v[0] 140 | """ 141 | 142 | i = '(({u1})*({v2}) - ({u2})*({v1}))'.format(u1=u[1], u2=u[2], v1=v[1], v2=v[2]) 143 | j = '(({u2})*({v0}) - ({u0})*({v2}))'.format(u0=u[0], u2=u[2], v0=v[0], v2=v[2]) 144 | k = '(({u0})*({v1}) - ({u1})*({v0}))'.format(u0=u[0], u1=u[1], v0=v[0], v1=v[1]) 145 | return [i, j, k] 146 | 147 | 148 | def v_dot(v1, v2): 149 | for i, x in enumerate(v1): 150 | if i == 0: 151 | dot = '({})*({})'.format(v1[i], v2[i]) 152 | else: 153 | dot += '+({})*({})'.format(v1[i], v2[i]) 154 | dot = '(' + dot + ')' 155 | return dot 156 | 157 | 158 | def v_add(v1, v2): 159 | vector = [] 160 | for i, x in enumerate(v1): 161 | vector.append('(({})+({}))'.format(v1[i], v2[i])) 162 | return vector 163 | 164 | 165 | def v_subtract(v1, v2): 166 | vector = [] 167 | for i, x in enumerate(v1): 168 | vector.append('(({})-({}))'.format(v1[i], v2[i])) 169 | return vector 170 | 171 | 172 | def v_multiply(scalar, v1): 173 | """ Multiply vector by scalar""" 174 | vector = [] 175 | for i, x in enumerate(v1): 176 | vector.append('(({})*({}))'.format(scalar, v1[i])) 177 | return vector 178 | 179 | 180 | def v_length(v1): 181 | for i, x in enumerate(v1): 182 | if i == 0: 183 | length = '({})^2'.format(v1[i]) 184 | else: 185 | length += '+({})^2'.format(v1[i]) 186 | length = 'sqrt(' + length + ')' 187 | return length 188 | 189 | 190 | def v_normalize(v1): 191 | vector = [] 192 | length = v_length(v1) 193 | for i, x in enumerate(v1): 194 | vector.append('({})/({})'.format(v1[i], length)) 195 | return vector 196 | 197 | 198 | def torus_knot(t, p=3, q=4, scale=1.0, radius=2.0): 199 | """ A tight (small inner crossings) (p,q) torus knot parametric curve 200 | 201 | Source (for trefoil): https://en.wikipedia.org/wiki/Trefoil_knot 202 | 203 | """ 204 | return ['{scale}*(sin({t}) + ({radius})*sin({p}*({t})))'.format(t=t, p=p, scale=scale, radius=radius), 205 | '{scale}*(cos({t}) - ({radius})*cos({p}*({t})))'.format(t=t, p=p, scale=scale, radius=radius), 206 | '{scale}*(-sin({q}*({t})))'.format(t=t, q=q, scale=scale)] 207 | 208 | 209 | def torus_knot_bbox(scale=1.0, radius=2.0): 210 | """ Bounding box of the sprecified torus knot 211 | 212 | """ 213 | return [2*scale*(1 + radius), 2*scale*(1 + radius), 2*scale] 214 | 215 | 216 | def vert_attr(script, name='radius', function='x^2 + y^2'): 217 | """ Add a new Per-Vertex scalar attribute to current mesh and fill it with 218 | the defined function. 219 | 220 | The specified name can be used in other filter functions. 221 | 222 | It's possible to use parenthesis, per-vertex variables and boolean operator: 223 | (, ), and, or, <, >, = 224 | It's possible to use the following per-vertex variables in the expression: 225 | 226 | Variables: 227 | x, y, z (coordinates) 228 | nx, ny, nz (normal) 229 | r, g, b, a (color) 230 | q (quality) 231 | rad 232 | vi (vertex index) 233 | ?vtu, vtv (texture coordinates) 234 | ?ti (texture index) 235 | ?vsel (is the vertex selected? 1 yes, 0 no) 236 | and all custom vertex attributes already defined by user. 237 | 238 | Args: 239 | script: the FilterScript object or script filename to write 240 | the filter] to. 241 | name (str): the name of new attribute. You can access attribute in 242 | other filters through this name. 243 | function (str): function to calculate custom attribute value for each 244 | vertex 245 | 246 | Layer stack: 247 | No impacts 248 | 249 | MeshLab versions: 250 | 2016.12 251 | 1.3.4BETA 252 | """ 253 | filter_xml = ''.join([ 254 | ' \n', 255 | ' \n', 260 | ' \n', 265 | ' \n']) 266 | util.write_filter(script, filter_xml) 267 | return None 268 | 269 | 270 | def face_attr(script, name='radiosity', function='fi'): 271 | """ Add a new Per-Face attribute to current mesh and fill it with 272 | the defined function.. 273 | 274 | The specified name can be used in other filter functions. 275 | 276 | It's possible to use parenthesis, per-face variables and boolean operator: 277 | (, ), and, or, <, >, = 278 | 279 | It's possible to use per-face variables like attributes associated to the 280 | three vertices of every face. 281 | 282 | It's possible to use the following per-face variables in the expression: 283 | 284 | Variables: 285 | x0,y0,z0 (first vertex); x1,y1,z1 (second vertex); x2,y2,z2 (third vertex) 286 | nx0,ny0,nz0; nx1,ny1,nz1; nx2,ny2,nz2 (normals) 287 | r0,g0,b0 (color) 288 | q0,q1,q2 (quality) 289 | wtu0, wtv0; wtu1, wtv1; wtu2, wtv2 (per wedge tex coord) 290 | fi (face index) 291 | ?fsel (is the vertex selected? 1 yes, 0 no) 292 | fi (face index) 293 | and all custom face attributes already defined by user. 294 | 295 | Args: 296 | script: the FilterScript object or script filename to write 297 | the filter] to. 298 | name (str): the name of new attribute. You can access attribute in 299 | other filters through this name. 300 | function (str): function to calculate custom attribute value for each 301 | face 302 | 303 | Layer stack: 304 | No impacts 305 | 306 | MeshLab versions: 307 | 2016.12 308 | 1.3.4BETA 309 | """ 310 | filter_xml = ''.join([ 311 | ' \n', 312 | ' \n', 317 | ' \n', 322 | ' \n']) 323 | util.write_filter(script, filter_xml) 324 | return None 325 | 326 | 327 | def vq_function(script, function='vi', normalize=False, color=False): 328 | """:Quality function using muparser to generate new Quality for every vertex
It's possibile to use the following per-vertex variables in the expression:
x, y, z, nx, ny, nz (normal), r, g, b (color), q (quality), rad, vi,
and all custom vertex attributes already defined by user. 329 | 330 | 331 | function function to generate new Quality for every vertex 332 | normalize if checked normalize all quality values in range [0..1] 333 | color if checked map quality generated values into per-vertex color 334 | 335 | """ 336 | filter_xml = ''.join([ 337 | ' \n', 338 | ' \n', 343 | ' \n', 348 | ' \n', 353 | ' \n']) 354 | util.write_filter(script, filter_xml) 355 | return None 356 | 357 | 358 | def fq_function(script, function='x0+y0+z0', normalize=False, color=False): 359 | """ :Quality function using muparser to generate new Quality for every face
Insert three function each one for quality of the three vertex of a face
It's possibile to use per-face variables like attributes associated to the three vertex of every face.
x0,y0,z0 for first vertex; x1,y1,z1 for second vertex; x2,y2,z2 for third vertex.
and also nx0,ny0,nz0 nx1,ny1,nz1 etc. for normals and r0,g0,b0 for color,q0,q1,q2 for quality.
360 | 361 | function function to generate new Quality for each face 362 | normalize if checked normalize all quality values in range [0..1] 363 | color if checked map quality generated values into per-vertex color 364 | 365 | 366 | """ 367 | filter_xml = ''.join([ 368 | ' \n', 369 | ' \n', 374 | ' \n', 379 | ' \n', 384 | ' \n']) 385 | util.write_filter(script, filter_xml) 386 | return None 387 | -------------------------------------------------------------------------------- /meshlabxml/normals.py: -------------------------------------------------------------------------------- 1 | """ MeshLabXML functions for mesh normals """ 2 | 3 | from . import util 4 | 5 | def reorient(script): 6 | """ Re-orient in a consistent way all the faces of the mesh. 7 | 8 | The filter visits a mesh face to face, reorienting any unvisited face so 9 | that it is coherent to the already visited faces. If the surface is 10 | orientable it will end with a consistent orientation of all the faces. If 11 | the surface is not orientable (e.g. it is non manifold or non orientable 12 | like a moebius strip) the filter will not build a consistent orientation 13 | simply because it is not possible. The filter can end up in a consistent 14 | orientation that can be exactly the opposite of the expected one; in that 15 | case simply invert the whole mesh orientation. 16 | 17 | Args: 18 | script: the FilterScript object or script filename to write 19 | the filter to. 20 | 21 | Layer stack: 22 | No impacts 23 | 24 | MeshLab versions: 25 | 2016.12 26 | 1.3.4BETA 27 | """ 28 | filter_xml = ' \n' 29 | util.write_filter(script, filter_xml) 30 | return None 31 | 32 | 33 | def flip(script, force_flip=False, selected=False): 34 | """ Invert faces orientation, flipping the normals of the mesh. 35 | 36 | If requested, it tries to guess the right orientation; mainly it decides to 37 | flip all the faces if the minimum/maximum vertexes have not outward point 38 | normals for a few directions. Works well for single component watertight 39 | objects. 40 | 41 | Args: 42 | script: the FilterScript object or script filename to write 43 | the filter to. 44 | force_flip (bool): If selected, the normals will always be flipped; 45 | otherwise, the filter tries to set them outside. 46 | selected (bool): If selected, only selected faces will be affected. 47 | 48 | Layer stack: 49 | No impacts 50 | 51 | MeshLab versions: 52 | 2016.12 53 | 1.3.4BETA 54 | """ 55 | filter_xml = ''.join([ 56 | ' \n', 57 | ' \n', 62 | ' \n', 67 | ' \n']) 68 | util.write_filter(script, filter_xml) 69 | return None 70 | 71 | 72 | def fix(script): 73 | """ Will reorient normals & ensure they are oriented outwards 74 | 75 | Layer stack: 76 | No impacts 77 | 78 | MeshLab versions: 79 | 2016.12 80 | 1.3.4BETA 81 | """ 82 | reorient(script) 83 | flip(script) 84 | return 85 | 86 | 87 | def point_sets(script, neighbors=10, smooth_iteration=0, flip=False, 88 | viewpoint_pos=(0.0, 0.0, 0.0)): 89 | """ Compute the normals of the vertices of a mesh without exploiting the 90 | triangle connectivity, useful for dataset with no faces. 91 | 92 | Args: 93 | script: the FilterScript object or script filename to write 94 | the filter to. 95 | neighbors (int): The number of neighbors used to estimate normals. 96 | smooth_iteration (int): The number of smoothing iteration done on the 97 | p used to estimate and propagate normals. 98 | flip (bool): Flip normals w.r.t. viewpoint. If the 'viewpoint' (i.e. 99 | scanner position) is known, it can be used to disambiguate normals 100 | orientation, so that all the normals will be oriented in the same 101 | direction. 102 | viewpoint_pos (single xyz point, tuple or list): Set the x, y, z 103 | coordinates of the viewpoint position. 104 | 105 | Layer stack: 106 | No impacts 107 | 108 | MeshLab versions: 109 | 2016.12 110 | 1.3.4BETA 111 | """ 112 | filter_xml = ''.join([ 113 | ' \n', 114 | ' \n', 119 | ' \n', 124 | ' \n', 129 | ' \n', 135 | ' \n']) 136 | util.write_filter(script, filter_xml) 137 | return None 138 | -------------------------------------------------------------------------------- /meshlabxml/sampling.py: -------------------------------------------------------------------------------- 1 | """ MeshLabXML sampling functions """ 2 | 3 | from . import FilterScript 4 | from . import util 5 | 6 | 7 | def hausdorff_distance(script, sampled_layer=1, target_layer=0, 8 | save_sample=False, sample_vert=True, sample_edge=True, 9 | sample_faux_edge=False, sample_face=True, 10 | sample_num=1000, maxdist=10): 11 | """ Compute the Hausdorff Distance between two meshes, sampling one of the 12 | two and finding for each sample the closest point over the other mesh. 13 | 14 | Args: 15 | script: the FilterScript object or script filename to write 16 | the filter to. 17 | sampled_layer (int): The mesh layer whose surface is sampled. For each 18 | sample we search the closest point on the target mesh layer. 19 | target_layer (int): The mesh that is sampled for the comparison. 20 | save_sample (bool): Save the position and distance of all the used 21 | samples on both the two surfaces, creating two new layers with two 22 | point clouds representing the used samples. 23 | sample_vert (bool): For the search of maxima it is useful to sample 24 | vertices and edges of the mesh with a greater care. It is quite 25 | probable that the farthest points falls along edges or on mesh 26 | vertexes, and with uniform montecarlo sampling approaches the 27 | probability of taking a sample over a vertex or an edge is 28 | theoretically null. On the other hand this kind of sampling could 29 | make the overall sampling distribution slightly biased and slightly 30 | affects the cumulative results. 31 | sample_edge (bool): see sample_vert 32 | sample_faux_edge (bool): see sample_vert 33 | sample_face (bool): see sample_vert 34 | sample_num (int): The desired number of samples. It can be smaller or 35 | larger than the mesh size, and according to the chosen sampling 36 | strategy it will try to adapt. 37 | maxdist (int): Sample points for which we do not find anything within 38 | this distance are rejected and not considered neither for averaging 39 | nor for max. 40 | 41 | Layer stack: 42 | If save_sample is True, two new layers are created: 'Hausdorff Closest 43 | Points' and 'Hausdorff Sample Point'; and the current layer is 44 | changed to the last newly created layer. 45 | If save_sample is False, no impacts 46 | 47 | MeshLab versions: 48 | 2016.12 49 | 1.3.4BETA 50 | """ 51 | # MeshLab defaults: 52 | # sample_num = number of vertices 53 | # maxdist = 0.05 * AABB['diag'] #5% of AABB[diag] 54 | # maxdist_max = AABB['diag'] 55 | maxdist_max = 2*maxdist 56 | # TODO: parse output (min, max, mean, etc.) 57 | filter_xml = ''.join([ 58 | ' \n', 59 | ' \n', 64 | ' \n', 69 | ' \n', 74 | ' \n', 79 | ' \n', 84 | ' \n', 89 | ' \n', 95 | ' \n', 100 | ' \n', 108 | ' \n']) 109 | util.write_filter(script, filter_xml) 110 | if isinstance(script, FilterScript): 111 | script.parse_hausdorff = True 112 | if isinstance(script, FilterScript) and save_sample: 113 | script.add_layer('Hausdorff Closest Points') 114 | script.add_layer('Hausdorff Sample Point') 115 | return None 116 | 117 | 118 | def poisson_disk(script, sample_num=1000, radius=0.0, 119 | montecarlo_rate=20, save_montecarlo=False, 120 | approx_geodesic_dist=False, subsample=False, refine=False, 121 | refine_layer=0, best_sample=True, best_sample_pool=10, 122 | exact_num=False, radius_variance=1.0): 123 | """ Create a new layer populated with a point sampling of the current mesh. 124 | 125 | Samples are generated according to a Poisson-disk distribution, using the 126 | algorithm described in: 127 | 128 | 'Efficient and Flexible Sampling with Blue Noise Properties of Triangular Meshes' 129 | Massimiliano Corsini, Paolo Cignoni, Roberto Scopigno 130 | IEEE TVCG 2012 131 | 132 | Args: 133 | script: the FilterScript object or script filename to write 134 | the filter to. 135 | sample_num (int): The desired number of samples. The radius of the disk 136 | is calculated according to the sampling density. 137 | radius (float): If not zero this parameter overrides the previous 138 | parameter to allow exact radius specification. 139 | montecarlo_rate (int): The over-sampling rate that is used to generate 140 | the intial Monte Carlo samples (e.g. if this parameter is 'K' means 141 | that 'K * sample_num' points will be used). The generated 142 | Poisson-disk samples are a subset of these initial Monte Carlo 143 | samples. Larger numbers slow the process but make it a bit more 144 | accurate. 145 | save_montecarlo (bool): If True, it will generate an additional Layer 146 | with the Monte Carlo sampling that was pruned to build the Poisson 147 | distribution. 148 | approx_geodesic_dist (bool): If True Poisson-disk distances are 149 | computed using an approximate geodesic distance, e.g. an Euclidean 150 | distance weighted by a function of the difference between the 151 | normals of the two points. 152 | subsample (bool): If True the original vertices of the base mesh are 153 | used as base set of points. In this case the sample_num should be 154 | obviously much smaller than the original vertex number. Note that 155 | this option is very useful in the case you want to subsample a 156 | dense point cloud. 157 | refine (bool): If True the vertices of the refine_layer mesh layer are 158 | used as starting vertices, and they will be utterly refined by 159 | adding more and more points until possible. 160 | refine_layer (int): Used only if refine is True. 161 | best_sample (bool): If True it will use a simple heuristic for choosing 162 | the samples. At a small cost (it can slow the process a bit) it 163 | usually improves the maximality of the generated sampling. 164 | best_sample_pool (bool): Used only if best_sample is True. It controls 165 | the number of attempts that it makes to get the best sample. It is 166 | reasonable that it is smaller than the Monte Carlo oversampling 167 | factor. 168 | exact_num (bool): If True it will try to do a dicotomic search for the 169 | best Poisson-disk radius that will generate the requested number of 170 | samples with a tolerance of the 0.5%. Obviously it takes much 171 | longer. 172 | radius_variance (float): The radius of the disk is allowed to vary 173 | between r and r*var. If this parameter is 1 the sampling is the 174 | same as the Poisson-disk Sampling. 175 | 176 | Layer stack: 177 | Creates new layer 'Poisson-disk Samples'. Current layer is NOT changed 178 | to the new layer (see Bugs). 179 | If save_montecarlo is True, creates a new layer 'Montecarlo Samples'. 180 | Current layer is NOT changed to the new layer (see Bugs). 181 | 182 | MeshLab versions: 183 | 2016.12 184 | 1.3.4BETA 185 | 186 | Bugs: 187 | Current layer is NOT changed to the new layer, which is inconsistent 188 | with the majority of filters that create new layers. 189 | """ 190 | filter_xml = ''.join([ 191 | ' \n', 192 | ' \n', 197 | ' \n', 204 | ' \n', 209 | ' \n', 214 | ' \n', 219 | ' \n', 224 | ' \n', 229 | ' \n', 234 | ' \n', 239 | ' \n', 244 | ' \n', 249 | ' \n', 254 | ' \n']) 255 | util.write_filter(script, filter_xml) 256 | if isinstance(script, FilterScript): 257 | script.add_layer('Poisson-disk Samples') 258 | if save_montecarlo: 259 | script.add_layer('Montecarlo Samples') 260 | return None 261 | 262 | 263 | def mesh_element(script, sample_num=1000, element='VERT'): 264 | """ Create a new layer populated with a point sampling of the current mesh, 265 | at most one sample for each element of the mesh is created. 266 | 267 | Samples are taking in a uniform way, one for each element 268 | (vertex/edge/face); all the elements have the same probabilty of being 269 | choosen. 270 | 271 | Args: 272 | script: the FilterScript object or script filename to write 273 | the filter to. 274 | sample_num (int): The desired number of elements that must be chosen. 275 | Being a subsampling of the original elements if this number should 276 | not be larger than the number of elements of the original mesh. 277 | element (enum in ['VERT', 'EDGE', 'FACE']): Choose what mesh element 278 | will be used for the subsampling. At most one point sample will 279 | be added for each one of the chosen elements 280 | 281 | Layer stack: 282 | Creates new layer 'Sampled Mesh'. Current layer is changed to the new 283 | layer. 284 | 285 | MeshLab versions: 286 | 2016.12 287 | 1.3.4BETA 288 | """ 289 | if element.lower() == 'vert': 290 | element_num = 0 291 | elif element.lower() == 'edge': 292 | element_num = 1 293 | elif element.lower() == 'face': 294 | element_num = 2 295 | filter_xml = ''.join([ 296 | ' \n', 297 | ' \n', 306 | ' \n', 311 | ' \n']) 312 | util.write_filter(script, filter_xml) 313 | if isinstance(script, FilterScript): 314 | script.add_layer('Sampled Mesh') 315 | return None 316 | 317 | 318 | def clustered_vert(script, cell_size=1.0, strategy='AVERAGE', selected=False): 319 | """ "Create a new layer populated with a subsampling of the vertexes of the 320 | current mesh 321 | 322 | The subsampling is driven by a simple one-per-gridded cell strategy. 323 | 324 | Args: 325 | script: the FilterScript object or script filename to write 326 | the filter to. 327 | cell_size (float): The size of the cell of the clustering grid. Smaller the cell finer the resulting mesh. For obtaining a very coarse mesh use larger values. 328 | strategy (enum 'AVERAGE' or 'CENTER'): <b>Average</b>: for each cell we take the average of the sample falling into. The resulting point is a new point.<br><b>Closest to center</b>: for each cell we take the sample that is closest to the center of the cell. Choosen vertices are a subset of the original ones. 329 | selected (bool): If true only for the filter is applied only on the selected subset of the mesh. 330 | 331 | Layer stack: 332 | Creates new layer 'Cluster Samples'. Current layer is changed to the new 333 | layer. 334 | 335 | MeshLab versions: 336 | 2016.12 337 | 1.3.4BETA 338 | """ 339 | if strategy.lower() == 'average': 340 | strategy_num = 0 341 | elif strategy.lower() == 'center': 342 | strategy_num = 1 343 | 344 | filter_xml = ''.join([ 345 | ' \n', 346 | ' \n', 353 | ' \n', 361 | ' \n', 366 | ' \n']) 367 | util.write_filter(script, filter_xml) 368 | if isinstance(script, FilterScript): 369 | script.add_layer('Cluster Samples') 370 | return None 371 | -------------------------------------------------------------------------------- /meshlabxml/select.py: -------------------------------------------------------------------------------- 1 | """MeshLabXML selection functions""" 2 | 3 | from . import util 4 | 5 | 6 | def all(script, face=True, vert=True): 7 | """ Select all the faces of the current mesh 8 | 9 | Args: 10 | script: the FilterScript object or script filename to write 11 | the filter to. 12 | faces (bool): If True the filter will select all the faces. 13 | verts (bool): If True the filter will select all the vertices. 14 | 15 | Layer stack: 16 | No impacts 17 | 18 | MeshLab versions: 19 | 2016.12 20 | 1.3.4BETA 21 | """ 22 | filter_xml = ''.join([ 23 | ' \n', 24 | ' \n', 29 | ' \n', 34 | ' \n']) 35 | util.write_filter(script, filter_xml) 36 | return None 37 | 38 | 39 | def none(script, face=True, vert=True): 40 | """ Clear the current set of selected faces 41 | 42 | Args: 43 | script: the FilterScript object or script filename to write 44 | the filter to. 45 | faces (bool): If True the filter will deselect all the faces. 46 | verts (bool): If True the filter will deselect all the vertices. 47 | 48 | Layer stack: 49 | No impacts 50 | 51 | MeshLab versions: 52 | 2016.12 53 | 1.3.4BETA 54 | """ 55 | filter_xml = ''.join([ 56 | ' \n', 57 | ' \n', 62 | ' \n', 67 | ' \n']) 68 | util.write_filter(script, filter_xml) 69 | return None 70 | 71 | 72 | def invert(script, face=True, vert=True): 73 | """ Invert the current set of selected faces 74 | 75 | Args: 76 | script: the FilterScript object or script filename to write 77 | the filter to. 78 | faces (bool): If True the filter will invert the selected faces. 79 | verts (bool): If True the filter will invert the selected vertices. 80 | 81 | Layer stack: 82 | No impacts 83 | 84 | MeshLab versions: 85 | 2016.12 86 | 1.3.4BETA 87 | """ 88 | filter_xml = ''.join([ 89 | ' \n', 90 | ' \n', 95 | ' \n', 100 | ' \n']) 101 | util.write_filter(script, filter_xml) 102 | return None 103 | 104 | 105 | def border(script): 106 | """ Select vertices and faces on the boundary 107 | 108 | Args: 109 | script: the FilterScript object or script filename to write 110 | the filter to. 111 | 112 | Layer stack: 113 | No impacts 114 | 115 | MeshLab versions: 116 | 2016.12 117 | 1.3.4BETA 118 | """ 119 | filter_xml = ' \n' 120 | util.write_filter(script, filter_xml) 121 | return None 122 | 123 | 124 | def grow(script, iterations=1): 125 | """ Grow (dilate, expand) the current set of selected faces 126 | 127 | Args: 128 | script: the FilterScript object or script filename to write 129 | the filter to. 130 | iterations (int): the number of times to grow the selection. 131 | 132 | Layer stack: 133 | No impacts 134 | 135 | MeshLab versions: 136 | 2016.12 137 | 1.3.4BETA 138 | """ 139 | filter_xml = ' \n' 140 | for _ in range(iterations): 141 | util.write_filter(script, filter_xml) 142 | return None 143 | 144 | 145 | def shrink(script, iterations=1): 146 | """ Shrink (erode, reduce) the current set of selected faces 147 | 148 | Args: 149 | script: the FilterScript object or script filename to write 150 | the filter to. 151 | iterations (int): the number of times to shrink the selection. 152 | 153 | Layer stack: 154 | No impacts 155 | 156 | MeshLab versions: 157 | 2016.12 158 | 1.3.4BETA 159 | """ 160 | filter_xml = ' \n' 161 | for _ in range(iterations): 162 | util.write_filter(script, filter_xml) 163 | return None 164 | 165 | 166 | def self_intersecting_face(script): 167 | """ Select only self intersecting faces 168 | 169 | Args: 170 | script: the FilterScript object or script filename to write 171 | the filter to. 172 | 173 | Layer stack: 174 | No impacts 175 | 176 | MeshLab versions: 177 | 2016.12 178 | 1.3.4BETA 179 | """ 180 | filter_xml = ' \n' 181 | util.write_filter(script, filter_xml) 182 | return None 183 | 184 | 185 | def nonmanifold_vert(script): 186 | """ Select the non manifold vertices that do not belong to non manifold 187 | edges. 188 | 189 | For example two cones connected by their apex. Vertices incident on 190 | non manifold edges are ignored. 191 | 192 | Args: 193 | script: the FilterScript object or script filename to write 194 | the filter to. 195 | 196 | Layer stack: 197 | No impacts 198 | 199 | MeshLab versions: 200 | 2016.12 201 | 1.3.4BETA 202 | """ 203 | filter_xml = ' \n' 204 | util.write_filter(script, filter_xml) 205 | return None 206 | 207 | 208 | def nonmanifold_edge(script): 209 | """ Select the faces and the vertices incident on non manifold edges (e.g. 210 | edges where more than two faces are incident). 211 | 212 | Note that this function selects the components that are related to 213 | non manifold edges. The case of non manifold vertices is specifically 214 | managed by nonmanifold_vert. 215 | 216 | Args: 217 | script: the FilterScript object or script filename to write 218 | the filter to. 219 | 220 | Layer stack: 221 | No impacts 222 | 223 | MeshLab versions: 224 | 2016.12 225 | 1.3.4BETA 226 | """ 227 | filter_xml = ' \n' 228 | util.write_filter(script, filter_xml) 229 | return None 230 | 231 | 232 | def small_parts(script, ratio=0.2, non_closed_only=False): 233 | """ Select the small disconnected parts (components) of a mesh. 234 | 235 | Args: 236 | script: the FilterScript object or script filename to write 237 | the filter to. 238 | ratio (float): This ratio (between 0 and 1) defines the meaning of 239 | 'small' as the threshold ratio between the number of faces of the 240 | largest component and the other ones. A larger value will select 241 | more components. 242 | non_closed_only (bool): Select only non-closed components. 243 | 244 | Layer stack: 245 | No impacts 246 | 247 | MeshLab versions: 248 | 2016.12 249 | 1.3.4BETA 250 | """ 251 | if script.ml_version == '1.3.4BETA' or script.ml_version == '2016.12': 252 | filter_name = 'Small component selection' 253 | else: 254 | filter_name = 'Select small disconnected component' 255 | filter_xml = ''.join([ 256 | ' \n'.format(filter_name), 257 | ' \n', 262 | ' \n', 267 | ' \n']) 268 | util.write_filter(script, filter_xml) 269 | return None 270 | 271 | 272 | def vert_quality(script, min_quality=0.0, max_quality=0.05, inclusive=True): 273 | """ Select all the faces and vertexes within the specified vertex quality 274 | range. 275 | 276 | Args: 277 | script: the FilterScript object or script filename to write 278 | the filter] to. 279 | min_quality (float): Minimum acceptable quality value. 280 | max_quality (float): Maximum acceptable quality value. 281 | inclusive (bool): If True only the faces with ALL the vertices within 282 | the specified range are selected. Otherwise any face with at least 283 | one vertex within the range is selected. 284 | 285 | Layer stack: 286 | No impacts 287 | 288 | MeshLab versions: 289 | 2016.12 290 | 1.3.4BETA 291 | """ 292 | filter_xml = ''.join([ 293 | ' \n', 294 | ' \n', 301 | ' \n', 308 | ' \n', 313 | ' \n']) 314 | util.write_filter(script, filter_xml) 315 | return None 316 | 317 | 318 | def face_function(script, function='(fi == 0)'): 319 | """Boolean function using muparser lib to perform face selection over 320 | current mesh. 321 | 322 | See help(mlx.muparser_ref) for muparser reference documentation. 323 | 324 | It's possible to use parenthesis, per-vertex variables and boolean operator: 325 | (, ), and, or, <, >, = 326 | It's possible to use per-face variables like attributes associated to the three 327 | vertices of every face. 328 | 329 | Variables (per face): 330 | x0, y0, z0 for first vertex; x1,y1,z1 for second vertex; x2,y2,z2 for third vertex 331 | nx0, ny0, nz0, nx1, ny1, nz1, etc. for vertex normals 332 | r0, g0, b0, a0, etc. for vertex color 333 | q0, q1, q2 for quality 334 | wtu0, wtv0, wtu1, wtv1, wtu2, wtv2 (per wedge texture coordinates) 335 | ti for face texture index (>= ML2016.12) 336 | vsel0, vsel1, vsel2 for vertex selection (1 yes, 0 no) (>= ML2016.12) 337 | fr, fg, fb, fa for face color (>= ML2016.12) 338 | fq for face quality (>= ML2016.12) 339 | fnx, fny, fnz for face normal (>= ML2016.12) 340 | fsel face selection (1 yes, 0 no) (>= ML2016.12) 341 | 342 | Args: 343 | script: the FilterScript object or script filename to write 344 | the filter] to. 345 | function (str): a boolean function that will be evaluated in order 346 | to select a subset of faces. 347 | 348 | Layer stack: 349 | No impacts 350 | 351 | MeshLab versions: 352 | 2016.12 353 | 1.3.4BETA 354 | """ 355 | filter_xml = ''.join([ 356 | ' \n', 357 | ' \n', 362 | ' \n']) 363 | util.write_filter(script, filter_xml) 364 | return None 365 | 366 | 367 | def vert_function(script, function='(q < 0)', strict_face_select=True): 368 | """Boolean function using muparser lib to perform vertex selection over current mesh. 369 | 370 | See help(mlx.muparser_ref) for muparser reference documentation. 371 | 372 | It's possible to use parenthesis, per-vertex variables and boolean operator: 373 | (, ), and, or, <, >, = 374 | It's possible to use the following per-vertex variables in the expression: 375 | 376 | Variables: 377 | x, y, z (coordinates) 378 | nx, ny, nz (normal) 379 | r, g, b, a (color) 380 | q (quality) 381 | rad 382 | vi (vertex index) 383 | vtu, vtv (texture coordinates) 384 | ti (texture index) 385 | vsel (is the vertex selected? 1 yes, 0 no) 386 | and all custom vertex attributes already defined by user. 387 | 388 | Args: 389 | script: the FilterScript object or script filename to write 390 | the filter] to. 391 | function (str): a boolean function that will be evaluated in order 392 | to select a subset of vertices. Example: (y > 0) and (ny > 0) 393 | strict_face_select (bool): if True a face is selected if ALL its 394 | vertices are selected. If False a face is selected if at least 395 | one of its vertices is selected. ML v1.3.4BETA only; this is 396 | ignored in 2016.12. In 2016.12 only vertices are selected. 397 | 398 | Layer stack: 399 | No impacts 400 | 401 | MeshLab versions: 402 | 2016.12 403 | 1.3.4BETA 404 | """ 405 | if script.ml_version == '1.3.4BETA': 406 | strict_select = ''.join([ 407 | ' \n', 412 | ]) 413 | else: 414 | strict_select = '' 415 | 416 | filter_xml = ''.join([ 417 | ' \n', 418 | ' \n', 423 | strict_select, 424 | ' \n']) 425 | util.write_filter(script, filter_xml) 426 | return None 427 | 428 | 429 | def cylindrical_vert(script, radius=1.0, inside=True): 430 | """Select all vertices within a cylindrical radius 431 | 432 | Args: 433 | radius (float): radius of the sphere 434 | center_pt (3 coordinate tuple or list): center point of the sphere 435 | 436 | Layer stack: 437 | No impacts 438 | 439 | MeshLab versions: 440 | 2016.12 441 | 1.3.4BETA 442 | """ 443 | if inside: 444 | function = 'sqrt(x^2+y^2)<={}'.format(radius) 445 | else: 446 | function = 'sqrt(x^2+y^2)>={}'.format(radius) 447 | vert_function(script, function=function) 448 | return None 449 | 450 | 451 | def spherical_vert(script, radius=1.0, center_pt=(0.0, 0.0, 0.0)): 452 | """Select all vertices within a spherical radius 453 | 454 | Args: 455 | radius (float): radius of the sphere 456 | center_pt (3 coordinate tuple or list): center point of the sphere 457 | 458 | Layer stack: 459 | No impacts 460 | 461 | MeshLab versions: 462 | 2016.12 463 | 1.3.4BETA 464 | """ 465 | function = 'sqrt((x-{})^2+(y-{})^2+(z-{})^2)<={}'.format( 466 | center_pt[0], center_pt[1], center_pt[2], radius) 467 | vert_function(script, function=function) 468 | return None 469 | -------------------------------------------------------------------------------- /meshlabxml/smooth.py: -------------------------------------------------------------------------------- 1 | """ MeshLabXML smoothing functions """ 2 | 3 | from . import util 4 | 5 | def laplacian(script, iterations=1, boundary=True, cotangent_weight=True, 6 | selected=False): 7 | """ Laplacian smooth of the mesh: for each vertex it calculates the average 8 | position with nearest vertex 9 | 10 | Args: 11 | script: the FilterScript object or script filename to write 12 | the filter to. 13 | iterations (int): The number of times that the whole algorithm (normal 14 | smoothing + vertex fitting) is iterated. 15 | boundary (bool): If true the boundary edges are smoothed only by 16 | themselves (e.g. the polyline forming the boundary of the mesh is 17 | independently smoothed). Can reduce the shrinking on the border but 18 | can have strange effects on very small boundaries. 19 | cotangent_weight (bool): If True the cotangent weighting scheme is 20 | computed for the averaging of the position. Otherwise (False) the 21 | simpler umbrella scheme (1 if the edge is present) is used. 22 | selected (bool): If selected the filter is performed only on the 23 | selected faces 24 | 25 | Layer stack: 26 | No impacts 27 | 28 | MeshLab versions: 29 | 2016.12 30 | 1.3.4BETA 31 | """ 32 | filter_xml = ''.join([ 33 | ' \n', 34 | ' \n', 39 | ' \n', 44 | ' \n', 49 | ' \n', 54 | ' \n']) 55 | util.write_filter(script, filter_xml) 56 | return None 57 | 58 | 59 | def hc_laplacian(script): 60 | """ HC Laplacian Smoothing, extended version of Laplacian Smoothing, based 61 | on the paper of Vollmer, Mencl, and Muller 62 | 63 | Layer stack: 64 | No impacts 65 | 66 | MeshLab versions: 67 | 2016.12 68 | 1.3.4BETA 69 | """ 70 | filter_xml = ' \n' 71 | util.write_filter(script, filter_xml) 72 | return None 73 | 74 | 75 | def taubin(script, iterations=10, t_lambda=0.5, t_mu=-0.53, selected=False): 76 | """ The lambda & mu Taubin smoothing, it make two steps of smoothing, forth 77 | and back, for each iteration. 78 | 79 | Based on: 80 | Gabriel Taubin 81 | "A signal processing approach to fair surface design" 82 | Siggraph 1995 83 | 84 | Args: 85 | script: the FilterScript object or script filename to write 86 | the filter to. 87 | iterations (int): The number of times that the taubin smoothing is 88 | iterated. Usually it requires a larger number of iteration than the 89 | classical laplacian. 90 | t_lambda (float): The lambda parameter of the Taubin Smoothing algorithm 91 | t_mu (float): The mu parameter of the Taubin Smoothing algorithm 92 | selected (bool): If selected the filter is performed only on the 93 | selected faces 94 | 95 | Layer stack: 96 | No impacts 97 | 98 | MeshLab versions: 99 | 2016.12 100 | 1.3.4BETA 101 | """ 102 | filter_xml = ''.join([ 103 | ' \n', 104 | ' \n', 109 | ' \n', 114 | ' \n', 119 | ' \n', 124 | ' \n']) 125 | util.write_filter(script, filter_xml) 126 | return None 127 | 128 | 129 | def twostep(script, iterations=3, angle_threshold=60, normal_steps=20, fit_steps=20, 130 | selected=False): 131 | """ Two Step Smoothing, a feature preserving/enhancing fairing filter. 132 | 133 | It is based on a Normal Smoothing step where similar normals are averaged 134 | together and a step where the vertexes are fitted on the new normals. 135 | 136 | Based on: 137 | A. Belyaev and Y. Ohtake, 138 | "A Comparison of Mesh Smoothing Methods" 139 | Proc. Israel-Korea Bi-National Conf. Geometric Modeling and Computer 140 | Graphics, pp. 83-87, 2003. 141 | 142 | Args: 143 | script: the FilterScript object or script filename to write 144 | the filter to. 145 | iterations (int): The number of times that the whole algorithm (normal 146 | smoothing + vertex fitting) is iterated. 147 | angle_threshold (float): Specify a threshold angle (0..90) for features 148 | that you want to be preserved. Features forming angles LARGER than 149 | the specified threshold will be preserved. 150 | 0 -> no smoothing 151 | 90 -> all faces will be smoothed 152 | normal_steps (int): Number of iterations of normal smoothing step. The 153 | larger the better and (the slower) 154 | fit_steps (int): Number of iterations of the vertex fitting procedure 155 | selected (bool): If selected the filter is performed only on the 156 | selected faces 157 | 158 | Layer stack: 159 | No impacts 160 | 161 | MeshLab versions: 162 | 2016.12 163 | 1.3.4BETA 164 | """ 165 | filter_xml = ''.join([ 166 | ' \n', 167 | ' \n', 172 | ' \n', 177 | ' \n', 182 | ' \n', 187 | ' \n', 192 | ' \n']) 193 | util.write_filter(script, filter_xml) 194 | return None 195 | 196 | 197 | def depth(script, iterations=3, viewpoint=(0, 0, 0), selected=False): 198 | """ A laplacian smooth that is constrained to move vertices only along the 199 | view direction. 200 | 201 | Args: 202 | script: the FilterScript object or script filename to write 203 | the filter to. 204 | iterations (int): The number of times that the whole algorithm (normal 205 | smoothing + vertex fitting) is iterated. 206 | viewpoint (vector tuple or list): The position of the view point that 207 | is used to get the constraint direction. 208 | selected (bool): If selected the filter is performed only on the 209 | selected faces 210 | 211 | Layer stack: 212 | No impacts 213 | 214 | MeshLab versions: 215 | 2016.12 216 | 1.3.4BETA 217 | """ 218 | filter_xml = ''.join([ 219 | ' \n', 220 | ' \n', 225 | ' \n', 232 | ' \n', 237 | ' \n']) 238 | util.write_filter(script, filter_xml) 239 | return None 240 | -------------------------------------------------------------------------------- /meshlabxml/subdivide.py: -------------------------------------------------------------------------------- 1 | """ MeshLabXML subdivide functions """ 2 | 3 | from . import util 4 | 5 | def loop(script, iterations=1, loop_weight=0, edge_threshold=0, 6 | selected=False): 7 | """ Apply Loop's Subdivision Surface algorithm. 8 | 9 | It is an approximant subdivision method and it works for every triangle 10 | and has rules for extraordinary vertices. 11 | 12 | Args: 13 | script: the FilterScript object or script filename to write 14 | the filter to. 15 | iterations (int): Number of times the model is subdivided. 16 | loop_weight (int): Change the weights used. Allow to optimize some 17 | behaviours in spite of others. Valid values are: 18 | 0 - Loop (default) 19 | 1 - Enhance regularity 20 | 2 - Enhance continuity 21 | edge_threshold (float): All the edges longer than this threshold will 22 | be refined. Setting this value to zero will force a uniform 23 | refinement. 24 | selected (bool): If selected the filter is performed only on the 25 | selected faces. 26 | 27 | Layer stack: 28 | No impacts 29 | 30 | MeshLab versions: 31 | 2016.12 32 | 1.3.4BETA 33 | """ 34 | filter_xml = ''.join([ 35 | ' \n', 36 | ' \n', 45 | ' \n', 50 | ' \n', 57 | ' \n', 62 | ' \n']) 63 | util.write_filter(script, filter_xml) 64 | return None 65 | 66 | 67 | def ls3loop(script, iterations=1, loop_weight=0, edge_threshold=0, 68 | selected=False): 69 | """ Apply LS3 Subdivision Surface algorithm using Loop's weights. 70 | 71 | This refinement method take normals into account. 72 | See: Boye', S. Guennebaud, G. & Schlick, C. 73 | "Least squares subdivision surfaces" 74 | Computer Graphics Forum, 2010. 75 | 76 | Alternatives weighting schemes are based on the paper: 77 | Barthe, L. & Kobbelt, L. 78 | "Subdivision scheme tuning around extraordinary vertices" 79 | Computer Aided Geometric Design, 2004, 21, 561-583. 80 | 81 | The current implementation of these schemes don't handle vertices of 82 | valence > 12 83 | 84 | Args: 85 | script: the FilterScript object or script filename to write 86 | the filter to. 87 | iterations (int): Number of times the model is subdivided. 88 | loop_weight (int): Change the weights used. Allow to optimize some 89 | behaviours in spite of others. Valid values are: 90 | 0 - Loop (default) 91 | 1 - Enhance regularity 92 | 2 - Enhance continuity 93 | edge_threshold (float): All the edges longer than this threshold will 94 | be refined. Setting this value to zero will force a uniform 95 | refinement. 96 | selected (bool): If selected the filter is performed only on the 97 | selected faces. 98 | 99 | Layer stack: 100 | No impacts 101 | 102 | MeshLab versions: 103 | 2016.12 104 | 1.3.4BETA 105 | """ 106 | filter_xml = ''.join([ 107 | ' \n', 108 | ' \n', 117 | ' \n', 122 | ' \n', 129 | ' \n', 134 | ' \n']) 135 | util.write_filter(script, filter_xml) 136 | return None 137 | 138 | 139 | def midpoint(script, iterations=1, edge_threshold=0, selected=False): 140 | """ Apply a plain subdivision scheme where every edge is split on its 141 | midpoint. 142 | 143 | Useful to uniformly refine a mesh substituting each triangle with four 144 | smaller triangles. 145 | 146 | Args: 147 | script: the FilterScript object or script filename to write 148 | the filter to. 149 | iterations (int): Number of times the model is subdivided. 150 | edge_threshold (float): All the edges longer than this threshold will 151 | be refined. Setting this value to zero will force a uniform 152 | refinement. 153 | selected (bool): If selected the filter is performed only on the 154 | selected faces. 155 | 156 | Layer stack: 157 | No impacts 158 | 159 | MeshLab versions: 160 | 2016.12 161 | 1.3.4BETA 162 | """ 163 | filter_xml = ''.join([ 164 | ' \n', 165 | ' \n', 170 | ' \n', 177 | ' \n', 182 | ' \n']) 183 | util.write_filter(script, filter_xml) 184 | return None 185 | 186 | 187 | def butterfly(script, iterations=1, edge_threshold=0, selected=False): 188 | """ Apply Butterfly Subdivision Surface algorithm. 189 | 190 | It is an interpolated method, defined on arbitrary triangular meshes. 191 | The scheme is known to be C1 but not C2 on regular meshes. 192 | 193 | Args: 194 | script: the FilterScript object or script filename to write 195 | the filter to. 196 | iterations (int): Number of times the model is subdivided. 197 | edge_threshold (float): All the edges longer than this threshold will 198 | be refined. Setting this value to zero will force a uniform 199 | refinement. 200 | selected (bool): If selected the filter is performed only on the 201 | selected faces. 202 | 203 | Layer stack: 204 | No impacts 205 | 206 | MeshLab versions: 207 | 2016.12 208 | 1.3.4BETA 209 | """ 210 | filter_xml = ''.join([ 211 | ' \n', 212 | ' \n', 217 | ' \n', 224 | ' \n', 229 | ' \n']) 230 | util.write_filter(script, filter_xml) 231 | return None 232 | 233 | 234 | def catmull_clark(script): 235 | """ Apply the Catmull-Clark Subdivision Surfaces. 236 | 237 | Note that position of the new vertices is simply linearly interpolated. 238 | If the mesh is triangle based (no faux edges) it generates a quad mesh, 239 | otherwise it honors the faux-edge bits. 240 | 241 | Args: 242 | script: the FilterScript object or script filename to write 243 | the filter to. 244 | 245 | Layer stack: 246 | No impacts 247 | 248 | MeshLab versions: 249 | 2016.12 250 | 1.3.4BETA 251 | """ 252 | filter_xml = ' \n' 253 | util.write_filter(script, filter_xml) 254 | return None 255 | -------------------------------------------------------------------------------- /meshlabxml/transfer.py: -------------------------------------------------------------------------------- 1 | """ MeshLabXML functions to transfer attributes """ 2 | 3 | from . import util 4 | 5 | 6 | def tex2vc(script): 7 | """Transfer texture colors to vertex colors 8 | 9 | BUG: this does not work correctly if the file has multiple textures; it 10 | only uses one texture and remaps all of the UVs to that 11 | https://github.com/cnr-isti-vclab/meshlab/issues/124 12 | should be fixed in post 2016.12 release 13 | 14 | """ 15 | filter_xml = ' \n' 16 | util.write_filter(script, filter_xml) 17 | return None 18 | 19 | 20 | def vc2tex(script, tex_name='TEMP3D_texture.png', tex_width=1024, 21 | tex_height=1024, overwrite_tex=False, assign_tex=False, 22 | fill_tex=True): 23 | """Transfer vertex colors to texture colors 24 | 25 | Args: 26 | script: the FilterScript object or script filename to write 27 | the filter to. 28 | tex_name (str): The texture file to be created 29 | tex_width (int): The texture width 30 | tex_height (int): The texture height 31 | overwrite_tex (bool): If current mesh has a texture will be overwritten (with provided texture dimension) 32 | assign_tex (bool): Assign the newly created texture 33 | fill_tex (bool): If enabled the unmapped texture space is colored using a pull push filling algorithm, if false is set to black 34 | """ 35 | if script.ml_version == '1.3.4BETA': 36 | filter_name = 'Vertex Color to Texture' 37 | else: 38 | filter_name = 'Transfer: Vertex Color to Texture' 39 | filter_xml = ''.join([ 40 | ' \n' % filter_name, 41 | ' \n', 46 | ' \n', 51 | ' \n', 56 | ' \n', 61 | ' \n', 66 | ' \n', 71 | ' \n']) 72 | util.write_filter(script, filter_xml) 73 | return None 74 | 75 | 76 | def fc2vc(script): 77 | """Transfer face colors to vertex colors 78 | 79 | Args: 80 | script: the FilterScript object or script filename to write 81 | the filter to. 82 | """ 83 | filter_xml = ' \n' 84 | util.write_filter(script, filter_xml) 85 | return None 86 | 87 | 88 | def vc2fc(script): 89 | """Transfer vertex colors to face colors 90 | 91 | Args: 92 | script: the FilterScript object or script filename to write 93 | the filter to. 94 | """ 95 | filter_xml = ' \n' 96 | util.write_filter(script, filter_xml) 97 | return None 98 | 99 | 100 | def mesh2fc(script, all_visible_layers=False): 101 | """Transfer mesh colors to face colors 102 | 103 | Args: 104 | script: the FilterScript object or script filename to write 105 | the filter to. 106 | all_visible_layers (bool): If true the color mapping is applied to all the meshes 107 | """ 108 | filter_xml = ''.join([ 109 | ' \n', 110 | ' \n', 115 | ' \n']) 116 | util.write_filter(script, filter_xml) 117 | return None 118 | 119 | 120 | def vert_attr_2_meshes(script, source_mesh=0, target_mesh=1, 121 | geometry=False, normal=False, color=True, 122 | quality=False, selection=False, 123 | quality_distance=False, max_distance=0.5): 124 | """Vertex Attribute Transfer (between 2 meshes) 125 | 126 | Transfer the chosen per-vertex attributes from one mesh to another. Useful to transfer attributes to different representations of the same object. For each vertex of the target mesh the closest point (not vertex!) on the source mesh is computed, and the requested interpolated attributes from that source point are copied into the target vertex. 127 | 128 | The algorithm assumes that the two meshes are reasonably similar and aligned. 129 | 130 | UpperBound: absolute value (not percentage) 131 | 132 | Args: 133 | script: the FilterScript object or script filename to write 134 | the filter to. 135 | source_mesh (int): The mesh that contains the source data that we want to transfer 136 | target_mesh (int): The mesh whose vertexes will receive the data from the source 137 | geometry (bool): If enabled, the position of each vertex of the target mesh will be snapped onto the corresponding closest point on the source mesh 138 | normal (bool): If enabled, the normal of each vertex of the target mesh will get the (interpolated) normal of the corresponding closest point on the source mesh 139 | color (bool): If enabled, the color of each vertex of the target mesh will become the color of the corresponding closest point on the source mesh 140 | quality (bool): If enabled, the quality of each vertex of the target mesh will become the quality of the corresponding closest point on the source mesh 141 | selection (bool): If enabled, each vertex of the target mesh will be selected if the corresponding closest point on the source mesh falls in a selected face 142 | quality_distance (bool): If enabled, we store the distance of the transferred value as in the vertex quality 143 | max_distance (float): Sample points for which we do not find anything within this distance are rejected and not considered for recovering attributes 144 | 145 | """ 146 | filter_xml = ''.join([ 147 | ' \n', 148 | ' \n', 153 | ' \n', 158 | ' \n', 163 | ' \n', 168 | ' \n', 173 | ' \n', 178 | ' \n', 183 | ' \n', 188 | ' \n', 195 | ' \n']) 196 | util.write_filter(script, filter_xml) 197 | return None 198 | 199 | 200 | def vert_attr2tex_2_meshes(script, source_mesh=0, target_mesh=1, attribute=0, 201 | max_distance=0.5, tex_name='TEMP3D_texture.png', 202 | tex_width=1024, tex_height=1024, 203 | overwrite_tex=True, assign_tex=False, 204 | fill_tex=True): 205 | """Transfer Vertex Attributes to Texture (between 2 meshes) 206 | 207 | Target mesh must be saved to disk or filter will fail 208 | 209 | Created texture seems to be created with absolute pathname. To set relative pathname, 210 | use mlx.texture.set_texture afterwards 211 | 212 | Args: 213 | script: the FilterScript object or script filename to write 214 | the filter to. 215 | source_mesh (int): The mesh that contains the source data that we want to transfer 216 | target_mesh (int): The mesh whose texture will be filled according to source mesh data 217 | attribute (int): Choose what attribute has to be transferred onto the target texture. You can choose between Per vertex attributes (color, normal, quality) or to transfer color information from source mesh texture 218 | max_distance (float): Sample points for which we do not find anything within this distance are rejected and not considered for recovering data 219 | tex_name (str): The texture file to be created 220 | tex_width (int): The texture width 221 | tex_height (int): The texture height 222 | overwrite_tex (bool): If target mesh has a texture will be overwritten (with provided texture dimension) 223 | assign_tex (bool): Assign the newly created texture to target mesh 224 | fill_tex (bool): If enabled the unmapped texture space is colored using a pull push filling algorithm, if false is set to black 225 | 226 | Layer stack: 227 | No impacts 228 | 229 | MeshLab versions: 230 | 2016.12 231 | 1.3.4BETA 232 | """ 233 | if script.ml_version == '1.3.4BETA': 234 | filter_name = 'Transfer Vertex Attributes to Texture (between 2 meshes)' 235 | else: 236 | filter_name = 'Transfer: Vertex Attributes to Texture (1 or 2 meshes)' 237 | filter_xml = ''.join([ 238 | ' \n'.format(filter_name), 239 | ' \n', 244 | ' \n', 249 | ' \n', 259 | ' \n', 266 | ' \n', 271 | ' \n', 276 | ' \n', 281 | ' \n', 286 | ' \n', 291 | ' \n', 296 | ' \n']) 297 | util.write_filter(script, filter_xml) 298 | return None 299 | 300 | 301 | def tex2vc_2_meshes(script, source_mesh=0, target_mesh=1, max_distance=0.5): 302 | """Transfer texture colors to vertex colors (between 2 meshes) 303 | 304 | Args: 305 | script: the FilterScript object or script filename to write 306 | the filter to. 307 | source_mesh (int): The mesh with associated texture that we want to sample from 308 | target_mesh (int): The mesh whose vertex color will be filled according to source mesh texture 309 | max_distance (float): Sample points for which we do not find anything within this distance are rejected and not considered for recovering color 310 | 311 | Layer stack: 312 | No impacts 313 | 314 | MeshLab versions: 315 | 2016.12 316 | 1.3.4BETA 317 | """ 318 | if script.ml_version == '1.3.4BETA': 319 | filter_name = 'Texture to Vertex Color (between 2 meshes)' 320 | else: 321 | filter_name = 'Transfer: Texture to Vertex Color (1 or 2 meshes)' 322 | filter_xml = ''.join([ 323 | ' \n'.format(filter_name), 324 | ' \n', 329 | ' \n', 334 | ' \n', 341 | ' \n']) 342 | util.write_filter(script, filter_xml) 343 | return None 344 | -------------------------------------------------------------------------------- /meshlabxml/util.py: -------------------------------------------------------------------------------- 1 | """ MeshLabXML utility functions """ 2 | 3 | import os 4 | import sys 5 | import inspect 6 | from glob import glob 7 | 8 | #from . import FilterScript 9 | import meshlabxml as mlx 10 | 11 | def is_number(num): 12 | """ Check if a variable is a number by trying to convert it to a float. 13 | 14 | """ 15 | try: 16 | float(num) 17 | return True 18 | except: 19 | return False 20 | 21 | 22 | def to_float(num): 23 | """ Convert a variable to a float """ 24 | try: 25 | float(num) 26 | return float(num) 27 | except ValueError: 28 | return float('NaN') 29 | 30 | 31 | def delete_all(filename): 32 | """ Delete all files in the current directory that match a pattern. 33 | 34 | Intended primarily for temp files, e.g. mlx.delete_all('TEMP3D*'). 35 | 36 | """ 37 | for fread in glob(filename): 38 | os.remove(fread) 39 | 40 | 41 | def color_values(color): 42 | """Read color_names.txt and find the red, green, and blue values 43 | for a named color. 44 | """ 45 | # Get the directory where this script file is located: 46 | this_dir = os.path.dirname( 47 | os.path.realpath( 48 | inspect.getsourcefile( 49 | lambda: 0))) 50 | color_name_file = os.path.join(this_dir, 'color_names.txt') 51 | found = False 52 | for line in open(color_name_file, 'r'): 53 | line = line.rstrip() 54 | if color.lower() == line.split()[0]: 55 | #hex_color = line.split()[1] 56 | red = line.split()[2] 57 | green = line.split()[3] 58 | blue = line.split()[4] 59 | found = True 60 | break 61 | if not found: 62 | print('Color name "%s" not found, using default (white)' % color) 63 | red = 255 64 | green = 255 65 | blue = 255 66 | return red, green, blue 67 | 68 | 69 | def check_list(var, num_terms): 70 | """ Check if a variable is a list and is the correct length. 71 | 72 | If variable is not a list it will make it a list of the correct length with 73 | all terms identical. 74 | """ 75 | if not isinstance(var, list): 76 | if isinstance(var, tuple): 77 | var = list(var) 78 | else: 79 | var = [var] 80 | for _ in range(1, num_terms): 81 | var.append(var[0]) 82 | if len(var) != num_terms: 83 | print( 84 | '"%s" has the wrong number of terms; it needs %s. Exiting ...' % 85 | (var, num_terms)) 86 | sys.exit(1) 87 | return var 88 | 89 | 90 | def make_list(var, num_terms=1): 91 | """ Make a variable a list if it is not already 92 | 93 | If variable is not a list it will make it a list of the correct length with 94 | all terms identical. 95 | """ 96 | if not isinstance(var, list): 97 | if isinstance(var, tuple): 98 | var = list(var) 99 | else: 100 | var = [var] 101 | #if len(var) == 1: 102 | for _ in range(1, num_terms): 103 | var.append(var[0]) 104 | return var 105 | 106 | 107 | def write_filter(script, filter_xml): 108 | """ Write filter to FilterScript object or filename 109 | 110 | Args: 111 | script (FilterScript object or filename str): the FilterScript object 112 | or script filename to write the filter to. 113 | filter_xml (str): the xml filter string 114 | 115 | """ 116 | if isinstance(script, mlx.FilterScript): 117 | script.filters.append(filter_xml) 118 | elif isinstance(script, str): 119 | script_file = open(script, 'a') 120 | script_file.write(filter_xml) 121 | script_file.close() 122 | else: 123 | print(filter_xml) 124 | return None 125 | 126 | 127 | # Matrix Math 128 | def mat_transpose(matrix): 129 | """ Transpose 2D matrix 130 | 131 | Matrix must be a list of lists, i.e. [[1, 2], [3, 4]] 132 | 133 | """ 134 | # Using nested list comprehension 135 | result = [[matrix[j][i] for j in range(len(matrix))] for i in range(len(matrix[0]))] 136 | return result 137 | 138 | 139 | def matmul(matrix_a, matrix_b): 140 | """ Multiply two 2D matrices 141 | 142 | Matrix must be a list of lists, i.e. [[1, 2], [3, 4]] 143 | 144 | """ 145 | cols_a = len(matrix_a[0]) 146 | rows_b = len(matrix_b) 147 | if cols_a != rows_b: 148 | print("Error: Matrices cannot be multiplied, columns_a != rows_b") 149 | return 150 | 151 | # Using nested list comprehension 152 | result = [[sum(x*y for x, y in zip(matrix_a_row, matrix_b_col)) for matrix_b_col in zip(*matrix_b)] for matrix_a_row in matrix_a] 153 | return result 154 | -------------------------------------------------------------------------------- /meshlabxml/vert_color.py: -------------------------------------------------------------------------------- 1 | """ MeshLabXML vertex color functions """ 2 | 3 | import math 4 | 5 | from . import util 6 | from .color_names import color_name 7 | 8 | def function(script, red=255, green=255, blue=255, alpha=255, color=None): 9 | """Color function using muparser lib to generate new RGBA color for every 10 | vertex 11 | 12 | Red, Green, Blue and Alpha channels may be defined by specifying a function 13 | for each. 14 | 15 | See help(mlx.muparser_ref) for muparser reference documentation. 16 | 17 | It's possible to use the following per-vertex variables in the expression: 18 | 19 | Variables (per vertex): 20 | x, y, z (coordinates) 21 | nx, ny, nz (normal) 22 | r, g, b, a (color) 23 | q (quality) 24 | rad (radius) 25 | vi (vertex index) 26 | vtu, vtv (texture coordinates) 27 | ti (texture index) 28 | vsel (is the vertex selected? 1 yes, 0 no) 29 | and all custom vertex attributes already defined by user. 30 | 31 | Args: 32 | script: the FilterScript object or script filename to write 33 | the filter to. 34 | red (str [0, 255]): function to generate red component 35 | green (str [0, 255]): function to generate green component 36 | blue (str [0, 255]): function to generate blue component 37 | alpha (str [0, 255]): function to generate alpha component 38 | color (str): name of one of the 140 HTML Color Names defined 39 | in CSS & SVG. 40 | Ref: https://en.wikipedia.org/wiki/Web_colors#X11_color_names 41 | If not None this will override the per component variables. 42 | 43 | Layer stack: 44 | No impacts 45 | 46 | MeshLab versions: 47 | 2016.12 48 | 1.3.4BETA 49 | """ 50 | # TODO: add options for HSV 51 | # https://www.cs.rit.edu/~ncs/color/t_convert.html 52 | if color is not None: 53 | red, green, blue, _ = color_name[color.lower()] 54 | filter_xml = ''.join([ 55 | ' \n', 56 | ' \n', 61 | ' \n', 66 | ' \n', 71 | ' \n', 76 | ' \n']) 77 | util.write_filter(script, filter_xml) 78 | return None 79 | 80 | 81 | def voronoi(script, target_layer=0, source_layer=1, backward=True): 82 | """ Given a Mesh 'M' and a Pointset 'P', the filter projects each vertex of 83 | P over M and color M according to the geodesic distance from these 84 | projected points. Projection and coloring are done on a per vertex 85 | basis. 86 | 87 | Args: 88 | script: the FilterScript object or script filename to write 89 | the filter to. 90 | target_layer (int): The mesh layer whose surface is colored. For each 91 | vertex of this mesh we decide the color according to the following 92 | arguments. 93 | source_layer (int): The mesh layer whose vertexes are used as seed 94 | points for the color computation. These seeds point are projected 95 | onto the target_layer mesh. 96 | backward (bool): If True the mesh is colored according to the distance 97 | from the frontier of the voronoi diagram induced by the 98 | source_layer seeds. 99 | 100 | Layer stack: 101 | No impacts 102 | 103 | MeshLab versions: 104 | 2016.12 105 | 1.3.4BETA 106 | """ 107 | filter_xml = ''.join([ 108 | ' \n', 109 | ' \n', 114 | ' \n', 119 | ' \n', 124 | ' \n']) 125 | util.write_filter(script, filter_xml) 126 | return None 127 | 128 | 129 | def cyclic_rainbow(script, direction='sphere', start_pt=(0, 0, 0), 130 | amplitude=255 / 2, center=255 / 2, freq=0.8, 131 | phase=(0, 120, 240, 0), alpha=False): 132 | """ Color mesh vertices in a repeating sinusiodal rainbow pattern 133 | 134 | Sine wave follows the following equation for each color channel (RGBA): 135 | channel = sin(freq*increment + phase)*amplitude + center 136 | 137 | Args: 138 | script: the FilterScript object or script filename to write 139 | the filter to. 140 | direction (str) = the direction that the sine wave will travel; this 141 | and the start_pt determine the 'increment' of the sine function. 142 | Valid values are: 143 | 'sphere' - radiate sine wave outward from start_pt (default) 144 | 'x' - sine wave travels along the X axis 145 | 'y' - sine wave travels along the Y axis 146 | 'z' - sine wave travels along the Z axis 147 | or define the increment directly using a muparser function, e.g. 148 | '2x + y'. In this case start_pt will not be used; include it in 149 | the function directly. 150 | start_pt (3 coordinate tuple or list): start point of the sine wave. For a 151 | sphere this is the center of the sphere. 152 | amplitude (float [0, 255], single value or 4 term tuple or list): amplitude 153 | of the sine wave, with range between 0-255. If a single value is 154 | specified it will be used for all channels, otherwise specify each 155 | channel individually. 156 | center (float [0, 255], single value or 4 term tuple or list): center 157 | of the sine wave, with range between 0-255. If a single value is 158 | specified it will be used for all channels, otherwise specify each 159 | channel individually. 160 | freq (float, single value or 4 term tuple or list): frequency of the sine 161 | wave. If a single value is specified it will be used for all channels, 162 | otherwise specifiy each channel individually. 163 | phase (float [0, 360], single value or 4 term tuple or list): phase 164 | of the sine wave in degrees, with range between 0-360. If a single 165 | value is specified it will be used for all channels, otherwise specify 166 | each channel individually. 167 | alpha (bool): if False the alpha channel will be set to 255 (full opacity). 168 | 169 | Layer stack: 170 | No impacts 171 | 172 | MeshLab versions: 173 | 2016.12 174 | 1.3.4BETA 175 | """ 176 | start_pt = util.make_list(start_pt, 3) 177 | amplitude = util.make_list(amplitude, 4) 178 | center = util.make_list(center, 4) 179 | freq = util.make_list(freq, 4) 180 | phase = util.make_list(phase, 4) 181 | 182 | if direction.lower() == 'sphere': 183 | increment = 'sqrt((x-{})^2+(y-{})^2+(z-{})^2)'.format( 184 | start_pt[0], start_pt[1], start_pt[2]) 185 | elif direction.lower() == 'x': 186 | increment = 'x - {}'.format(start_pt[0]) 187 | elif direction.lower() == 'y': 188 | increment = 'y - {}'.format(start_pt[1]) 189 | elif direction.lower() == 'z': 190 | increment = 'z - {}'.format(start_pt[2]) 191 | else: 192 | increment = direction 193 | 194 | red_func = '{a}*sin({f}*{i} + {p}) + {c}'.format( 195 | f=freq[0], i=increment, p=math.radians(phase[0]), 196 | a=amplitude[0], c=center[0]) 197 | green_func = '{a}*sin({f}*{i} + {p}) + {c}'.format( 198 | f=freq[1], i=increment, p=math.radians(phase[1]), 199 | a=amplitude[1], c=center[1]) 200 | blue_func = '{a}*sin({f}*{i} + {p}) + {c}'.format( 201 | f=freq[2], i=increment, p=math.radians(phase[2]), 202 | a=amplitude[2], c=center[2]) 203 | if alpha: 204 | alpha_func = '{a}*sin({f}*{i} + {p}) + {c}'.format( 205 | f=freq[3], i=increment, p=math.radians(phase[3]), 206 | a=amplitude[3], c=center[3]) 207 | else: 208 | alpha_func = 255 209 | 210 | function(script, red=red_func, green=green_func, blue=blue_func, 211 | alpha=alpha_func) 212 | return None 213 | -------------------------------------------------------------------------------- /models/bunny.txt: -------------------------------------------------------------------------------- 1 | Stanford Bunny 2 | 3 | The Stanford Bunny model is courtesy of the Stanford University Computer 4 | Graphics Laboratory, and is included with MeshLabXML with permission. The 5 | original file can be found at http://graphics.stanford.edu/data/3Dscanrep/ 6 | with additional information on the model's venerable history at 7 | http://www.cc.gatech.edu/~turk/bunny/bunny.html . 8 | 9 | Terms of use (retrieved from the first link above): 10 | Please be sure to acknowledge the source of the data and models you take 11 | from this repository. In each of the listings below, we have cited the 12 | source of the range data and reconstructed models. You are welcome to use 13 | the data and models for research purposes. You are also welcome to mirror 14 | or redistribute them for free. Finally, you may publish images made using 15 | these models, or the images on this web site, in a scholarly article or 16 | book - as long as credit is given to the Stanford Computer Graphics 17 | Laboratory. However, such models or images are not to be used for 18 | commercial purposes, nor should they appear in a product for sale (with 19 | the exception of scholarly journals or books), without our permission. 20 | 21 | Source: Stanford University Computer Graphics Laboratory 22 | Scanner: Cyberware 3030 MS 23 | Number of scans: 10 24 | Total size of scans: 362,272 points (about 725,000 triangles) 25 | Reconstruction: zipper 26 | Size of reconstruction: 35947 vertices, 69451 triangles 27 | Comments: contains 5 holes in the bottom 28 | 29 | Files: 30 | The following files are included with MeshLabXML. Note that the values in the 31 | parentheses in the filenames indicate metadata about the model, specifically 32 | scale factor (with negative values indicating an inverse scale factor) and 33 | "up" axis (either Y or Z). 34 | 35 | Filename: bunny_raw(-1250Y).ply 36 | MeshLabXML name: bunny_raw 37 | Description: model is identical to the original source, except it has been 38 | converted to a binary ply file to reduce file size. 39 | 40 | Filename: bunny_flat(1Z).ply 41 | MeshLabXML name: bunny 42 | Description: model has had the following changes applied: 43 | - Scaled to actual size in mm (see "Notes on units and scale" below) 44 | - Hole under chin has been filled 45 | - Unreferenced vertices have been deleted 46 | - Bottom has been sliced flat and placed on the XY plane 47 | - Model has been rotated to Z up 48 | The above changes result in a model which is watertight and manifold, e.g. 49 | suitable for 3D printing. 50 | 51 | Notes on units and scale: 52 | The units and scale of the original model are unknown. The principals 53 | involved (Marc Levoy and Greg Turk) were contacted however they no longer 54 | have this information available. Based on the reported size of the actual 55 | object (approximately 7.5 inches high and 8 inches long), units of mm and a 56 | scale factor of 1:1250 is assumed for the original model to approximate the 57 | correct size. 58 | -------------------------------------------------------------------------------- /models/bunny_flat(1Z).ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3DLIRIOUS/MeshLabXML/acd129d3527bf9c985896f752116a1ae5e267353/models/bunny_flat(1Z).ply -------------------------------------------------------------------------------- /models/bunny_raw(-1250Y).ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3DLIRIOUS/MeshLabXML/acd129d3527bf9c985896f752116a1ae5e267353/models/bunny_raw(-1250Y).ply -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | 4 | # read the contents of your README file 5 | import io 6 | from os import path 7 | this_directory = path.abspath(path.dirname(__file__)) 8 | with io.open(path.join(this_directory, 'README.md'), encoding='utf-8') as f: 9 | long_description = f.read() 10 | 11 | setup(name='MeshLabXML', 12 | version='2021.7', 13 | description='Create and run MeshLab XML scripts', 14 | long_description=long_description, 15 | long_description_content_type='text/markdown', 16 | url='https://github.com/3DLIRIOUS/MeshLabXML', 17 | author='3DLirious, LLC', 18 | author_email='3DLirious@gmail.com', 19 | license='LGPL-2.1', 20 | packages=['meshlabxml'], 21 | include_package_data=True) 22 | -------------------------------------------------------------------------------- /test/black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3DLIRIOUS/MeshLabXML/acd129d3527bf9c985896f752116a1ae5e267353/test/black.png -------------------------------------------------------------------------------- /test/blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3DLIRIOUS/MeshLabXML/acd129d3527bf9c985896f752116a1ae5e267353/test/blue.png -------------------------------------------------------------------------------- /test/cyan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3DLIRIOUS/MeshLabXML/acd129d3527bf9c985896f752116a1ae5e267353/test/cyan.png -------------------------------------------------------------------------------- /test/green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3DLIRIOUS/MeshLabXML/acd129d3527bf9c985896f752116a1ae5e267353/test/green.png -------------------------------------------------------------------------------- /test/magenta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3DLIRIOUS/MeshLabXML/acd129d3527bf9c985896f752116a1ae5e267353/test/magenta.png -------------------------------------------------------------------------------- /test/red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3DLIRIOUS/MeshLabXML/acd129d3527bf9c985896f752116a1ae5e267353/test/red.png -------------------------------------------------------------------------------- /test/white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3DLIRIOUS/MeshLabXML/acd129d3527bf9c985896f752116a1ae5e267353/test/white.png -------------------------------------------------------------------------------- /test/yellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3DLIRIOUS/MeshLabXML/acd129d3527bf9c985896f752116a1ae5e267353/test/yellow.png --------------------------------------------------------------------------------