├── .codeclimate.yml ├── .coveragerc ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ ├── codeql.yml │ ├── main.yml │ └── stale.yml ├── .gitignore ├── .pyup.yml ├── CONTRIBUTING.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── SECURITY.md ├── appveyor.yml ├── build.cmd ├── docs ├── Makefile ├── _theme │ ├── LICENSE │ ├── flask_theme_support.py │ └── wolph │ │ ├── layout.html │ │ ├── relations.html │ │ ├── static │ │ ├── flasky.css_t │ │ └── small_flask.css │ │ └── theme.conf ├── conf.py ├── index.rst ├── make.bat ├── requirements.txt ├── stl.rst ├── tests.rst └── usage.rst ├── pytest.ini ├── ruff.toml ├── setup.cfg ├── setup.py ├── stl ├── __about__.py ├── __init__.py ├── _speedups.pyx ├── base.py ├── main.py ├── mesh.py ├── py.typed ├── stl.py └── utils.py ├── tests ├── 3mf │ └── Moon.3mf ├── __init__.py ├── conftest.py ├── qt-lc_numeric-reproducer ├── requirements.txt ├── stl_ascii │ ├── Cube.stl │ ├── HalfDonut.stl │ ├── Moon.stl │ ├── Moon_Chinese.stl │ └── Star.stl ├── stl_binary │ ├── Cube.stl │ ├── CubeInvalidUnicode.stl │ ├── HalfDonut.stl │ ├── Moon.stl │ ├── Star.stl │ ├── StarWithEmptyHeader.stl │ └── rear_case.stl ├── stl_corruption.py ├── stl_tests │ ├── bwb.stl │ ├── dos.stl │ ├── triamid.stl │ └── unix.stl ├── test_ascii.py ├── test_binary.py ├── test_commandline.py ├── test_convert.py ├── test_line_endings.py ├── test_mesh.py ├── test_mesh_properties.py ├── test_multiple.py ├── test_rotate.py ├── tmp │ ├── test_args_False_0 │ │ └── binary.stl │ ├── test_args_False_current │ ├── test_args_True_0 │ │ └── binary.stl │ └── test_args_True_current └── utils.py └── tox.ini /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | languages: 2 | Ruby: true 3 | JavaScript: true 4 | PHP: true 5 | Python: true 6 | exclude_paths: 7 | - 'tests/*' 8 | - 'docs/*' 9 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | fail_under = 100 3 | exclude_lines = 4 | pragma: no cover 5 | def __repr__ 6 | if self.debug: 7 | if settings.DEBUG 8 | raise AssertionError 9 | raise NotImplementedError 10 | if 0: 11 | if __name__ == .__main__.: 12 | 13 | [run] 14 | source = stl 15 | branch = True 16 | 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # force CRLF line endings for dos test file 2 | tests/stl_tests/dos.stl text eol=crlf 3 | 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: wolph 2 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ 'develop' ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [ 'develop' ] 9 | schedule: 10 | - cron: '26 17 * * 2' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | timeout-minutes: 5 17 | permissions: 18 | actions: read 19 | contents: read 20 | security-events: write 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | language: [ 'python' ] 26 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 27 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 28 | 29 | steps: 30 | - name: Checkout repository 31 | uses: actions/checkout@v3 32 | 33 | # Initializes the CodeQL tools for scanning. 34 | - name: Initialize CodeQL 35 | uses: github/codeql-action/init@v2 36 | with: 37 | languages: ${{ matrix.language }} 38 | # If you wish to specify custom queries, you can do so here or in a config file. 39 | # By default, queries listed here will override any specified in a config file. 40 | # Prefix the list here with "+" to use these queries and those in the config file. 41 | 42 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 43 | queries: +security-and-quality 44 | 45 | 46 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 47 | # If this step fails, then you should remove it and run the build manually (see below) 48 | - name: Autobuild 49 | uses: github/codeql-action/autobuild@v2 50 | 51 | # ℹ️ Command-line programs to run using the OS shell. 52 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 53 | 54 | # If the Autobuild fails above, remove it and uncomment the following three lines. 55 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 56 | 57 | # - run: | 58 | # echo "Run, Build Application using script" 59 | # ./location_of_script_within_repo/buildscript.sh 60 | 61 | - name: Perform CodeQL Analysis 62 | uses: github/codeql-action/analyze@v2 63 | with: 64 | category: "/language:${{matrix.language}}" 65 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: tox 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | timeout-minutes: 10 12 | strategy: 13 | matrix: 14 | python-version: ['3.9', '3.10', '3.11', '3.12'] 15 | numpy-version: ['numpy1', 'numpy2'] 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Set up Python ${{ matrix.python-version }} 20 | uses: actions/setup-python@v4 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | pip install tox tox-gh-actions 27 | - name: python version 28 | env: 29 | TOXENV: "py${{ matrix.python }}-${{ matrix.numpy-version }}" 30 | run: | 31 | TOXENV=${{ env.TOXENV }} 32 | TOXENV=${TOXENV//.} # replace all dots 33 | echo TOXENV=${TOXENV} >> $GITHUB_ENV # update GitHub ENV vars 34 | - name: print env 35 | run: echo ${{ env.TOXENV }} 36 | - name: Test with tox 37 | run: tox 38 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Close stale issues and pull requests 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 0 * * *' # Run every day at midnight 7 | 8 | jobs: 9 | stale: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/stale@v8 13 | with: 14 | days-before-stale: 30 15 | days-before-pr-stale: -1 16 | exempt-issue-labels: in progress,help-wanted,pinned,security,enhancement 17 | stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 7 days' 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | docs/_build 4 | cover 5 | *.egg-info 6 | .cache 7 | .coverage 8 | **/htmlcov 9 | **/*.pyc 10 | **/*.pyo 11 | **/*.so 12 | /*.stl 13 | /.tox 14 | /stl/*.c 15 | -------------------------------------------------------------------------------- /.pyup.yml: -------------------------------------------------------------------------------- 1 | # autogenerated pyup.io config file 2 | # see https://pyup.io/docs/configuration/ for all available options 3 | 4 | update: insecure 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Contributing 3 | ============ 4 | 5 | Contributions are welcome, and they are greatly appreciated! Every 6 | little bit helps, and credit will always be given. 7 | 8 | You can contribute in many ways: 9 | 10 | Types of Contributions 11 | ---------------------- 12 | 13 | Report Bugs 14 | ~~~~~~~~~~~ 15 | 16 | Report bugs at https://github.com/WoLpH/numpy-stl/issues. 17 | 18 | If you are reporting a bug, please include: 19 | 20 | * Your operating system name and version. 21 | * Any details about your local setup that might be helpful in troubleshooting. 22 | * Detailed steps to reproduce the bug. 23 | 24 | Fix Bugs 25 | ~~~~~~~~ 26 | 27 | Look through the GitHub issues for bugs. Anything tagged with "bug" 28 | is open to whoever wants to implement it. 29 | 30 | Implement Features 31 | ~~~~~~~~~~~~~~~~~~ 32 | 33 | Look through the GitHub issues for features. Anything tagged with "feature" 34 | is open to whoever wants to implement it. 35 | 36 | Write Documentation 37 | ~~~~~~~~~~~~~~~~~~~ 38 | 39 | numpy-stl could always use more documentation, whether as part of the 40 | official numpy-stl docs, in docstrings, or even on the web in blog posts, 41 | articles, and such. 42 | 43 | Submit Feedback 44 | ~~~~~~~~~~~~~~~ 45 | 46 | The best way to send feedback is to file an issue at https://github.com/WoLpH/numpy-stl/issues. 47 | 48 | If you are proposing a feature: 49 | 50 | * Explain in detail how it would work. 51 | * Keep the scope as narrow as possible, to make it easier to implement. 52 | * Remember that this is a volunteer-driven project, and that contributions 53 | are welcome :) 54 | 55 | Get Started! 56 | ------------ 57 | 58 | Ready to contribute? Here's how to set up `numpy-stl` for local development. 59 | 60 | 1. Fork the `numpy-stl` repo on GitHub. 61 | 2. Clone your fork locally:: 62 | 63 | $ git clone --branch develop git@github.com:your_name_here/numpy-stl.git numpy_stl 64 | 65 | 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: 66 | 67 | $ mkvirtualenv numpy_stl 68 | $ cd numpy_stl/ 69 | $ pip install -e . 70 | 71 | 4. Create a branch for local development with `git-flow-avh`_:: 72 | 73 | $ git-flow feature start name-of-your-bugfix-or-feature 74 | 75 | Or without git-flow: 76 | 77 | $ git checkout -b feature/name-of-your-bugfix-or-feature 78 | 79 | Now you can make your changes locally. 80 | 81 | 5. When you're done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox:: 82 | 83 | $ flake8 --ignore=W391 stl tests 84 | $ py.test 85 | $ tox 86 | 87 | To get flake8 and tox, just pip install them into your virtualenv using the requirements file. 88 | 89 | $ pip install -r tests/requirements.txt 90 | 91 | 6. Commit your changes and push your branch to GitHub with `git-flow-avh`_:: 92 | 93 | $ git add . 94 | $ git commit -m "Your detailed description of your changes." 95 | $ git-flow feature publish 96 | 97 | Or without git-flow: 98 | 99 | $ git add . 100 | $ git commit -m "Your detailed description of your changes." 101 | $ git push -u origin feature/name-of-your-bugfix-or-feature 102 | 103 | 7. Submit a pull request through the GitHub website. 104 | 105 | Pull Request Guidelines 106 | ----------------------- 107 | 108 | Before you submit a pull request, check that it meets these guidelines: 109 | 110 | 1. The pull request should include tests. 111 | 2. If the pull request adds functionality, the docs should be updated. Put 112 | your new functionality into a function with a docstring, and add the 113 | feature to the list in README.rst. 114 | 3. The pull request should work for Python 2.7, 3.3, 3.4 and 3.5. Check 115 | https://travis-ci.org/WoLpH/numpy-stl/pull_requests 116 | and make sure that the tests pass for all supported Python versions. 117 | 118 | Tips 119 | ---- 120 | 121 | To run a subset of tests:: 122 | 123 | $ py.test tests/some_test.py 124 | 125 | .. _git-flow-avh: https://github.com/petervanderdoes/gitflow 126 | 127 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Rick van Hattem All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | 3. Neither the name of the copyright holder nor the names of its contributors 14 | may be used to endorse or promote products derived from this software without 15 | specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 21 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 23 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 24 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 25 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # Include all documentation files 2 | include-recursive *.rst 3 | include stl/*.pyx 4 | include stl/*.c 5 | include LICENSE 6 | 7 | # Include docs and tests 8 | graft tests 9 | graft docs 10 | 11 | # Skip compiled python files 12 | global-exclude *.py[co] 13 | 14 | # Remove all build directories 15 | prune docs/_build 16 | 17 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | numpy-stl 2 | ============================================================================== 3 | 4 | .. image:: https://github.com/WoLpH/numpy-stl/actions/workflows/main.yml/badge.svg?branch=master 5 | :alt: numpy-stl test status 6 | :target: https://github.com/WoLpH/numpy-stl/actions/workflows/main.yml 7 | 8 | .. image:: https://ci.appveyor.com/api/projects/status/cbv7ak2i59wf3lpj?svg=true 9 | :alt: numpy-stl test status 10 | :target: https://ci.appveyor.com/project/WoLpH/numpy-stl 11 | 12 | .. image:: https://badge.fury.io/py/numpy-stl.svg 13 | :alt: numpy-stl Pypi version 14 | :target: https://pypi.python.org/pypi/numpy-stl 15 | 16 | .. image:: https://coveralls.io/repos/WoLpH/numpy-stl/badge.svg?branch=master 17 | :alt: numpy-stl code coverage 18 | :target: https://coveralls.io/r/WoLpH/numpy-stl?branch=master 19 | 20 | .. image:: https://img.shields.io/pypi/pyversions/numpy-stl.svg 21 | 22 | Simple library to make working with STL files (and 3D objects in general) fast 23 | and easy. 24 | 25 | Due to all operations heavily relying on `numpy` this is one of the fastest 26 | STL editing libraries for Python available. 27 | 28 | Security contact information 29 | ------------------------------------------------------------------------------ 30 | 31 | To report a security vulnerability, please use the 32 | `Tidelift security contact `_. 33 | Tidelift will coordinate the fix and disclosure. 34 | 35 | Issues 36 | ------ 37 | 38 | If you encounter any issues, make sure you report them `here `_. Be sure to search for existing issues however. Many issues have been covered before. 39 | While this project uses `numpy` as it's main dependency, it is not in any way affiliated to the `numpy` project or the NumFocus organisation. 40 | 41 | Links 42 | ----- 43 | 44 | - The source: https://github.com/WoLpH/numpy-stl 45 | - Project page: https://pypi.python.org/pypi/numpy-stl 46 | - Reporting bugs: https://github.com/WoLpH/numpy-stl/issues 47 | - Documentation: http://numpy-stl.readthedocs.org/en/latest/ 48 | - My blog: https://wol.ph/ 49 | 50 | Requirements for installing: 51 | ------------------------------------------------------------------------------ 52 | 53 | - `numpy`_ any recent version 54 | - `python-utils`_ version 1.6 or greater 55 | 56 | Installation: 57 | ------------------------------------------------------------------------------ 58 | 59 | `pip install numpy-stl` 60 | 61 | Initial usage: 62 | ------------------------------------------------------------------------------ 63 | 64 | After installing the package, you should be able to run the following commands 65 | similar to how you can run `pip`. 66 | 67 | .. code-block:: shell 68 | 69 | $ stl2bin your_ascii_stl_file.stl new_binary_stl_file.stl 70 | $ stl2ascii your_binary_stl_file.stl new_ascii_stl_file.stl 71 | $ stl your_ascii_stl_file.stl new_binary_stl_file.stl 72 | 73 | Contributing: 74 | ------------------------------------------------------------------------------ 75 | 76 | Contributions are always welcome. Please view the guidelines to get started: 77 | https://github.com/WoLpH/numpy-stl/blob/develop/CONTRIBUTING.rst 78 | 79 | Quickstart 80 | ------------------------------------------------------------------------------ 81 | 82 | .. code-block:: python 83 | 84 | import numpy 85 | from stl import mesh 86 | 87 | # Using an existing stl file: 88 | your_mesh = mesh.Mesh.from_file('some_file.stl') 89 | 90 | # Or creating a new mesh (make sure not to overwrite the `mesh` import by 91 | # naming it `mesh`): 92 | VERTICE_COUNT = 100 93 | data = numpy.zeros(VERTICE_COUNT, dtype=mesh.Mesh.dtype) 94 | your_mesh = mesh.Mesh(data, remove_empty_areas=False) 95 | 96 | # The mesh normals (calculated automatically) 97 | your_mesh.normals 98 | # The mesh vectors 99 | your_mesh.v0, your_mesh.v1, your_mesh.v2 100 | # Accessing individual points (concatenation of v0, v1 and v2 in triplets) 101 | assert (your_mesh.points[0][0:3] == your_mesh.v0[0]).all() 102 | assert (your_mesh.points[0][3:6] == your_mesh.v1[0]).all() 103 | assert (your_mesh.points[0][6:9] == your_mesh.v2[0]).all() 104 | assert (your_mesh.points[1][0:3] == your_mesh.v0[1]).all() 105 | 106 | your_mesh.save('new_stl_file.stl') 107 | 108 | Plotting using `matplotlib`_ is equally easy: 109 | ------------------------------------------------------------------------------ 110 | 111 | .. code-block:: python 112 | 113 | from stl import mesh 114 | from mpl_toolkits import mplot3d 115 | from matplotlib import pyplot 116 | 117 | # Create a new plot 118 | figure = pyplot.figure() 119 | axes = figure.add_subplot(projection='3d') 120 | 121 | # Load the STL files and add the vectors to the plot 122 | your_mesh = mesh.Mesh.from_file('tests/stl_binary/HalfDonut.stl') 123 | axes.add_collection3d(mplot3d.art3d.Poly3DCollection(your_mesh.vectors)) 124 | 125 | # Auto scale to the mesh size 126 | scale = your_mesh.points.flatten() 127 | axes.auto_scale_xyz(scale, scale, scale) 128 | 129 | # Show the plot to the screen 130 | pyplot.show() 131 | 132 | .. _numpy: http://numpy.org/ 133 | .. _matplotlib: http://matplotlib.org/ 134 | .. _python-utils: https://github.com/WoLpH/python-utils 135 | 136 | Experimental support for reading 3MF files 137 | ------------------------------------------------------------------------------ 138 | 139 | .. code-block:: python 140 | 141 | import pathlib 142 | import stl 143 | 144 | path = pathlib.Path('tests/3mf/Moon.3mf') 145 | 146 | # Load the 3MF file 147 | for m in stl.Mesh.from_3mf_file(path): 148 | # Do something with the mesh 149 | print('mesh', m) 150 | 151 | Note that this is still experimental and may not work for all 3MF files. 152 | Additionally it only allows reading 3mf files, not writing them. 153 | 154 | Modifying Mesh objects 155 | ------------------------------------------------------------------------------ 156 | 157 | .. code-block:: python 158 | 159 | from stl import mesh 160 | import math 161 | import numpy 162 | 163 | # Create 3 faces of a cube 164 | data = numpy.zeros(6, dtype=mesh.Mesh.dtype) 165 | 166 | # Top of the cube 167 | data['vectors'][0] = numpy.array([[0, 1, 1], 168 | [1, 0, 1], 169 | [0, 0, 1]]) 170 | data['vectors'][1] = numpy.array([[1, 0, 1], 171 | [0, 1, 1], 172 | [1, 1, 1]]) 173 | # Front face 174 | data['vectors'][2] = numpy.array([[1, 0, 0], 175 | [1, 0, 1], 176 | [1, 1, 0]]) 177 | data['vectors'][3] = numpy.array([[1, 1, 1], 178 | [1, 0, 1], 179 | [1, 1, 0]]) 180 | # Left face 181 | data['vectors'][4] = numpy.array([[0, 0, 0], 182 | [1, 0, 0], 183 | [1, 0, 1]]) 184 | data['vectors'][5] = numpy.array([[0, 0, 0], 185 | [0, 0, 1], 186 | [1, 0, 1]]) 187 | 188 | # Since the cube faces are from 0 to 1 we can move it to the middle by 189 | # substracting .5 190 | data['vectors'] -= .5 191 | 192 | # Generate 4 different meshes so we can rotate them later 193 | meshes = [mesh.Mesh(data.copy()) for _ in range(4)] 194 | 195 | # Rotate 90 degrees over the Y axis 196 | meshes[0].rotate([0.0, 0.5, 0.0], math.radians(90)) 197 | 198 | # Translate 2 points over the X axis 199 | meshes[1].x += 2 200 | 201 | # Rotate 90 degrees over the X axis 202 | meshes[2].rotate([0.5, 0.0, 0.0], math.radians(90)) 203 | # Translate 2 points over the X and Y points 204 | meshes[2].x += 2 205 | meshes[2].y += 2 206 | 207 | # Rotate 90 degrees over the X and Y axis 208 | meshes[3].rotate([0.5, 0.0, 0.0], math.radians(90)) 209 | meshes[3].rotate([0.0, 0.5, 0.0], math.radians(90)) 210 | # Translate 2 points over the Y axis 211 | meshes[3].y += 2 212 | 213 | 214 | # Optionally render the rotated cube faces 215 | from matplotlib import pyplot 216 | from mpl_toolkits import mplot3d 217 | 218 | # Create a new plot 219 | figure = pyplot.figure() 220 | axes = figure.add_subplot(projection='3d') 221 | 222 | # Render the cube faces 223 | for m in meshes: 224 | axes.add_collection3d(mplot3d.art3d.Poly3DCollection(m.vectors)) 225 | 226 | # Auto scale to the mesh size 227 | scale = numpy.concatenate([m.points for m in meshes]).flatten() 228 | axes.auto_scale_xyz(scale, scale, scale) 229 | 230 | # Show the plot to the screen 231 | pyplot.show() 232 | 233 | Extending Mesh objects 234 | ------------------------------------------------------------------------------ 235 | 236 | .. code-block:: python 237 | 238 | from stl import mesh 239 | import math 240 | import numpy 241 | 242 | # Create 3 faces of a cube 243 | data = numpy.zeros(6, dtype=mesh.Mesh.dtype) 244 | 245 | # Top of the cube 246 | data['vectors'][0] = numpy.array([[0, 1, 1], 247 | [1, 0, 1], 248 | [0, 0, 1]]) 249 | data['vectors'][1] = numpy.array([[1, 0, 1], 250 | [0, 1, 1], 251 | [1, 1, 1]]) 252 | # Front face 253 | data['vectors'][2] = numpy.array([[1, 0, 0], 254 | [1, 0, 1], 255 | [1, 1, 0]]) 256 | data['vectors'][3] = numpy.array([[1, 1, 1], 257 | [1, 0, 1], 258 | [1, 1, 0]]) 259 | # Left face 260 | data['vectors'][4] = numpy.array([[0, 0, 0], 261 | [1, 0, 0], 262 | [1, 0, 1]]) 263 | data['vectors'][5] = numpy.array([[0, 0, 0], 264 | [0, 0, 1], 265 | [1, 0, 1]]) 266 | 267 | # Since the cube faces are from 0 to 1 we can move it to the middle by 268 | # substracting .5 269 | data['vectors'] -= .5 270 | 271 | cube_back = mesh.Mesh(data.copy()) 272 | cube_front = mesh.Mesh(data.copy()) 273 | 274 | # Rotate 90 degrees over the X axis followed by the Y axis followed by the 275 | # X axis 276 | cube_back.rotate([0.5, 0.0, 0.0], math.radians(90)) 277 | cube_back.rotate([0.0, 0.5, 0.0], math.radians(90)) 278 | cube_back.rotate([0.5, 0.0, 0.0], math.radians(90)) 279 | 280 | cube = mesh.Mesh(numpy.concatenate([ 281 | cube_back.data.copy(), 282 | cube_front.data.copy(), 283 | ])) 284 | 285 | # Optionally render the rotated cube faces 286 | from matplotlib import pyplot 287 | from mpl_toolkits import mplot3d 288 | 289 | # Create a new plot 290 | figure = pyplot.figure() 291 | axes = figure.add_subplot(projection='3d') 292 | 293 | # Render the cube 294 | axes.add_collection3d(mplot3d.art3d.Poly3DCollection(cube.vectors)) 295 | 296 | # Auto scale to the mesh size 297 | scale = cube_back.points.flatten() 298 | axes.auto_scale_xyz(scale, scale, scale) 299 | 300 | # Show the plot to the screen 301 | pyplot.show() 302 | 303 | Creating a single triangle 304 | ---------------------------------- 305 | 306 | .. code-block:: python 307 | 308 | import numpy 309 | from stl import mesh 310 | 311 | # A unit triangle 312 | tri_vectors = [[0,0,0],[0,1,0],[0,0,1]] 313 | 314 | # Create the vector data. It’s a numpy structured array with N entries, where N is the number of triangles (here N=1), and each entry is in the format ('normals','vectors','attr') 315 | data = numpy.array([( 316 | 0, # Set 'normals' to zero, and the mesh class will automatically calculate them at initialization 317 | tri_vectors, # 'vectors' 318 | 0 # 'attr' 319 | )], dtype = mesh.Mesh.dtype) # The structure defined by the mesh class (N x ('normals','vectors','attr')) 320 | 321 | # Create the mesh object from the structured array 322 | tri_mesh = mesh.Mesh(data) 323 | 324 | # Optionally make a plot for fun 325 | # Load the plot tools 326 | from matplotlib import pyplot 327 | from mpl_toolkits import mplot3d 328 | 329 | # Create a new plot 330 | figure = pyplot.figure() 331 | axes = figure.add_subplot(projection='3d') 332 | 333 | # Add mesh to plot 334 | axes.add_collection3d(mplot3d.art3d.Poly3DCollection(tri_mesh.vectors)) # Just need the 'vectors' attribute for display 335 | 336 | Creating Mesh objects from a list of vertices and faces 337 | ------------------------------------------------------------------------------ 338 | 339 | .. code-block:: python 340 | 341 | import numpy as np 342 | from stl import mesh 343 | 344 | # Define the 8 vertices of the cube 345 | vertices = np.array([\ 346 | [-1, -1, -1], 347 | [+1, -1, -1], 348 | [+1, +1, -1], 349 | [-1, +1, -1], 350 | [-1, -1, +1], 351 | [+1, -1, +1], 352 | [+1, +1, +1], 353 | [-1, +1, +1]]) 354 | # Define the 12 triangles composing the cube 355 | faces = np.array([\ 356 | [0,3,1], 357 | [1,3,2], 358 | [0,4,7], 359 | [0,7,3], 360 | [4,5,6], 361 | [4,6,7], 362 | [5,1,2], 363 | [5,2,6], 364 | [2,3,6], 365 | [3,7,6], 366 | [0,1,5], 367 | [0,5,4]]) 368 | 369 | # Create the mesh 370 | cube = mesh.Mesh(np.zeros(faces.shape[0], dtype=mesh.Mesh.dtype)) 371 | for i, f in enumerate(faces): 372 | for j in range(3): 373 | cube.vectors[i][j] = vertices[f[j],:] 374 | 375 | # Write the mesh to file "cube.stl" 376 | cube.save('cube.stl') 377 | 378 | 379 | Evaluating Mesh properties (Volume, Center of gravity, Inertia, Convexity) 380 | ------------------------------------------------------------------------------ 381 | 382 | .. code-block:: python 383 | 384 | import numpy as np 385 | from stl import mesh 386 | 387 | # Using an existing closed stl file: 388 | your_mesh = mesh.Mesh.from_file('some_file.stl') 389 | 390 | volume, cog, inertia = your_mesh.get_mass_properties() 391 | print("Volume = {0}".format(volume)) 392 | print("Position of the center of gravity (COG) = {0}".format(cog)) 393 | print("Inertia matrix at expressed at the COG = {0}".format(inertia[0,:])) 394 | print(" {0}".format(inertia[1,:])) 395 | print(" {0}".format(inertia[2,:])) 396 | print("Your mesh is convex: {0}".format(your_mesh.is_convex())) 397 | Combining multiple STL files 398 | ------------------------------------------------------------------------------ 399 | 400 | .. code-block:: python 401 | 402 | import math 403 | import stl 404 | from stl import mesh 405 | import numpy 406 | 407 | 408 | # find the max dimensions, so we can know the bounding box, getting the height, 409 | # width, length (because these are the step size)... 410 | def find_mins_maxs(obj): 411 | minx = obj.x.min() 412 | maxx = obj.x.max() 413 | miny = obj.y.min() 414 | maxy = obj.y.max() 415 | minz = obj.z.min() 416 | maxz = obj.z.max() 417 | return minx, maxx, miny, maxy, minz, maxz 418 | 419 | 420 | def translate(_solid, step, padding, multiplier, axis): 421 | if 'x' == axis: 422 | items = 0, 3, 6 423 | elif 'y' == axis: 424 | items = 1, 4, 7 425 | elif 'z' == axis: 426 | items = 2, 5, 8 427 | else: 428 | raise RuntimeError('Unknown axis %r, expected x, y or z' % axis) 429 | 430 | # _solid.points.shape == [:, ((x, y, z), (x, y, z), (x, y, z))] 431 | _solid.points[:, items] += (step * multiplier) + (padding * multiplier) 432 | 433 | 434 | def copy_obj(obj, dims, num_rows, num_cols, num_layers): 435 | w, l, h = dims 436 | copies = [] 437 | for layer in range(num_layers): 438 | for row in range(num_rows): 439 | for col in range(num_cols): 440 | # skip the position where original being copied is 441 | if row == 0 and col == 0 and layer == 0: 442 | continue 443 | _copy = mesh.Mesh(obj.data.copy()) 444 | # pad the space between objects by 10% of the dimension being 445 | # translated 446 | if col != 0: 447 | translate(_copy, w, w / 10., col, 'x') 448 | if row != 0: 449 | translate(_copy, l, l / 10., row, 'y') 450 | if layer != 0: 451 | translate(_copy, h, h / 10., layer, 'z') 452 | copies.append(_copy) 453 | return copies 454 | 455 | # Using an existing stl file: 456 | main_body = mesh.Mesh.from_file('ball_and_socket_simplified_-_main_body.stl') 457 | 458 | # rotate along Y 459 | main_body.rotate([0.0, 0.5, 0.0], math.radians(90)) 460 | 461 | minx, maxx, miny, maxy, minz, maxz = find_mins_maxs(main_body) 462 | w1 = maxx - minx 463 | l1 = maxy - miny 464 | h1 = maxz - minz 465 | copies = copy_obj(main_body, (w1, l1, h1), 2, 2, 1) 466 | 467 | # I wanted to add another related STL to the final STL 468 | twist_lock = mesh.Mesh.from_file('ball_and_socket_simplified_-_twist_lock.stl') 469 | minx, maxx, miny, maxy, minz, maxz = find_mins_maxs(twist_lock) 470 | w2 = maxx - minx 471 | l2 = maxy - miny 472 | h2 = maxz - minz 473 | translate(twist_lock, w1, w1 / 10., 3, 'x') 474 | copies2 = copy_obj(twist_lock, (w2, l2, h2), 2, 2, 1) 475 | combined = mesh.Mesh(numpy.concatenate([main_body.data, twist_lock.data] + 476 | [copy.data for copy in copies] + 477 | [copy.data for copy in copies2])) 478 | 479 | combined.save('combined.stl', mode=stl.Mode.ASCII) # save as ASCII 480 | 481 | Known limitations 482 | ------------------------------------------------------------------------------ 483 | 484 | - When speedups are enabled the STL name is automatically converted to 485 | lowercase. 486 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security contact information 2 | ------------------------------------------------------------------------------ 3 | 4 | To report a security vulnerability, please use the 5 | [Tidelift security contact](https://tidelift.com/security>). 6 | Tidelift will coordinate the fix and disclosure. 7 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | image: 2 | - Visual Studio 2019 3 | 4 | environment: 5 | matrix: 6 | - TOXENV: py38 7 | - TOXENV: py39 8 | - TOXENV: py310 9 | 10 | install: 11 | # Download setup scripts and unzip 12 | # - ps: "wget https://github.com/cloudify-cosmo/appveyor-utils/archive/master.zip -OutFile ./master.zip" 13 | # - "7z e master.zip */appveyor/* -oappveyor" 14 | 15 | # Install Python (from the official .msi of http://python.org) and pip when 16 | # not already installed. 17 | # - "powershell ./appveyor/install.ps1" 18 | 19 | # Prepend newly installed Python to the PATH of this build (this cannot be 20 | # done from inside the powershell script as it would require to restart 21 | # the parent CMD process). 22 | # - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" 23 | 24 | # Check that we have the expected version and architecture for Python 25 | - py --version 26 | - py -c "import struct; print(struct.calcsize('P') * 8)" 27 | 28 | build: false # Not a C# project, build stuff at the test step instead. 29 | 30 | before_test: 31 | - py -m pip install tox numpy cython wheel setuptools 32 | 33 | test_script: 34 | - "py -m tox -e %TOXENV%" 35 | 36 | after_test: 37 | - py setup.py build_ext --inplace 38 | - py setup.py sdist bdist_wheel 39 | - ps: "ls dist" 40 | 41 | artifacts: 42 | - path: dist\* 43 | -------------------------------------------------------------------------------- /build.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | :: To build extensions for 64 bit Python 3, we need to configure environment 3 | :: variables to use the MSVC 2010 C++ compilers from GRMSDKX_EN_DVD.iso of: 4 | :: MS Windows SDK for Windows 7 and .NET Framework 4 5 | :: 6 | :: More details at: 7 | :: https://github.com/cython/cython/wiki/CythonExtensionsOnWindows 8 | 9 | IF "%DISTUTILS_USE_SDK%"=="1" ( 10 | ECHO Configuring environment to build with MSVC on a 64bit architecture 11 | ECHO Using Windows SDK 7.1 12 | "C:\Program Files\Microsoft SDKs\Windows\v7.1\Setup\WindowsSdkVer.exe" -q -version:v7.1 13 | CALL "C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\SetEnv.cmd" /x64 /release 14 | SET MSSdk=1 15 | REM Need the following to allow tox to see the SDK compiler 16 | SET TOX_TESTENV_PASSENV=DISTUTILS_USE_SDK MSSdk INCLUDE LIB 17 | ) ELSE ( 18 | ECHO Using default MSVC build environment 19 | ) 20 | 21 | CALL %* 22 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PythonUtils.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PythonUtils.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/PythonUtils" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PythonUtils" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /docs/_theme/LICENSE: -------------------------------------------------------------------------------- 1 | Modifications: 2 | 3 | Copyright (c) 2012 Rick van Hattem. 4 | 5 | 6 | Original Projects: 7 | 8 | Copyright (c) 2010 Kenneth Reitz. 9 | Copyright (c) 2010 by Armin Ronacher. 10 | 11 | 12 | Some rights reserved. 13 | 14 | Redistribution and use in source and binary forms of the theme, with or 15 | without modification, are permitted provided that the following conditions 16 | are met: 17 | 18 | * Redistributions of source code must retain the above copyright 19 | notice, this list of conditions and the following disclaimer. 20 | 21 | * Redistributions in binary form must reproduce the above 22 | copyright notice, this list of conditions and the following 23 | disclaimer in the documentation and/or other materials provided 24 | with the distribution. 25 | 26 | * The names of the contributors may not be used to endorse or 27 | promote products derived from this software without specific 28 | prior written permission. 29 | 30 | We kindly ask you to only use these themes in an unmodified manner just 31 | for Flask and Flask-related products, not for unrelated projects. If you 32 | like the visual style and want to use it for your own projects, please 33 | consider making some larger changes to the themes (such as changing 34 | font faces, sizes, colors or margins). 35 | 36 | THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 37 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 38 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 39 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 40 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 41 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 42 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 43 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 44 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 45 | ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE 46 | POSSIBILITY OF SUCH DAMAGE. 47 | -------------------------------------------------------------------------------- /docs/_theme/flask_theme_support.py: -------------------------------------------------------------------------------- 1 | # flasky extensions. flasky pygments style based on tango style 2 | from pygments.style import Style 3 | from pygments.token import ( 4 | Comment, 5 | Error, 6 | Generic, 7 | Keyword, 8 | Literal, 9 | Name, 10 | Number, 11 | Operator, 12 | Other, 13 | Punctuation, 14 | String, 15 | Whitespace, 16 | ) 17 | 18 | 19 | class FlaskyStyle(Style): 20 | background_color = '#f8f8f8' 21 | default_style = '' 22 | 23 | styles = { 24 | # No corresponding class for the following: 25 | # Text: "", # class: '' 26 | Whitespace: 'underline #f8f8f8', # class: 'w' 27 | Error: '#a40000 border:#ef2929', # class: 'err' 28 | Other: '#000000', # class 'x' 29 | Comment: 'italic #8f5902', # class: 'c' 30 | Comment.Preproc: 'noitalic', # class: 'cp' 31 | Keyword: 'bold #004461', # class: 'k' 32 | Keyword.Constant: 'bold #004461', # class: 'kc' 33 | Keyword.Declaration: 'bold #004461', # class: 'kd' 34 | Keyword.Namespace: 'bold #004461', # class: 'kn' 35 | Keyword.Pseudo: 'bold #004461', # class: 'kp' 36 | Keyword.Reserved: 'bold #004461', # class: 'kr' 37 | Keyword.Type: 'bold #004461', # class: 'kt' 38 | Operator: '#582800', # class: 'o' 39 | Operator.Word: 'bold #004461', # class: 'ow' - like keywords 40 | Punctuation: 'bold #000000', # class: 'p' 41 | # because special names such as Name.Class, Name.Function, etc. 42 | # are not recognized as such later in the parsing, we choose them 43 | # to look the same as ordinary variables. 44 | Name: '#000000', # class: 'n' 45 | Name.Attribute: '#c4a000', # class: 'na' - to be revised 46 | Name.Builtin: '#004461', # class: 'nb' 47 | Name.Builtin.Pseudo: '#3465a4', # class: 'bp' 48 | Name.Class: '#000000', # class: 'nc' - to be revised 49 | Name.Constant: '#000000', # class: 'no' - to be revised 50 | Name.Decorator: '#888', # class: 'nd' - to be revised 51 | Name.Entity: '#ce5c00', # class: 'ni' 52 | Name.Exception: 'bold #cc0000', # class: 'ne' 53 | Name.Function: '#000000', # class: 'nf' 54 | Name.Property: '#000000', # class: 'py' 55 | Name.Label: '#f57900', # class: 'nl' 56 | Name.Namespace: '#000000', # class: 'nn' - to be revised 57 | Name.Other: '#000000', # class: 'nx' 58 | Name.Tag: 'bold #004461', # class: 'nt' - like a keyword 59 | Name.Variable: '#000000', # class: 'nv' - to be revised 60 | Name.Variable.Class: '#000000', # class: 'vc' - to be revised 61 | Name.Variable.Global: '#000000', # class: 'vg' - to be revised 62 | Name.Variable.Instance: '#000000', # class: 'vi' - to be revised 63 | Number: '#990000', # class: 'm' 64 | Literal: '#000000', # class: 'l' 65 | Literal.Date: '#000000', # class: 'ld' 66 | String: '#4e9a06', # class: 's' 67 | String.Backtick: '#4e9a06', # class: 'sb' 68 | String.Char: '#4e9a06', # class: 'sc' 69 | String.Doc: 'italic #8f5902', # class: 'sd' - like a comment 70 | String.Double: '#4e9a06', # class: 's2' 71 | String.Escape: '#4e9a06', # class: 'se' 72 | String.Heredoc: '#4e9a06', # class: 'sh' 73 | String.Interpol: '#4e9a06', # class: 'si' 74 | String.Other: '#4e9a06', # class: 'sx' 75 | String.Regex: '#4e9a06', # class: 'sr' 76 | String.Single: '#4e9a06', # class: 's1' 77 | String.Symbol: '#4e9a06', # class: 'ss' 78 | Generic: '#000000', # class: 'g' 79 | Generic.Deleted: '#a40000', # class: 'gd' 80 | Generic.Emph: 'italic #000000', # class: 'ge' 81 | Generic.Error: '#ef2929', # class: 'gr' 82 | Generic.Heading: 'bold #000080', # class: 'gh' 83 | Generic.Inserted: '#00A000', # class: 'gi' 84 | Generic.Output: '#888', # class: 'go' 85 | Generic.Prompt: '#745334', # class: 'gp' 86 | Generic.Strong: 'bold #000000', # class: 'gs' 87 | Generic.Subheading: 'bold #800080', # class: 'gu' 88 | Generic.Traceback: 'bold #a40000', # class: 'gt' 89 | } 90 | -------------------------------------------------------------------------------- /docs/_theme/wolph/layout.html: -------------------------------------------------------------------------------- 1 | {%- extends "basic/layout.html" %} 2 | {%- block extrahead %} 3 | {{ super() }} 4 | {% if theme_touch_icon %} 5 | 6 | {% endif %} 7 | 9 | {% endblock %} 10 | {%- block relbar2 %}{% endblock %} 11 | {%- block footer %} 12 | 16 | {%- endblock %} 17 | -------------------------------------------------------------------------------- /docs/_theme/wolph/relations.html: -------------------------------------------------------------------------------- 1 |

