├── .gitignore ├── LICENSE ├── README.md ├── example ├── empty.stl ├── env.py ├── sample.obj ├── sample.py ├── sample.stl ├── sample_ascii.stl ├── sample_bin.stl └── sample_out.stl ├── pymesh ├── __init__.py ├── base.py ├── obj.py ├── stl.py └── utils.py ├── runtest.sh ├── setup.py └── tests ├── __init__.py ├── data ├── data_ascii.stl └── data_bin.stl ├── test_obj.py └── test_stl.py /.gitignore: -------------------------------------------------------------------------------- 1 | ### Python template 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | # Created by .ignore support plugin (hsz.mobi) 62 | 63 | # IDE 64 | .idea 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright © 2015 Takuro Wada 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Mesh Library 2 | ## Feature 3 | - Supported format 4 | - STL(Binary, ASCII) 5 | - OBJ(Wavefront, no material supported) 6 | 7 | - Transform 8 | - Translate 9 | - Rotate 10 | - Scale 11 | 12 | - Join 13 | 14 | - Analyze 15 | - Volume 16 | 17 | - Numpy is used for inner calculation so that it runs fast. 18 | 19 | ## Install 20 | ``` 21 | pip install pymesh 22 | ``` 23 | 24 | ## Requirement 25 | [numpy](http://www.numpy.org/) is required. 26 | 27 | ## Usage 28 | ### Load data 29 | ``` 30 | # STL 31 | from pymesh import stl 32 | m = stl.Stl("sample.stl") 33 | 34 | # OBJ 35 | from pymesh import obj 36 | m = obj.Obj("sample.obj") 37 | ``` 38 | 39 | ### Save data 40 | ``` 41 | # STL 42 | m.save_stl("out.stl") 43 | 44 | # OBJ 45 | m.save_obj("out.obj") 46 | ``` 47 | 48 | ### Create empty data 49 | ``` 50 | # STL 51 | m = stl.Stl() 52 | 53 | # OBJ 54 | m = obj.Obj() 55 | ``` 56 | 57 | ### Transform 58 | ``` 59 | # Translate 60 | m.translate_x(10) 61 | 62 | # Rotate 63 | m.rotate_y(30) 64 | 65 | # Scale 66 | m.scale(1, 2, 1) 67 | 68 | # Method chain supported 69 | m.translate_x(10).rotate_y(30).scale(10, 1, 1) 70 | ``` 71 | 72 | ### Join 73 | - Combine multiple mesh data into one mesh 74 | ``` 75 | # Join 76 | m.join(another) 77 | ``` 78 | 79 | ### Analyze 80 | ``` 81 | # Volume 82 | m.get_volume() 83 | ``` 84 | 85 | ### Support 86 | - Python 2.7+ and Python 3 are both supported 87 | 88 | ## LICENSE 89 | [MIT License](http://takuro.mit-license.org/) 90 | 91 | ## MISC 92 | This library is inspired by [numpy-stl](https://github.com/WoLpH/numpy-stl). 93 | -------------------------------------------------------------------------------- /example/empty.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taxpon/pymesh/a90b3b2ed1408d793f3b5208dd8087b08fb7c92e/example/empty.stl -------------------------------------------------------------------------------- /example/env.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | this = os.path.normpath(os.path.abspath(os.path.dirname(__file__))) 5 | sys.path.insert(0, "/".join(this.split("/")[:-1])) 6 | # sys.path.append("/".join(this.split("/")[:-1])) 7 | -------------------------------------------------------------------------------- /example/sample.obj: -------------------------------------------------------------------------------- 1 | #### 2 | # 3 | # OBJ File Generated by Meshlab 4 | # 5 | #### 6 | # Object sample.obj 7 | # 8 | # Vertices: 8 9 | # Faces: 12 10 | # 11 | #### 12 | vn -0.785398 0.000000 0.000000 13 | v -1.000000 -1.000000 1.000000 14 | vn 0.000000 0.000000 -1.570796 15 | v -1.000000 1.000000 -1.000000 16 | vn 0.000000 0.000000 -0.785398 17 | v 1.000000 1.000000 -1.000000 18 | vn 0.000000 0.000000 -1.570796 19 | v 1.000000 -1.000000 -1.000000 20 | vn 0.000000 0.000000 -0.785398 21 | v -1.000000 -1.000000 -1.000000 22 | vn 0.000000 0.000000 0.785398 23 | v 1.000000 -1.000000 1.000000 24 | vn 0.000000 0.000000 1.570796 25 | v 1.000000 1.000000 1.000000 26 | vn 0.000000 0.000000 0.785398 27 | v -1.000000 1.000000 1.000000 28 | # 8 vertices, 0 vertices normals 29 | 30 | f 2//2 5//5 1//1 31 | f 1//1 8//8 2//2 32 | f 8//8 7//7 3//3 33 | f 3//3 2//2 8//8 34 | f 7//7 6//6 4//4 35 | f 4//4 3//3 7//7 36 | f 5//5 4//4 6//6 37 | f 6//6 1//1 5//5 38 | f 5//5 2//2 3//3 39 | f 3//3 4//4 5//5 40 | f 6//6 7//7 8//8 41 | f 8//8 1//1 6//6 42 | # 12 faces, 0 coords texture 43 | 44 | # End of File 45 | -------------------------------------------------------------------------------- /example/sample.py: -------------------------------------------------------------------------------- 1 | import env 2 | from pymesh import stl 3 | from pymesh import obj 4 | 5 | 6 | def main(): 7 | print(stl.__file__) 8 | empty = stl.Stl() 9 | e2 = obj.Obj() 10 | m = stl.Stl('sample.stl') 11 | m2 = obj.Obj('sample.obj') 12 | print(m.get_volume()) 13 | m.scale(1, 2, 1) 14 | m.rotate_x(90) 15 | m.rotate_y(30) 16 | m.translate_x(2) 17 | m.join(m2) 18 | empty.join(m2) 19 | empty.join(e2) 20 | m.save_stl("sample_out.stl", update_normals=True) 21 | empty.save_stl("empty.stl") 22 | 23 | 24 | if __name__ == '__main__': 25 | main() 26 | -------------------------------------------------------------------------------- /example/sample.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taxpon/pymesh/a90b3b2ed1408d793f3b5208dd8087b08fb7c92e/example/sample.stl -------------------------------------------------------------------------------- /example/sample_ascii.stl: -------------------------------------------------------------------------------- 1 | solid STL generated by MeshLab 2 | facet normal -1.000000e+00 0.000000e+00 0.000000e+00 3 | outer loop 4 | vertex -1.000000e+00 1.000000e+00 -1.000000e+00 5 | vertex -1.000000e+00 -1.000000e+00 -1.000000e+00 6 | vertex -1.000000e+00 -1.000000e+00 1.000000e+00 7 | endloop 8 | endfacet 9 | facet normal -1.000000e+00 0.000000e+00 0.000000e+00 10 | outer loop 11 | vertex -1.000000e+00 -1.000000e+00 1.000000e+00 12 | vertex -1.000000e+00 1.000000e+00 1.000000e+00 13 | vertex -1.000000e+00 1.000000e+00 -1.000000e+00 14 | endloop 15 | endfacet 16 | facet normal -0.000000e+00 1.000000e+00 0.000000e+00 17 | outer loop 18 | vertex -1.000000e+00 1.000000e+00 1.000000e+00 19 | vertex 1.000000e+00 1.000000e+00 1.000000e+00 20 | vertex 1.000000e+00 1.000000e+00 -1.000000e+00 21 | endloop 22 | endfacet 23 | facet normal 0.000000e+00 1.000000e+00 0.000000e+00 24 | outer loop 25 | vertex 1.000000e+00 1.000000e+00 -1.000000e+00 26 | vertex -1.000000e+00 1.000000e+00 -1.000000e+00 27 | vertex -1.000000e+00 1.000000e+00 1.000000e+00 28 | endloop 29 | endfacet 30 | facet normal 1.000000e+00 0.000000e+00 0.000000e+00 31 | outer loop 32 | vertex 1.000000e+00 1.000000e+00 1.000000e+00 33 | vertex 1.000000e+00 -1.000000e+00 1.000000e+00 34 | vertex 1.000000e+00 -1.000000e+00 -1.000000e+00 35 | endloop 36 | endfacet 37 | facet normal 1.000000e+00 0.000000e+00 0.000000e+00 38 | outer loop 39 | vertex 1.000000e+00 -1.000000e+00 -1.000000e+00 40 | vertex 1.000000e+00 1.000000e+00 -1.000000e+00 41 | vertex 1.000000e+00 1.000000e+00 1.000000e+00 42 | endloop 43 | endfacet 44 | facet normal 0.000000e+00 -1.000000e+00 0.000000e+00 45 | outer loop 46 | vertex -1.000000e+00 -1.000000e+00 -1.000000e+00 47 | vertex 1.000000e+00 -1.000000e+00 -1.000000e+00 48 | vertex 1.000000e+00 -1.000000e+00 1.000000e+00 49 | endloop 50 | endfacet 51 | facet normal -0.000000e+00 -1.000000e+00 0.000000e+00 52 | outer loop 53 | vertex 1.000000e+00 -1.000000e+00 1.000000e+00 54 | vertex -1.000000e+00 -1.000000e+00 1.000000e+00 55 | vertex -1.000000e+00 -1.000000e+00 -1.000000e+00 56 | endloop 57 | endfacet 58 | facet normal 0.000000e+00 0.000000e+00 -1.000000e+00 59 | outer loop 60 | vertex -1.000000e+00 -1.000000e+00 -1.000000e+00 61 | vertex -1.000000e+00 1.000000e+00 -1.000000e+00 62 | vertex 1.000000e+00 1.000000e+00 -1.000000e+00 63 | endloop 64 | endfacet 65 | facet normal 0.000000e+00 -0.000000e+00 -1.000000e+00 66 | outer loop 67 | vertex 1.000000e+00 1.000000e+00 -1.000000e+00 68 | vertex 1.000000e+00 -1.000000e+00 -1.000000e+00 69 | vertex -1.000000e+00 -1.000000e+00 -1.000000e+00 70 | endloop 71 | endfacet 72 | facet normal 0.000000e+00 -0.000000e+00 1.000000e+00 73 | outer loop 74 | vertex 1.000000e+00 -1.000000e+00 1.000000e+00 75 | vertex 1.000000e+00 1.000000e+00 1.000000e+00 76 | vertex -1.000000e+00 1.000000e+00 1.000000e+00 77 | endloop 78 | endfacet 79 | facet normal 0.000000e+00 0.000000e+00 1.000000e+00 80 | outer loop 81 | vertex -1.000000e+00 1.000000e+00 1.000000e+00 82 | vertex -1.000000e+00 -1.000000e+00 1.000000e+00 83 | vertex 1.000000e+00 -1.000000e+00 1.000000e+00 84 | endloop 85 | endfacet 86 | endsolid vcg 87 | -------------------------------------------------------------------------------- /example/sample_bin.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taxpon/pymesh/a90b3b2ed1408d793f3b5208dd8087b08fb7c92e/example/sample_bin.stl -------------------------------------------------------------------------------- /example/sample_out.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taxpon/pymesh/a90b3b2ed1408d793f3b5208dd8087b08fb7c92e/example/sample_out.stl -------------------------------------------------------------------------------- /pymesh/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # PyMesh 5 | # 6 | 7 | __title__ = "pymesh" 8 | __versioninfo__ = (1, 0, 2) 9 | __version__ = ".".join(map(str, __versioninfo__)) 10 | __author__ = "Takuro Wada" 11 | __license__ = "MIT" 12 | __copyright__ = "Copyright 2015 Takuro Wada" 13 | __url__ = "https://github.com/taxpon/pymesh" 14 | -------------------------------------------------------------------------------- /pymesh/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import absolute_import, print_function 4 | import datetime 5 | import math 6 | import numpy 7 | import os 8 | import struct 9 | from . import __title__ 10 | from . import __version__ 11 | from . import __url__ 12 | 13 | MODE_STL_AUTO = 0 14 | MODE_STL_ASCII = 1 15 | MODE_STL_BINARY = 2 16 | 17 | 18 | class BaseMesh(object): 19 | 20 | stl_dtype = numpy.dtype([ 21 | ('normals', numpy.float32, (3, )), 22 | ('vectors', numpy.float32, (3, 3)), 23 | ('attr', numpy.uint16, (1, )), 24 | ]) 25 | 26 | def __init__(self): 27 | self.data = None 28 | self.normals = [] 29 | self.vectors = [] 30 | self.attr = [] 31 | self.mode = MODE_STL_BINARY 32 | 33 | def set_initial_values(self): 34 | """Set initial values form existing self.data value 35 | :return: None 36 | """ 37 | self.normals = self.data['normals'] 38 | self.vectors = numpy.ones(( 39 | self.data['vectors'].shape[0], 40 | self.data['vectors'].shape[1], 41 | self.data['vectors'].shape[2] + 1 42 | )) 43 | self.vectors[:, :, :-1] = self.data['vectors'] 44 | self.attr = self.data['attr'] 45 | return 46 | 47 | def rotate_x(self, deg): 48 | """Rotate mesh around x-axis 49 | 50 | :param float deg: Rotation angle (degree) 51 | :return: 52 | """ 53 | rad = math.radians(deg) 54 | mat = numpy.array([ 55 | [1, 0, 0, 0], 56 | [0, math.cos(rad), math.sin(rad), 0], 57 | [0, -math.sin(rad), math.cos(rad), 0], 58 | [0, 0, 0, 1] 59 | ]) 60 | self.vectors = self.vectors.dot(mat) 61 | return self 62 | 63 | def rotate_y(self, deg): 64 | """Rotate mesh around y-axis 65 | 66 | :param float deg: Rotation angle (degree) 67 | """ 68 | rad = math.radians(deg) 69 | mat = numpy.array([ 70 | [math.cos(rad), 0, -math.sin(rad), 0], 71 | [0, 1, 0, 0], 72 | [math.sin(rad), 0, math.cos(rad), 0], 73 | [0, 0, 0, 1] 74 | ]) 75 | self.vectors = self.vectors.dot(mat) 76 | return self 77 | 78 | def rotate_z(self, deg): 79 | """Rotate mesh around z-axis 80 | 81 | :param float deg: Rotation angle (degree) 82 | """ 83 | rad = math.radians(deg) 84 | mat = numpy.array([ 85 | [math.cos(rad), math.sin(rad), 0, 0], 86 | [-math.sin(rad), math.cos(rad), 0, 0], 87 | [0, 0, 1, 0], 88 | [0, 0, 0, 1] 89 | ]) 90 | self.vectors = self.vectors.dot(mat) 91 | return self 92 | 93 | def translate_x(self, d): 94 | """Translate mesh for x-direction 95 | 96 | :param float d: Amount to translate 97 | """ 98 | mat = numpy.array([ 99 | [1, 0, 0, 0], 100 | [0, 1, 0, 0], 101 | [0, 0, 1, 0], 102 | [d, 0, 0, 1] 103 | ]) 104 | self.vectors = self.vectors.dot(mat) 105 | return self 106 | 107 | def translate_y(self, d): 108 | """Translate mesh for y-direction 109 | 110 | :param float d: Amount to translate 111 | """ 112 | mat = numpy.array([ 113 | [1, 0, 0, 0], 114 | [0, 1, 0, 0], 115 | [0, 0, 1, 0], 116 | [0, d, 0, 1] 117 | ]) 118 | self.vectors = self.vectors.dot(mat) 119 | return self 120 | 121 | def translate_z(self, d): 122 | """Translate mesh for z-direction 123 | 124 | :param float d: Amount to translate 125 | """ 126 | mat = numpy.array([ 127 | [1, 0, 0, 0], 128 | [0, 1, 0, 0], 129 | [0, 0, 1, 0], 130 | [0, 0, d, 1] 131 | ]) 132 | self.vectors = self.vectors.dot(mat) 133 | return self 134 | 135 | def scale(self, sx, sy, sz): 136 | """Scale mesh 137 | 138 | :param float sx: Amount to scale for x-direction 139 | :param float sy: Amount to scale for y-direction 140 | :param float sz: Amount to scale for z-direction 141 | """ 142 | mat = numpy.array([ 143 | [sx, 0, 0, 0], 144 | [0, sy, 0, 0], 145 | [0, 0, sz, 0], 146 | [0, 0, 0, 1] 147 | ]) 148 | self.vectors = self.vectors.dot(mat) 149 | return self 150 | 151 | def join(self, another): 152 | """ 153 | 154 | :param m: BaseMesh 155 | :return: 156 | """ 157 | if another is None: 158 | raise AttributeError("another BaseMesh instance is required") 159 | 160 | if not isinstance(another, BaseMesh): 161 | raise TypeError("anther must be an instance of BaseMesh") 162 | 163 | self.data = numpy.append(self.data, another.data) 164 | self.normals = numpy.append(self.normals, another.normals, axis=0) 165 | self.vectors = numpy.append(self.vectors, another.vectors, axis=0) 166 | self.attr = numpy.append(self.attr, another.attr, axis=0) 167 | return self 168 | 169 | def update_normals(self): 170 | v0 = self.vectors[:, 0, :3] 171 | v1 = self.vectors[:, 1, :3] 172 | v2 = self.vectors[:, 2, :3] 173 | _normals = numpy.cross(v1 - v0, v2 - v0) 174 | 175 | for i in range(len(_normals)): 176 | norm = numpy.linalg.norm(_normals[i]) 177 | if norm != 0: 178 | _normals[i] /= numpy.linalg.norm(_normals[i]) 179 | 180 | self.normals[:] = _normals 181 | return self 182 | 183 | ##################################################################### 184 | # Analyze functions 185 | # 186 | def get_volume(self): 187 | total_volume = 0 188 | for triangle in self.vectors: 189 | total_volume += BaseMesh.__calc_signed_volume(triangle) 190 | return total_volume 191 | 192 | @staticmethod 193 | def __calc_signed_volume(triangle): 194 | """ Calculate signed volume of given triangle 195 | :param list of list triangle: 196 | :rtype float 197 | """ 198 | v321 = triangle[2][0] * triangle[1][1] * triangle[0][2] 199 | v231 = triangle[1][0] * triangle[2][1] * triangle[0][2] 200 | v312 = triangle[2][0] * triangle[0][1] * triangle[1][2] 201 | v132 = triangle[0][0] * triangle[2][1] * triangle[1][2] 202 | v213 = triangle[1][0] * triangle[0][1] * triangle[2][2] 203 | v123 = triangle[0][0] * triangle[1][1] * triangle[2][2] 204 | 205 | signed_volume = (-v321 + v231 + v312 - v132 - v213 + v123) / 6.0 206 | return signed_volume 207 | 208 | ##################################################################### 209 | # Save functions 210 | # 211 | 212 | # STL 213 | def save_stl(self, path, mode=MODE_STL_AUTO, update_normals=True): 214 | """Save data with stl format 215 | :param str path: 216 | :param int mode: 217 | :param bool update_normals: 218 | """ 219 | if update_normals: 220 | self.update_normals() 221 | 222 | filename = os.path.split(path)[-1] 223 | 224 | if mode is MODE_STL_AUTO: 225 | if self.mode == MODE_STL_BINARY: 226 | save_func = self.__save_stl_binary 227 | 228 | elif self.mode == MODE_STL_ASCII: 229 | save_func = self.__save_stl_ascii 230 | 231 | else: 232 | raise ValueError("Mode %r is invalid" % mode) 233 | 234 | elif mode is MODE_STL_BINARY: 235 | save_func = self.__save_stl_binary 236 | 237 | else: 238 | raise ValueError("Mode %r is invalid" % mode) 239 | 240 | with open(path, 'wb') as fh: 241 | save_func(fh, filename) 242 | 243 | def __save_stl_binary(self, fh, name): 244 | fh.write(("%s (%s) %s %s" % ( 245 | "{}".format(__title__), 246 | "{}".format(__version__), 247 | datetime.datetime.now(), 248 | name 249 | ))[:80].ljust(80, ' ')) 250 | 251 | bin_data = numpy.zeros(self.data.size, BaseMesh.stl_dtype) 252 | bin_data['normals'] = self.normals[:] 253 | bin_data['vectors'] = self.vectors[:, :, :3] 254 | bin_data['attr'] = self.attr 255 | fh.write(struct.pack('i', bin_data.size)) 256 | bin_data.tofile(fh) 257 | 258 | def __save_stl_ascii(self, fh, name): 259 | print("solid {}".format(name), file=fh) 260 | for i in range(len(self.vectors)): 261 | print("facet normal %f %f %f" % tuple(self.normals[i][:3]), file=fh) 262 | print(" outer loop", file=fh) 263 | print(" vertex %f %f %f" % tuple(self.vectors[i][0][:3]), file=fh) 264 | print(" vertex %f %f %f" % tuple(self.vectors[i][1][:3]), file=fh) 265 | print(" vertex %f %f %f" % tuple(self.vectors[i][2][:3]), file=fh) 266 | print(" endloop", file=fh) 267 | print("endfacet", file=fh) 268 | print("endsolid {}".format(name), file=fh) 269 | 270 | # OBJ 271 | def save_obj(self, path, update_normals=True): 272 | """Save data with OBJ format 273 | :param stl path: 274 | :param bool update_normals: 275 | """ 276 | if update_normals: 277 | self.update_normals() 278 | 279 | # Create triangle_list 280 | vectors_key_list = [] 281 | vectors_list = [] 282 | normals_key_list = [] 283 | normals_list = [] 284 | triangle_list = [] 285 | for i, vector in enumerate(self.vectors): 286 | one_triangle = [] 287 | for j in range(3): 288 | v_key = ",".join(map(str, self.vectors[i][j][:3])) 289 | if v_key in vectors_key_list: 290 | v_index = vectors_key_list.index(v_key) 291 | else: 292 | v_index = len(vectors_key_list) 293 | vectors_key_list.append(v_key) 294 | vectors_list.append(self.vectors[i][j][:3]) 295 | one_triangle.append(v_index + 1) 296 | 297 | n_key = ",".join(map(str, self.normals[i][:3])) 298 | if n_key in normals_key_list: 299 | n_index = normals_key_list.index(n_key) 300 | else: 301 | n_index = len(normals_key_list) 302 | normals_key_list.append(n_key) 303 | normals_list.append(self.normals[i][:3]) 304 | 305 | # print(normals_list) 306 | triangle_list.append((one_triangle, n_index + 1)) 307 | 308 | with open(path, "wb") as fh: 309 | print("# {} {}".format(__title__, __version__), file=fh) 310 | print("# {}".format(datetime.datetime.now()), file=fh) 311 | print("# {}".format(__url__), file=fh) 312 | print("", file=fh) 313 | for v in vectors_list: 314 | print("v {} {} {}".format(v[0], v[1], v[2]), file=fh) 315 | for vn in normals_list: 316 | print("vn {} {} {}".format(vn[0], vn[1], vn[2]), file=fh) 317 | for t in triangle_list: 318 | faces = t[0] 319 | normal = t[1] 320 | 321 | print("f {}//{} {}//{} {}//{}".format( 322 | faces[0], normal, 323 | faces[1], normal, 324 | faces[2], normal, 325 | ), file=fh) 326 | 327 | -------------------------------------------------------------------------------- /pymesh/obj.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import absolute_import, print_function 4 | import numpy 5 | from .base import BaseMesh 6 | 7 | 8 | class Obj(BaseMesh): 9 | 10 | obj_dtype = numpy.dtype([ 11 | ('normals', numpy.float32, (3, )), 12 | ('vectors', numpy.float32, (3, 3)), 13 | ('attr', numpy.uint16, (1, )), 14 | ]) 15 | 16 | def __init__(self, path=None): 17 | """Create an instance of Obj (Wavefront) 18 | :param str path: 19 | """ 20 | super(Obj, self).__init__() 21 | 22 | if path is None: 23 | # Create EMPTY data 24 | self.name = "empty" 25 | self.data = numpy.zeros(0, dtype=Obj.obj_dtype) 26 | 27 | else: 28 | # Create data from file 29 | with open(path, "rb") as fh: 30 | data = Obj.__load(fh) 31 | self.name = path 32 | self.data = data 33 | 34 | super(Obj, self).set_initial_values() 35 | return 36 | 37 | @staticmethod 38 | def __load(fh): 39 | return numpy.fromiter(Obj.__read(fh), dtype=Obj.obj_dtype) 40 | 41 | @staticmethod 42 | def __read(fh): 43 | vertices_list = [] 44 | triangles_list = [] 45 | 46 | try: 47 | while True: 48 | line = fh.readline() 49 | if line == "": 50 | break 51 | 52 | elif line.lstrip().startswith("vn"): 53 | continue 54 | 55 | elif line.lstrip().startswith("v"): 56 | vertices = line.replace("\n", "").split(" ")[1:] 57 | vertices_list.append(map(float, vertices)) 58 | 59 | elif line.lstrip().startswith("f"): 60 | t_index_list = [] 61 | for t in line.replace("\n", "").split(" ")[1:]: 62 | t_index = t.split("/")[0] 63 | t_index_list.append(int(t_index) - 1) 64 | triangles_list.append(t_index_list) 65 | 66 | else: 67 | continue 68 | 69 | for t in triangles_list: 70 | yield ([0, 0, 0], (vertices_list[t[0]], vertices_list[t[1]], vertices_list[t[2]]), 0) 71 | 72 | except: 73 | raise RuntimeError("Failed to load OBJ file.") 74 | -------------------------------------------------------------------------------- /pymesh/stl.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import absolute_import, print_function 4 | import numpy 5 | import os 6 | import struct 7 | from .base import BaseMesh 8 | 9 | 10 | class Stl(BaseMesh): 11 | 12 | MODE_AUTO = 0 13 | MODE_ASCII = 1 14 | MODE_BINARY = 2 15 | 16 | HEADER_SIZE = 80 17 | COUNT_SIZE = 4 18 | MAX_COUNT = 1e6 19 | BUFFER_SIZE = 4096 20 | 21 | stl_dtype = numpy.dtype([ 22 | ('normals', numpy.float32, (3, )), 23 | ('vectors', numpy.float32, (3, 3)), 24 | ('attr', numpy.uint16, (1, )), 25 | ]) 26 | 27 | def __init__(self, path=None, mode_policy=MODE_AUTO): 28 | """Craete a instance of Stl. 29 | :param str path: The file path to open 30 | :param int mode_policy: The mode to open, default is :py:data:`AUTOMATIC`. 31 | """ 32 | super(Stl, self).__init__() 33 | 34 | if path is None: 35 | # Create EMPTY data 36 | self.name = "empty" 37 | self.data = numpy.zeros(0, dtype=Stl.stl_dtype) 38 | self.mode = Stl.MODE_BINARY 39 | 40 | else: 41 | # Create data from file 42 | with open(path, "rb") as fh: 43 | name, data, mode = Stl.__load(fh, mode=mode_policy) 44 | self.name = name 45 | self.data = data 46 | self.mode = mode 47 | 48 | super(Stl, self).set_initial_values() 49 | return 50 | 51 | @staticmethod 52 | def __load(fh, mode=MODE_AUTO): 53 | """Load Mesh from STL file 54 | 55 | :param FileIO fh: The file handle to open 56 | :param int mode: The mode to open, default is :py:data:`AUTOMATIC`. 57 | :return: 58 | """ 59 | header = fh.read(Stl.HEADER_SIZE).lower() 60 | name = "" 61 | data = None 62 | if not header.strip(): 63 | return 64 | 65 | if mode in (Stl.MODE_AUTO, Stl.MODE_ASCII) and header.startswith('solid'): 66 | try: 67 | name = header.split('\n', 1)[0][:5].strip() 68 | data = Stl.__load_ascii(fh, header) 69 | mode = Stl.MODE_ASCII 70 | 71 | except: 72 | pass 73 | 74 | else: 75 | data = Stl.__load_binary(fh) 76 | mode = Stl.MODE_BINARY 77 | 78 | return name, data, mode 79 | 80 | @staticmethod 81 | def __load_binary(fh): 82 | # Read the triangle count 83 | count, = struct.unpack("i", fh.read(Stl.COUNT_SIZE)) 84 | assert count < Stl.MAX_COUNT, \ 85 | 'File too large, got {} triangles which exceeds the maximum of {}' .format( 86 | count, Stl.MAX_COUNT 87 | ) 88 | return numpy.fromfile(fh, Stl.stl_dtype, count=count) 89 | 90 | @staticmethod 91 | def __load_ascii(fh, header): 92 | return numpy.fromiter(Stl.__ascii_reader(fh, header), dtype=Stl.stl_dtype) 93 | 94 | @staticmethod 95 | def __ascii_reader(fh, header): 96 | """ 97 | :param fh: 98 | :param header: 99 | :return: 100 | """ 101 | 102 | lines = header.split('\n') 103 | recoverable = [True] 104 | 105 | def get(prefix=''): 106 | if lines: 107 | line = lines.pop(0) 108 | else: 109 | raise RuntimeError(recoverable[0], 'Unable to find more lines') 110 | 111 | if not lines: 112 | recoverable[0] = False 113 | 114 | # Read more lines and make sure we prepend any old data 115 | lines[:] = fh.read(Stl.BUFFER_SIZE).split('\n') 116 | line += lines.pop(0) 117 | line = line.lower().strip() 118 | if prefix: 119 | if line.startswith(prefix): 120 | values = line.replace(prefix, '', 1).strip().split() 121 | elif line.startswith('endsolid'): 122 | raise StopIteration() 123 | else: 124 | raise RuntimeError(recoverable[0], 125 | '%r should start with %r' % (line, 126 | prefix)) 127 | 128 | if len(values) == 3: 129 | vertex = [float(v) for v in values] 130 | return vertex 131 | else: # pragma: no cover 132 | raise RuntimeError(recoverable[0], 133 | 'Incorrect value %r' % line) 134 | else: 135 | return line 136 | 137 | line = get() 138 | if not line.startswith('solid ') and line.startswith('solid'): 139 | print("Error") 140 | 141 | if not lines: 142 | raise RuntimeError(recoverable[0], 143 | 'No lines found, impossible to read') 144 | 145 | while True: 146 | # Read from the header lines first, until that point we can recover 147 | # and go to the binary option. After that we cannot due to 148 | # unseekable files such as sys.stdin 149 | # 150 | # Numpy doesn't support any non-file types so wrapping with a 151 | # buffer and/or StringIO does not work. 152 | try: 153 | normals = get('facet normal') 154 | assert get() == 'outer loop' 155 | v0 = get('vertex') 156 | v1 = get('vertex') 157 | v2 = get('vertex') 158 | assert get() == 'endloop' 159 | assert get() == 'endfacet' 160 | attrs = 0 161 | yield (normals, (v0, v1, v2), attrs) 162 | except AssertionError as e: 163 | raise RuntimeError(recoverable[0], e) 164 | except StopIteration: 165 | if any(lines): 166 | # Seek back to where the next solid should begin 167 | fh.seek(-len('\n'.join(lines)), os.SEEK_CUR) 168 | raise 169 | 170 | -------------------------------------------------------------------------------- /pymesh/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class Validator(object): 5 | 6 | @staticmethod 7 | def is_string(value): 8 | if value is None or not isinstance(value, (str, unicode)): 9 | return False 10 | return True 11 | -------------------------------------------------------------------------------- /runtest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | python -m unittest discover -v -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | import pymesh 3 | 4 | setup( 5 | name=pymesh.__title__, 6 | packages=[pymesh.__title__], 7 | version=pymesh.__version__, 8 | author=pymesh.__author__, 9 | author_email="taxpon@gmail.com", 10 | description="Library for manipulating (Translate, Rotate and Scale) 3D data using numpy.", 11 | url=pymesh.__url__, 12 | license=pymesh.__license__, 13 | classifiers=[ 14 | 'License :: OSI Approved :: MIT License', 15 | "Programming Language :: Python", 16 | 17 | ], 18 | install_requires=[ 19 | 'numpy' 20 | ], 21 | ) 22 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taxpon/pymesh/a90b3b2ed1408d793f3b5208dd8087b08fb7c92e/tests/__init__.py -------------------------------------------------------------------------------- /tests/data/data_ascii.stl: -------------------------------------------------------------------------------- 1 | solid STL generated by MeshLab 2 | facet normal -1.000000e+00 0.000000e+00 0.000000e+00 3 | outer loop 4 | vertex -1.000000e+00 1.000000e+00 -1.000000e+00 5 | vertex -1.000000e+00 -1.000000e+00 -1.000000e+00 6 | vertex -1.000000e+00 -1.000000e+00 1.000000e+00 7 | endloop 8 | endfacet 9 | facet normal -1.000000e+00 0.000000e+00 0.000000e+00 10 | outer loop 11 | vertex -1.000000e+00 -1.000000e+00 1.000000e+00 12 | vertex -1.000000e+00 1.000000e+00 1.000000e+00 13 | vertex -1.000000e+00 1.000000e+00 -1.000000e+00 14 | endloop 15 | endfacet 16 | facet normal -0.000000e+00 1.000000e+00 0.000000e+00 17 | outer loop 18 | vertex -1.000000e+00 1.000000e+00 1.000000e+00 19 | vertex 1.000000e+00 1.000000e+00 1.000000e+00 20 | vertex 1.000000e+00 1.000000e+00 -1.000000e+00 21 | endloop 22 | endfacet 23 | facet normal 0.000000e+00 1.000000e+00 0.000000e+00 24 | outer loop 25 | vertex 1.000000e+00 1.000000e+00 -1.000000e+00 26 | vertex -1.000000e+00 1.000000e+00 -1.000000e+00 27 | vertex -1.000000e+00 1.000000e+00 1.000000e+00 28 | endloop 29 | endfacet 30 | facet normal 1.000000e+00 0.000000e+00 0.000000e+00 31 | outer loop 32 | vertex 1.000000e+00 1.000000e+00 1.000000e+00 33 | vertex 1.000000e+00 -1.000000e+00 1.000000e+00 34 | vertex 1.000000e+00 -1.000000e+00 -1.000000e+00 35 | endloop 36 | endfacet 37 | facet normal 1.000000e+00 0.000000e+00 0.000000e+00 38 | outer loop 39 | vertex 1.000000e+00 -1.000000e+00 -1.000000e+00 40 | vertex 1.000000e+00 1.000000e+00 -1.000000e+00 41 | vertex 1.000000e+00 1.000000e+00 1.000000e+00 42 | endloop 43 | endfacet 44 | facet normal 0.000000e+00 -1.000000e+00 0.000000e+00 45 | outer loop 46 | vertex -1.000000e+00 -1.000000e+00 -1.000000e+00 47 | vertex 1.000000e+00 -1.000000e+00 -1.000000e+00 48 | vertex 1.000000e+00 -1.000000e+00 1.000000e+00 49 | endloop 50 | endfacet 51 | facet normal -0.000000e+00 -1.000000e+00 0.000000e+00 52 | outer loop 53 | vertex 1.000000e+00 -1.000000e+00 1.000000e+00 54 | vertex -1.000000e+00 -1.000000e+00 1.000000e+00 55 | vertex -1.000000e+00 -1.000000e+00 -1.000000e+00 56 | endloop 57 | endfacet 58 | facet normal 0.000000e+00 0.000000e+00 -1.000000e+00 59 | outer loop 60 | vertex -1.000000e+00 -1.000000e+00 -1.000000e+00 61 | vertex -1.000000e+00 1.000000e+00 -1.000000e+00 62 | vertex 1.000000e+00 1.000000e+00 -1.000000e+00 63 | endloop 64 | endfacet 65 | facet normal 0.000000e+00 -0.000000e+00 -1.000000e+00 66 | outer loop 67 | vertex 1.000000e+00 1.000000e+00 -1.000000e+00 68 | vertex 1.000000e+00 -1.000000e+00 -1.000000e+00 69 | vertex -1.000000e+00 -1.000000e+00 -1.000000e+00 70 | endloop 71 | endfacet 72 | facet normal 0.000000e+00 -0.000000e+00 1.000000e+00 73 | outer loop 74 | vertex 1.000000e+00 -1.000000e+00 1.000000e+00 75 | vertex 1.000000e+00 1.000000e+00 1.000000e+00 76 | vertex -1.000000e+00 1.000000e+00 1.000000e+00 77 | endloop 78 | endfacet 79 | facet normal 0.000000e+00 0.000000e+00 1.000000e+00 80 | outer loop 81 | vertex -1.000000e+00 1.000000e+00 1.000000e+00 82 | vertex -1.000000e+00 -1.000000e+00 1.000000e+00 83 | vertex 1.000000e+00 -1.000000e+00 1.000000e+00 84 | endloop 85 | endfacet 86 | endsolid vcg 87 | -------------------------------------------------------------------------------- /tests/data/data_bin.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taxpon/pymesh/a90b3b2ed1408d793f3b5208dd8087b08fb7c92e/tests/data/data_bin.stl -------------------------------------------------------------------------------- /tests/test_obj.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import unittest 4 | from pymesh import stl 5 | 6 | 7 | class SimpleTest(unittest.TestCase): 8 | def setUp(self): 9 | pass 10 | 11 | def tearDown(self): 12 | pass 13 | 14 | def test_sample(self): 15 | pass 16 | 17 | 18 | if __name__ == "__main__": 19 | unittest.main() 20 | -------------------------------------------------------------------------------- /tests/test_stl.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import unittest 4 | from pymesh import stl 5 | 6 | 7 | class SimpleTest(unittest.TestCase): 8 | def setUp(self): 9 | pass 10 | 11 | def tearDown(self): 12 | pass 13 | 14 | def test_sample(self): 15 | pass 16 | 17 | 18 | if __name__ == "__main__": 19 | unittest.main() 20 | --------------------------------------------------------------------------------