Related Topics

2 | 20 | -------------------------------------------------------------------------------- /docs/_theme/wolph/static/flasky.css_t: -------------------------------------------------------------------------------- 1 | /* 2 | * flasky.css_t 3 | * ~~~~~~~~~~~~ 4 | * 5 | * :copyright: Copyright 2010 by Armin Ronacher. Modifications by Kenneth Reitz. 6 | * :license: Flask Design License, see LICENSE for details. 7 | */ 8 | 9 | {% set page_width = '940px' %} 10 | {% set sidebar_width = '220px' %} 11 | 12 | @import url("basic.css"); 13 | 14 | /* -- page layout ----------------------------------------------------------- */ 15 | 16 | body { 17 | font-family: 'goudy old style', 'minion pro', 'bell mt', Georgia, 'Hiragino Mincho Pro'; 18 | font-size: 17px; 19 | background-color: white; 20 | color: #000; 21 | margin: 0; 22 | padding: 0; 23 | } 24 | 25 | div.document { 26 | width: {{ page_width }}; 27 | margin: 30px auto 0 auto; 28 | } 29 | 30 | div.documentwrapper { 31 | float: left; 32 | width: 100%; 33 | } 34 | 35 | div.bodywrapper { 36 | margin: 0 0 0 {{ sidebar_width }}; 37 | } 38 | 39 | div.sphinxsidebar { 40 | width: {{ sidebar_width }}; 41 | } 42 | 43 | hr { 44 | border: 1px solid #B1B4B6; 45 | } 46 | 47 | div.body { 48 | background-color: #ffffff; 49 | color: #3E4349; 50 | padding: 0 30px 0 30px; 51 | } 52 | 53 | img.floatingflask { 54 | padding: 0 0 10px 10px; 55 | float: right; 56 | } 57 | 58 | div.footer { 59 | width: {{ page_width }}; 60 | margin: 20px auto 30px auto; 61 | font-size: 14px; 62 | color: #888; 63 | text-align: right; 64 | } 65 | 66 | div.footer a { 67 | color: #888; 68 | } 69 | 70 | div.related { 71 | display: none; 72 | } 73 | 74 | div.sphinxsidebar a { 75 | color: #444; 76 | text-decoration: none; 77 | border-bottom: 1px dotted #999; 78 | } 79 | 80 | div.sphinxsidebar a:hover { 81 | border-bottom: 1px solid #999; 82 | } 83 | 84 | div.sphinxsidebar { 85 | font-size: 14px; 86 | line-height: 1.5; 87 | } 88 | 89 | div.sphinxsidebarwrapper { 90 | padding: 0px 10px; 91 | } 92 | 93 | div.sphinxsidebarwrapper p.logo { 94 | padding: 0 0 20px 0; 95 | margin: 0; 96 | text-align: center; 97 | } 98 | 99 | div.sphinxsidebar h3, 100 | div.sphinxsidebar h4 { 101 | font-family: 'Garamond', 'Georgia', serif; 102 | color: #555; 103 | font-size: 24px; 104 | font-weight: normal; 105 | margin: 0 0 5px 0; 106 | padding: 0; 107 | } 108 | 109 | div.sphinxsidebar h4 { 110 | font-size: 20px; 111 | } 112 | 113 | div.sphinxsidebar h3 a { 114 | color: #444; 115 | } 116 | 117 | div.sphinxsidebar p.logo a, 118 | div.sphinxsidebar h3 a, 119 | div.sphinxsidebar p.logo a:hover, 120 | div.sphinxsidebar h3 a:hover { 121 | border: none; 122 | } 123 | 124 | div.sphinxsidebar p { 125 | color: #555; 126 | margin: 10px 0; 127 | } 128 | 129 | div.sphinxsidebar ul { 130 | margin: 10px 0; 131 | padding: 0; 132 | color: #000; 133 | } 134 | 135 | div.sphinxsidebar input[type="text"] { 136 | width: 160px!important; 137 | } 138 | div.sphinxsidebar input { 139 | border: 1px solid #ccc; 140 | font-family: 'Georgia', serif; 141 | font-size: 1em; 142 | } 143 | 144 | /* -- body styles ----------------------------------------------------------- */ 145 | 146 | a { 147 | color: #004B6B; 148 | text-decoration: underline; 149 | } 150 | 151 | a:hover { 152 | color: #6D4100; 153 | text-decoration: underline; 154 | } 155 | 156 | div.body h1, 157 | div.body h2, 158 | div.body h3, 159 | div.body h4, 160 | div.body h5, 161 | div.body h6 { 162 | font-family: 'Garamond', 'Georgia', serif; 163 | font-weight: normal; 164 | margin: 30px 0px 10px 0px; 165 | padding: 0; 166 | } 167 | 168 | div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } 169 | div.body h2 { font-size: 180%; } 170 | div.body h3 { font-size: 150%; } 171 | div.body h4 { font-size: 130%; } 172 | div.body h5 { font-size: 100%; } 173 | div.body h6 { font-size: 100%; } 174 | 175 | a.headerlink { 176 | color: #ddd; 177 | padding: 0 4px; 178 | text-decoration: none; 179 | } 180 | 181 | a.headerlink:hover { 182 | color: #444; 183 | background: #eaeaea; 184 | } 185 | 186 | div.body p, div.body dd, div.body li { 187 | line-height: 1.4em; 188 | } 189 | 190 | div.admonition { 191 | background: #fafafa; 192 | margin: 20px -30px; 193 | padding: 10px 30px; 194 | border-top: 1px solid #ccc; 195 | border-bottom: 1px solid #ccc; 196 | } 197 | 198 | div.admonition tt.xref, div.admonition a tt { 199 | border-bottom: 1px solid #fafafa; 200 | } 201 | 202 | dd div.admonition { 203 | margin-left: -60px; 204 | padding-left: 60px; 205 | } 206 | 207 | div.admonition p.admonition-title { 208 | font-family: 'Garamond', 'Georgia', serif; 209 | font-weight: normal; 210 | font-size: 24px; 211 | margin: 0 0 10px 0; 212 | padding: 0; 213 | line-height: 1; 214 | } 215 | 216 | div.admonition p.last { 217 | margin-bottom: 0; 218 | } 219 | 220 | div.highlight { 221 | background-color: white; 222 | } 223 | 224 | dt:target, .highlight { 225 | background: #FAF3E8; 226 | } 227 | 228 | div.note { 229 | background-color: #eee; 230 | border: 1px solid #ccc; 231 | } 232 | 233 | div.seealso { 234 | background-color: #ffc; 235 | border: 1px solid #ff6; 236 | } 237 | 238 | div.topic { 239 | background-color: #eee; 240 | } 241 | 242 | p.admonition-title { 243 | display: inline; 244 | } 245 | 246 | p.admonition-title:after { 247 | content: ":"; 248 | } 249 | 250 | pre, tt { 251 | font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; 252 | font-size: 0.9em; 253 | } 254 | 255 | img.screenshot { 256 | } 257 | 258 | tt.descname, tt.descclassname { 259 | font-size: 0.95em; 260 | } 261 | 262 | tt.descname { 263 | padding-right: 0.08em; 264 | } 265 | 266 | img.screenshot { 267 | -moz-box-shadow: 2px 2px 4px #eee; 268 | -webkit-box-shadow: 2px 2px 4px #eee; 269 | box-shadow: 2px 2px 4px #eee; 270 | } 271 | 272 | table.docutils { 273 | border: 1px solid #888; 274 | -moz-box-shadow: 2px 2px 4px #eee; 275 | -webkit-box-shadow: 2px 2px 4px #eee; 276 | box-shadow: 2px 2px 4px #eee; 277 | } 278 | 279 | table.docutils td, table.docutils th { 280 | border: 1px solid #888; 281 | padding: 0.25em 0.7em; 282 | } 283 | 284 | table.field-list, table.footnote { 285 | border: none; 286 | -moz-box-shadow: none; 287 | -webkit-box-shadow: none; 288 | box-shadow: none; 289 | } 290 | 291 | table.footnote { 292 | margin: 15px 0; 293 | width: 100%; 294 | border: 1px solid #eee; 295 | background: #fdfdfd; 296 | font-size: 0.9em; 297 | } 298 | 299 | table.footnote + table.footnote { 300 | margin-top: -15px; 301 | border-top: none; 302 | } 303 | 304 | table.field-list th { 305 | padding: 0 0.8em 0 0; 306 | } 307 | 308 | table.field-list td { 309 | padding: 0; 310 | } 311 | 312 | table.footnote td.label { 313 | width: 0px; 314 | padding: 0.3em 0 0.3em 0.5em; 315 | } 316 | 317 | table.footnote td { 318 | padding: 0.3em 0.5em; 319 | } 320 | 321 | dl { 322 | margin: 0; 323 | padding: 0; 324 | } 325 | 326 | dl dd { 327 | margin-left: 30px; 328 | } 329 | 330 | blockquote { 331 | margin: 0 0 0 30px; 332 | padding: 0; 333 | } 334 | 335 | ul, ol { 336 | margin: 10px 0 10px 30px; 337 | padding: 0; 338 | } 339 | 340 | pre { 341 | background: #eee; 342 | padding: 7px 30px; 343 | margin: 15px -30px; 344 | line-height: 1.3em; 345 | } 346 | 347 | dl pre, blockquote pre, li pre { 348 | margin-left: -60px; 349 | padding-left: 60px; 350 | } 351 | 352 | dl dl pre { 353 | margin-left: -90px; 354 | padding-left: 90px; 355 | } 356 | 357 | tt { 358 | background-color: #ecf0f3; 359 | color: #222; 360 | /* padding: 1px 2px; */ 361 | } 362 | 363 | tt.xref, a tt { 364 | background-color: #FBFBFB; 365 | border-bottom: 1px solid white; 366 | } 367 | 368 | a.reference { 369 | text-decoration: none; 370 | border-bottom: 1px dotted #004B6B; 371 | } 372 | 373 | a.reference:hover { 374 | border-bottom: 1px solid #6D4100; 375 | } 376 | 377 | a.footnote-reference { 378 | text-decoration: none; 379 | font-size: 0.7em; 380 | vertical-align: top; 381 | border-bottom: 1px dotted #004B6B; 382 | } 383 | 384 | a.footnote-reference:hover { 385 | border-bottom: 1px solid #6D4100; 386 | } 387 | 388 | a:hover tt { 389 | background: #EEE; 390 | } 391 | 392 | 393 | /* scrollbars */ 394 | 395 | ::-webkit-scrollbar { 396 | width: 6px; 397 | height: 6px; 398 | } 399 | 400 | ::-webkit-scrollbar-button:start:decrement, 401 | ::-webkit-scrollbar-button:end:increment { 402 | display: block; 403 | height: 10px; 404 | } 405 | 406 | ::-webkit-scrollbar-button:vertical:increment { 407 | background-color: #fff; 408 | } 409 | 410 | ::-webkit-scrollbar-track-piece { 411 | background-color: #eee; 412 | -webkit-border-radius: 3px; 413 | } 414 | 415 | ::-webkit-scrollbar-thumb:vertical { 416 | height: 50px; 417 | background-color: #ccc; 418 | -webkit-border-radius: 3px; 419 | } 420 | 421 | ::-webkit-scrollbar-thumb:horizontal { 422 | width: 50px; 423 | background-color: #ccc; 424 | -webkit-border-radius: 3px; 425 | } 426 | 427 | /* misc. */ 428 | 429 | .revsys-inline { 430 | display: none!important; 431 | } 432 | -------------------------------------------------------------------------------- /docs/_theme/wolph/static/small_flask.css: -------------------------------------------------------------------------------- 1 | /* 2 | * small_flask.css_t 3 | * ~~~~~~~~~~~~~~~~~ 4 | * 5 | * :copyright: Copyright 2010 by Armin Ronacher. 6 | * :license: Flask Design License, see LICENSE for details. 7 | */ 8 | 9 | body { 10 | margin: 0; 11 | padding: 20px 30px; 12 | } 13 | 14 | div.documentwrapper { 15 | float: none; 16 | background: white; 17 | } 18 | 19 | div.sphinxsidebar { 20 | display: block; 21 | float: none; 22 | width: 102.5%; 23 | margin: 50px -30px -20px -30px; 24 | padding: 10px 20px; 25 | background: #333; 26 | color: white; 27 | } 28 | 29 | div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, 30 | div.sphinxsidebar h3 a { 31 | color: white; 32 | } 33 | 34 | div.sphinxsidebar a { 35 | color: #aaa; 36 | } 37 | 38 | div.sphinxsidebar p.logo { 39 | display: none; 40 | } 41 | 42 | div.document { 43 | width: 100%; 44 | margin: 0; 45 | } 46 | 47 | div.related { 48 | display: block; 49 | margin: 0; 50 | padding: 10px 0 20px 0; 51 | } 52 | 53 | div.related ul, 54 | div.related ul li { 55 | margin: 0; 56 | padding: 0; 57 | } 58 | 59 | div.footer { 60 | display: none; 61 | } 62 | 63 | div.bodywrapper { 64 | margin: 0; 65 | } 66 | 67 | div.body { 68 | min-height: 0; 69 | padding: 0; 70 | } 71 | -------------------------------------------------------------------------------- /docs/_theme/wolph/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = basic 3 | stylesheet = flasky.css 4 | pygments_style = flask_theme_support.FlaskyStyle 5 | 6 | [options] 7 | touch_icon = 8 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # 2 | # Documentation build configuration file, created by 3 | # sphinx-quickstart on Thu Feb 27 20:00:23 2014. 4 | # 5 | # This file is execfile()d with the current directory set to its 6 | # containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import datetime 15 | import os 16 | import sys 17 | 18 | try: 19 | import numpy as np 20 | 21 | assert np 22 | except ImportError: 23 | # From the readthedocs manual 24 | # http://read-the-docs.readthedocs.org/en/latest/faq.html?highlight=numpy 25 | from unittest import mock 26 | 27 | MOCK_MODULES = ['pygtk', 'gtk', 'gobject', 'argparse', 'numpy', 'pandas'] 28 | for mod_name in MOCK_MODULES: 29 | sys.modules[mod_name] = mock.Mock() 30 | 31 | # If extensions (or modules to document with autodoc) are in another directory, 32 | # add these directories to sys.path here. If the directory is relative to the 33 | # documentation root, use os.path.abspath to make it absolute, like shown here. 34 | sys.path.insert(0, os.path.abspath('..')) 35 | from stl import __about__ as metadata 36 | 37 | # -- General configuration ------------------------------------------------ 38 | 39 | # If your documentation needs a minimal Sphinx version, state it here. 40 | # needs_sphinx = '1.0' 41 | 42 | # Add any Sphinx extension module names here, as strings. They can be 43 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 44 | # ones. 45 | extensions = [ 46 | 'sphinx.ext.autodoc', 47 | 'sphinx.ext.doctest', 48 | 'sphinx.ext.intersphinx', 49 | 'sphinx.ext.todo', 50 | 'sphinx.ext.coverage', 51 | 'sphinx.ext.mathjax', 52 | 'sphinx.ext.ifconfig', 53 | 'sphinx.ext.viewcode', 54 | ] 55 | 56 | # Add any paths that contain templates here, relative to this directory. 57 | templates_path = ['_templates'] 58 | 59 | # The suffix of source filenames. 60 | source_suffix = '.rst' 61 | 62 | # The encoding of source files. 63 | # source_encoding = 'utf-8-sig' 64 | 65 | # The master toctree document. 66 | master_doc = 'index' 67 | 68 | # General information about the project. 69 | project = metadata.__package_name__.replace('-', ' ').capitalize() 70 | copyright = f'{datetime.date.today().year}, {metadata.__author__}' 71 | 72 | # The version info for the project you're documenting, acts as replacement for 73 | # |version| and |release|, also used in various other places throughout the 74 | # built documents. 75 | # 76 | # The short X.Y version. 77 | version = metadata.__version__ 78 | # The full version, including alpha/beta/rc tags. 79 | release = metadata.__version__ 80 | 81 | # The language for content autogenerated by Sphinx. Refer to documentation 82 | # for a list of supported languages. 83 | # language = None 84 | 85 | # There are two options for replacing |today|: either, you set today to some 86 | # non-false value, then it is used: 87 | # today = '' 88 | # Else, today_fmt is used as the format for a strftime call. 89 | # today_fmt = '%B %d, %Y' 90 | 91 | # List of patterns, relative to source directory, that match files and 92 | # directories to ignore when looking for source files. 93 | exclude_patterns = ['_build'] 94 | 95 | # The reST default role (used for this markup: `text`) to use for all 96 | # documents. 97 | # default_role = None 98 | 99 | # If true, '()' will be appended to :func: etc. cross-reference text. 100 | # add_function_parentheses = True 101 | 102 | # If true, the current module name will be prepended to all description 103 | # unit titles (such as .. function::). 104 | # add_module_names = True 105 | 106 | # If true, sectionauthor and moduleauthor directives will be shown in the 107 | # output. They are ignored by default. 108 | # show_authors = False 109 | 110 | # The name of the Pygments (syntax highlighting) style to use. 111 | pygments_style = 'sphinx' 112 | 113 | # A list of ignored prefixes for module index sorting. 114 | # modindex_common_prefix = [] 115 | 116 | # If true, keep warnings as 'system message' paragraphs in the built documents. 117 | # keep_warnings = False 118 | 119 | 120 | # -- Options for HTML output ---------------------------------------------- 121 | 122 | # The theme to use for HTML and HTML Help pages. See the documentation for 123 | # a list of builtin themes. 124 | html_theme = 'wolph' 125 | 126 | # Theme options are theme-specific and customize the look and feel of a theme 127 | # further. For a list of options available for each theme, see the 128 | # documentation. 129 | # html_theme_options = {} 130 | 131 | # Add any paths that contain custom themes here, relative to this directory. 132 | html_theme_path = ['_theme'] 133 | 134 | # The name for this set of Sphinx documents. If None, it defaults to 135 | # ' v documentation'. 136 | # html_title = None 137 | 138 | # A shorter title for the navigation bar. Default is the same as html_title. 139 | # html_short_title = None 140 | 141 | # The name of an image file (relative to this directory) to place at the top 142 | # of the sidebar. 143 | # html_logo = None 144 | 145 | # The name of an image file (within the static path) to use as favicon of the 146 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 147 | # pixels large. 148 | # html_favicon = None 149 | 150 | # Add any paths that contain custom static files (such as style sheets) here, 151 | # relative to this directory. They are copied after the builtin static files, 152 | # so a file named 'default.css' will overwrite the builtin 'default.css'. 153 | # html_static_path = ['_static'] 154 | 155 | # Add any extra paths that contain custom files (such as robots.txt or 156 | # .htaccess) here, relative to this directory. These files are copied 157 | # directly to the root of the documentation. 158 | # html_extra_path = [] 159 | 160 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 161 | # using the given strftime format. 162 | # html_last_updated_fmt = '%b %d, %Y' 163 | 164 | # If true, SmartyPants will be used to convert quotes and dashes to 165 | # typographically correct entities. 166 | # html_use_smartypants = True 167 | 168 | # Custom sidebar templates, maps document names to template names. 169 | # html_sidebars = {} 170 | 171 | # Additional templates that should be rendered to pages, maps page names to 172 | # template names. 173 | # html_additional_pages = {} 174 | 175 | # If false, no module index is generated. 176 | # html_domain_indices = True 177 | 178 | # If false, no index is generated. 179 | # html_use_index = True 180 | 181 | # If true, the index is split into individual pages for each letter. 182 | # html_split_index = False 183 | 184 | # If true, links to the reST sources are added to the pages. 185 | # html_show_sourcelink = True 186 | 187 | # If true, 'Created using Sphinx' is shown in the HTML footer. Default is True. 188 | # html_show_sphinx = True 189 | 190 | # If true, '(C) Copyright ...' is shown in the HTML footer. Default is True. 191 | # html_show_copyright = True 192 | 193 | # If true, an OpenSearch description file will be output, and all pages will 194 | # contain a tag referring to it. The value of this option must be the 195 | # base URL from which the finished HTML is served. 196 | # html_use_opensearch = '' 197 | 198 | # This is the file name suffix for HTML files (e.g. '.xhtml'). 199 | # html_file_suffix = None 200 | 201 | # Output file base name for HTML help builder. 202 | htmlhelp_basename = metadata.__package_name__ + '-doc' 203 | 204 | 205 | # -- Options for LaTeX output --------------------------------------------- 206 | 207 | latex_elements = { 208 | # The paper size ('letterpaper' or 'a4paper'). 209 | #'papersize': 'letterpaper', 210 | # The font size ('10pt', '11pt' or '12pt'). 211 | #'pointsize': '10pt', 212 | # Additional stuff for the LaTeX preamble. 213 | #'preamble': '', 214 | } 215 | 216 | # Grouping the document tree into LaTeX files. List of tuples (source start 217 | # file, target name, title, author, documentclass [howto/manual]). 218 | latex_documents = [ 219 | ( 220 | 'index', 221 | f'{metadata.__package_name__}.tex', 222 | '{} Documentation'.format( 223 | metadata.__package_name__.replace('-', ' ').capitalize() 224 | ), 225 | metadata.__author__, 226 | 'manual', 227 | ) 228 | ] 229 | 230 | # The name of an image file (relative to this directory) to place at the top of 231 | # the title page. 232 | # latex_logo = None 233 | 234 | # For 'manual' documents, if this is true, then toplevel headings are parts, 235 | # not chapters. 236 | # latex_use_parts = False 237 | 238 | # If true, show page references after internal links. 239 | # latex_show_pagerefs = False 240 | 241 | # If true, show URL addresses after external links. 242 | # latex_show_urls = False 243 | 244 | # Documents to append as an appendix to all manuals. 245 | # latex_appendices = [] 246 | 247 | # If false, no module index is generated. 248 | # latex_domain_indices = True 249 | 250 | 251 | # -- Options for manual page output --------------------------------------- 252 | 253 | # One entry per manual page. List of tuples 254 | # (source start file, name, description, authors, manual section). 255 | man_pages = [ 256 | ( 257 | 'index', 258 | metadata.__package_name__, 259 | '{} Documentation'.format( 260 | metadata.__package_name__.replace('-', ' ').capitalize() 261 | ), 262 | [metadata.__author__], 263 | 1, 264 | ) 265 | ] 266 | 267 | # If true, show URL addresses after external links. 268 | # man_show_urls = False 269 | 270 | 271 | # -- Options for Texinfo output ------------------------------------------- 272 | 273 | # Grouping the document tree into Texinfo files. List of tuples 274 | # (source start file, target name, title, author, 275 | # dir menu entry, description, category) 276 | texinfo_documents = [ 277 | ( 278 | 'index', 279 | metadata.__package_name__, 280 | '{} Documentation'.format( 281 | metadata.__package_name__.replace('-', ' ').capitalize() 282 | ), 283 | metadata.__author__, 284 | metadata.__package_name__, 285 | metadata.__description__, 286 | 'Miscellaneous', 287 | ) 288 | ] 289 | 290 | # Documents to append as an appendix to all manuals. 291 | # texinfo_appendices = [] 292 | 293 | # If false, no module index is generated. 294 | # texinfo_domain_indices = True 295 | 296 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 297 | # texinfo_show_urls = 'footnote' 298 | 299 | # If true, do not generate a @detailmenu in the 'Top' node's menu. 300 | # texinfo_no_detailmenu = False 301 | 302 | 303 | # -- Options for Epub output ---------------------------------------------- 304 | 305 | # Bibliographic Dublin Core info. 306 | epub_title = metadata.__package_name__.replace('-', ' ').capitalize() 307 | epub_author = metadata.__author__ 308 | epub_publisher = metadata.__author__ 309 | epub_copyright = copyright 310 | 311 | # The HTML theme for the epub output. Since the default themes are not 312 | # optimized for small screen space, using the same theme for HTML and epub 313 | # output is usually not wise. This defaults to 'epub', a theme designed to 314 | # save visual space. epub_theme = 'epub' 315 | 316 | # The language of the text. It defaults to the language option 317 | # or en if the language is not set. 318 | # epub_language = '' 319 | 320 | # The scheme of the identifier. Typical schemes are ISBN or URL. 321 | # epub_scheme = '' 322 | 323 | # The unique identifier of the text. This can be a ISBN number 324 | # or the project homepage. 325 | # epub_identifier = '' 326 | 327 | # A unique identification for the text. 328 | # epub_uid = '' 329 | 330 | # A tuple containing the cover image and cover page html template filenames. 331 | # epub_cover = () 332 | 333 | # A sequence of (type, uri, title) tuples for the guide element of content.opf. 334 | # epub_guide = () 335 | 336 | # HTML files that should be inserted before the pages created by sphinx. 337 | # The format is a list of tuples containing the path and title. 338 | # epub_pre_files = [] 339 | 340 | # HTML files shat should be inserted after the pages created by sphinx. 341 | # The format is a list of tuples containing the path and title. 342 | # epub_post_files = [] 343 | 344 | # A list of files that should not be packed into the epub file. 345 | epub_exclude_files = ['search.html'] 346 | 347 | # The depth of the table of contents in toc.ncx. 348 | # epub_tocdepth = 3 349 | 350 | # Allow duplicate toc entries. 351 | # epub_tocdup = True 352 | 353 | # Choose between 'default' and 'includehidden'. 354 | # epub_tocscope = 'default' 355 | 356 | # Fix unsupported image types using the PIL. 357 | # epub_fix_images = False 358 | 359 | # Scale large images. 360 | # epub_max_image_width = 0 361 | 362 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 363 | # epub_show_urls = 'inline' 364 | 365 | # If false, no index is generated. 366 | # epub_use_index = True 367 | 368 | 369 | # Example configuration for intersphinx: refer to the Python standard library. 370 | intersphinx_mapping = { 371 | 'python': ('http://docs.python.org/2', None), 372 | 'pythonutils': ('http://python-utils.readthedocs.org/en/latest/', None), 373 | 'numpy': ('http://docs.scipy.org/doc/numpy/', None), 374 | 'scipy': ('http://docs.scipy.org/doc/scipy/reference/', None), 375 | 'matplotlib': ('http://matplotlib.sourceforge.net/', None), 376 | } 377 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to numpy-stl's documentation! 2 | ======================================== 3 | 4 | Contents: 5 | 6 | .. toctree:: 7 | :maxdepth: 4 8 | 9 | usage 10 | tests 11 | stl 12 | 13 | Indices and tables 14 | ================== 15 | 16 | * :ref:`genindex` 17 | * :ref:`modindex` 18 | * :ref:`search` 19 | 20 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. linkcheck to check all external links for integrity 37 | echo. doctest to run all doctests embedded in the documentation if enabled 38 | goto end 39 | ) 40 | 41 | if "%1" == "clean" ( 42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 43 | del /q /s %BUILDDIR%\* 44 | goto end 45 | ) 46 | 47 | if "%1" == "html" ( 48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 49 | if errorlevel 1 exit /b 1 50 | echo. 51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 52 | goto end 53 | ) 54 | 55 | if "%1" == "dirhtml" ( 56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 57 | if errorlevel 1 exit /b 1 58 | echo. 59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 60 | goto end 61 | ) 62 | 63 | if "%1" == "singlehtml" ( 64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 68 | goto end 69 | ) 70 | 71 | if "%1" == "pickle" ( 72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished; now you can process the pickle files. 76 | goto end 77 | ) 78 | 79 | if "%1" == "json" ( 80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished; now you can process the JSON files. 84 | goto end 85 | ) 86 | 87 | if "%1" == "htmlhelp" ( 88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can run HTML Help Workshop with the ^ 92 | .hhp project file in %BUILDDIR%/htmlhelp. 93 | goto end 94 | ) 95 | 96 | if "%1" == "qthelp" ( 97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 101 | .qhcp project file in %BUILDDIR%/qthelp, like this: 102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\PythonUtils.qhcp 103 | echo.To view the help file: 104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\PythonUtils.ghc 105 | goto end 106 | ) 107 | 108 | if "%1" == "devhelp" ( 109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished. 113 | goto end 114 | ) 115 | 116 | if "%1" == "epub" ( 117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 121 | goto end 122 | ) 123 | 124 | if "%1" == "latex" ( 125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 129 | goto end 130 | ) 131 | 132 | if "%1" == "text" ( 133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The text files are in %BUILDDIR%/text. 137 | goto end 138 | ) 139 | 140 | if "%1" == "man" ( 141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 145 | goto end 146 | ) 147 | 148 | if "%1" == "texinfo" ( 149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 153 | goto end 154 | ) 155 | 156 | if "%1" == "gettext" ( 157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 161 | goto end 162 | ) 163 | 164 | if "%1" == "changes" ( 165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.The overview file is in %BUILDDIR%/changes. 169 | goto end 170 | ) 171 | 172 | if "%1" == "linkcheck" ( 173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 174 | if errorlevel 1 exit /b 1 175 | echo. 176 | echo.Link check complete; look for any errors in the above output ^ 177 | or in %BUILDDIR%/linkcheck/output.txt. 178 | goto end 179 | ) 180 | 181 | if "%1" == "doctest" ( 182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 183 | if errorlevel 1 exit /b 1 184 | echo. 185 | echo.Testing of doctests in the sources finished, look at the ^ 186 | results in %BUILDDIR%/doctest/output.txt. 187 | goto end 188 | ) 189 | 190 | :end 191 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | -e .[docs] 2 | -------------------------------------------------------------------------------- /docs/stl.rst: -------------------------------------------------------------------------------- 1 | stl package 2 | =========== 3 | 4 | stl.Mesh 5 | -------- 6 | 7 | .. autoclass:: stl.Mesh 8 | :members: 9 | :undoc-members: 10 | :inherited-members: 11 | :show-inheritance: 12 | 13 | stl.main module 14 | --------------- 15 | 16 | .. automodule:: stl.main 17 | :members: 18 | :undoc-members: 19 | :inherited-members: 20 | :show-inheritance: 21 | 22 | stl.base module 23 | --------------- 24 | 25 | .. automodule:: stl.base 26 | :members: 27 | :undoc-members: 28 | :inherited-members: 29 | :show-inheritance: 30 | 31 | stl.mesh module 32 | --------------- 33 | 34 | .. automodule:: stl.mesh 35 | :members: 36 | :undoc-members: 37 | :inherited-members: 38 | :show-inheritance: 39 | 40 | stl.stl module 41 | -------------- 42 | 43 | .. automodule:: stl.stl 44 | :members: 45 | :undoc-members: 46 | :inherited-members: 47 | :show-inheritance: 48 | 49 | -------------------------------------------------------------------------------- /docs/tests.rst: -------------------------------------------------------------------------------- 1 | tests and examples 2 | ================== 3 | 4 | tests.stl_corruption module 5 | --------------------------- 6 | 7 | .. literalinclude:: ../tests/stl_corruption.py 8 | 9 | tests.test_commandline module 10 | ----------------------------- 11 | 12 | .. literalinclude:: ../tests/test_commandline.py 13 | 14 | tests.test_convert module 15 | ------------------------- 16 | 17 | .. literalinclude:: ../tests/test_convert.py 18 | 19 | tests.test_mesh module 20 | ---------------------- 21 | 22 | .. literalinclude:: ../tests/test_mesh.py 23 | 24 | tests.test_multiple module 25 | -------------------------- 26 | 27 | .. literalinclude:: ../tests/test_multiple.py 28 | 29 | tests.test_rotate module 30 | ------------------------ 31 | 32 | .. literalinclude:: ../tests/test_rotate.py 33 | 34 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | 2 | .. include :: ../README.rst 3 | 4 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | 3 | doctest_optionflags = NORMALIZE_WHITESPACE 4 | 5 | python_files = 6 | stl/*.py 7 | tests/*.py 8 | 9 | addopts = 10 | --doctest-modules 11 | --cov stl 12 | --cov-report term-missing 13 | --cov-report html 14 | --no-cov-on-fail 15 | --ignore=build 16 | --basetemp=tmp 17 | -------------------------------------------------------------------------------- /ruff.toml: -------------------------------------------------------------------------------- 1 | # We keep the ruff configuration separate so it can easily be shared across 2 | # all projects 3 | 4 | target-version = 'py39' 5 | 6 | src = ['stl'] 7 | exclude = [ 8 | '.tox', 9 | # Ignore local test files/directories/old-stuff 10 | 'test.py', 11 | '*_old.py', 12 | ] 13 | 14 | line-length = 79 15 | 16 | [lint] 17 | ignore = [ 18 | 'A001', # Variable {name} is shadowing a Python builtin 19 | 'A002', # Argument {name} is shadowing a Python builtin 20 | 'A003', # Class attribute {name} is shadowing a Python builtin 21 | 'B023', # function-uses-loop-variable 22 | 'B024', # `FormatWidgetMixin` is an abstract base class, but it has no abstract methods 23 | 'D205', # blank-line-after-summary 24 | 'D212', # multi-line-summary-first-line 25 | 'RET505', # Unnecessary `else` after `return` statement 26 | 'TRY003', # Avoid specifying long messages outside the exception class 27 | 'RET507', # Unnecessary `elif` after `continue` statement 28 | 'C405', # Unnecessary {obj_type} literal (rewrite as a set literal) 29 | 'C406', # Unnecessary {obj_type} literal (rewrite as a dict literal) 30 | 'C408', # Unnecessary {obj_type} call (rewrite as a literal) 31 | 'SIM114', # Combine `if` branches using logical `or` operator 32 | 'RET506', # Unnecessary `else` after `raise` statement 33 | 'Q001', # Remove bad quotes 34 | 'Q002', # Remove bad quotes 35 | 'FA100', # Missing `from __future__ import annotations`, but uses `typing.Optional` 36 | 'COM812', # Missing trailing comma in a list 37 | 'ISC001', # String concatenation with implicit str conversion 38 | 'SIM108', # Ternary operators are not always more readable 39 | 'RUF100', # Unused noqa directives. Due to multiple Python versions, we need to keep them 40 | ] 41 | 42 | select = [ 43 | 'A', # flake8-builtins 44 | 'ASYNC', # flake8 async checker 45 | 'B', # flake8-bugbear 46 | 'C4', # flake8-comprehensions 47 | 'C90', # mccabe 48 | 'COM', # flake8-commas 49 | 50 | ## Require docstrings for all public methods, would be good to enable at some point 51 | # 'D', # pydocstyle 52 | 53 | 'E', # pycodestyle error ('W' for warning) 54 | 'F', # pyflakes 55 | 'FA', # flake8-future-annotations 56 | 'I', # isort 57 | 'ICN', # flake8-import-conventions 58 | 'INP', # flake8-no-pep420 59 | 'ISC', # flake8-implicit-str-concat 60 | 'N', # pep8-naming 61 | 'NPY', # NumPy-specific rules 62 | 'PERF', # perflint, 63 | 'PIE', # flake8-pie 64 | 'Q', # flake8-quotes 65 | 66 | 'RET', # flake8-return 67 | 'RUF', # Ruff-specific rules 68 | 'SIM', # flake8-simplify 69 | 'T20', # flake8-print 70 | 'TD', # flake8-todos 71 | 'TRY', # tryceratops 72 | 'UP', # pyupgrade 73 | ] 74 | 75 | [lint.per-file-ignores] 76 | 'tests/*' = ['SIM115', 'SIM117', 'T201', 'B007'] 77 | 'docs/*' = ['INP001', 'RUF012'] 78 | 79 | [lint.pydocstyle] 80 | convention = 'google' 81 | ignore-decorators = [ 82 | 'typing.overload', 83 | 'typing.override', 84 | ] 85 | 86 | [lint.isort] 87 | case-sensitive = true 88 | combine-as-imports = true 89 | force-wrap-aliases = true 90 | 91 | [lint.flake8-quotes] 92 | docstring-quotes = 'single' 93 | inline-quotes = 'single' 94 | multiline-quotes = 'single' 95 | 96 | [format] 97 | line-ending = 'lf' 98 | indent-style = 'space' 99 | quote-style = 'single' 100 | docstring-code-format = true 101 | skip-magic-trailing-comma = false 102 | exclude = [ 103 | '__init__.py', 104 | ] 105 | 106 | [lint.pycodestyle] 107 | max-line-length = 79 108 | 109 | [lint.flake8-pytest-style] 110 | mark-parentheses = true 111 | 112 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [build_sphinx] 2 | source-dir = docs/ 3 | build-dir = docs/_build 4 | all_files = 1 5 | 6 | [upload_sphinx] 7 | upload-dir = docs/_build/html 8 | 9 | [bdist_wheel] 10 | universal = 0 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import warnings 4 | 5 | from setuptools import extension, setup 6 | from setuptools.command.build_ext import build_ext 7 | 8 | setup_kwargs = {} 9 | 10 | 11 | def error(*lines): 12 | for _line in lines: 13 | pass 14 | 15 | 16 | try: 17 | from stl import stl 18 | 19 | if not hasattr(stl, 'BaseStl'): 20 | error( 21 | 'ERROR', 22 | 'You have an incompatible stl package installed' 23 | 'Please run "pip uninstall -y stl" first', 24 | ) 25 | sys.exit(1) 26 | except ImportError: 27 | pass 28 | 29 | 30 | try: 31 | import numpy as np 32 | from Cython import Build 33 | 34 | setup_kwargs['ext_modules'] = Build.cythonize( 35 | [ 36 | extension.Extension( 37 | 'stl._speedups', 38 | ['stl/_speedups.pyx'], 39 | include_dirs=[np.get_include()], 40 | ), 41 | ] 42 | ) 43 | except ImportError: 44 | error( 45 | 'WARNING', 46 | 'Cython and Numpy is required for building extension.', 47 | 'Falling back to pure Python implementation.', 48 | ) 49 | 50 | # To prevent importing about and thereby breaking the coverage info we use this 51 | # exec hack 52 | about = {} 53 | with open('stl/__about__.py') as fh: 54 | exec(fh.read(), about) 55 | 56 | 57 | if os.path.isfile('README.rst'): 58 | with open('README.rst') as fh: 59 | long_description = fh.read() 60 | else: 61 | long_description = 'See http://pypi.python.org/pypi/{}/'.format( 62 | about['__package_name__'] 63 | ) 64 | 65 | install_requires = [ 66 | 'numpy', 67 | 'python-utils>=3.4.5', 68 | ] 69 | 70 | 71 | tests_require = ['pytest'] 72 | 73 | 74 | class BuildExt(build_ext): 75 | def run(self): 76 | try: 77 | build_ext.run(self) 78 | except Exception as e: 79 | warnings.warn( 80 | f""" 81 | Unable to build speedups module, defaulting to pure Python. Note 82 | that the pure Python version is more than fast enough in most cases 83 | {e!r} 84 | """, 85 | stacklevel=2, 86 | ) 87 | 88 | 89 | if __name__ == '__main__': 90 | setup( 91 | python_requires='>=3.9.0', 92 | name=about['__package_name__'], 93 | version=about['__version__'], 94 | author=about['__author__'], 95 | author_email=about['__author_email__'], 96 | description=about['__description__'], 97 | url=about['__url__'], 98 | license='BSD', 99 | packages=['stl'], 100 | package_data={about['__import_name__']: ['py.typed']}, 101 | long_description=long_description, 102 | tests_require=tests_require, 103 | entry_points={ 104 | 'console_scripts': [ 105 | 'stl = {}.main:main'.format(about['__import_name__']), 106 | 'stl2ascii = {}.main:to_ascii'.format( 107 | about['__import_name__'] 108 | ), 109 | 'stl2bin = {}.main:to_binary'.format(about['__import_name__']), 110 | ], 111 | }, 112 | classifiers=[ 113 | 'Development Status :: 6 - Mature', 114 | 'Intended Audience :: Developers', 115 | 'License :: OSI Approved :: BSD License', 116 | 'Operating System :: OS Independent', 117 | 'Natural Language :: English', 118 | 'Programming Language :: Python', 119 | 'Programming Language :: Python :: 3', 120 | 'Programming Language :: Python :: 3.9', 121 | 'Programming Language :: Python :: 3.10', 122 | 'Programming Language :: Python :: 3.11', 123 | 'Programming Language :: Python :: 3.12', 124 | 'Programming Language :: Python :: 3.13', 125 | 'Topic :: Software Development :: Libraries :: Python Modules', 126 | ], 127 | install_requires=install_requires, 128 | cmdclass=dict( 129 | build_ext=BuildExt, 130 | ), 131 | extras_require={ 132 | 'docs': [ 133 | 'mock', 134 | 'sphinx', 135 | 'python-utils', 136 | ], 137 | 'tests': [ 138 | 'cov-core', 139 | 'coverage', 140 | 'docutils', 141 | 'execnet', 142 | 'numpy', 143 | 'cython', 144 | 'pep8', 145 | 'py', 146 | 'pyflakes', 147 | 'pytest', 148 | 'pytest-cache', 149 | 'pytest-cov', 150 | 'python-utils', 151 | 'Sphinx', 152 | 'flake8', 153 | 'wheel', 154 | ], 155 | }, 156 | **setup_kwargs, 157 | ) 158 | -------------------------------------------------------------------------------- /stl/__about__.py: -------------------------------------------------------------------------------- 1 | __package_name__ = 'numpy-stl' 2 | __import_name__ = 'stl' 3 | __version__ = '3.2.0' 4 | __author__ = 'Rick van Hattem' 5 | __author_email__ = 'Wolph@Wol.ph' 6 | __description__ = ' '.join( 7 | """ 8 | Library to make reading, writing and modifying both binary and ascii STL files 9 | easy. 10 | """.split() 11 | ) 12 | __url__ = 'https://github.com/WoLpH/numpy-stl/' 13 | -------------------------------------------------------------------------------- /stl/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import Dimension, RemoveDuplicates 2 | from .mesh import Mesh 3 | from .stl import BUFFER_SIZE, COUNT_SIZE, HEADER_SIZE, MAX_COUNT, Mode 4 | 5 | __all__ = [ 6 | 'BUFFER_SIZE', 7 | 'HEADER_SIZE', 8 | 'COUNT_SIZE', 9 | 'MAX_COUNT', 10 | 'Mode', 11 | 'Dimension', 12 | 'RemoveDuplicates', 13 | 'Mesh', 14 | ] 15 | -------------------------------------------------------------------------------- /stl/_speedups.pyx: -------------------------------------------------------------------------------- 1 | # cython: language_level=2 2 | from libc.stdio cimport * 3 | from libc.string cimport memcpy, strcmp, strstr, strcpy 4 | 5 | IF UNAME_SYSNAME == 'Windows': 6 | cdef extern from 'io.h': 7 | int dup(int fd) 8 | ELSE: 9 | cdef extern from 'unistd.h': 10 | int dup(int fd) 11 | 12 | IF UNAME_SYSNAME == 'Linux': 13 | cdef extern from 'locale.h': 14 | ctypedef struct locale_t: 15 | pass 16 | 17 | locale_t uselocale(locale_t __dataset) 18 | locale_t newlocale(int __category_mask, const char *__locale, 19 | locale_t __base) 20 | void freelocale(locale_t __dataset) 21 | 22 | enum: LC_NUMERIC_MASK 23 | 24 | import numpy as np 25 | cimport numpy as np 26 | 27 | np.import_array() 28 | 29 | cdef packed struct Facet: 30 | np.float32_t n[3] 31 | np.float32_t v[3][3] 32 | np.uint16_t attr 33 | 34 | dtype = np.dtype([ 35 | ('normals', np.float32, 3), 36 | ('vectors', np.float32, (3, 3)), 37 | ('attr', np.uint16, (1,)), 38 | ]) 39 | 40 | DEF ALLOC_SIZE = 200000 41 | DEF BUF_SIZE = 8192 42 | DEF LINE_SIZE = 8192 43 | 44 | cdef struct s_State: 45 | FILE* fp 46 | char buf[BUF_SIZE] 47 | char line[LINE_SIZE] 48 | size_t pos 49 | size_t size 50 | size_t line_num 51 | int recoverable 52 | 53 | ctypedef s_State State 54 | 55 | cdef char* readline(State* state) except NULL: 56 | 57 | cdef size_t line_pos = 0 58 | cdef char current; 59 | while True: 60 | if state.pos == state.size: 61 | if feof(state.fp): 62 | if line_pos != 0: 63 | state.line[line_pos] = '\0' 64 | return state.line 65 | raise RuntimeError(state.recoverable, 'Unexpected EOF') 66 | 67 | state.size = fread(state.buf, 1, BUF_SIZE, state.fp) 68 | state.pos = 0 69 | state.recoverable = 0 70 | 71 | if line_pos == LINE_SIZE: 72 | raise RuntimeError( 73 | state.recoverable, 'Line longer than %d, probably non-ascii' % 74 | LINE_SIZE) 75 | 76 | current = state.buf[state.pos] 77 | state.pos += 1 78 | 79 | if line_pos != 0 or (current != ' ' \ 80 | and current != '\t' \ 81 | and current != '\r'): 82 | if current == '\n': 83 | state.line_num += 1 84 | if line_pos != 0: 85 | state.line[line_pos] = '\0' 86 | return state.line 87 | elif 0x40 < current < 0x5b: 88 | # Change all ascii characters to lower case 89 | state.line[line_pos] = current | 0x60 90 | line_pos += 1 91 | else: 92 | state.line[line_pos] = current 93 | line_pos += 1 94 | 95 | 96 | def ascii_read(fh, buf): 97 | cdef char* line 98 | cdef char name[LINE_SIZE] 99 | cdef np.ndarray[Facet, cast=True] arr = np.zeros(ALLOC_SIZE, dtype = dtype) 100 | cdef size_t offset; 101 | cdef Facet* facet = arr.data 102 | cdef size_t pos = 0 103 | cdef State state 104 | 105 | 106 | IF UNAME_SYSNAME == 'Linux': 107 | cdef locale_t new_locale = newlocale(LC_NUMERIC_MASK, 'C', 108 | NULL) 109 | cdef locale_t old_locale = uselocale(new_locale) 110 | 111 | try: 112 | state.size = len(buf) 113 | memcpy(state.buf, buf, state.size) 114 | state.pos = 0 115 | state.line_num = 0 116 | state.recoverable = 1 117 | state.fp = fdopen(dup(fh.fileno()), 'rb') 118 | fseek(state.fp, fh.tell(), SEEK_SET) 119 | 120 | line = readline(&state) 121 | 122 | if strstr(line, 'solid') != line: 123 | raise RuntimeError(state.recoverable, 124 | 'Solid name not found (%i:%s)' % (state.line_num, line)) 125 | 126 | strcpy(name, line+5) 127 | 128 | while True: 129 | 130 | line = readline(&state) 131 | line = state.line 132 | 133 | if strstr(line, 'endsolid') != NULL \ 134 | or strstr(line, 'end solid') != NULL: 135 | arr.resize(facet - arr.data, refcheck=False) 136 | return (name).strip(), arr 137 | 138 | if strcmp(line, 'color') == 0: 139 | readline(&state) 140 | continue 141 | elif sscanf(line, '%*s %*s %e %e %e', 142 | facet.n, facet.n+1, facet.n+2) != 3: 143 | raise RuntimeError(state.recoverable, 144 | 'Cannot read normals (%i:%s)' % (state.line_num, line)) 145 | 146 | readline(&state) # outer loop 147 | 148 | for i in range(3): 149 | line = readline(&state) 150 | if sscanf(line, '%*s %e %e %e', 151 | facet.v[i], facet.v[i]+1, facet.v[i]+2) != 3: 152 | raise RuntimeError(state.recoverable, 153 | 'Cannot read vertex (%i:%s)' % (state.line_num, line)) 154 | 155 | readline(&state) # endloop 156 | readline(&state) # endfacet 157 | 158 | facet += 1 159 | offset = facet - arr.data 160 | if arr.shape[0] == offset: 161 | arr.resize(arr.shape[0] + ALLOC_SIZE, refcheck=False) 162 | facet = arr.data + offset 163 | 164 | finally: 165 | if state.recoverable == 0: 166 | pos = ftell(state.fp) - state.size + state.pos 167 | fclose(state.fp) 168 | fh.seek(pos, SEEK_SET) 169 | 170 | IF UNAME_SYSNAME == 'Linux': 171 | uselocale(old_locale) 172 | freelocale(new_locale) 173 | 174 | 175 | def ascii_write(fh, name, np.ndarray[Facet, mode = 'c', cast=True] arr): 176 | cdef FILE* fp 177 | cdef Facet* facet = arr.data 178 | cdef Facet* end = arr.data + arr.shape[0] 179 | cdef size_t pos = 0 180 | 181 | try: 182 | fp = fdopen(dup(fh.fileno()), 'wb') 183 | fseek(fp, fh.tell(), SEEK_SET) 184 | fprintf(fp, 'solid %s\n', name) 185 | while facet != end: 186 | fprintf(fp, 187 | 'facet normal %f %f %f\n' 188 | ' outer loop\n' 189 | ' vertex %f %f %f\n' 190 | ' vertex %f %f %f\n' 191 | ' vertex %f %f %f\n' 192 | ' endloop\n' 193 | 'endfacet\n', 194 | facet.n[0], facet.n[1], facet.n[2], 195 | facet.v[0][0], facet.v[0][1], facet.v[0][2], 196 | facet.v[1][0], facet.v[1][1], facet.v[1][2], 197 | facet.v[2][0], facet.v[2][1], facet.v[2][2]) 198 | facet += 1 199 | fprintf(fp, 'endsolid %s\n', name) 200 | finally: 201 | pos = ftell(fp) 202 | fclose(fp) 203 | fh.seek(pos, SEEK_SET) 204 | 205 | -------------------------------------------------------------------------------- /stl/main.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import random 3 | import sys 4 | 5 | from . import stl 6 | 7 | 8 | def _get_parser(description): 9 | parser = argparse.ArgumentParser(description=description) 10 | parser.add_argument( 11 | 'infile', 12 | nargs='?', 13 | type=argparse.FileType('rb'), 14 | default=sys.stdin, 15 | help='STL file to read', 16 | ) 17 | parser.add_argument( 18 | 'outfile', 19 | nargs='?', 20 | type=argparse.FileType('wb'), 21 | default=sys.stdout, 22 | help='STL file to write', 23 | ) 24 | parser.add_argument('--name', nargs='?', help='Name of the mesh') 25 | parser.add_argument( 26 | '-n', 27 | '--use-file-normals', 28 | action='store_true', 29 | help='Read the normals from the file instead of recalculating them', 30 | ) 31 | parser.add_argument( 32 | '-r', 33 | '--remove-empty-areas', 34 | action='store_true', 35 | help='Remove areas with 0 surface areas to prevent errors during ' 36 | 'normal calculation', 37 | ) 38 | parser.add_argument( 39 | '-s', 40 | '--disable-speedups', 41 | action='store_true', 42 | help='Disable Cython speedups', 43 | ) 44 | return parser 45 | 46 | 47 | def _get_name(args): 48 | names = [ 49 | args.name, 50 | getattr(args.outfile, 'name', None), 51 | getattr(args.infile, 'name', None), 52 | 'numpy-stl-%06d' % random.randint(0, 1_000_000), 53 | ] 54 | 55 | for name in names: # pragma: no branch 56 | if not isinstance(name, str): 57 | continue 58 | elif name.startswith('<'): # pragma: no cover 59 | continue 60 | elif r'\AppData\Local\Temp' in name: # pragma: no cover 61 | # Windows temp file 62 | continue 63 | else: 64 | return name 65 | return None # pragma: no cover 66 | 67 | 68 | def main(): 69 | parser = _get_parser('Convert STL files from ascii to binary and back') 70 | parser.add_argument( 71 | '-a', 72 | '--ascii', 73 | action='store_true', 74 | help='Write ASCII file (default is binary)', 75 | ) 76 | parser.add_argument( 77 | '-b', 78 | '--binary', 79 | action='store_true', 80 | help='Force binary file (for TTYs)', 81 | ) 82 | 83 | args = parser.parse_args() 84 | name = _get_name(args) 85 | stl_file = stl.StlMesh( 86 | filename=name, 87 | fh=args.infile, 88 | calculate_normals=False, 89 | remove_empty_areas=args.remove_empty_areas, 90 | speedups=not args.disable_speedups, 91 | ) 92 | 93 | if args.binary: 94 | mode = stl.BINARY 95 | elif args.ascii: 96 | mode = stl.ASCII 97 | else: 98 | mode = stl.AUTOMATIC 99 | 100 | stl_file.save( 101 | name, args.outfile, mode=mode, update_normals=not args.use_file_normals 102 | ) 103 | 104 | 105 | def to_ascii(): 106 | parser = _get_parser('Convert STL files to ASCII (text) format') 107 | args = parser.parse_args() 108 | name = _get_name(args) 109 | stl_file = stl.StlMesh( 110 | filename=name, 111 | fh=args.infile, 112 | calculate_normals=False, 113 | remove_empty_areas=args.remove_empty_areas, 114 | speedups=not args.disable_speedups, 115 | ) 116 | stl_file.save( 117 | name, 118 | args.outfile, 119 | mode=stl.ASCII, 120 | update_normals=not args.use_file_normals, 121 | ) 122 | 123 | 124 | def to_binary(): 125 | parser = _get_parser('Convert STL files to binary format') 126 | args = parser.parse_args() 127 | name = _get_name(args) 128 | stl_file = stl.StlMesh( 129 | filename=name, 130 | fh=args.infile, 131 | calculate_normals=False, 132 | remove_empty_areas=args.remove_empty_areas, 133 | speedups=not args.disable_speedups, 134 | ) 135 | stl_file.save( 136 | name, 137 | args.outfile, 138 | mode=stl.BINARY, 139 | update_normals=not args.use_file_normals, 140 | ) 141 | -------------------------------------------------------------------------------- /stl/mesh.py: -------------------------------------------------------------------------------- 1 | from . import stl 2 | 3 | 4 | class Mesh(stl.BaseStl): 5 | pass 6 | -------------------------------------------------------------------------------- /stl/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolph/numpy-stl/8ad2b16f8a0e611fce8259befb15c7601836e7f0/stl/py.typed -------------------------------------------------------------------------------- /stl/stl.py: -------------------------------------------------------------------------------- 1 | # type: ignore[reportAttributeAccessIssue] 2 | import datetime 3 | import enum 4 | import io 5 | import os 6 | import struct 7 | import zipfile 8 | from xml.etree import ElementTree as ET 9 | 10 | import numpy as np 11 | 12 | from . import ( 13 | __about__ as metadata, 14 | base, 15 | ) 16 | from .utils import b 17 | 18 | try: 19 | from . import _speedups 20 | except ImportError: # pragma: no cover 21 | _speedups = None 22 | 23 | 24 | class Mode(enum.IntEnum): 25 | #: Automatically detect whether the output is a TTY, if so, write ASCII 26 | #: otherwise write BINARY 27 | AUTOMATIC = 0 28 | #: Force writing ASCII 29 | ASCII = 1 30 | #: Force writing BINARY 31 | BINARY = 2 32 | 33 | 34 | # For backwards compatibility, leave the original references 35 | AUTOMATIC = Mode.AUTOMATIC 36 | ASCII = Mode.ASCII 37 | BINARY = Mode.BINARY 38 | 39 | #: Amount of bytes to read while using buffered reading 40 | BUFFER_SIZE = 4096 41 | #: The amount of bytes in the header field 42 | HEADER_SIZE = 80 43 | #: The amount of bytes in the count field 44 | COUNT_SIZE = 4 45 | #: The maximum amount of triangles we can read from binary files 46 | MAX_COUNT = 1e8 47 | #: The header format, can be safely monkeypatched. Limited to 80 characters 48 | HEADER_FORMAT = '{package_name} ({version}) {now} {name}' 49 | 50 | 51 | class BaseStl(base.BaseMesh): 52 | @classmethod 53 | def load(cls, fh, mode=AUTOMATIC, speedups=True): 54 | """Load Mesh from STL file 55 | 56 | Automatically detects binary versus ascii STL files. 57 | 58 | :param file fh: The file handle to open 59 | :param int mode: Automatically detect the filetype or force binary 60 | """ 61 | header = fh.read(HEADER_SIZE) 62 | if not header: 63 | return None 64 | 65 | if isinstance(header, str): # pragma: no branch 66 | header = b(header) 67 | 68 | if mode is AUTOMATIC: 69 | if header.lstrip().lower().startswith(b'solid'): 70 | try: 71 | name, data = cls._load_ascii(fh, header, speedups=speedups) 72 | except RuntimeError as exception: 73 | (recoverable, e) = exception.args 74 | # If we didn't read beyond the header the stream is still 75 | # readable through the binary reader 76 | if recoverable: 77 | name, data = cls._load_binary( 78 | fh, header, check_size=False 79 | ) 80 | else: 81 | # Apparently we've read beyond the header. Let's try 82 | # seeking :) 83 | # Note that this fails when reading from stdin, we 84 | # can't recover from that. 85 | fh.seek(HEADER_SIZE) 86 | 87 | # Since we know this is a seekable file now and we're 88 | # not 100% certain it's binary, check the size while 89 | # reading 90 | name, data = cls._load_binary( 91 | fh, header, check_size=True 92 | ) 93 | else: 94 | name, data = cls._load_binary(fh, header) 95 | elif mode is ASCII: 96 | name, data = cls._load_ascii(fh, header, speedups=speedups) 97 | else: 98 | name, data = cls._load_binary(fh, header) 99 | 100 | return name, data 101 | 102 | @classmethod 103 | def _load_binary(cls, fh, header, check_size=False): 104 | # Read the triangle count 105 | count_data = fh.read(COUNT_SIZE) 106 | if len(count_data) != COUNT_SIZE: 107 | count = 0 108 | else: 109 | (count,) = struct.unpack(' 0: 179 | position = fh.tell() 180 | fh.seek(position - size_unprocessedlines) 181 | raise StopIteration() 182 | else: 183 | raise RuntimeError( 184 | recoverable[0], 185 | f'{line!r} should start with {prefix!r}', 186 | ) 187 | 188 | if len(values) == 3: 189 | return [float(v) for v in values] 190 | else: # pragma: no cover 191 | raise RuntimeError( 192 | recoverable[0], f'Incorrect value {line!r}' 193 | ) 194 | else: 195 | return b(raw_line) 196 | 197 | line = get() 198 | if not lines: 199 | raise RuntimeError( 200 | recoverable[0], 'No lines found, impossible to read' 201 | ) 202 | 203 | # Yield the name 204 | yield line[5:].strip() 205 | 206 | while True: 207 | # Read from the header lines first, until that point we can recover 208 | # and go to the binary option. After that we cannot due to 209 | # unseekable files such as sys.stdin 210 | # 211 | # Numpy doesn't support any non-file types so wrapping with a 212 | # buffer and/or StringIO does not work. 213 | try: 214 | normals = get('facet normal') 215 | assert get().lower() == b('outer loop') 216 | v0 = get('vertex') 217 | v1 = get('vertex') 218 | v2 = get('vertex') 219 | assert get().lower() == b('endloop') 220 | assert get().lower() == b('endfacet') 221 | attrs = 0 222 | yield (normals, (v0, v1, v2), attrs) 223 | except AssertionError as e: # pragma: no cover # noqa: PERF203 224 | raise RuntimeError(recoverable[0], e) from e 225 | except StopIteration: 226 | return 227 | 228 | @classmethod 229 | def _load_ascii(cls, fh, header, speedups=True): 230 | # Speedups does not support non file-based streams 231 | try: 232 | fh.fileno() 233 | except io.UnsupportedOperation: 234 | speedups = False 235 | # The speedups module is covered by travis but it can't be tested in 236 | # all environments, this makes coverage checks easier 237 | if _speedups and speedups: # pragma: no cover 238 | return _speedups.ascii_read(fh, header) 239 | else: 240 | iterator = cls._ascii_reader(fh, header) 241 | name = next(iterator) 242 | return name, np.fromiter(iterator, dtype=cls.dtype) 243 | 244 | def save(self, filename, fh=None, mode=AUTOMATIC, update_normals=True): 245 | # noqa: C901 246 | """Save the STL to a (binary) file 247 | 248 | If mode is :py:data:`AUTOMATIC` an :py:data:`ASCII` file will be 249 | written if the output is a TTY and a :py:data:`BINARY` file otherwise. 250 | 251 | :param str filename: The file to load 252 | :param file fh: The file handle to open 253 | :param int mode: The mode to write, default is :py:data:`AUTOMATIC`. 254 | :param bool update_normals: Whether to update the normals 255 | """ 256 | assert filename, 'Filename is required for the STL headers' 257 | if update_normals: 258 | self.update_normals() 259 | 260 | if mode is AUTOMATIC: 261 | # Try to determine if the file is a TTY. 262 | if fh: 263 | try: 264 | if os.isatty(fh.fileno()): # pragma: no cover 265 | write = self._write_ascii 266 | else: 267 | write = self._write_binary 268 | except OSError: 269 | # If TTY checking fails then it's an io.BytesIO() (or one 270 | # of its siblings from io). Assume binary. 271 | write = self._write_binary 272 | else: 273 | write = self._write_binary 274 | elif mode is BINARY: 275 | write = self._write_binary 276 | elif mode is ASCII: 277 | write = self._write_ascii 278 | else: 279 | raise ValueError(f'Mode {mode!r} is invalid') 280 | 281 | if isinstance(fh, io.TextIOBase): 282 | # Provide a more helpful error if the user mistakenly 283 | # assumes ASCII files should be text files. 284 | raise TypeError( 285 | 'File handles should be in binary mode - even when' 286 | ' writing an ASCII STL.' 287 | ) 288 | 289 | name = self.name 290 | if not name: 291 | name = os.path.split(filename)[-1] 292 | 293 | try: 294 | if fh: 295 | write(fh, name) 296 | else: 297 | with open(filename, 'wb') as fh: 298 | write(fh, name) 299 | except OSError: # pragma: no cover 300 | pass 301 | 302 | def _write_ascii(self, fh, name): 303 | try: 304 | fh.fileno() 305 | speedups = self.speedups 306 | except io.UnsupportedOperation: 307 | speedups = False 308 | 309 | if _speedups and speedups: # pragma: no cover 310 | _speedups.ascii_write(fh, b(name), self.data) 311 | else: 312 | 313 | def p(s, file): 314 | file.write(b(s) + b'\n') 315 | 316 | p(b'solid ' + b(name), file=fh) 317 | 318 | for row in self.data: 319 | # Explicitly convert each component to standard float for 320 | # normals and vertices to be compatible with numpy 2.x 321 | normals = tuple(float(n) for n in row['normals']) 322 | vectors = row['vectors'] 323 | p('facet normal {:f} {:f} {:f}'.format(*normals), file=fh) 324 | p(' outer loop', file=fh) 325 | p( 326 | ' vertex {:f} {:f} {:f}'.format( 327 | *tuple(float(v) for v in vectors[0]) 328 | ), 329 | file=fh, 330 | ) 331 | p( 332 | ' vertex {:f} {:f} {:f}'.format( 333 | *tuple(float(v) for v in vectors[1]) 334 | ), 335 | file=fh, 336 | ) 337 | p( 338 | ' vertex {:f} {:f} {:f}'.format( 339 | *tuple(float(v) for v in vectors[2]) 340 | ), 341 | file=fh, 342 | ) 343 | p(' endloop', file=fh) 344 | p('endfacet', file=fh) 345 | 346 | p(b'endsolid ' + b(name), file=fh) 347 | 348 | def get_header(self, name): 349 | # Format the header 350 | header = HEADER_FORMAT.format( 351 | package_name=metadata.__package_name__, 352 | version=metadata.__version__, 353 | now=datetime.datetime.now(), 354 | name=name, 355 | ) 356 | 357 | # Make it exactly 80 characters 358 | return header[:80].ljust(80, ' ') 359 | 360 | def _write_binary(self, fh, name): 361 | header = self.get_header(name) 362 | packed = struct.pack(' 84, ( 384 | 'numpy silently refused to write our file. Note that writing ' 385 | 'to `StringIO` objects is not supported by `numpy`' 386 | ) 387 | 388 | @classmethod 389 | def from_file( 390 | cls, 391 | filename, 392 | calculate_normals=True, 393 | fh=None, 394 | mode=Mode.AUTOMATIC, 395 | speedups=True, 396 | **kwargs, 397 | ): 398 | """Load a mesh from a STL file 399 | 400 | :param str filename: The file to load 401 | :param bool calculate_normals: Whether to update the normals 402 | :param file fh: The file handle to open 403 | :param dict kwargs: The same as for :py:class:`stl.mesh.Mesh` 404 | 405 | """ 406 | if fh: 407 | name, data = cls.load(fh, mode=mode, speedups=speedups) 408 | else: 409 | with open(filename, 'rb') as fh: 410 | name, data = cls.load(fh, mode=mode, speedups=speedups) 411 | 412 | return cls( 413 | data, calculate_normals, name=name, speedups=speedups, **kwargs 414 | ) 415 | 416 | @classmethod 417 | def from_multi_file( 418 | cls, 419 | filename, 420 | calculate_normals=True, 421 | fh=None, 422 | mode=Mode.AUTOMATIC, 423 | speedups=True, 424 | **kwargs, 425 | ): 426 | """Load multiple meshes from a STL file 427 | 428 | Note: mode is hardcoded to ascii since binary stl files do not support 429 | the multi format 430 | 431 | :param str filename: The file to load 432 | :param bool calculate_normals: Whether to update the normals 433 | :param file fh: The file handle to open 434 | :param dict kwargs: The same as for :py:class:`stl.mesh.Mesh` 435 | """ 436 | if fh: 437 | close = False 438 | else: 439 | fh = open(filename, 'rb') # noqa: SIM115 440 | close = True 441 | 442 | try: 443 | raw_data = cls.load(fh, mode=mode, speedups=speedups) 444 | while raw_data: 445 | name, data = raw_data 446 | yield cls( 447 | data, 448 | calculate_normals, 449 | name=name, 450 | speedups=speedups, 451 | **kwargs, 452 | ) 453 | raw_data = cls.load(fh, mode=ASCII, speedups=speedups) 454 | 455 | finally: 456 | if close: 457 | fh.close() 458 | 459 | @classmethod 460 | def from_files( 461 | cls, 462 | filenames, 463 | calculate_normals=True, 464 | mode=Mode.AUTOMATIC, 465 | speedups=True, 466 | **kwargs, 467 | ): 468 | """Load multiple meshes from STL files into a single mesh 469 | 470 | Note: mode is hardcoded to ascii since binary stl files do not support 471 | the multi format 472 | 473 | :param list(str) filenames: The files to load 474 | :param bool calculate_normals: Whether to update the normals 475 | :param file fh: The file handle to open 476 | :param dict kwargs: The same as for :py:class:`stl.mesh.Mesh` 477 | """ 478 | meshes = [ 479 | cls.from_file( 480 | filename, 481 | calculate_normals=calculate_normals, 482 | mode=mode, 483 | speedups=speedups, 484 | **kwargs, 485 | ) 486 | for filename in filenames 487 | ] 488 | 489 | data = np.concatenate([mesh.data for mesh in meshes]) 490 | return cls(data, calculate_normals=calculate_normals, **kwargs) 491 | 492 | @classmethod 493 | def from_3mf_file(cls, filename, calculate_normals=True, **kwargs): 494 | with zipfile.ZipFile(filename) as zip: 495 | with zip.open('_rels/.rels') as rels_fh: 496 | model = None 497 | root = ET.parse(rels_fh).getroot() 498 | for child in root: # pragma: no branch 499 | type_ = child.attrib.get('Type', '') 500 | if type_.endswith('3dmodel'): # pragma: no branch 501 | model = child.attrib.get('Target', '') 502 | break 503 | 504 | assert model, f'No 3D model found in {filename}' 505 | with zip.open(model.lstrip('/')) as fh: 506 | root = ET.parse(fh).getroot() 507 | 508 | elements = root.findall('./{*}resources/{*}object/{*}mesh') 509 | for mesh_element in elements: # pragma: no branch 510 | triangles = [] 511 | vertices = [] 512 | 513 | for element in mesh_element: 514 | tag = element.tag 515 | if tag.endswith('vertices'): 516 | # Collect all the vertices 517 | for vertice in element: 518 | a = { 519 | k: float(v) 520 | for k, v in vertice.attrib.items() 521 | } 522 | vertices.append([a['x'], a['y'], a['z']]) 523 | 524 | elif tag.endswith('triangles'): # pragma: no branch 525 | # Map the triangles to the vertices and collect 526 | for triangle in element: 527 | a = { 528 | k: int(v) 529 | for k, v in triangle.attrib.items() 530 | } 531 | triangles.append( 532 | [ 533 | vertices[a['v1']], 534 | vertices[a['v2']], 535 | vertices[a['v3']], 536 | ] 537 | ) 538 | 539 | mesh = cls(np.zeros(len(triangles), dtype=cls.dtype)) 540 | mesh.vectors[:] = np.array(triangles) 541 | yield mesh 542 | 543 | 544 | StlMesh = BaseStl.from_file 545 | -------------------------------------------------------------------------------- /stl/utils.py: -------------------------------------------------------------------------------- 1 | def b(s, encoding='ascii', errors='replace'): # pragma: no cover 2 | if isinstance(s, str): 3 | return bytes(s, encoding, errors) 4 | else: 5 | return s 6 | # return bytes(s, encoding, errors) 7 | -------------------------------------------------------------------------------- /tests/3mf/Moon.3mf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolph/numpy-stl/8ad2b16f8a0e611fce8259befb15c7601836e7f0/tests/3mf/Moon.3mf -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolph/numpy-stl/8ad2b16f8a0e611fce8259befb15c7601836e7f0/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | import pytest 4 | 5 | 6 | def pytest_generate_tests(metafunc): 7 | # Run all tests both with and without speedups 8 | metafunc.fixturenames.append('speedups') 9 | metafunc.parametrize('speedups', [False, True]) 10 | 11 | 12 | @pytest.fixture(scope='session') 13 | def cwd() -> pathlib.Path: 14 | return pathlib.Path(__file__).parent 15 | 16 | 17 | @pytest.fixture(scope='session') 18 | def ascii_path(cwd) -> pathlib.Path: 19 | return cwd / 'stl_ascii' 20 | 21 | 22 | @pytest.fixture(scope='session') 23 | def binary_path(cwd) -> pathlib.Path: 24 | return cwd / 'stl_binary' 25 | 26 | 27 | @pytest.fixture(scope='session') 28 | def three_mf_path(cwd) -> pathlib.Path: 29 | return cwd / '3mf' 30 | 31 | 32 | @pytest.fixture(scope='session', params=['ascii', 'binary']) 33 | def binary_ascii_path(request, ascii_path, binary_path) -> pathlib.Path: 34 | return ascii_path if request.param == 'ascii' else binary_path 35 | 36 | 37 | @pytest.fixture(scope='session') 38 | def ascii_file(ascii_path) -> str: 39 | return str(ascii_path / 'HalfDonut.stl') 40 | 41 | 42 | @pytest.fixture(scope='session') 43 | def binary_file(binary_path) -> str: 44 | return str(binary_path / 'HalfDonut.stl') 45 | -------------------------------------------------------------------------------- /tests/qt-lc_numeric-reproducer: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import os.path 4 | import sys 5 | 6 | from stl import mesh 7 | 8 | try: 9 | from PySide2 import QtWidgets 10 | except ImportError: 11 | from PyQt5 import QtWidgets 12 | 13 | app = QtWidgets.QApplication([]) 14 | 15 | dir_path = os.path.dirname(os.path.realpath(__file__)) 16 | stl_path = os.path.join(dir_path, 'stl_ascii/Star.stl') 17 | 18 | your_mesh = mesh.Mesh.from_file(stl_path, speedups=True) 19 | -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | cov-core 2 | coverage 3 | docutils 4 | execnet 5 | numpy 6 | cython 7 | pep8 8 | py 9 | pyflakes 10 | pytest 11 | pytest-cache 12 | pytest-cov 13 | python-utils 14 | Sphinx 15 | flake8 16 | wheel 17 | -------------------------------------------------------------------------------- /tests/stl_ascii/Cube.stl: -------------------------------------------------------------------------------- 1 | solid PRO2STL version 1.0 2 | facet normal -1 0 0 3 | outer loop 4 | vertex 0 2 2 5 | vertex 0 2 0 6 | vertex 0 0 0 7 | endloop 8 | endfacet 9 | facet normal -1 0 0 10 | outer loop 11 | vertex 0 0 0 12 | vertex 0 0 2 13 | vertex 0 2 2 14 | endloop 15 | endfacet 16 | facet normal 0 1 -0 17 | outer loop 18 | vertex 2 2 2 19 | vertex 2 2 0 20 | vertex 0 2 0 21 | endloop 22 | endfacet 23 | facet normal -0 1 0 24 | outer loop 25 | vertex 0 2 0 26 | vertex 0 2 2 27 | vertex 2 2 2 28 | endloop 29 | endfacet 30 | facet normal 1 0 0 31 | outer loop 32 | vertex 2 0 2 33 | vertex 2 0 0 34 | vertex 2 2 0 35 | endloop 36 | endfacet 37 | facet normal 1 0 0 38 | outer loop 39 | vertex 2 2 0 40 | vertex 2 2 2 41 | vertex 2 0 2 42 | endloop 43 | endfacet 44 | facet normal 0 -1 0 45 | outer loop 46 | vertex 0 0 2 47 | vertex 0 0 0 48 | vertex 2 0 0 49 | endloop 50 | endfacet 51 | facet normal -0 -1 -0 52 | outer loop 53 | vertex 2 0 0 54 | vertex 2 0 2 55 | vertex 0 0 2 56 | endloop 57 | endfacet 58 | facet normal -0 0 1 59 | outer loop 60 | vertex 2 2 2 61 | vertex 0 2 2 62 | vertex 0 0 2 63 | endloop 64 | endfacet 65 | facet normal 0 -0 1 66 | outer loop 67 | vertex 0 0 2 68 | vertex 2 0 2 69 | vertex 2 2 2 70 | endloop 71 | endfacet 72 | facet normal 0 0 -1 73 | outer loop 74 | vertex 2 0 0 75 | vertex 0 0 0 76 | vertex 0 2 0 77 | endloop 78 | endfacet 79 | facet normal -0 -0 -1 80 | outer loop 81 | vertex 0 2 0 82 | vertex 2 2 0 83 | vertex 2 0 0 84 | endloop 85 | endfacet 86 | endsolid PRO2STL version 1.0 87 | -------------------------------------------------------------------------------- /tests/stl_ascii/Moon.stl: -------------------------------------------------------------------------------- 1 | solid Moon.stl 2 | facet normal -0.091618 0.000000 -0.072093 3 | outer loop 4 | vertex 0.360463 0.000000 2.525000 5 | vertex 0.000000 0.000000 2.983090 6 | vertex 0.360463 0.200000 2.525000 7 | endloop 8 | endfacet 9 | facet normal -0.091618 0.000000 -0.072093 10 | outer loop 11 | vertex 0.000000 0.000000 2.983090 12 | vertex 0.000000 0.200000 2.983090 13 | vertex 0.360463 0.200000 2.525000 14 | endloop 15 | endfacet 16 | facet normal -0.003382 0.000000 0.025682 17 | outer loop 18 | vertex 0.000000 0.000000 2.983090 19 | vertex 0.128412 0.000000 3.000000 20 | vertex 0.000000 0.200000 2.983090 21 | endloop 22 | endfacet 23 | facet normal -0.003382 0.000000 0.025682 24 | outer loop 25 | vertex 0.128412 0.000000 3.000000 26 | vertex 0.128412 0.200000 3.000000 27 | vertex 0.000000 0.200000 2.983090 28 | endloop 29 | endfacet 30 | facet normal 0.010222 -0.000000 0.077646 31 | outer loop 32 | vertex 0.516641 0.000000 2.948890 33 | vertex 0.516641 0.200000 2.948890 34 | vertex 0.128412 0.000000 3.000000 35 | endloop 36 | endfacet 37 | facet normal 0.010222 -0.000000 0.077646 38 | outer loop 39 | vertex 0.128412 0.000000 3.000000 40 | vertex 0.516641 0.200000 2.948890 41 | vertex 0.128412 0.200000 3.000000 42 | endloop 43 | endfacet 44 | facet normal 0.029970 -0.000000 0.072354 45 | outer loop 46 | vertex 0.878412 0.000000 2.799040 47 | vertex 0.878412 0.200000 2.799040 48 | vertex 0.516641 0.000000 2.948890 49 | endloop 50 | endfacet 51 | facet normal 0.029970 -0.000000 0.072354 52 | outer loop 53 | vertex 0.516641 0.000000 2.948890 54 | vertex 0.878412 0.200000 2.799040 55 | vertex 0.516641 0.200000 2.948890 56 | endloop 57 | endfacet 58 | facet normal 0.047676 -0.000000 0.062132 59 | outer loop 60 | vertex 1.189070 0.000000 2.560660 61 | vertex 1.189070 0.200000 2.560660 62 | vertex 0.878412 0.000000 2.799040 63 | endloop 64 | endfacet 65 | facet normal 0.047676 -0.000000 0.062132 66 | outer loop 67 | vertex 0.878412 0.000000 2.799040 68 | vertex 1.189070 0.200000 2.560660 69 | vertex 0.878412 0.200000 2.799040 70 | endloop 71 | endfacet 72 | facet normal 0.062132 -0.000000 0.047676 73 | outer loop 74 | vertex 1.427450 0.000000 2.250000 75 | vertex 1.427450 0.200000 2.250000 76 | vertex 1.189070 0.000000 2.560660 77 | endloop 78 | endfacet 79 | facet normal 0.062132 -0.000000 0.047676 80 | outer loop 81 | vertex 1.189070 0.000000 2.560660 82 | vertex 1.427450 0.200000 2.250000 83 | vertex 1.189070 0.200000 2.560660 84 | endloop 85 | endfacet 86 | facet normal 0.072354 -0.000000 0.029970 87 | outer loop 88 | vertex 1.577300 0.000000 1.888230 89 | vertex 1.577300 0.200000 1.888230 90 | vertex 1.427450 0.000000 2.250000 91 | endloop 92 | endfacet 93 | facet normal 0.072354 -0.000000 0.029970 94 | outer loop 95 | vertex 1.427450 0.000000 2.250000 96 | vertex 1.577300 0.200000 1.888230 97 | vertex 1.427450 0.200000 2.250000 98 | endloop 99 | endfacet 100 | facet normal 0.077646 -0.000000 0.010222 101 | outer loop 102 | vertex 1.628410 0.000000 1.500000 103 | vertex 1.628410 0.200000 1.500000 104 | vertex 1.577300 0.000000 1.888230 105 | endloop 106 | endfacet 107 | facet normal 0.077646 -0.000000 0.010222 108 | outer loop 109 | vertex 1.577300 0.000000 1.888230 110 | vertex 1.628410 0.200000 1.500000 111 | vertex 1.577300 0.200000 1.888230 112 | endloop 113 | endfacet 114 | facet normal 0.077646 0.000000 -0.010222 115 | outer loop 116 | vertex 1.577300 0.000000 1.111770 117 | vertex 1.577300 0.200000 1.111770 118 | vertex 1.628410 0.000000 1.500000 119 | endloop 120 | endfacet 121 | facet normal 0.077646 0.000000 -0.010222 122 | outer loop 123 | vertex 1.628410 0.000000 1.500000 124 | vertex 1.577300 0.200000 1.111770 125 | vertex 1.628410 0.200000 1.500000 126 | endloop 127 | endfacet 128 | facet normal 0.072354 0.000000 -0.029970 129 | outer loop 130 | vertex 1.427450 0.000000 0.750000 131 | vertex 1.427450 0.200000 0.750000 132 | vertex 1.577300 0.000000 1.111770 133 | endloop 134 | endfacet 135 | facet normal 0.072354 0.000000 -0.029970 136 | outer loop 137 | vertex 1.577300 0.000000 1.111770 138 | vertex 1.427450 0.200000 0.750000 139 | vertex 1.577300 0.200000 1.111770 140 | endloop 141 | endfacet 142 | facet normal 0.062132 0.000000 -0.047676 143 | outer loop 144 | vertex 1.189070 0.000000 0.439340 145 | vertex 1.189070 0.200000 0.439340 146 | vertex 1.427450 0.000000 0.750000 147 | endloop 148 | endfacet 149 | facet normal 0.062132 0.000000 -0.047676 150 | outer loop 151 | vertex 1.427450 0.000000 0.750000 152 | vertex 1.189070 0.200000 0.439340 153 | vertex 1.427450 0.200000 0.750000 154 | endloop 155 | endfacet 156 | facet normal 0.047676 0.000000 -0.062132 157 | outer loop 158 | vertex 0.878412 0.000000 0.200962 159 | vertex 0.878412 0.200000 0.200962 160 | vertex 1.189070 0.000000 0.439340 161 | endloop 162 | endfacet 163 | facet normal 0.047676 0.000000 -0.062132 164 | outer loop 165 | vertex 1.189070 0.000000 0.439340 166 | vertex 0.878412 0.200000 0.200962 167 | vertex 1.189070 0.200000 0.439340 168 | endloop 169 | endfacet 170 | facet normal 0.029970 0.000000 -0.072354 171 | outer loop 172 | vertex 0.516641 0.000000 0.051111 173 | vertex 0.516641 0.200000 0.051111 174 | vertex 0.878412 0.000000 0.200962 175 | endloop 176 | endfacet 177 | facet normal 0.029970 0.000000 -0.072354 178 | outer loop 179 | vertex 0.878412 0.000000 0.200962 180 | vertex 0.516641 0.200000 0.051111 181 | vertex 0.878412 0.200000 0.200962 182 | endloop 183 | endfacet 184 | facet normal 0.010222 0.000000 -0.077646 185 | outer loop 186 | vertex 0.128412 0.000000 0.000000 187 | vertex 0.128412 0.200000 0.000000 188 | vertex 0.516641 0.000000 0.051111 189 | endloop 190 | endfacet 191 | facet normal 0.010222 0.000000 -0.077646 192 | outer loop 193 | vertex 0.516641 0.000000 0.051111 194 | vertex 0.128412 0.200000 0.000000 195 | vertex 0.516641 0.200000 0.051111 196 | endloop 197 | endfacet 198 | facet normal -0.003381 0.000000 -0.025682 199 | outer loop 200 | vertex 0.128412 0.000000 0.000000 201 | vertex 0.000000 0.000000 0.016906 202 | vertex 0.128412 0.200000 0.000000 203 | endloop 204 | endfacet 205 | facet normal -0.003381 0.000000 -0.025682 206 | outer loop 207 | vertex 0.000000 0.000000 0.016906 208 | vertex 0.000000 0.200000 0.016906 209 | vertex 0.128412 0.200000 0.000000 210 | endloop 211 | endfacet 212 | facet normal -0.091619 0.000000 0.072093 213 | outer loop 214 | vertex 0.000000 0.000000 0.016906 215 | vertex 0.360463 0.000000 0.475000 216 | vertex 0.000000 0.200000 0.016906 217 | endloop 218 | endfacet 219 | facet normal -0.091619 0.000000 0.072093 220 | outer loop 221 | vertex 0.360463 0.000000 0.475000 222 | vertex 0.360463 0.200000 0.475000 223 | vertex 0.000000 0.200000 0.016906 224 | endloop 225 | endfacet 226 | facet normal -0.098884 0.000000 0.039960 227 | outer loop 228 | vertex 0.360463 0.000000 0.475000 229 | vertex 0.560264 0.000000 0.969421 230 | vertex 0.360463 0.200000 0.475000 231 | endloop 232 | endfacet 233 | facet normal -0.098884 0.000000 0.039960 234 | outer loop 235 | vertex 0.560264 0.000000 0.969421 236 | vertex 0.560264 0.200000 0.969421 237 | vertex 0.360463 0.200000 0.475000 238 | endloop 239 | endfacet 240 | facet normal -0.106116 0.000000 0.013630 241 | outer loop 242 | vertex 0.560264 0.000000 0.969421 243 | vertex 0.628412 0.000000 1.500000 244 | vertex 0.560264 0.200000 0.969421 245 | endloop 246 | endfacet 247 | facet normal -0.106116 0.000000 0.013630 248 | outer loop 249 | vertex 0.628412 0.000000 1.500000 250 | vertex 0.628412 0.200000 1.500000 251 | vertex 0.560264 0.200000 0.969421 252 | endloop 253 | endfacet 254 | facet normal -0.106116 0.000000 -0.013630 255 | outer loop 256 | vertex 0.628412 0.000000 1.500000 257 | vertex 0.560264 0.000000 2.030580 258 | vertex 0.628412 0.200000 1.500000 259 | endloop 260 | endfacet 261 | facet normal -0.106116 0.000000 -0.013630 262 | outer loop 263 | vertex 0.560264 0.000000 2.030580 264 | vertex 0.560264 0.200000 2.030580 265 | vertex 0.628412 0.200000 1.500000 266 | endloop 267 | endfacet 268 | facet normal -0.098884 0.000000 -0.039960 269 | outer loop 270 | vertex 0.560264 0.000000 2.030580 271 | vertex 0.360463 0.000000 2.525000 272 | vertex 0.560264 0.200000 2.030580 273 | endloop 274 | endfacet 275 | facet normal -0.098884 0.000000 -0.039960 276 | outer loop 277 | vertex 0.360463 0.000000 2.525000 278 | vertex 0.360463 0.200000 2.525000 279 | vertex 0.560264 0.200000 2.030580 280 | endloop 281 | endfacet 282 | facet normal 0.000000 -0.064920 0.000000 283 | outer loop 284 | vertex 0.360463 0.000000 2.525000 285 | vertex 0.128412 0.000000 3.000000 286 | vertex 0.000000 0.000000 2.983090 287 | endloop 288 | endfacet 289 | facet normal 0.000000 -0.172549 0.000000 290 | outer loop 291 | vertex 0.360463 0.000000 2.525000 292 | vertex 0.516641 0.000000 2.948890 293 | vertex 0.128412 0.000000 3.000000 294 | endloop 295 | endfacet 296 | facet normal 0.000000 -0.176754 0.000000 297 | outer loop 298 | vertex 0.360463 0.000000 2.525000 299 | vertex 0.878412 0.000000 2.799040 300 | vertex 0.516641 0.000000 2.948890 301 | endloop 302 | endfacet 303 | facet normal 0.000000 -0.310838 0.000000 304 | outer loop 305 | vertex 0.560264 0.000000 2.030580 306 | vertex 0.878412 0.000000 2.799040 307 | vertex 0.360463 0.000000 2.525000 308 | endloop 309 | endfacet 310 | facet normal 0.000000 -0.314568 0.000000 311 | outer loop 312 | vertex 0.560264 0.000000 2.030580 313 | vertex 1.189070 0.000000 2.560660 314 | vertex 0.878412 0.000000 2.799040 315 | endloop 316 | endfacet 317 | facet normal 0.000000 -0.321705 0.000000 318 | outer loop 319 | vertex 0.560264 0.000000 2.030580 320 | vertex 1.427450 0.000000 2.250000 321 | vertex 1.189070 0.000000 2.560660 322 | endloop 323 | endfacet 324 | facet normal 0.000000 -0.475065 0.000000 325 | outer loop 326 | vertex 0.628412 0.000000 1.500000 327 | vertex 1.427450 0.000000 2.250000 328 | vertex 0.560264 0.000000 2.030580 329 | endloop 330 | endfacet 331 | facet normal 0.000000 -0.401456 0.000000 332 | outer loop 333 | vertex 0.628412 0.000000 1.500000 334 | vertex 1.577300 0.000000 1.888230 335 | vertex 1.427450 0.000000 2.250000 336 | endloop 337 | endfacet 338 | facet normal 0.000000 -0.388229 0.000000 339 | outer loop 340 | vertex 0.628412 0.000000 1.500000 341 | vertex 1.628410 0.000000 1.500000 342 | vertex 1.577300 0.000000 1.888230 343 | endloop 344 | endfacet 345 | facet normal 0.000000 -0.530578 0.000000 346 | outer loop 347 | vertex 0.560264 0.000000 0.969421 348 | vertex 1.628410 0.000000 1.500000 349 | vertex 0.628412 0.000000 1.500000 350 | endloop 351 | endfacet 352 | facet normal 0.000000 -0.387568 0.000000 353 | outer loop 354 | vertex 0.560264 0.000000 0.969421 355 | vertex 1.577300 0.000000 1.111770 356 | vertex 1.628410 0.000000 1.500000 357 | endloop 358 | endfacet 359 | facet normal 0.000000 -0.346602 0.000000 360 | outer loop 361 | vertex 1.427450 0.000000 0.750000 362 | vertex 1.577300 0.000000 1.111770 363 | vertex 0.560264 0.000000 0.969421 364 | endloop 365 | endfacet 366 | facet normal 0.000000 -0.472596 0.000000 367 | outer loop 368 | vertex 0.360463 0.000000 0.475000 369 | vertex 1.427450 0.000000 0.750000 370 | vertex 0.560264 0.000000 0.969421 371 | endloop 372 | endfacet 373 | facet normal 0.000000 -0.265916 0.000000 374 | outer loop 375 | vertex 1.189070 0.000000 0.439340 376 | vertex 1.427450 0.000000 0.750000 377 | vertex 0.360463 0.000000 0.475000 378 | endloop 379 | endfacet 380 | facet normal 0.000000 -0.208600 0.000000 381 | outer loop 382 | vertex 0.878412 0.000000 0.200962 383 | vertex 1.189070 0.000000 0.439340 384 | vertex 0.360463 0.000000 0.475000 385 | endloop 386 | endfacet 387 | facet normal 0.000000 -0.176754 0.000000 388 | outer loop 389 | vertex 0.516641 0.000000 0.051111 390 | vertex 0.878412 0.000000 0.200962 391 | vertex 0.360463 0.000000 0.475000 392 | endloop 393 | endfacet 394 | facet normal 0.000000 -0.172548 0.000000 395 | outer loop 396 | vertex 0.128412 0.000000 0.000000 397 | vertex 0.516641 0.000000 0.051111 398 | vertex 0.360463 0.000000 0.475000 399 | endloop 400 | endfacet 401 | facet normal 0.000000 -0.064919 0.000000 402 | outer loop 403 | vertex 0.128412 0.000000 0.000000 404 | vertex 0.360463 0.000000 0.475000 405 | vertex 0.000000 0.000000 0.016906 406 | endloop 407 | endfacet 408 | facet normal 0.000000 0.037063 0.000000 409 | outer loop 410 | vertex 1.383780 0.350000 0.775216 411 | vertex 1.577980 0.350000 1.500000 412 | vertex 1.528590 0.350000 1.124820 413 | endloop 414 | endfacet 415 | facet normal 0.000000 0.108667 0.000000 416 | outer loop 417 | vertex 1.153410 0.350000 0.475000 418 | vertex 1.577980 0.350000 1.500000 419 | vertex 1.383780 0.350000 0.775216 420 | endloop 421 | endfacet 422 | facet normal 0.000000 0.209914 0.000000 423 | outer loop 424 | vertex 0.853196 0.350000 0.244637 425 | vertex 1.577980 0.350000 1.500000 426 | vertex 1.153410 0.350000 0.475000 427 | endloop 428 | endfacet 429 | facet normal 0.000000 0.333927 0.000000 430 | outer loop 431 | vertex 0.503588 0.350000 0.099824 432 | vertex 1.577980 0.350000 1.500000 433 | vertex 0.853196 0.350000 0.244637 434 | endloop 435 | endfacet 436 | facet normal 0.000000 0.472246 0.000000 437 | outer loop 438 | vertex 0.128412 0.350000 0.050432 439 | vertex 1.577980 0.350000 1.500000 440 | vertex 0.503588 0.350000 0.099824 441 | endloop 442 | endfacet 443 | facet normal 0.000000 0.615443 0.000000 444 | outer loop 445 | vertex 0.128412 0.350000 0.050432 446 | vertex 1.528590 0.350000 1.875180 447 | vertex 1.577980 0.350000 1.500000 448 | endloop 449 | endfacet 450 | facet normal 0.000000 0.037062 0.000000 451 | outer loop 452 | vertex 1.383780 0.350000 2.224780 453 | vertex 0.853196 0.350000 2.755360 454 | vertex 1.153410 0.350000 2.525000 455 | endloop 456 | endfacet 457 | facet normal 0.000000 0.108656 0.000000 458 | outer loop 459 | vertex 1.383780 0.350000 2.224780 460 | vertex 0.503588 0.350000 2.900180 461 | vertex 0.853196 0.350000 2.755360 462 | endloop 463 | endfacet 464 | facet normal 0.000000 0.209921 0.000000 465 | outer loop 466 | vertex 1.383780 0.350000 2.224780 467 | vertex 0.128412 0.350000 2.949570 468 | vertex 0.503588 0.350000 2.900180 469 | endloop 470 | endfacet 471 | facet normal 0.000000 0.333920 0.000000 472 | outer loop 473 | vertex 1.528590 0.350000 1.875180 474 | vertex 0.128412 0.350000 2.949570 475 | vertex 1.383780 0.350000 2.224780 476 | endloop 477 | endfacet 478 | facet normal 0.000000 0.015158 0.000000 479 | outer loop 480 | vertex 0.404155 0.350000 2.550330 481 | vertex 0.093609 0.350000 2.944990 482 | vertex 0.128412 0.350000 2.949570 483 | endloop 484 | endfacet 485 | facet normal 0.000000 0.262752 0.000000 486 | outer loop 487 | vertex 1.528590 0.350000 1.875180 488 | vertex 0.404155 0.350000 2.550330 489 | vertex 0.128412 0.350000 2.949570 490 | endloop 491 | endfacet 492 | facet normal 0.000000 0.431753 0.000000 493 | outer loop 494 | vertex 1.528590 0.350000 1.875180 495 | vertex 0.609035 0.350000 2.043340 496 | vertex 0.404155 0.350000 2.550330 497 | endloop 498 | endfacet 499 | facet normal 0.000000 0.487895 -0.000000 500 | outer loop 501 | vertex 0.678823 0.350000 1.500000 502 | vertex 0.609035 0.350000 2.043340 503 | vertex 1.528590 0.350000 1.875180 504 | endloop 505 | endfacet 506 | facet normal 0.000000 0.435529 0.000000 507 | outer loop 508 | vertex 0.609035 0.350000 0.956661 509 | vertex 0.678823 0.350000 1.500000 510 | vertex 1.528590 0.350000 1.875180 511 | endloop 512 | endfacet 513 | facet normal 0.000000 0.015158 -0.000000 514 | outer loop 515 | vertex 0.128412 0.350000 0.050432 516 | vertex 0.093609 0.350000 0.055013 517 | vertex 0.404155 0.350000 0.449671 518 | endloop 519 | endfacet 520 | facet normal 0.000000 0.278019 0.000000 521 | outer loop 522 | vertex 0.404155 0.350000 0.449671 523 | vertex 0.609035 0.350000 0.956661 524 | vertex 1.528590 0.350000 1.875180 525 | endloop 526 | endfacet 527 | facet normal 0.000000 0.055844 0.000000 528 | outer loop 529 | vertex 0.128412 0.350000 0.050432 530 | vertex 0.404155 0.350000 0.449671 531 | vertex 1.528590 0.350000 1.875180 532 | endloop 533 | endfacet 534 | facet normal -0.002537 0.006475 0.019262 535 | outer loop 536 | vertex 0.000000 0.200000 2.983090 537 | vertex 0.128412 0.200000 3.000000 538 | vertex 0.093609 0.350000 2.944990 539 | endloop 540 | endfacet 541 | facet normal -0.000687 0.001755 0.005220 542 | outer loop 543 | vertex 0.128412 0.200000 3.000000 544 | vertex 0.128412 0.350000 2.949570 545 | vertex 0.093609 0.350000 2.944990 546 | endloop 547 | endfacet 548 | facet normal -0.059199 0.025110 -0.046582 549 | outer loop 550 | vertex 0.360463 0.200000 2.525000 551 | vertex 0.093609 0.350000 2.944990 552 | vertex 0.404155 0.350000 2.550330 553 | endloop 554 | endfacet 555 | facet normal -0.068713 0.029148 -0.054069 556 | outer loop 557 | vertex 0.360463 0.200000 2.525000 558 | vertex 0.000000 0.200000 2.983090 559 | vertex 0.093609 0.350000 2.944990 560 | endloop 561 | endfacet 562 | facet normal -0.074163 0.026663 -0.029970 563 | outer loop 564 | vertex 0.560264 0.200000 2.030580 565 | vertex 0.360463 0.200000 2.525000 566 | vertex 0.609035 0.350000 2.043340 567 | endloop 568 | endfacet 569 | facet normal -0.076048 0.027341 -0.030732 570 | outer loop 571 | vertex 0.360463 0.200000 2.525000 572 | vertex 0.404155 0.350000 2.550330 573 | vertex 0.609035 0.350000 2.043340 574 | endloop 575 | endfacet 576 | facet normal -0.079587 0.026747 -0.010222 577 | outer loop 578 | vertex 0.628412 0.200000 1.500000 579 | vertex 0.560264 0.200000 2.030580 580 | vertex 0.678823 0.350000 1.500000 581 | endloop 582 | endfacet 583 | facet normal -0.081501 0.027390 -0.010468 584 | outer loop 585 | vertex 0.560264 0.200000 2.030580 586 | vertex 0.609035 0.350000 2.043340 587 | vertex 0.678823 0.350000 1.500000 588 | endloop 589 | endfacet 590 | facet normal -0.081501 0.027390 0.010468 591 | outer loop 592 | vertex 0.560264 0.200000 0.969421 593 | vertex 0.678823 0.350000 1.500000 594 | vertex 0.609035 0.350000 0.956661 595 | endloop 596 | endfacet 597 | facet normal -0.079587 0.026747 0.010222 598 | outer loop 599 | vertex 0.560264 0.200000 0.969421 600 | vertex 0.628412 0.200000 1.500000 601 | vertex 0.678823 0.350000 1.500000 602 | endloop 603 | endfacet 604 | facet normal -0.076048 0.027341 0.030732 605 | outer loop 606 | vertex 0.360463 0.200000 0.475000 607 | vertex 0.609035 0.350000 0.956661 608 | vertex 0.404155 0.350000 0.449671 609 | endloop 610 | endfacet 611 | facet normal -0.074163 0.026663 0.029970 612 | outer loop 613 | vertex 0.360463 0.200000 0.475000 614 | vertex 0.560264 0.200000 0.969421 615 | vertex 0.609035 0.350000 0.956661 616 | endloop 617 | endfacet 618 | facet normal -0.068714 0.029146 0.054069 619 | outer loop 620 | vertex 0.000000 0.200000 0.016906 621 | vertex 0.360463 0.200000 0.475000 622 | vertex 0.093609 0.350000 0.055013 623 | endloop 624 | endfacet 625 | facet normal -0.059199 0.025109 0.046582 626 | outer loop 627 | vertex 0.360463 0.200000 0.475000 628 | vertex 0.404155 0.350000 0.449671 629 | vertex 0.093609 0.350000 0.055013 630 | endloop 631 | endfacet 632 | facet normal -0.000687 0.001755 -0.005220 633 | outer loop 634 | vertex 0.128412 0.200000 0.000000 635 | vertex 0.093609 0.350000 0.055013 636 | vertex 0.128412 0.350000 0.050432 637 | endloop 638 | endfacet 639 | facet normal -0.002536 0.006476 -0.019262 640 | outer loop 641 | vertex 0.128412 0.200000 0.000000 642 | vertex 0.000000 0.200000 0.016906 643 | vertex 0.093609 0.350000 0.055013 644 | endloop 645 | endfacet 646 | facet normal 0.007667 0.019579 -0.058234 647 | outer loop 648 | vertex 0.128412 0.200000 0.000000 649 | vertex 0.503588 0.350000 0.099824 650 | vertex 0.516641 0.200000 0.051111 651 | endloop 652 | endfacet 653 | facet normal 0.007409 0.018921 -0.056276 654 | outer loop 655 | vertex 0.128412 0.200000 0.000000 656 | vertex 0.128412 0.350000 0.050432 657 | vertex 0.503588 0.350000 0.099824 658 | endloop 659 | endfacet 660 | facet normal 0.022478 0.019579 -0.054266 661 | outer loop 662 | vertex 0.516641 0.200000 0.051111 663 | vertex 0.853196 0.350000 0.244637 664 | vertex 0.878412 0.200000 0.200962 665 | endloop 666 | endfacet 667 | facet normal 0.021722 0.018921 -0.052441 668 | outer loop 669 | vertex 0.516641 0.200000 0.051111 670 | vertex 0.503588 0.350000 0.099824 671 | vertex 0.853196 0.350000 0.244637 672 | endloop 673 | endfacet 674 | facet normal 0.035757 0.019579 -0.046599 675 | outer loop 676 | vertex 0.878412 0.200000 0.200962 677 | vertex 1.153410 0.350000 0.475000 678 | vertex 1.189070 0.200000 0.439340 679 | endloop 680 | endfacet 681 | facet normal 0.034554 0.018921 -0.045032 682 | outer loop 683 | vertex 0.878412 0.200000 0.200962 684 | vertex 0.853196 0.350000 0.244637 685 | vertex 1.153410 0.350000 0.475000 686 | endloop 687 | endfacet 688 | facet normal 0.046599 0.019579 -0.035757 689 | outer loop 690 | vertex 1.189070 0.200000 0.439340 691 | vertex 1.153410 0.350000 0.475000 692 | vertex 1.427450 0.200000 0.750000 693 | endloop 694 | endfacet 695 | facet normal 0.045032 0.018919 -0.034556 696 | outer loop 697 | vertex 1.427450 0.200000 0.750000 698 | vertex 1.153410 0.350000 0.475000 699 | vertex 1.383780 0.350000 0.775216 700 | endloop 701 | endfacet 702 | facet normal 0.054266 0.019577 -0.022478 703 | outer loop 704 | vertex 1.427450 0.200000 0.750000 705 | vertex 1.383780 0.350000 0.775216 706 | vertex 1.577300 0.200000 1.111770 707 | endloop 708 | endfacet 709 | facet normal 0.052441 0.018919 -0.021721 710 | outer loop 711 | vertex 1.577300 0.200000 1.111770 712 | vertex 1.383780 0.350000 0.775216 713 | vertex 1.528590 0.350000 1.124820 714 | endloop 715 | endfacet 716 | facet normal 0.058234 0.019578 -0.007667 717 | outer loop 718 | vertex 1.577300 0.200000 1.111770 719 | vertex 1.528590 0.350000 1.124820 720 | vertex 1.628410 0.200000 1.500000 721 | endloop 722 | endfacet 723 | facet normal 0.056277 0.018920 -0.007409 724 | outer loop 725 | vertex 1.628410 0.200000 1.500000 726 | vertex 1.528590 0.350000 1.124820 727 | vertex 1.577980 0.350000 1.500000 728 | endloop 729 | endfacet 730 | facet normal 0.058234 0.019578 0.007667 731 | outer loop 732 | vertex 1.628410 0.200000 1.500000 733 | vertex 1.528590 0.350000 1.875180 734 | vertex 1.577300 0.200000 1.888230 735 | endloop 736 | endfacet 737 | facet normal 0.056277 0.018920 0.007409 738 | outer loop 739 | vertex 1.628410 0.200000 1.500000 740 | vertex 1.577980 0.350000 1.500000 741 | vertex 1.528590 0.350000 1.875180 742 | endloop 743 | endfacet 744 | facet normal 0.054266 0.019578 0.022478 745 | outer loop 746 | vertex 1.577300 0.200000 1.888230 747 | vertex 1.383780 0.350000 2.224780 748 | vertex 1.427450 0.200000 2.250000 749 | endloop 750 | endfacet 751 | facet normal 0.052440 0.018919 0.021721 752 | outer loop 753 | vertex 1.577300 0.200000 1.888230 754 | vertex 1.528590 0.350000 1.875180 755 | vertex 1.383780 0.350000 2.224780 756 | endloop 757 | endfacet 758 | facet normal 0.046599 0.019579 0.035757 759 | outer loop 760 | vertex 1.427450 0.200000 2.250000 761 | vertex 1.153410 0.350000 2.525000 762 | vertex 1.189070 0.200000 2.560660 763 | endloop 764 | endfacet 765 | facet normal 0.045033 0.018921 0.034556 766 | outer loop 767 | vertex 1.427450 0.200000 2.250000 768 | vertex 1.383780 0.350000 2.224780 769 | vertex 1.153410 0.350000 2.525000 770 | endloop 771 | endfacet 772 | facet normal 0.034554 0.018922 0.045032 773 | outer loop 774 | vertex 0.878412 0.200000 2.799040 775 | vertex 1.153410 0.350000 2.525000 776 | vertex 0.853196 0.350000 2.755360 777 | endloop 778 | endfacet 779 | facet normal 0.035757 0.019579 0.046599 780 | outer loop 781 | vertex 1.189070 0.200000 2.560660 782 | vertex 1.153410 0.350000 2.525000 783 | vertex 0.878412 0.200000 2.799040 784 | endloop 785 | endfacet 786 | facet normal 0.022477 0.019581 0.054266 787 | outer loop 788 | vertex 0.878412 0.200000 2.799040 789 | vertex 0.853196 0.350000 2.755360 790 | vertex 0.516641 0.200000 2.948890 791 | endloop 792 | endfacet 793 | facet normal 0.021723 0.018920 0.052441 794 | outer loop 795 | vertex 0.516641 0.200000 2.948890 796 | vertex 0.853196 0.350000 2.755360 797 | vertex 0.503588 0.350000 2.900180 798 | endloop 799 | endfacet 800 | facet normal 0.007667 0.019578 0.058234 801 | outer loop 802 | vertex 0.516641 0.200000 2.948890 803 | vertex 0.503588 0.350000 2.900180 804 | vertex 0.128412 0.200000 3.000000 805 | endloop 806 | endfacet 807 | facet normal 0.007408 0.018920 0.056276 808 | outer loop 809 | vertex 0.128412 0.200000 3.000000 810 | vertex 0.503588 0.350000 2.900180 811 | vertex 0.128412 0.350000 2.949570 812 | endloop 813 | endfacet 814 | endsolid Moon.stl 815 | -------------------------------------------------------------------------------- /tests/stl_ascii/Star.stl: -------------------------------------------------------------------------------- 1 | solid Star.stl 2 | facet normal 0.000000 -0.645975 0.000000 3 | outer loop 4 | vertex 0.834040 0.000000 0.694596 5 | vertex 0.369040 0.000000 1.500000 6 | vertex 0.000002 0.000000 0.750000 7 | endloop 8 | endfacet 9 | facet normal 0.000000 -0.645973 0.000000 10 | outer loop 11 | vertex 0.369040 0.000000 1.500000 12 | vertex 0.834040 0.000000 2.305400 13 | vertex 0.000002 0.000000 2.250000 14 | endloop 15 | endfacet 16 | facet normal 0.000000 -0.645974 0.000000 17 | outer loop 18 | vertex 1.299040 0.000000 0.000000 19 | vertex 1.764040 0.000000 0.694596 20 | vertex 0.834040 0.000000 0.694596 21 | endloop 22 | endfacet 23 | facet normal 0.000000 -0.645978 0.000000 24 | outer loop 25 | vertex 0.834040 0.000000 2.305400 26 | vertex 1.764040 0.000000 2.305400 27 | vertex 1.299040 0.000000 3.000000 28 | endloop 29 | endfacet 30 | facet normal 0.000000 -0.645976 0.000000 31 | outer loop 32 | vertex 1.764040 0.000000 0.694596 33 | vertex 2.598080 0.000000 0.750000 34 | vertex 2.229040 0.000000 1.500000 35 | endloop 36 | endfacet 37 | facet normal 0.000000 -0.645975 0.000000 38 | outer loop 39 | vertex 2.229040 0.000000 1.500000 40 | vertex 2.598080 0.000000 2.250000 41 | vertex 1.764040 0.000000 2.305400 42 | endloop 43 | endfacet 44 | facet normal 0.000000 -0.749024 0.000000 45 | outer loop 46 | vertex 0.834040 0.000000 0.694596 47 | vertex 0.834040 0.000000 2.305400 48 | vertex 0.369040 0.000000 1.500000 49 | endloop 50 | endfacet 51 | facet normal 0.000000 -1.498048 0.000000 52 | outer loop 53 | vertex 0.834040 0.000000 0.694596 54 | vertex 1.764040 0.000000 0.694596 55 | vertex 0.834040 0.000000 2.305400 56 | endloop 57 | endfacet 58 | facet normal 0.000000 -1.498049 0.000000 59 | outer loop 60 | vertex 1.764040 0.000000 0.694596 61 | vertex 2.229040 0.000000 1.500000 62 | vertex 0.834040 0.000000 2.305400 63 | endloop 64 | endfacet 65 | facet normal 0.000000 -0.749022 0.000000 66 | outer loop 67 | vertex 2.229040 0.000000 1.500000 68 | vertex 1.764040 0.000000 2.305400 69 | vertex 0.834040 0.000000 2.305400 70 | endloop 71 | endfacet 72 | facet normal -0.187500 0.000000 -0.092260 73 | outer loop 74 | vertex 0.369040 0.000000 1.500000 75 | vertex 0.000002 0.000000 2.250000 76 | vertex 0.369040 0.250000 1.500000 77 | endloop 78 | endfacet 79 | facet normal -0.187500 0.000000 -0.092260 80 | outer loop 81 | vertex 0.000002 0.000000 2.250000 82 | vertex 0.000002 0.250000 2.250000 83 | vertex 0.369040 0.250000 1.500000 84 | endloop 85 | endfacet 86 | facet normal -0.013850 0.000000 0.208509 87 | outer loop 88 | vertex 0.000002 0.000000 2.250000 89 | vertex 0.834040 0.000000 2.305400 90 | vertex 0.000002 0.250000 2.250000 91 | endloop 92 | endfacet 93 | facet normal -0.013850 0.000000 0.208509 94 | outer loop 95 | vertex 0.834040 0.000000 2.305400 96 | vertex 0.834040 0.250000 2.305400 97 | vertex 0.000002 0.250000 2.250000 98 | endloop 99 | endfacet 100 | facet normal -0.173650 0.000000 0.116250 101 | outer loop 102 | vertex 0.834040 0.000000 2.305400 103 | vertex 1.299040 0.000000 3.000000 104 | vertex 0.834040 0.250000 2.305400 105 | endloop 106 | endfacet 107 | facet normal -0.173650 0.000000 0.116250 108 | outer loop 109 | vertex 1.299040 0.000000 3.000000 110 | vertex 1.299040 0.250000 3.000000 111 | vertex 0.834040 0.250000 2.305400 112 | endloop 113 | endfacet 114 | facet normal 0.173650 -0.000000 0.116250 115 | outer loop 116 | vertex 1.764040 0.000000 2.305400 117 | vertex 1.764040 0.250000 2.305400 118 | vertex 1.299040 0.000000 3.000000 119 | endloop 120 | endfacet 121 | facet normal 0.173650 -0.000000 0.116250 122 | outer loop 123 | vertex 1.299040 0.000000 3.000000 124 | vertex 1.764040 0.250000 2.305400 125 | vertex 1.299040 0.250000 3.000000 126 | endloop 127 | endfacet 128 | facet normal 0.013850 -0.000000 0.208510 129 | outer loop 130 | vertex 2.598080 0.000000 2.250000 131 | vertex 2.598080 0.250000 2.250000 132 | vertex 1.764040 0.000000 2.305400 133 | endloop 134 | endfacet 135 | facet normal 0.013850 -0.000000 0.208510 136 | outer loop 137 | vertex 1.764040 0.000000 2.305400 138 | vertex 2.598080 0.250000 2.250000 139 | vertex 1.764040 0.250000 2.305400 140 | endloop 141 | endfacet 142 | facet normal 0.187500 0.000000 -0.092260 143 | outer loop 144 | vertex 2.229040 0.000000 1.500000 145 | vertex 2.229040 0.250000 1.500000 146 | vertex 2.598080 0.000000 2.250000 147 | endloop 148 | endfacet 149 | facet normal 0.187500 0.000000 -0.092260 150 | outer loop 151 | vertex 2.598080 0.000000 2.250000 152 | vertex 2.229040 0.250000 1.500000 153 | vertex 2.598080 0.250000 2.250000 154 | endloop 155 | endfacet 156 | facet normal 0.187500 -0.000000 0.092260 157 | outer loop 158 | vertex 2.598080 0.000000 0.750000 159 | vertex 2.598080 0.250000 0.750000 160 | vertex 2.229040 0.000000 1.500000 161 | endloop 162 | endfacet 163 | facet normal 0.187500 -0.000000 0.092260 164 | outer loop 165 | vertex 2.229040 0.000000 1.500000 166 | vertex 2.598080 0.250000 0.750000 167 | vertex 2.229040 0.250000 1.500000 168 | endloop 169 | endfacet 170 | facet normal 0.013851 0.000000 -0.208510 171 | outer loop 172 | vertex 1.764040 0.000000 0.694596 173 | vertex 1.764040 0.250000 0.694596 174 | vertex 2.598080 0.000000 0.750000 175 | endloop 176 | endfacet 177 | facet normal 0.013851 0.000000 -0.208510 178 | outer loop 179 | vertex 2.598080 0.000000 0.750000 180 | vertex 1.764040 0.250000 0.694596 181 | vertex 2.598080 0.250000 0.750000 182 | endloop 183 | endfacet 184 | facet normal 0.173649 0.000000 -0.116250 185 | outer loop 186 | vertex 1.299040 0.000000 0.000000 187 | vertex 1.299040 0.250000 0.000000 188 | vertex 1.764040 0.000000 0.694596 189 | endloop 190 | endfacet 191 | facet normal 0.173649 0.000000 -0.116250 192 | outer loop 193 | vertex 1.764040 0.000000 0.694596 194 | vertex 1.299040 0.250000 0.000000 195 | vertex 1.764040 0.250000 0.694596 196 | endloop 197 | endfacet 198 | facet normal -0.173649 0.000000 -0.116250 199 | outer loop 200 | vertex 1.299040 0.000000 0.000000 201 | vertex 0.834040 0.000000 0.694596 202 | vertex 1.299040 0.250000 0.000000 203 | endloop 204 | endfacet 205 | facet normal -0.173649 0.000000 -0.116250 206 | outer loop 207 | vertex 0.834040 0.000000 0.694596 208 | vertex 0.834040 0.250000 0.694596 209 | vertex 1.299040 0.250000 0.000000 210 | endloop 211 | endfacet 212 | facet normal -0.013851 0.000000 -0.208509 213 | outer loop 214 | vertex 0.834040 0.000000 0.694596 215 | vertex 0.000002 0.000000 0.750000 216 | vertex 0.834040 0.250000 0.694596 217 | endloop 218 | endfacet 219 | facet normal -0.013851 0.000000 -0.208509 220 | outer loop 221 | vertex 0.000002 0.000000 0.750000 222 | vertex 0.000002 0.250000 0.750000 223 | vertex 0.834040 0.250000 0.694596 224 | endloop 225 | endfacet 226 | facet normal -0.187500 0.000000 0.092260 227 | outer loop 228 | vertex 0.000002 0.000000 0.750000 229 | vertex 0.369040 0.000000 1.500000 230 | vertex 0.000002 0.250000 0.750000 231 | endloop 232 | endfacet 233 | facet normal -0.187500 0.000000 0.092260 234 | outer loop 235 | vertex 0.369040 0.000000 1.500000 236 | vertex 0.369040 0.250000 1.500000 237 | vertex 0.000002 0.250000 0.750000 238 | endloop 239 | endfacet 240 | facet normal 0.000000 0.500424 -0.000000 241 | outer loop 242 | vertex 0.480490 0.350000 1.500000 243 | vertex 0.155677 0.350000 2.160120 244 | vertex 0.889765 0.350000 2.208880 245 | endloop 246 | endfacet 247 | facet normal 0.000000 0.500426 0.000000 248 | outer loop 249 | vertex 0.889765 0.350000 0.791115 250 | vertex 0.155677 0.350000 0.839879 251 | vertex 0.480490 0.350000 1.500000 252 | endloop 253 | endfacet 254 | facet normal 0.000000 0.500426 0.000000 255 | outer loop 256 | vertex 0.889765 0.350000 2.208880 257 | vertex 1.299040 0.350000 2.820240 258 | vertex 1.708310 0.350000 2.208880 259 | endloop 260 | endfacet 261 | facet normal 0.000000 0.500423 -0.000000 262 | outer loop 263 | vertex 1.299040 0.350000 0.179758 264 | vertex 0.889765 0.350000 0.791115 265 | vertex 1.708310 0.350000 0.791115 266 | endloop 267 | endfacet 268 | facet normal 0.000000 0.500425 -0.000000 269 | outer loop 270 | vertex 2.117590 0.350000 1.500000 271 | vertex 1.708310 0.350000 2.208880 272 | vertex 2.442400 0.350000 2.160120 273 | endloop 274 | endfacet 275 | facet normal 0.000000 0.500427 0.000000 276 | outer loop 277 | vertex 1.708310 0.350000 0.791115 278 | vertex 2.117590 0.350000 1.500000 279 | vertex 2.442400 0.350000 0.839879 280 | endloop 281 | endfacet 282 | facet normal 0.000000 0.580256 -0.000000 283 | outer loop 284 | vertex 0.889765 0.350000 0.791115 285 | vertex 0.480490 0.350000 1.500000 286 | vertex 0.889765 0.350000 2.208880 287 | endloop 288 | endfacet 289 | facet normal 0.000000 1.160504 0.000000 290 | outer loop 291 | vertex 0.889765 0.350000 0.791115 292 | vertex 0.889765 0.350000 2.208880 293 | vertex 1.708310 0.350000 2.208880 294 | endloop 295 | endfacet 296 | facet normal 0.000000 1.160513 0.000000 297 | outer loop 298 | vertex 0.889765 0.350000 0.791115 299 | vertex 1.708310 0.350000 2.208880 300 | vertex 2.117590 0.350000 1.500000 301 | endloop 302 | endfacet 303 | facet normal 0.000000 0.580254 0.000000 304 | outer loop 305 | vertex 0.889765 0.350000 0.791115 306 | vertex 2.117590 0.350000 1.500000 307 | vertex 1.708310 0.350000 0.791115 308 | endloop 309 | endfacet 310 | facet normal -0.075000 0.083587 0.036904 311 | outer loop 312 | vertex 0.000002 0.250000 0.750000 313 | vertex 0.369040 0.250000 1.500000 314 | vertex 0.155677 0.350000 0.839879 315 | endloop 316 | endfacet 317 | facet normal -0.066012 0.073570 0.032481 318 | outer loop 319 | vertex 0.369040 0.250000 1.500000 320 | vertex 0.480490 0.350000 1.500000 321 | vertex 0.155677 0.350000 0.839879 322 | endloop 323 | endfacet 324 | facet normal -0.066012 0.073570 -0.032481 325 | outer loop 326 | vertex 0.369040 0.250000 1.500000 327 | vertex 0.155677 0.350000 2.160120 328 | vertex 0.480490 0.350000 1.500000 329 | endloop 330 | endfacet 331 | facet normal -0.075000 0.083587 -0.036904 332 | outer loop 333 | vertex 0.369040 0.250000 1.500000 334 | vertex 0.000002 0.250000 2.250000 335 | vertex 0.155677 0.350000 2.160120 336 | endloop 337 | endfacet 338 | facet normal -0.005540 0.083588 0.083404 339 | outer loop 340 | vertex 0.000002 0.250000 2.250000 341 | vertex 0.834040 0.250000 2.305400 342 | vertex 0.155677 0.350000 2.160120 343 | endloop 344 | endfacet 345 | facet normal -0.004876 0.073571 0.073409 346 | outer loop 347 | vertex 0.834040 0.250000 2.305400 348 | vertex 0.889765 0.350000 2.208880 349 | vertex 0.155677 0.350000 2.160120 350 | endloop 351 | endfacet 352 | facet normal -0.069460 0.083588 0.046500 353 | outer loop 354 | vertex 0.834040 0.250000 2.305400 355 | vertex 1.299040 0.250000 3.000000 356 | vertex 1.299040 0.350000 2.820240 357 | endloop 358 | endfacet 359 | facet normal -0.061136 0.073571 0.040927 360 | outer loop 361 | vertex 0.834040 0.250000 2.305400 362 | vertex 1.299040 0.350000 2.820240 363 | vertex 0.889765 0.350000 2.208880 364 | endloop 365 | endfacet 366 | facet normal 0.069460 0.083588 0.046500 367 | outer loop 368 | vertex 1.764040 0.250000 2.305400 369 | vertex 1.299040 0.350000 2.820240 370 | vertex 1.299040 0.250000 3.000000 371 | endloop 372 | endfacet 373 | facet normal 0.061136 0.073574 0.040927 374 | outer loop 375 | vertex 1.764040 0.250000 2.305400 376 | vertex 1.708310 0.350000 2.208880 377 | vertex 1.299040 0.350000 2.820240 378 | endloop 379 | endfacet 380 | facet normal 0.005540 0.083588 0.083404 381 | outer loop 382 | vertex 2.598080 0.250000 2.250000 383 | vertex 2.442400 0.350000 2.160120 384 | vertex 1.764040 0.250000 2.305400 385 | endloop 386 | endfacet 387 | facet normal 0.004876 0.073572 0.073409 388 | outer loop 389 | vertex 1.764040 0.250000 2.305400 390 | vertex 2.442400 0.350000 2.160120 391 | vertex 1.708310 0.350000 2.208880 392 | endloop 393 | endfacet 394 | facet normal 0.075000 0.083591 -0.036904 395 | outer loop 396 | vertex 2.229040 0.250000 1.500000 397 | vertex 2.442400 0.350000 2.160120 398 | vertex 2.598080 0.250000 2.250000 399 | endloop 400 | endfacet 401 | facet normal 0.066012 0.073570 -0.032481 402 | outer loop 403 | vertex 2.229040 0.250000 1.500000 404 | vertex 2.117590 0.350000 1.500000 405 | vertex 2.442400 0.350000 2.160120 406 | endloop 407 | endfacet 408 | facet normal 0.075000 0.083591 0.036904 409 | outer loop 410 | vertex 2.598080 0.250000 0.750000 411 | vertex 2.442400 0.350000 0.839879 412 | vertex 2.229040 0.250000 1.500000 413 | endloop 414 | endfacet 415 | facet normal 0.066012 0.073570 0.032481 416 | outer loop 417 | vertex 2.229040 0.250000 1.500000 418 | vertex 2.442400 0.350000 0.839879 419 | vertex 2.117590 0.350000 1.500000 420 | endloop 421 | endfacet 422 | facet normal 0.005540 0.083588 -0.083404 423 | outer loop 424 | vertex 1.764040 0.250000 0.694596 425 | vertex 2.442400 0.350000 0.839879 426 | vertex 2.598080 0.250000 0.750000 427 | endloop 428 | endfacet 429 | facet normal 0.004876 0.073571 -0.073409 430 | outer loop 431 | vertex 1.764040 0.250000 0.694596 432 | vertex 1.708310 0.350000 0.791115 433 | vertex 2.442400 0.350000 0.839879 434 | endloop 435 | endfacet 436 | facet normal 0.061136 0.073573 -0.040927 437 | outer loop 438 | vertex 1.764040 0.250000 0.694596 439 | vertex 1.299040 0.350000 0.179758 440 | vertex 1.708310 0.350000 0.791115 441 | endloop 442 | endfacet 443 | facet normal 0.069460 0.083587 -0.046500 444 | outer loop 445 | vertex 1.299040 0.250000 0.000000 446 | vertex 1.299040 0.350000 0.179758 447 | vertex 1.764040 0.250000 0.694596 448 | endloop 449 | endfacet 450 | facet normal -0.061136 0.073571 -0.040927 451 | outer loop 452 | vertex 0.834040 0.250000 0.694596 453 | vertex 0.889765 0.350000 0.791115 454 | vertex 1.299040 0.350000 0.179758 455 | endloop 456 | endfacet 457 | facet normal -0.069460 0.083587 -0.046500 458 | outer loop 459 | vertex 1.299040 0.250000 0.000000 460 | vertex 0.834040 0.250000 0.694596 461 | vertex 1.299040 0.350000 0.179758 462 | endloop 463 | endfacet 464 | facet normal -0.004876 0.073571 -0.073409 465 | outer loop 466 | vertex 0.834040 0.250000 0.694596 467 | vertex 0.155677 0.350000 0.839879 468 | vertex 0.889765 0.350000 0.791115 469 | endloop 470 | endfacet 471 | facet normal -0.005540 0.083587 -0.083404 472 | outer loop 473 | vertex 0.834040 0.250000 0.694596 474 | vertex 0.000002 0.250000 0.750000 475 | vertex 0.155677 0.350000 0.839879 476 | endloop 477 | endfacet 478 | endsolid Star.stl 479 | -------------------------------------------------------------------------------- /tests/stl_binary/Cube.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolph/numpy-stl/8ad2b16f8a0e611fce8259befb15c7601836e7f0/tests/stl_binary/Cube.stl -------------------------------------------------------------------------------- /tests/stl_binary/CubeInvalidUnicode.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolph/numpy-stl/8ad2b16f8a0e611fce8259befb15c7601836e7f0/tests/stl_binary/CubeInvalidUnicode.stl -------------------------------------------------------------------------------- /tests/stl_binary/HalfDonut.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolph/numpy-stl/8ad2b16f8a0e611fce8259befb15c7601836e7f0/tests/stl_binary/HalfDonut.stl -------------------------------------------------------------------------------- /tests/stl_binary/Moon.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolph/numpy-stl/8ad2b16f8a0e611fce8259befb15c7601836e7f0/tests/stl_binary/Moon.stl -------------------------------------------------------------------------------- /tests/stl_binary/Star.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolph/numpy-stl/8ad2b16f8a0e611fce8259befb15c7601836e7f0/tests/stl_binary/Star.stl -------------------------------------------------------------------------------- /tests/stl_binary/StarWithEmptyHeader.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolph/numpy-stl/8ad2b16f8a0e611fce8259befb15c7601836e7f0/tests/stl_binary/StarWithEmptyHeader.stl -------------------------------------------------------------------------------- /tests/stl_binary/rear_case.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolph/numpy-stl/8ad2b16f8a0e611fce8259befb15c7601836e7f0/tests/stl_binary/rear_case.stl -------------------------------------------------------------------------------- /tests/stl_corruption.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import sys 3 | 4 | import numpy as np 5 | import pytest 6 | 7 | from stl import mesh 8 | 9 | _STL_FILE = """ 10 | solid test.stl 11 | facet normal -0.014565 0.073223 -0.002897 12 | outer loop 13 | vertex 0.399344 0.461940 1.044090 14 | vertex 0.500000 0.500000 1.500000 15 | vertex 0.576120 0.500000 1.117320 16 | endloop 17 | endfacet 18 | endsolid test.stl 19 | """.lstrip() 20 | 21 | 22 | def test_valid_ascii(tmpdir, speedups): 23 | tmp_file = tmpdir.join('tmp.stl') 24 | with tmp_file.open('w+') as fh: 25 | fh.write(_STL_FILE) 26 | fh.seek(0) 27 | mesh.Mesh.from_file(str(tmp_file), fh=fh, speedups=speedups) 28 | 29 | 30 | def test_end_solid(tmpdir, speedups): 31 | tmp_file = tmpdir.join('tmp.stl') 32 | with tmp_file.open('w+') as fh: 33 | fh.write(_STL_FILE.replace('endsolid', 'end solid')) 34 | fh.seek(0) 35 | mesh.Mesh.from_file(str(tmp_file), fh=fh, speedups=speedups) 36 | 37 | 38 | def test_ascii_with_missing_name(tmpdir, speedups): 39 | tmp_file = tmpdir.join('tmp.stl') 40 | with tmp_file.open('w+') as fh: 41 | # Split the file into lines 42 | lines = _STL_FILE.splitlines() 43 | 44 | # Remove everything except solid 45 | lines[0] = lines[0].split()[0] 46 | 47 | # Join the lines to test files that start with solid without space 48 | fh.write('\n'.join(lines)) 49 | fh.seek(0) 50 | mesh.Mesh.from_file(str(tmp_file), fh=fh, speedups=speedups) 51 | 52 | 53 | def test_ascii_with_blank_lines(tmpdir, speedups): 54 | _stl_file = """ 55 | solid test.stl 56 | 57 | 58 | facet normal -0.014565 0.073223 -0.002897 59 | 60 | outer loop 61 | 62 | vertex 0.399344 0.461940 1.044090 63 | vertex 0.500000 0.500000 1.500000 64 | 65 | vertex 0.576120 0.500000 1.117320 66 | 67 | endloop 68 | 69 | endfacet 70 | 71 | endsolid test.stl 72 | """.lstrip() 73 | 74 | tmp_file = tmpdir.join('tmp.stl') 75 | with tmp_file.open('w+') as fh: 76 | fh.write(_stl_file) 77 | fh.seek(0) 78 | mesh.Mesh.from_file(str(tmp_file), fh=fh, speedups=speedups) 79 | 80 | 81 | def test_incomplete_ascii_file(tmpdir, speedups): 82 | tmp_file = tmpdir.join('tmp.stl') 83 | with tmp_file.open('w+') as fh: 84 | fh.write('solid some_file.stl') 85 | fh.seek(0) 86 | with pytest.raises(AssertionError): 87 | mesh.Mesh.from_file(str(tmp_file), fh=fh, speedups=speedups) 88 | 89 | for offset in (-20, 82, 100): 90 | with tmp_file.open('w+') as fh: 91 | fh.write(_STL_FILE[:-offset]) 92 | fh.seek(0) 93 | with pytest.raises(AssertionError): 94 | mesh.Mesh.from_file(str(tmp_file), fh=fh, speedups=speedups) 95 | 96 | 97 | def test_corrupt_ascii_file(tmpdir, speedups): 98 | tmp_file = tmpdir.join('tmp.stl') 99 | with tmp_file.open('w+') as fh: 100 | fh.write(_STL_FILE) 101 | fh.seek(40) 102 | print('####\n' * 100, file=fh) 103 | fh.seek(0) 104 | if speedups and sys.version_info.major != 2: 105 | with pytest.raises(AssertionError): 106 | mesh.Mesh.from_file(str(tmp_file), fh=fh, speedups=speedups) 107 | 108 | with tmp_file.open('w+') as fh: 109 | fh.write(_STL_FILE) 110 | fh.seek(40) 111 | print(' ' * 100, file=fh) 112 | fh.seek(80) 113 | fh.write(struct.pack(' 84 49 | assert fh.getvalue()[84:] == mesh_.data.tobytes() 50 | 51 | read = mesh.Mesh.from_file('nameless', fh=io.BytesIO(fh.getvalue())) 52 | assert np.allclose(read.vectors, mesh_.vectors) 53 | 54 | 55 | def test_binary_file(): 56 | list(mesh.Mesh.from_multi_file(TESTS_PATH / 'stl_tests' / 'triamid.stl')) 57 | -------------------------------------------------------------------------------- /tests/test_commandline.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import sys 3 | 4 | from stl import main 5 | 6 | 7 | def test_main(ascii_file, binary_file, tmpdir, speedups): 8 | original_argv = sys.argv[:] 9 | args_pre = ['stl'] 10 | args_post = [str(tmpdir.join('output.stl'))] 11 | 12 | if not speedups: 13 | args_pre.append('-s') 14 | 15 | try: 16 | sys.argv[:] = [*args_pre, ascii_file, *args_post] 17 | main.main() 18 | sys.argv[:] = [*args_pre, '-r', ascii_file, *args_post] 19 | main.main() 20 | sys.argv[:] = [*args_pre, '-a', binary_file, *args_post] 21 | main.main() 22 | sys.argv[:] = [*args_pre, '-b', ascii_file, *args_post] 23 | main.main() 24 | finally: 25 | sys.argv[:] = original_argv 26 | 27 | 28 | def test_args(ascii_file, tmpdir): 29 | parser = main._get_parser('') 30 | 31 | def _get_name(*args) -> str: 32 | return str(main._get_name(parser.parse_args(list(map(str, args))))) 33 | 34 | assert _get_name('--name', 'foobar') == 'foobar' 35 | assert _get_name('-', tmpdir.join('binary.stl')).endswith('binary.stl') 36 | assert _get_name(ascii_file, '-').endswith('HalfDonut.stl') 37 | assert _get_name('-', '-') 38 | 39 | 40 | def test_ascii(binary_file, tmpdir, speedups): 41 | original_argv = sys.argv[:] 42 | try: 43 | sys.argv[:] = [ 44 | 'stl', 45 | '-s' if not speedups else '', 46 | binary_file, 47 | str(tmpdir.join('ascii.stl')), 48 | ] 49 | with contextlib.suppress(SystemExit): 50 | main.to_ascii() 51 | finally: 52 | sys.argv[:] = original_argv 53 | 54 | 55 | def test_binary(ascii_file, tmpdir, speedups): 56 | original_argv = sys.argv[:] 57 | try: 58 | sys.argv[:] = [ 59 | 'stl', 60 | '-s' if not speedups else '', 61 | ascii_file, 62 | str(tmpdir.join('binary.stl')), 63 | ] 64 | with contextlib.suppress(SystemExit): 65 | main.to_binary() 66 | finally: 67 | sys.argv[:] = original_argv 68 | -------------------------------------------------------------------------------- /tests/test_convert.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | 3 | import py.path # type: ignore[import] 4 | import pytest 5 | 6 | from stl import stl 7 | 8 | 9 | def _test_conversion(from_, to, mode, speedups): 10 | # For some reason the test fails when using pathlib instead of py.path 11 | from_ = py.path.local(from_) 12 | to = py.path.local(to) 13 | 14 | for name in from_.listdir(): 15 | source_file = from_.join(name) 16 | expected_file = to.join(name) 17 | if not expected_file.exists(): 18 | continue 19 | 20 | mesh = stl.StlMesh(source_file, speedups=speedups) 21 | with open(str(expected_file), 'rb') as expected_fh: 22 | expected = expected_fh.read() 23 | # For binary files, skip the header 24 | if mode is stl.BINARY: 25 | expected = expected[80:] 26 | 27 | with tempfile.TemporaryFile() as dest_fh: 28 | mesh.save(name, dest_fh, mode) 29 | # Go back to the beginning to read 30 | dest_fh.seek(0) 31 | dest = dest_fh.read() 32 | # For binary files, skip the header 33 | if mode is stl.BINARY: 34 | dest = dest[80:] 35 | 36 | assert dest.strip() == expected.strip() 37 | 38 | 39 | def test_ascii_to_binary(ascii_path, binary_path, speedups): 40 | _test_conversion( 41 | ascii_path, binary_path, mode=stl.BINARY, speedups=speedups 42 | ) 43 | 44 | 45 | def test_binary_to_ascii(ascii_path, binary_path, speedups): 46 | _test_conversion( 47 | binary_path, ascii_path, mode=stl.ASCII, speedups=speedups 48 | ) 49 | 50 | 51 | def test_stl_mesh(ascii_file, tmpdir, speedups): 52 | tmp_file = tmpdir.join('tmp.stl') 53 | 54 | mesh = stl.StlMesh(ascii_file, speedups=speedups) 55 | with pytest.raises(ValueError): 56 | mesh.save(filename=str(tmp_file), mode='test') 57 | # type: ignore[reportArgumentType] 58 | 59 | mesh.save(str(tmp_file)) 60 | mesh.save(str(tmp_file), update_normals=False) 61 | -------------------------------------------------------------------------------- /tests/test_line_endings.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | import pytest 4 | 5 | from stl import mesh 6 | 7 | FILES_PATH = pathlib.Path(__file__).parent / 'stl_tests' 8 | 9 | 10 | @pytest.mark.parametrize('line_ending', ['dos', 'unix']) 11 | def test_line_endings(line_ending, speedups): 12 | filename = FILES_PATH / (f'{line_ending}.stl') 13 | mesh.Mesh.from_file(filename, speedups=speedups) 14 | -------------------------------------------------------------------------------- /tests/test_mesh.py: -------------------------------------------------------------------------------- 1 | # type: ignore[reportAttributeAccessIssue] 2 | import numpy as np 3 | 4 | from stl.base import BaseMesh, RemoveDuplicates 5 | from stl.mesh import Mesh 6 | 7 | from . import utils 8 | 9 | 10 | def test_units_1d(): 11 | data = np.zeros(1, dtype=Mesh.dtype) 12 | data['vectors'][0] = np.array([[0, 0, 0], [1, 0, 0], [2, 0, 0]]) 13 | 14 | mesh = Mesh(data, remove_empty_areas=False) 15 | mesh.update_units() 16 | 17 | assert mesh.areas == 0 18 | assert np.allclose(mesh.centroids, [[1, 0, 0]]) 19 | utils.array_equals(mesh.normals, [0, 0, 0]) 20 | utils.array_equals(mesh.units, [0, 0, 0]) 21 | utils.array_equals(mesh.get_unit_normals(), [0, 0, 0]) 22 | 23 | 24 | def test_units_2d(): 25 | data = np.zeros(2, dtype=Mesh.dtype) 26 | data['vectors'][0] = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0]]) 27 | data['vectors'][1] = np.array([[1, 0, 0], [0, 1, 0], [1, 1, 0]]) 28 | 29 | mesh = Mesh(data, remove_empty_areas=False) 30 | mesh.update_units() 31 | 32 | assert np.allclose(mesh.areas, [0.5, 0.5]) 33 | assert np.allclose(mesh.centroids, [[1 / 3, 1 / 3, 0], [2 / 3, 2 / 3, 0]]) 34 | assert np.allclose(mesh.normals, [[0.0, 0.0, 1.0], [0.0, 0.0, -1.0]]) 35 | assert np.allclose(mesh.units, [[0, 0, 1], [0, 0, -1]]) 36 | assert np.allclose( 37 | mesh.get_unit_normals(), [[0.0, 0.0, 1.0], [0.0, 0.0, -1.0]] 38 | ) 39 | 40 | 41 | def test_units_3d(): 42 | data = np.zeros(1, dtype=Mesh.dtype) 43 | data['vectors'][0] = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 1.0]]) 44 | 45 | mesh = Mesh(data, remove_empty_areas=False) 46 | mesh.update_units() 47 | 48 | assert (mesh.areas - 2**0.5) < 0.0001 49 | assert np.allclose(mesh.centroids, [1 / 3, 1 / 3, 1 / 3]) 50 | assert np.allclose(mesh.normals, [0.0, -1.0, 1.0]) 51 | assert np.allclose(mesh.units[0], [0.0, -0.70710677, 0.70710677]) 52 | assert np.allclose(np.linalg.norm(mesh.units, axis=-1), 1) 53 | assert np.allclose(mesh.get_unit_normals(), [0.0, -0.70710677, 0.70710677]) 54 | 55 | 56 | def test_duplicate_polygons(): 57 | data = np.zeros(6, dtype=Mesh.dtype) 58 | data['vectors'][0] = np.array([[1, 0, 0], [0, 0, 0], [0, 0, 0]]) 59 | data['vectors'][1] = np.array([[2, 0, 0], [0, 0, 0], [0, 0, 0]]) 60 | data['vectors'][2] = np.array([[0, 0, 0], [0, 0, 0], [0, 0, 0]]) 61 | data['vectors'][3] = np.array([[2, 0, 0], [0, 0, 0], [0, 0, 0]]) 62 | data['vectors'][4] = np.array([[1, 0, 0], [0, 0, 0], [0, 0, 0]]) 63 | data['vectors'][5] = np.array([[0, 0, 0], [0, 0, 0], [0, 0, 0]]) 64 | 65 | mesh = Mesh(data) 66 | assert mesh.data.size == 6 67 | 68 | mesh = Mesh(data, remove_duplicate_polygons=0) 69 | assert mesh.data.size == 6 70 | 71 | mesh = Mesh(data, remove_duplicate_polygons=False) 72 | assert mesh.data.size == 6 73 | 74 | mesh = Mesh(data, remove_duplicate_polygons=None) 75 | assert mesh.data.size == 6 76 | 77 | mesh = Mesh(data, remove_duplicate_polygons=RemoveDuplicates.NONE) 78 | assert mesh.data.size == 6 79 | 80 | mesh = Mesh(data, remove_duplicate_polygons=RemoveDuplicates.SINGLE) 81 | assert mesh.data.size == 3 82 | 83 | mesh = Mesh(data, remove_duplicate_polygons=True) 84 | assert mesh.data.size == 3 85 | 86 | assert np.allclose( 87 | mesh.vectors[0], np.array([[1, 0, 0], [0, 0, 0], [0, 0, 0]]) 88 | ) 89 | assert np.allclose( 90 | mesh.vectors[1], np.array([[2, 0, 0], [0, 0, 0], [0, 0, 0]]) 91 | ) 92 | assert np.allclose( 93 | mesh.vectors[2], np.array([[0, 0, 0], [0, 0, 0], [0, 0, 0]]) 94 | ) 95 | 96 | mesh = Mesh(data, remove_duplicate_polygons=RemoveDuplicates.ALL) 97 | assert mesh.data.size == 3 98 | 99 | assert np.allclose( 100 | mesh.vectors[0], np.array([[1, 0, 0], [0, 0, 0], [0, 0, 0]]) 101 | ) 102 | assert np.allclose( 103 | mesh.vectors[1], np.array([[2, 0, 0], [0, 0, 0], [0, 0, 0]]) 104 | ) 105 | assert np.allclose( 106 | mesh.vectors[2], np.array([[0, 0, 0], [0, 0, 0], [0, 0, 0]]) 107 | ) 108 | 109 | 110 | def test_remove_all_duplicate_polygons(): 111 | data = np.zeros(5, dtype=Mesh.dtype) 112 | data['vectors'][0] = np.array([[0, 0, 0], [0, 0, 0], [0, 0, 0]]) 113 | data['vectors'][1] = np.array([[1, 0, 0], [0, 0, 0], [0, 0, 0]]) 114 | data['vectors'][2] = np.array([[2, 0, 0], [0, 0, 0], [0, 0, 0]]) 115 | data['vectors'][3] = np.array([[3, 0, 0], [0, 0, 0], [0, 0, 0]]) 116 | data['vectors'][4] = np.array([[3, 0, 0], [0, 0, 0], [0, 0, 0]]) 117 | 118 | mesh = Mesh(data, remove_duplicate_polygons=False) 119 | assert mesh.data.size == 5 120 | Mesh.remove_duplicate_polygons(mesh.data, RemoveDuplicates.NONE) 121 | 122 | mesh = Mesh(data, remove_duplicate_polygons=RemoveDuplicates.ALL) 123 | assert mesh.data.size == 3 124 | 125 | assert ( 126 | mesh.vectors[0] == np.array([[0, 0, 0], [0, 0, 0], [0, 0, 0]]) 127 | ).all() 128 | assert ( 129 | mesh.vectors[1] == np.array([[1, 0, 0], [0, 0, 0], [0, 0, 0]]) 130 | ).all() 131 | assert ( 132 | mesh.vectors[2] == np.array([[2, 0, 0], [0, 0, 0], [0, 0, 0]]) 133 | ).all() 134 | 135 | 136 | def test_empty_areas(): 137 | data = np.zeros(3, dtype=Mesh.dtype) 138 | data['vectors'][0] = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0]]) 139 | data['vectors'][1] = np.array([[1, 0, 0], [0, 1, 0], [1, 0, 0]]) 140 | data['vectors'][2] = np.array([[1, 0, 0], [0, 1, 0], [1, 0, 0]]) 141 | 142 | mesh = Mesh(data, calculate_normals=False, remove_empty_areas=False) 143 | assert mesh.data.size == 3 144 | 145 | # Test the normals recalculation which also calculates the areas by default 146 | mesh.areas[1] = 1 147 | mesh.areas[2] = 2 148 | assert np.allclose(mesh.areas, [[0.5], [1.0], [2.0]]) 149 | 150 | mesh.centroids[1] = [1, 2, 3] 151 | mesh.centroids[2] = [4, 5, 6] 152 | assert np.allclose( 153 | mesh.centroids, [[1 / 3, 1 / 3, 0], [1, 2, 3], [4, 5, 6]] 154 | ) 155 | 156 | mesh.update_normals(update_areas=False, update_centroids=False) 157 | assert np.allclose(mesh.areas, [[0.5], [1.0], [2.0]]) 158 | assert np.allclose( 159 | mesh.centroids, [[1 / 3, 1 / 3, 0], [1, 2, 3], [4, 5, 6]] 160 | ) 161 | 162 | mesh.update_normals(update_areas=True, update_centroids=True) 163 | assert np.allclose(mesh.areas, [[0.5], [0.0], [0.0]]) 164 | assert np.allclose( 165 | mesh.centroids, 166 | [[1 / 3, 1 / 3, 0], [2 / 3, 1 / 3, 0], [2 / 3, 1 / 3, 0]], 167 | ) 168 | 169 | mesh = Mesh(data, remove_empty_areas=True) 170 | assert mesh.data.size == 1 171 | 172 | 173 | def test_base_mesh(): 174 | data = np.zeros(10, dtype=BaseMesh.dtype) 175 | mesh = BaseMesh(data, remove_empty_areas=False) 176 | # Increment vector 0 item 0 177 | mesh.v0[0] += 1 178 | mesh.v1[0] += 2 179 | 180 | # Check item 0 (contains v0, v1 and v2) 181 | assert ( 182 | mesh[0] == np.array( 183 | [1.0, 1.0, 1.0, 2.0, 2.0, 2.0, 0.0, 0.0, 0.0], dtype=np.float32 184 | ) 185 | ).all() 186 | assert ( 187 | mesh.vectors[0] == np.array( 188 | [[1.0, 1.0, 1.0], [2.0, 2.0, 2.0], [0.0, 0.0, 0.0]], 189 | dtype=np.float32, 190 | ) 191 | ).all() 192 | assert (mesh.v0[0] == np.array([1.0, 1.0, 1.0], dtype=np.float32)).all() 193 | assert ( 194 | mesh.points[0] == np.array( 195 | [1.0, 1.0, 1.0, 2.0, 2.0, 2.0, 0.0, 0.0, 0.0], dtype=np.float32 196 | ) 197 | ).all() 198 | assert (mesh.x[0] == np.array([1.0, 2.0, 0.0], dtype=np.float32)).all() 199 | 200 | mesh[0] = 3 201 | assert ( 202 | mesh[0] == np.array( 203 | [3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0], dtype=np.float32 204 | ) 205 | ).all() 206 | 207 | assert len(mesh) == len(list(mesh)) 208 | assert (mesh.min_ < mesh.max_).all() 209 | mesh.update_normals() 210 | assert mesh.units.sum() == 0.0 211 | mesh.v0[:] = mesh.v1[:] = mesh.v2[:] = 0 212 | assert mesh.points.sum() == 0.0 213 | -------------------------------------------------------------------------------- /tests/test_mesh_properties.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | 4 | from stl import stl 5 | 6 | tolerance = 1e-5 7 | 8 | 9 | def close(a, b): 10 | return np.allclose(a, b, atol=tolerance) 11 | 12 | 13 | def test_mass_properties_for_half_donut(binary_ascii_path, speedups): 14 | """ 15 | Checks the results of method get_mass_properties() on 16 | STL ASCII and binary files HalfDonut.stl 17 | One checks the results obtained with stl 18 | with the ones obtained with meshlab 19 | """ 20 | filename = binary_ascii_path / 'HalfDonut.stl' 21 | mesh = stl.StlMesh(str(filename), speedups=speedups) 22 | volume, cog, inertia = mesh.get_mass_properties() 23 | assert close([volume], [2.343149]) 24 | assert close(cog, [1.500001, 0.209472, 1.500001]) 25 | assert close( 26 | inertia, 27 | [ 28 | [+1.390429, +0.000000, +0.000000], 29 | [+0.000000, +2.701025, +0.000000], 30 | [+0.000000, +0.000000, +1.390429], 31 | ], 32 | ) 33 | 34 | 35 | def test_mass_properties_for_moon(binary_ascii_path, speedups): 36 | """ 37 | Checks the results of method get_mass_properties() on 38 | STL ASCII and binary files Moon.stl 39 | One checks the results obtained with stl 40 | with the ones obtained with meshlab 41 | """ 42 | filename = binary_ascii_path / 'Moon.stl' 43 | mesh = stl.StlMesh(str(filename), speedups=speedups) 44 | volume, cog, inertia = mesh.get_mass_properties() 45 | assert close([volume], [0.888723]) 46 | assert close(cog, [0.906913, 0.170731, 1.500001]) 47 | assert close( 48 | inertia, 49 | [ 50 | [+0.562097, -0.000457, +0.000000], 51 | [-0.000457, +0.656851, +0.000000], 52 | [+0.000000, +0.000000, +0.112465], 53 | ], 54 | ) 55 | 56 | 57 | @pytest.mark.parametrize('filename', ('Star.stl', 'StarWithEmptyHeader.stl')) 58 | def test_mass_properties_for_star(binary_ascii_path, filename, speedups): 59 | """ 60 | Checks the results of method get_mass_properties() on 61 | STL ASCII and binary files Star.stl and 62 | STL binary file StarWithEmptyHeader.stl (with no header) 63 | One checks the results obtained with stl 64 | with the ones obtained with meshlab 65 | """ 66 | filename = binary_ascii_path / filename 67 | if not filename.exists(): 68 | pytest.skip('STL file does not exist') 69 | mesh = stl.StlMesh(str(filename), speedups=speedups) 70 | volume, cog, inertia = mesh.get_mass_properties() 71 | assert close([volume], [1.416599]) 72 | assert close(cog, [1.299040, 0.170197, 1.499999]) 73 | assert close( 74 | inertia, 75 | [ 76 | [+0.509549, +0.000000, -0.000000], 77 | [+0.000000, +0.991236, +0.000000], 78 | [-0.000000, +0.000000, +0.509550], 79 | ], 80 | ) 81 | 82 | 83 | def test_mass_properties_for_half_donut_with_density( 84 | binary_ascii_path, speedups 85 | ): 86 | """ 87 | Checks the results of method get_mass_properties_with_density() on 88 | STL ASCII and binary files HalfDonut.stl 89 | One checks the results obtained with stl 90 | with the ones obtained with meshlab 91 | """ 92 | filename = binary_ascii_path / 'HalfDonut.stl' 93 | mesh = stl.StlMesh(str(filename), speedups=speedups) 94 | volume, mass, cog, inertia = mesh.get_mass_properties_with_density(1.23) 95 | 96 | assert close([mass], [2.882083302268982]) 97 | assert close([volume], [2.343149026234945]) 98 | assert close(cog, [1.500001, 0.209472, 1.500001]) 99 | print('inertia') 100 | np.set_printoptions(suppress=True) 101 | print(inertia) 102 | assert close( 103 | inertia, 104 | [ 105 | [+1.71022851, +0.00000001, -0.00000011], 106 | [+0.00000001, +3.32226227, +0.00000002], 107 | [-0.00000011, +0.00000002, +1.71022859], 108 | ], 109 | ) 110 | 111 | 112 | @pytest.mark.parametrize('filename, expected_result', [ 113 | ('Cube.stl', True), 114 | ('HalfDonut.stl', False), 115 | ('Moon.stl', False), 116 | ('Star.stl', False), 117 | ]) 118 | def test_is_convex( 119 | binary_ascii_path, speedups, filename, expected_result 120 | ): 121 | """Check the is_convex() method on various STL files.""" 122 | filepath = binary_ascii_path / filename 123 | mesh = stl.StlMesh(str(filepath), speedups=speedups) 124 | assert mesh.is_convex() == expected_result 125 | -------------------------------------------------------------------------------- /tests/test_multiple.py: -------------------------------------------------------------------------------- 1 | import zipfile 2 | 3 | import pytest 4 | 5 | from stl import mesh 6 | 7 | _STL_FILE = b""" 8 | solid test.stl 9 | facet normal -0.014565 0.073223 -0.002897 10 | outer loop 11 | vertex 0.399344 0.461940 1.044090 12 | vertex 0.500000 0.500000 1.500000 13 | vertex 0.576120 0.500000 1.117320 14 | endloop 15 | endfacet 16 | endsolid test.stl 17 | """ 18 | 19 | 20 | def test_single_stl(tmpdir, speedups): 21 | tmp_file = tmpdir / 'tmp.stl' 22 | with tmp_file.open('wb+') as fh: 23 | fh.write(_STL_FILE) 24 | fh.seek(0) 25 | for m in mesh.Mesh.from_multi_file( 26 | str(tmp_file), fh=fh, speedups=speedups 27 | ): 28 | pass 29 | 30 | 31 | def test_multiple_stl(tmpdir, speedups): 32 | tmp_file = tmpdir / 'tmp.stl' 33 | with tmp_file.open('wb+') as fh: 34 | for _ in range(10): 35 | fh.write(_STL_FILE) 36 | fh.seek(0) 37 | i = 0 38 | for i, m in enumerate( 39 | mesh.Mesh.from_multi_file(str(tmp_file), fh=fh, speedups=speedups) 40 | ): 41 | assert m.name == b'test.stl' 42 | 43 | assert i == 9 44 | 45 | 46 | def test_single_stl_file(tmpdir, speedups): 47 | tmp_file = tmpdir / 'tmp.stl' 48 | with tmp_file.open('wb+') as fh: 49 | fh.write(_STL_FILE) 50 | fh.seek(0) 51 | for m in mesh.Mesh.from_multi_file(str(tmp_file), speedups=speedups): 52 | pass 53 | 54 | 55 | def test_multiple_stl_file(tmpdir, speedups): 56 | tmp_file = tmpdir / 'tmp.stl' 57 | with tmp_file.open('wb+') as fh: 58 | for _ in range(10): 59 | fh.write(_STL_FILE) 60 | 61 | fh.seek(0) 62 | i = -1 63 | for i, m in enumerate( 64 | mesh.Mesh.from_multi_file(str(tmp_file), speedups=speedups) 65 | ): 66 | assert m.name == b'test.stl' 67 | 68 | assert i == 9 69 | 70 | 71 | def test_multiple_stl_files(tmpdir, speedups): 72 | tmp_file = tmpdir / 'tmp.stl' 73 | with tmp_file.open('wb+') as fh: 74 | fh.write(_STL_FILE) 75 | fh.seek(0) 76 | 77 | filenames = [str(tmp_file)] * 10 78 | 79 | m = mesh.Mesh.from_files(filenames, speedups=speedups) 80 | assert m.data.size == 10 81 | 82 | 83 | def test_3mf_file(three_mf_path): 84 | for m in mesh.Mesh.from_3mf_file(three_mf_path / 'Moon.3mf'): 85 | print(m) 86 | 87 | 88 | def test_3mf_missing_file(three_mf_path): 89 | with pytest.raises(FileNotFoundError): 90 | for m in mesh.Mesh.from_3mf_file(three_mf_path / 'some_file.3mf'): 91 | print(m) 92 | 93 | 94 | def test_3mf_wrong_file(ascii_file): 95 | with pytest.raises(zipfile.BadZipfile): 96 | for m in mesh.Mesh.from_3mf_file(ascii_file): 97 | print(m) 98 | -------------------------------------------------------------------------------- /tests/test_rotate.py: -------------------------------------------------------------------------------- 1 | # type: ignore[reportAttributeAccessIssue] 2 | import math 3 | 4 | import numpy as np 5 | import pytest 6 | 7 | from stl.mesh import Mesh 8 | 9 | from . import utils 10 | 11 | 12 | def test_rotation(): 13 | # Create 6 faces of a cube 14 | data = np.zeros(6, dtype=Mesh.dtype) 15 | 16 | # Top of the cube 17 | data['vectors'][0] = np.array([[0, 1, 1], [1, 0, 1], [0, 0, 1]]) 18 | data['vectors'][1] = np.array([[1, 0, 1], [0, 1, 1], [1, 1, 1]]) 19 | # Right face 20 | data['vectors'][2] = np.array([[1, 0, 0], [1, 0, 1], [1, 1, 0]]) 21 | data['vectors'][3] = np.array([[1, 1, 1], [1, 0, 1], [1, 1, 0]]) 22 | # Left face 23 | data['vectors'][4] = np.array([[0, 0, 0], [1, 0, 0], [1, 0, 1]]) 24 | data['vectors'][5] = np.array([[0, 0, 0], [0, 0, 1], [1, 0, 1]]) 25 | 26 | mesh = Mesh(data, remove_empty_areas=False) 27 | 28 | # Since the cube faces are from 0 to 1 we can move it to the middle by 29 | # substracting .5 30 | data['vectors'] -= 0.5 31 | 32 | # Rotate 90 degrees over the X axis followed by the Y axis followed by the 33 | # X axis 34 | mesh.rotate([0.5, 0.0, 0.0], math.radians(90)) 35 | mesh.rotate([0.0, 0.5, 0.0], math.radians(90)) 36 | mesh.rotate([0.5, 0.0, 0.0], math.radians(90)) 37 | 38 | # Since the cube faces are from 0 to 1 we can move it to the middle by 39 | # substracting .5 40 | data['vectors'] += 0.5 41 | 42 | # We use a slightly higher absolute tolerance here, for ppc64le 43 | # https://github.com/WoLpH/numpy-stl/issues/78 44 | assert np.allclose( 45 | mesh.vectors, 46 | np.array( 47 | [ 48 | [[1, 0, 0], [0, 1, 0], [0, 0, 0]], 49 | [[0, 1, 0], [1, 0, 0], [1, 1, 0]], 50 | [[0, 1, 1], [0, 1, 0], [1, 1, 1]], 51 | [[1, 1, 0], [0, 1, 0], [1, 1, 1]], 52 | [[0, 0, 1], [0, 1, 1], [0, 1, 0]], 53 | [[0, 0, 1], [0, 0, 0], [0, 1, 0]], 54 | ] 55 | ), 56 | atol=1e-07, 57 | ) 58 | 59 | 60 | def test_rotation_over_point(): 61 | # Create a single face 62 | data = np.zeros(1, dtype=Mesh.dtype) 63 | 64 | data['vectors'][0] = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) 65 | 66 | mesh = Mesh(data, remove_empty_areas=False) 67 | 68 | mesh.rotate([1, 0, 0], math.radians(180), point=[1, 2, 3]) 69 | utils.array_equals( 70 | mesh.vectors, 71 | np.array([[[1.0, 4.0, 6.0], [0.0, 3.0, 6.0], [0.0, 4.0, 5.0]]]), 72 | ) 73 | 74 | mesh.rotate([1, 0, 0], math.radians(-180), point=[1, 2, 3]) 75 | utils.array_equals( 76 | mesh.vectors, np.array([[[1, 0, 0], [0, 1, 0], [0, 0, 1]]]) 77 | ) 78 | 79 | mesh.rotate([1, 0, 0], math.radians(180), point=0.0) 80 | utils.array_equals( 81 | mesh.vectors, 82 | np.array([[[1.0, 0.0, -0.0], [0.0, -1.0, -0.0], [0.0, 0.0, -1.0]]]), 83 | ) 84 | 85 | with pytest.raises(TypeError): 86 | mesh.rotate([1, 0, 0], math.radians(180), point='x') 87 | 88 | 89 | def test_double_rotation(): 90 | # Create a single face 91 | data = np.zeros(1, dtype=Mesh.dtype) 92 | 93 | data['vectors'][0] = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) 94 | 95 | mesh = Mesh(data, remove_empty_areas=False) 96 | 97 | rotation_matrix = mesh.rotation_matrix([1, 0, 0], math.radians(180)) 98 | combined_rotation_matrix = np.dot(rotation_matrix, rotation_matrix) 99 | 100 | mesh.rotate_using_matrix(combined_rotation_matrix) 101 | utils.array_equals( 102 | mesh.vectors, 103 | np.array([[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]]), 104 | ) 105 | 106 | 107 | def test_no_rotation(): 108 | # Create a single face 109 | data = np.zeros(1, dtype=Mesh.dtype) 110 | 111 | data['vectors'][0] = np.array([[0, 1, 1], [1, 0, 1], [0, 0, 1]]) 112 | 113 | mesh = Mesh(data, remove_empty_areas=False) 114 | 115 | # Rotate by 0 degrees 116 | mesh.rotate([0.5, 0.0, 0.0], math.radians(0)) 117 | assert np.allclose( 118 | mesh.vectors, np.array([[[0, 1, 1], [1, 0, 1], [0, 0, 1]]]) 119 | ) 120 | 121 | # Use a zero rotation matrix 122 | mesh.rotate([0.0, 0.0, 0.0], math.radians(90)) 123 | assert np.allclose( 124 | mesh.vectors, np.array([[[0, 1, 1], [1, 0, 1], [0, 0, 1]]]) 125 | ) 126 | 127 | 128 | def test_no_translation(): 129 | # Create a single face 130 | data = np.zeros(1, dtype=Mesh.dtype) 131 | data['vectors'][0] = np.array([[0, 1, 1], [1, 0, 1], [0, 0, 1]]) 132 | 133 | mesh = Mesh(data, remove_empty_areas=False) 134 | assert np.allclose( 135 | mesh.vectors, np.array([[[0, 1, 1], [1, 0, 1], [0, 0, 1]]]) 136 | ) 137 | 138 | # Translate mesh with a zero vector 139 | mesh.translate([0.0, 0.0, 0.0]) 140 | assert np.allclose( 141 | mesh.vectors, np.array([[[0, 1, 1], [1, 0, 1], [0, 0, 1]]]) 142 | ) 143 | 144 | 145 | def test_translation(): 146 | # Create a single face 147 | data = np.zeros(1, dtype=Mesh.dtype) 148 | data['vectors'][0] = np.array([[0, 1, 1], [1, 0, 1], [0, 0, 1]]) 149 | 150 | mesh = Mesh(data, remove_empty_areas=False) 151 | assert np.allclose( 152 | mesh.vectors, np.array([[[0, 1, 1], [1, 0, 1], [0, 0, 1]]]) 153 | ) 154 | 155 | # Translate mesh with vector [1, 2, 3] 156 | mesh.translate([1.0, 2.0, 3.0]) 157 | assert np.allclose( 158 | mesh.vectors, np.array([[[1, 3, 4], [2, 2, 4], [1, 2, 4]]]) 159 | ) 160 | 161 | 162 | def test_no_transformation(): 163 | # Create a single face 164 | data = np.zeros(1, dtype=Mesh.dtype) 165 | data['vectors'][0] = np.array([[0, 1, 1], [1, 0, 1], [0, 0, 1]]) 166 | 167 | mesh = Mesh(data, remove_empty_areas=False) 168 | assert np.allclose( 169 | mesh.vectors, np.array([[[0, 1, 1], [1, 0, 1], [0, 0, 1]]]) 170 | ) 171 | 172 | # Transform mesh with identity matrix 173 | mesh.transform(np.eye(4)) 174 | assert np.allclose( 175 | mesh.vectors, np.array([[[0, 1, 1], [1, 0, 1], [0, 0, 1]]]) 176 | ) 177 | assert np.allclose(mesh.areas, 0.5) 178 | 179 | 180 | def test_transformation(): 181 | # Create a single face 182 | data = np.zeros(1, dtype=Mesh.dtype) 183 | data['vectors'][0] = np.array([[0, 1, 1], [1, 0, 1], [0, 0, 1]]) 184 | 185 | mesh = Mesh(data, remove_empty_areas=False) 186 | assert np.allclose( 187 | mesh.vectors, np.array([[[0, 1, 1], [1, 0, 1], [0, 0, 1]]]) 188 | ) 189 | 190 | # Transform mesh with identity matrix 191 | tr = np.zeros((4, 4)) 192 | tr[0:3, 0:3] = Mesh.rotation_matrix([0, 0, 1], 0.5 * np.pi) 193 | tr[0:3, 3] = [1, 2, 3] 194 | mesh.transform(tr) 195 | assert np.allclose( 196 | mesh.vectors, np.array([[[0, 2, 4], [1, 3, 4], [1, 2, 4]]]) 197 | ) 198 | assert np.allclose(mesh.areas, 0.5) 199 | -------------------------------------------------------------------------------- /tests/tmp/test_args_False_0/binary.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolph/numpy-stl/8ad2b16f8a0e611fce8259befb15c7601836e7f0/tests/tmp/test_args_False_0/binary.stl -------------------------------------------------------------------------------- /tests/tmp/test_args_False_current: -------------------------------------------------------------------------------- 1 | /Volumes/workspace/numpy-stl/tests/tmp/test_args_False_0 -------------------------------------------------------------------------------- /tests/tmp/test_args_True_0/binary.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolph/numpy-stl/8ad2b16f8a0e611fce8259befb15c7601836e7f0/tests/tmp/test_args_True_0/binary.stl -------------------------------------------------------------------------------- /tests/tmp/test_args_True_current: -------------------------------------------------------------------------------- 1 | /Volumes/workspace/numpy-stl/tests/tmp/test_args_True_0 -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def to_array(array, round): 5 | __tracebackhide__ = True 6 | 7 | if not isinstance(array, np.ndarray): 8 | array = np.array(array) 9 | 10 | if round: 11 | array = array.round(round) 12 | 13 | return array 14 | 15 | 16 | def array_equals(left, right, round=6): 17 | __tracebackhide__ = True 18 | left = to_array(left, round) 19 | right = to_array(right, round) 20 | 21 | message = f'Arrays are unequal:\n{left}\n{right}' 22 | if left.size == right.size: 23 | message += '\nDifference:\n%s' % (left - right) 24 | 25 | assert (left == right).all(), message 26 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | env_list = ruff, black, pypy3, py3{9,10,11,12,13}-numpy{1,2}, docs, mypy, pyright 3 | skip_missing_interpreters = True 4 | 5 | [testenv] 6 | deps = 7 | numpy1: numpy==1.* 8 | numpy2: numpy==2.* 9 | -rtests/requirements.txt 10 | commands = 11 | python -m pip install -U pip wheel setuptools 12 | python setup.py build_ext --inplace 13 | python -m pytest -vvv {posargs} 14 | basepython = 15 | py39: python3.9 16 | py310: python3.10 17 | py311: python3.11 18 | py312: python3.12 19 | py313: python3.13 20 | pypy3: pypy3 21 | 22 | [gh-actions] 23 | python = 24 | 3.9: py39 25 | 3.10: py310 26 | 3.11: py311 27 | 3.12: py312 28 | 3.13: py313 29 | pypy3: pypy3 30 | 31 | 32 | [testenv:flake8] 33 | basepython=python 34 | commands = flake8 --ignore=W391 stl tests 35 | 36 | [testenv:docs] 37 | basepython=python 38 | changedir=docs 39 | commands= 40 | sphinx-build -b html -d {envtmpdir}/doctrees . {envtmpdir}/html 41 | # sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html 42 | 43 | # [testenv:py38] 44 | # # one optional test has PyQt5 dep, only test that once 45 | # deps = 46 | # -rtests/requirements.txt 47 | # PyQt5 48 | --------------------------------------------------------------------------------