├── .gitignore ├── LICENSE ├── README.md ├── stl_normalize.py └── test_stls ├── binary_cube.stl ├── cube.stl ├── nonmanifold_cube.stl ├── nonmanifold_cube2.stl ├── nonmanifold_cube3.stl ├── normalized_binary_cube.stl └── normalized_cube.stl /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | 59 | # Rope 60 | .ropeproject/ 61 | 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Revar Desmera 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | stl\_normalize.py 2 | ================ 3 | 4 | A script to normalize and validate STL files so that they play better with version control systems like git or mercurial. 5 | 6 | Some programs (like OpenSCAD) are highly inconsistent about how they write out STL files. Small changes to the model can result in major changes in the output STL file. This makes for larger diffs when checking these files into repositories. 7 | 8 | There are also problems with some programs outputting STL files that are not properly formed and manifold. 9 | 10 | The stl\_normalize.py script is designed to be run from a Makefile or other build script, to normalize STL files, and verify whether the files are properly manifold. With the validation, you can force a build script to fail on a bad STL, and force the developer to tweak the model to fix the issue. This script can actually visually show you where the manifold problems are. 11 | 12 | This script does the following to normalize STL files: 13 | * Reorders the triangle faces in a consistent physical ordering. 14 | * Reorders triangle vertex data in a consistent way. 15 | * Calculates any missing unit face normals. 16 | * Ensures vertex data is ordered counter-clockwise. (Right-hand rule.) 17 | * Rewrites the file in ASCII STL format. 18 | * Writes vertex coordinate data in a consistent compact way. 19 | 20 | 21 | Usage 22 | ----- 23 | 24 | ``` 25 | stl_normalize.py [-h] [-v] [-c] [-g] [-b] [-o OUTFILE] INFILE 26 | ``` 27 | 28 | Positional argument | What it is 29 | :------------------ | :-------------------------------- 30 | INFILE | Filename of STL file to read in. 31 | 32 | 33 | Optional arguments | What it does 34 | :----------------------------- | :-------------------- 35 | -h, --help | Show help message and exit 36 | -v, --verbose | Show verbose output. 37 | -c, --check-manifold | Perform manifold validation of model. 38 | -g, --gui-display | Show non-manifold edges in GUI. (using OpenGL) 39 | -b, --write-binary | Use binary STL format for output. 40 | -o OUTFILE, --outfile OUTFILE | Write normalized STL to file. 41 | 42 | 43 | Examples 44 | -------- 45 | 46 | ``` 47 | stl_normalize.py -o normalized.stl input.stl 48 | ``` 49 | This will read in the file ```input.stl```, normalize the data, and write it out as an ASCII STL file named ```normalized.stl``` 50 | 51 | ``` 52 | stl_normalize.py -b -o normalize.stl input.stl 53 | ``` 54 | This will read in the file ```input.stl```, normalize the data, and write it out as a binary STL file named ```normalized.stl``` 55 | 56 | ``` 57 | stl_normalize.py -c input.stl 58 | ``` 59 | This will validate the manifoldness of the file ```input.stl``` and print out any problems it finds. It will return with a non-zero return code if any problems were found. 60 | 61 | ``` 62 | stl_normalize.py -c -o normalized.stl input.stl 63 | ``` 64 | This will read in the file ```input.stl```, validate its manifoldness, and print out any problems it finds. If no problems are found, it will normalize the data, and write it out as an ASCII STL file named ```normalized.stl```. It will return with a non-zero return code if any problems were found. 65 | 66 | ``` 67 | stl_normalize.py -g input.stl 68 | ``` 69 | This will validate the manifoldness of the file ```input.stl```, and, if there are any problems, launch OpenSCAD to display the non-manifold edges. Holes will be ringed in red. Redundant faces will be ringed in purple. 70 | 71 | 72 | -------------------------------------------------------------------------------- /stl_normalize.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import sys 5 | import time 6 | import math 7 | import numpy 8 | import struct 9 | import numbers 10 | import argparse 11 | import platform as plat 12 | import subprocess 13 | 14 | from collections import namedtuple 15 | from pyquaternion import Quaternion 16 | 17 | try: 18 | from itertools import zip_longest as ziplong 19 | except ImportError: 20 | from itertools import izip_longest as ziplong 21 | 22 | try: 23 | from OpenGL.GL import * 24 | from OpenGL.GLU import * 25 | from OpenGL.GLUT import * 26 | except: 27 | print(''' Error PyOpenGL not installed properly !!''') 28 | sys.exit( ) 29 | 30 | 31 | def float_fmt(val): 32 | """ 33 | Returns a short, clean floating point string representation. 34 | Unnecessary trailing zeroes and decimal points are trimmed off. 35 | """ 36 | s = "{0:.6f}".format(val).rstrip('0').rstrip('.') 37 | return s if s != '-0' else '0' 38 | 39 | 40 | class Vector(object): 41 | """Class to represent an N dimentional vector.""" 42 | 43 | def __init__(self, *args): 44 | self._values = [] 45 | if len(args) == 1: 46 | val = args[0] 47 | if isinstance(val, numbers.Real): 48 | self._values = [val] 49 | return 50 | elif isinstance(val, numbers.Complex): 51 | self._values = [val.real, val.imag] 52 | return 53 | else: 54 | val = args 55 | try: 56 | for x in val: 57 | if not isinstance(x, numbers.Real): 58 | raise TypeError('Expected sequence of real numbers.') 59 | self._values.append(x) 60 | except: 61 | pass 62 | 63 | def __iter__(self): 64 | """Iterator generator for vector values.""" 65 | for idx in self._values: 66 | yield idx 67 | 68 | def __len__(self): 69 | return len(self._values) 70 | 71 | def __getitem__(self, idx): 72 | """Given a vertex number, returns a vertex coordinate vector.""" 73 | return self._values[idx] 74 | 75 | def __hash__(self): 76 | """Returns hash value for vector coords""" 77 | return hash(tuple(self._values)) 78 | 79 | def __eq__(self, other): 80 | """Equality comparison for points.""" 81 | return self._values == other._values 82 | 83 | def __cmp__(self, other): 84 | """Compare points for sort ordering in an arbitrary heirarchy.""" 85 | longzip = ziplong(self._values, other, fillvalue=0.0) 86 | for v1, v2 in reversed(list(longzip)): 87 | val = cmp(v1, v2) 88 | if val != 0: 89 | return val 90 | return 0 91 | 92 | def __sub__(self, v): 93 | return Vector(i - j for i, j in zip(self._values, v)) 94 | 95 | def __rsub__(self, v): 96 | return Vector(i - j for i, j in zip(v, self._values)) 97 | 98 | def __add__(self, v): 99 | return Vector(i + j for i, j in zip(self._values, v)) 100 | 101 | def __radd__(self, v): 102 | return Vector(i + j for i, j in zip(v, self._values)) 103 | 104 | def __div__(self, s): 105 | """Divide each element in a vector by a scalar.""" 106 | return Vector(x / (s+0.0) for x in self._values) 107 | 108 | def __mul__(self, s): 109 | """Multiplies each element in a vector by a scalar.""" 110 | return Vector(x * s for x in self._values) 111 | 112 | def __format__(self, fmt): 113 | vals = [float_fmt(x) for x in self._values] 114 | if "a" in fmt: 115 | return "[{0}]".format(", ".join(vals)) 116 | if "s" in fmt: 117 | return " ".join(vals) 118 | if "b" in fmt: 119 | return struct.pack('<{0:d}f'.format(len(self._values)), *self._values) 120 | return "({0})".format(", ".join(vals)) 121 | 122 | def __repr__(self): 123 | return "".format(self) 124 | 125 | def __str__(self): 126 | """Returns a standard array syntax string of the coordinates.""" 127 | return "{0:a}".format(self) 128 | 129 | def dot(self, v): 130 | """Dot (scalar) product of two vectors.""" 131 | return sum(p*q for p, q in zip(self, v)) 132 | 133 | def cross(self, v): 134 | """ 135 | Cross (vector) product against another 3D Vector. 136 | Returned 3D Vector will be perpendicular to both original 3D Vectors. 137 | """ 138 | return Vector( 139 | self._values[1]*v[2] - self._values[2]*v[1], 140 | self._values[2]*v[0] - self._values[0]*v[2], 141 | self._values[0]*v[1] - self._values[1]*v[0] 142 | ) 143 | 144 | def length(self): 145 | """Returns the length of the vector.""" 146 | return math.sqrt(sum(x*x for x in self._values)) 147 | 148 | def normalize(self): 149 | """Normalizes the given vector to be unit-length.""" 150 | return self / self.length() 151 | 152 | def angle(self, other): 153 | """Returns angle in radians between this and another vector.""" 154 | return math.acos(self.dot(other) / (self.length() * other.length())) 155 | 156 | 157 | class Point3D(object): 158 | """Class to represent a 3D Point.""" 159 | 160 | def __init__(self, *args): 161 | self._values = [0.0, 0.0, 0.0] 162 | if len(args) == 1: 163 | val = args[0] 164 | if isinstance(val, numbers.Real): 165 | self._values = [val, 0.0, 0.0] 166 | return 167 | elif isinstance(val, numbers.Complex): 168 | self._values = [val.real, val.imag, 0.0] 169 | return 170 | else: 171 | val = args 172 | try: 173 | for i, x in enumerate(val): 174 | if not isinstance(x, numbers.Real): 175 | raise TypeError('Expected sequence of real numbers.') 176 | self._values[i] = x 177 | except: 178 | pass 179 | 180 | def __iter__(self): 181 | """Iterator generator for point values.""" 182 | for idx in range(3): 183 | yield self[idx] 184 | 185 | def __len__(self): 186 | return 3 187 | 188 | def __getitem__(self, idx): 189 | """Given a vertex number, returns a vertex coordinate vector.""" 190 | if idx >= len(self._values): 191 | return 0.0 192 | return self._values[idx] 193 | 194 | def __hash__(self): 195 | """Returns hash value for point coords""" 196 | return hash(tuple(self._values)) 197 | 198 | def __cmp__(self, p): 199 | """Compare points for sort ordering in an arbitrary heirarchy.""" 200 | longzip = ziplong(self._values, p, fillvalue=0.0) 201 | for v1, v2 in reversed(list(longzip)): 202 | val = v1 - v2 203 | if val != 0: 204 | val /= abs(val) 205 | return val 206 | return 0 207 | 208 | def __eq__(self, other): 209 | """Equality comparison for points.""" 210 | return self._values == other._values 211 | 212 | def __lt__(self, other): 213 | return self.__cmp__(other) < 0 214 | 215 | def __gt__(self, other): 216 | return self.__cmp__(other) > 0 217 | 218 | def __sub__(self, v): 219 | return Point3D(self[i] - v[i] for i in range(3)) 220 | 221 | def __rsub__(self, v): 222 | return Point3D(v[i] - self[i] for i in range(3)) 223 | 224 | def __add__(self, v): 225 | return Vector(i + j for i, j in zip(self._values, v)) 226 | 227 | def __radd__(self, v): 228 | return Vector(i + j for i, j in zip(v, self._values)) 229 | 230 | def __div__(self, s): 231 | """Divide each element in a vector by a scalar.""" 232 | return Vector(x / s for x in self._values) 233 | 234 | def __format__(self, fmt): 235 | vals = [float_fmt(x) for x in self._values] 236 | if "a" in fmt: 237 | return "[{0}]".format(", ".join(vals)) 238 | if "s" in fmt: 239 | return " ".join(vals) 240 | if "b" in fmt: 241 | return struct.pack('<3f', *self._values) 242 | return "({0})".format(", ".join(vals)) 243 | 244 | def __repr__(self): 245 | return "".format(self) 246 | 247 | def __str__(self): 248 | """Returns a standard array syntax string of the coordinates.""" 249 | return "{0:a}".format(self) 250 | 251 | def distFromPoint(self, v): 252 | """Returns the distance from another point.""" 253 | return math.sqrt(sum(math.pow(x1-x2, 2.0) for x1, x2 in zip(v, self))) 254 | 255 | def distFromLine(self, pt, line): 256 | """ 257 | Returns the distance of a 3d point from a line defined by a sequence 258 | of two 3d points. 259 | """ 260 | w = Vector(pt - line[0]) 261 | v = Vector(line[1]-line[0]) 262 | return v.normalize().cross(w).length() 263 | 264 | 265 | class Point3DCache(object): 266 | """Cache class for 3D Points.""" 267 | 268 | def __init__(self): 269 | """Initialize as an empty cache.""" 270 | self.point_hash = {} 271 | self.minx = 9e99 272 | self.miny = 9e99 273 | self.minz = 9e99 274 | self.maxx = -9e99 275 | self.maxy = -9e99 276 | self.maxz = -9e99 277 | 278 | def __len__(self): 279 | """Length of sequence.""" 280 | return len(self.point_hash) 281 | 282 | def _update_volume(self, p): 283 | """Update the volume cube that contains all the points.""" 284 | if p[0] < self.minx: 285 | self.minx = p[0] 286 | if p[0] > self.maxx: 287 | self.maxx = p[0] 288 | if p[1] < self.miny: 289 | self.miny = p[1] 290 | if p[1] > self.maxy: 291 | self.maxy = p[1] 292 | if p[2] < self.minz: 293 | self.minz = p[2] 294 | if p[2] > self.maxz: 295 | self.maxz = p[2] 296 | 297 | def get_volume(self): 298 | """Returns the 3D volume that contains all the points in the cache.""" 299 | return ( 300 | self.minx, self.miny, self.minz, 301 | self.maxx, self.maxy, self.maxz 302 | ) 303 | 304 | def add(self, x, y, z): 305 | """Given XYZ coords, returns the (new or cached) Point3D instance.""" 306 | key = tuple(round(n, 4) for n in [x, y, z]) 307 | if key in self.point_hash: 308 | return self.point_hash[key] 309 | pt = Point3D(key) 310 | self.point_hash[key] = pt 311 | self._update_volume(pt) 312 | return pt 313 | 314 | def __iter__(self): 315 | """Creates an iterator for the points in the cache.""" 316 | for pt in self.point_hash.values(): 317 | yield pt 318 | 319 | 320 | class LineSegment3D(object): 321 | """A class to represent a 3D line segment.""" 322 | 323 | def __init__(self, p1, p2): 324 | """Initialize with twwo endpoints.""" 325 | if p1 > p2: 326 | p1, p2 = (p2, p1) 327 | self.p1 = p1 328 | self.p2 = p2 329 | self.count = 1 330 | 331 | def __len__(self): 332 | """Line segment always has two endpoints.""" 333 | return 2 334 | 335 | def __iter__(self): 336 | """Iterator generator for endpoints.""" 337 | yield self.p1 338 | yield self.p2 339 | 340 | def __getitem__(self, idx): 341 | """Given a vertex number, returns a vertex coordinate vector.""" 342 | if idx == 0: 343 | return self.p1 344 | if idx == 1: 345 | return self.p2 346 | raise LookupError() 347 | 348 | def __hash__(self): 349 | """Returns hash value for endpoints""" 350 | return hash((self.p1, self.p2)) 351 | 352 | def __cmp__(self, p): 353 | """Compare points for sort ordering in an arbitrary heirarchy.""" 354 | val = cmp(self[0], p[0]) 355 | if val != 0: 356 | return val 357 | return cmp(self[1], p[1]) 358 | 359 | def __format__(self, fmt): 360 | """Provides .format() support.""" 361 | pfx = "" 362 | sep = " - " 363 | sfx = "" 364 | if "a" in fmt: 365 | pfx = "[" 366 | sep = ", " 367 | sfx = "]" 368 | elif "s" in fmt: 369 | pfx = "" 370 | sep = " " 371 | sfx = "" 372 | p1 = self.p1.__format__(fmt) 373 | p2 = self.p2.__format__(fmt) 374 | return pfx + p1 + sep + p2 + sfx 375 | 376 | def __repr__(self): 377 | """Standard string representation.""" 378 | return "".format(self) 379 | 380 | def __str__(self): 381 | """Returns a human readable coordinate string.""" 382 | return "{0:a}".format(self) 383 | 384 | def length(self): 385 | """Returns the length of the line.""" 386 | return self.p1.distFromPoint(self.p2) 387 | 388 | 389 | class LineSegment3DCache(object): 390 | """Cache class for 3D Line Segments.""" 391 | 392 | def __init__(self): 393 | """Initialize as an empty cache.""" 394 | self.endhash = {} 395 | self.seghash = {} 396 | 397 | def _add_endpoint(self, p, seg): 398 | if p not in self.endhash: 399 | self.endhash[p] = [] 400 | self.endhash[p].append(seg) 401 | 402 | def endpoint_segments(self, p): 403 | if p not in self.endhash: 404 | return [] 405 | return self.endhash[p] 406 | 407 | def get(self, p1, p2): 408 | """Given 2 endpoints, return the cached LineSegment3D inst, if any.""" 409 | key = (p1, p2) if p1 < p2 else (p2, p1) 410 | if key not in self.seghash: 411 | return None 412 | return self.seghash[key] 413 | 414 | def add(self, p1, p2): 415 | """Given 2 endpoints, return the (new or cached) LineSegment3D inst.""" 416 | key = (p1, p2) if p1 < p2 else (p2, p1) 417 | if key in self.seghash: 418 | seg = self.seghash[key] 419 | seg.count += 1 420 | return seg 421 | seg = LineSegment3D(p1, p2) 422 | self.seghash[key] = seg 423 | self._add_endpoint(p1, seg) 424 | self._add_endpoint(p2, seg) 425 | return seg 426 | 427 | def __iter__(self): 428 | """Creates an iterator for the line segments in the cache.""" 429 | for pt in self.seghash.values(): 430 | yield pt 431 | 432 | def __len__(self): 433 | """Length of sequence.""" 434 | return len(self.seghash) 435 | 436 | 437 | class Facet3D(object): 438 | """Class to represent a 3D triangular face.""" 439 | 440 | def __init__(self, v1, v2, v3, norm): 441 | for x in [v1, v2, v3, norm]: 442 | try: 443 | n = len(x) 444 | except: 445 | n = 0 446 | if n != 3: 447 | raise TypeError('Expected 3D vector.') 448 | for y in x: 449 | if not isinstance(y, numbers.Real): 450 | raise TypeError('Expected 3D vector.') 451 | verts = [ 452 | Point3D(v1), 453 | Point3D(v2), 454 | Point3D(v3) 455 | ] 456 | # Re-order vertices in a normalized order. 457 | while verts[0] > verts[1] or verts[0] > verts[2]: 458 | verts = verts[1:] + verts[:1] 459 | self.vertices = verts 460 | self.norm = Vector(norm) 461 | self.count = 1 462 | self.fixup_normal() 463 | 464 | def __len__(self): 465 | """Length of sequence. Three vertices and a normal.""" 466 | return 4 467 | 468 | def __getitem__(self, idx): 469 | """Get vertices and normal by index.""" 470 | lst = self.vertices + [self.norm] 471 | return lst[idx] 472 | 473 | def __hash__(self): 474 | """Returns hash value for facet""" 475 | return hash((self.verts, self.norm)) 476 | 477 | def __cmp__(self, other): 478 | """Compare faces for sorting in an arbitrary heirarchy.""" 479 | cl1 = [sorted(v[i] for v in self.vertices) for i in range(3)] 480 | cl2 = [sorted(v[i] for v in other.vertices) for i in range(3)] 481 | for i in reversed(range(3)): 482 | for c1, c2 in ziplong(cl1[i], cl2[i]): 483 | if c1 is None: 484 | return -1 485 | val = cmp(c1, c2) 486 | if val != 0: 487 | return val 488 | return 0 489 | 490 | def __format__(self, fmt): 491 | """Provides .format() support.""" 492 | pfx = "" 493 | sep = " - " 494 | sfx = "" 495 | if "a" in fmt: 496 | pfx = "[" 497 | sep = ", " 498 | sfx = "]" 499 | elif "s" in fmt: 500 | pfx = "" 501 | sep = " " 502 | sfx = "" 503 | ifx = sep.join(n.__format__(fmt) for n in list(self)[0:3]) 504 | return pfx + ifx + sfx 505 | 506 | def is_clockwise(self): 507 | """ 508 | Returns true if the three vertices of the face are in clockwise 509 | order with respect to the normal vector. 510 | """ 511 | v1 = Vector(self.vertices[1]-self.vertices[0]) 512 | v2 = Vector(self.vertices[2]-self.vertices[0]) 513 | return self.norm.dot(v1.cross(v2)) < 0 514 | 515 | def fixup_normal(self): 516 | if self.norm.length() > 0: 517 | # Make sure vertex ordering is counter-clockwise, 518 | # relative to the outward facing normal. 519 | if self.is_clockwise(): 520 | self.vertices = [ 521 | self.vertices[0], 522 | self.vertices[2], 523 | self.vertices[1] 524 | ] 525 | else: 526 | # If no normal was specified, we should calculate it, relative 527 | # to the counter-clockwise vertices (as seen from outside). 528 | v1 = Vector(self.vertices[2] - self.vertices[0]) 529 | v2 = Vector(self.vertices[1] - self.vertices[0]) 530 | self.norm = v1.cross(v2) 531 | if self.norm.length() > 1e-6: 532 | self.norm = self.norm.normalize() 533 | 534 | 535 | class Facet3DCache(object): 536 | """Cache class for 3D Facets.""" 537 | 538 | def __init__(self): 539 | """Initialize as an empty cache.""" 540 | self.vertex_hash = {} 541 | self.edge_hash = {} 542 | self.facet_hash = {} 543 | 544 | def _add_vertex(self, pt, facet): 545 | """Remember that a given vertex touches a given facet.""" 546 | if pt not in self.vertex_hash: 547 | self.vertex_hash[pt] = [] 548 | self.vertex_hash[pt].append(facet) 549 | 550 | def _add_edge(self, p1, p2, facet): 551 | """Remember that a given edge touches a given facet.""" 552 | if p1 > p2: 553 | edge = (p1, p2) 554 | else: 555 | edge = (p2, p1) 556 | if edge not in self.edge_hash: 557 | self.edge_hash[edge] = [] 558 | self.edge_hash[edge].append(facet) 559 | 560 | def vertex_facets(self, pt): 561 | """Returns the facets that have a given facet.""" 562 | if pt not in self.vertex_hash: 563 | return [] 564 | return self.vertex_hash[pt] 565 | 566 | def edge_facets(self, p1, p2): 567 | """Returns the facets that have a given edge.""" 568 | if p1 > p2: 569 | edge = (p1, p2) 570 | else: 571 | edge = (p2, p1) 572 | if edge not in self.edge_hash: 573 | return [] 574 | return self.edge_hash[edge] 575 | 576 | def get(self, p1, p2, p3): 577 | """Given 3 vertices, return the cached Facet3D instance, if any.""" 578 | key = (p1, p2, p3) 579 | if key not in self.facet_hash: 580 | return None 581 | return self.facet_hash[key] 582 | 583 | def add(self, p1, p2, p3, norm): 584 | """ 585 | Given 3 vertices and a norm, return the (new or cached) Facet3d inst. 586 | """ 587 | key = (p1, p2, p3) 588 | if key in self.facet_hash: 589 | facet = self.facet_hash[key] 590 | facet.count += 1 591 | return facet 592 | facet = Facet3D(p1, p2, p3, norm) 593 | self.facet_hash[key] = facet 594 | self._add_edge(p1, p2, facet) 595 | self._add_edge(p2, p3, facet) 596 | self._add_edge(p3, p1, facet) 597 | self._add_vertex(p1, facet) 598 | self._add_vertex(p2, facet) 599 | self._add_vertex(p3, facet) 600 | return facet 601 | 602 | def sorted(self): 603 | """Returns a sorted iterator.""" 604 | vals = self.facet_hash.values() 605 | for pt in sorted(vals): 606 | yield pt 607 | 608 | def __iter__(self): 609 | """Creates an iterator for the facets in the cache.""" 610 | for pt in self.facet_hash.values(): 611 | yield pt 612 | 613 | def __len__(self): 614 | """Length of sequence.""" 615 | return len(self.facet_hash) 616 | 617 | 618 | class StlEndOfFileException(Exception): 619 | """Exception class for reaching the end of the STL file while reading.""" 620 | pass 621 | 622 | 623 | class StlMalformedLineException(Exception): 624 | """Exception class for malformed lines in the STL file being read.""" 625 | pass 626 | 627 | 628 | class StlData(object): 629 | """Class to read, write, and validate STL file data.""" 630 | 631 | def __init__(self): 632 | """Initialize with empty data set.""" 633 | self.points = Point3DCache() 634 | self.edges = LineSegment3DCache() 635 | self.facets = Facet3DCache() 636 | self.filename = "" 637 | self.dupe_faces = [] 638 | self.dupe_edges = [] 639 | self.hole_edges = [] 640 | self.wireframe = False 641 | self.show_facets = True 642 | self.perspective = True 643 | self.boundsrad = 1.0 644 | self.cx, self.cy, self.cz = 0.0, 0.0, 0.0 645 | self.width, self.height = 800, 600 646 | self._xstart, self._ystart = 0, 0 647 | self._model_list = None 648 | self._errs_list = None 649 | self._grid_list = None 650 | self._mouse_btn = GLUT_LEFT_BUTTON 651 | self._mouse_state = GLUT_UP 652 | self._action = None 653 | self.reset_view() 654 | 655 | def reset_view(self): 656 | self._view_q = Quaternion(axis=[0, 0, 1], degrees=25) 657 | self._view_q *= Quaternion(axis=[1, 0, 0], degrees=55) 658 | self._xtrans, self._ytrans= 0.0, 0.0 659 | self._zoom = 1.0 660 | 661 | def _read_ascii_line(self, f, watchwords=None): 662 | line = f.readline(1024).decode('utf-8') 663 | if line == "": 664 | raise StlEndOfFileException() 665 | words = line.strip(' \t\n\r').lower().split() 666 | if not words: 667 | return [] 668 | if words[0] == 'endsolid': 669 | raise StlEndOfFileException() 670 | argstart = 0 671 | if watchwords: 672 | watchwords = watchwords.lower().split() 673 | argstart = len(watchwords) 674 | for i in range(argstart): 675 | if words[i] != watchwords[i]: 676 | raise StlMalformedLineException() 677 | return [float(val) for val in words[argstart:]] 678 | 679 | def _read_ascii_vertex(self, f): 680 | point = self._read_ascii_line(f, watchwords='vertex') 681 | return self.points.add(*point) 682 | 683 | def _read_ascii_facet(self, f): 684 | while True: 685 | try: 686 | normal = self._read_ascii_line(f, watchwords='facet normal') 687 | self._read_ascii_line(f, watchwords='outer loop') 688 | vertex1 = self._read_ascii_vertex(f) 689 | vertex2 = self._read_ascii_vertex(f) 690 | vertex3 = self._read_ascii_vertex(f) 691 | self._read_ascii_line(f, watchwords='endloop') 692 | self._read_ascii_line(f, watchwords='endfacet') 693 | if vertex1 == vertex2: 694 | continue # zero area facet. Skip to next facet. 695 | if vertex2 == vertex3: 696 | continue # zero area facet. Skip to next facet. 697 | if vertex3 == vertex1: 698 | continue # zero area facet. Skip to next facet. 699 | except StlEndOfFileException: 700 | return None 701 | except StlMalformedLineException: 702 | continue # Skip to next facet. 703 | self.edges.add(vertex1, vertex2) 704 | self.edges.add(vertex2, vertex3) 705 | self.edges.add(vertex3, vertex1) 706 | return self.facets.add(vertex1, vertex2, vertex3, normal) 707 | 708 | def _read_binary_facet(self, f): 709 | data = struct.unpack('<3f 3f 3f 3f H', f.read(4*4*3+2)) 710 | normal = data[0:3] 711 | vertex1 = data[3:6] 712 | vertex2 = data[6:9] 713 | vertex3 = data[9:12] 714 | v1 = self.points.add(*vertex1) 715 | v2 = self.points.add(*vertex2) 716 | v3 = self.points.add(*vertex3) 717 | self.edges.add(v1, v2) 718 | self.edges.add(v2, v3) 719 | self.edges.add(v3, v1) 720 | return self.facets.add(v1, v2, v3, normal) 721 | 722 | def read_file(self, filename): 723 | self.filename = filename 724 | with open(filename, 'rb') as f: 725 | line = f.readline(80).strip(b' ') 726 | if line == "": 727 | return # End of file. 728 | if line[0:6].lower() == b"solid ": 729 | # Check if file is ASCII STL. 730 | pos = f.tell() 731 | line = f.readline(80).strip(b' ') 732 | f.seek(pos, 0) 733 | if line[0:6].lower() == b"facet ": 734 | # Reading ASCII STL file. 735 | while self._read_ascii_facet(f) is not None: 736 | pass 737 | return 738 | # Reading Binary STL file. 739 | chunk = f.read(4) 740 | facets = struct.unpack(' min(w,h)/2: 1094 | self._action = "ZROT" 1095 | else: 1096 | self._action = "XYROT" 1097 | elif button == GLUT_MIDDLE_BUTTON: 1098 | self._action = "ZOOM" 1099 | elif button == GLUT_RIGHT_BUTTON: 1100 | self._action = "TRANS" 1101 | elif button == 3: 1102 | self._zoom *= 1.01 1103 | self._zoom = min(10.0,max(0.1,self._zoom)) 1104 | elif button == 4: 1105 | self._zoom /= 1.01 1106 | self._zoom = min(10.0,max(0.1,self._zoom)) 1107 | else: 1108 | self._action = None 1109 | glutPostRedisplay() 1110 | 1111 | def _gl_mousemotion(self, x, y): 1112 | w, h = glutGet(GLUT_WINDOW_WIDTH), glutGet(GLUT_WINDOW_HEIGHT) 1113 | cx, cy = w/2.0, h/2.0 1114 | dx = x - self._xstart 1115 | dy = y - self._ystart 1116 | r = 5.0 * self.boundsrad / min(w, h) 1117 | if self._action == "TRANS": 1118 | self._xtrans += dx * r 1119 | self._ytrans -= dy * r 1120 | elif self._action == "ZOOM": 1121 | if dy >= 0: 1122 | self._zoom *= (1.0 + dy/100.0) 1123 | else: 1124 | self._zoom /= (1.0 - dy/100.0) 1125 | self._zoom = min(10.0,max(0.1,self._zoom)) 1126 | elif self._action == "XYROT": 1127 | qx = Quaternion(axis=[0, 1, 0], degrees=-dx*360.0/min(w,h)) 1128 | qy = Quaternion(axis=[1, 0, 0], degrees=-dy*360.0/min(w,h)) 1129 | self._view_q = self._view_q * qx * qy 1130 | self._view_q = self._view_q.unit 1131 | elif self._action == "ZROT": 1132 | oldang = math.atan2(self._ystart-cy, self._xstart-cx) 1133 | newang = math.atan2(y-cy, x-cx) 1134 | dang = newang - oldang 1135 | qz = Quaternion(axis=[0, 0, 1], radians=dang) 1136 | self._view_q = self._view_q * qz 1137 | self._view_q = self._view_q.unit 1138 | self._xstart = x 1139 | self._ystart = y 1140 | glutPostRedisplay() 1141 | 1142 | def gui_show(self, wireframe=False, show_facets=True): 1143 | self.wireframe = wireframe 1144 | self.show_facets = show_facets 1145 | 1146 | glutInit(sys.argv) 1147 | glutInitWindowSize(self.width, self.height) 1148 | glutInitDisplayMode(GLUT_DEPTH | GLUT_DOUBLE | GLUT_RGBA) 1149 | glutCreateWindow("STL Show") 1150 | glutDisplayFunc(self._gl_display) 1151 | glutKeyboardFunc(self._gl_keypressed) 1152 | glutMouseFunc(self._gl_mousebutton) 1153 | glutMotionFunc(self._gl_mousemotion) 1154 | glutReshapeFunc(self._gl_reshape) 1155 | 1156 | # Use depth buffering for hidden surface elimination. 1157 | glEnable(GL_DEPTH_TEST) 1158 | glDepthFunc(GL_LEQUAL) 1159 | 1160 | # cheap-assed Anti-aliasing 1161 | glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) 1162 | glEnable(GL_BLEND) 1163 | glHint(GL_POLYGON_SMOOTH_HINT, GL_NICEST) 1164 | glHint(GL_LINE_SMOOTH_HINT, GL_NICEST) 1165 | glPolygonMode(GL_FRONT_AND_BACK, GL_FILL) 1166 | glEnable(GL_POLYGON_SMOOTH) 1167 | glEnable(GL_LINE_SMOOTH) 1168 | 1169 | # Setup the view of the cube. 1170 | self._gl_reshape(glutGet(GLUT_WINDOW_WIDTH), glutGet(GLUT_WINDOW_HEIGHT)) 1171 | 1172 | if plat.system() == "Darwin": 1173 | os.system('''/usr/bin/osascript -e 'tell app "Finder" to set frontmost of process "Python" to true' ''') 1174 | 1175 | glutMainLoop() 1176 | 1177 | def _check_manifold_duplicate_faces(self): 1178 | return [facet for facet in self.facets if facet.count != 1] 1179 | 1180 | def _check_manifold_hole_edges(self): 1181 | return [edge for edge in self.edges if edge.count == 1] 1182 | 1183 | def _check_manifold_excess_edges(self): 1184 | return [edge for edge in self.edges if edge.count > 2] 1185 | 1186 | def check_manifold(self, verbose=False): 1187 | is_manifold = True 1188 | 1189 | self.dupe_faces = self._check_manifold_duplicate_faces() 1190 | for face in self.dupe_faces: 1191 | is_manifold = False 1192 | print("NON-MANIFOLD DUPLICATE FACE! {0}: {1}" 1193 | .format(self.filename, face)) 1194 | 1195 | self.hole_edges = self._check_manifold_hole_edges() 1196 | for edge in self.hole_edges: 1197 | is_manifold = False 1198 | print("NON-MANIFOLD HOLE EDGE! {0}: {1}" 1199 | .format(self.filename, edge)) 1200 | 1201 | self.dupe_edges = self._check_manifold_excess_edges() 1202 | for edge in self.dupe_edges: 1203 | is_manifold = False 1204 | print("NON-MANIFOLD DUPLICATE EDGE! {0}: {1}" 1205 | .format(self.filename, edge)) 1206 | 1207 | return is_manifold 1208 | 1209 | 1210 | def main(): 1211 | parser = argparse.ArgumentParser(prog='myprogram') 1212 | parser.add_argument('-v', '--verbose', 1213 | help='Show verbose output.', 1214 | action="store_true") 1215 | parser.add_argument('-c', '--check-manifold', 1216 | help='Perform manifold validation of model.', 1217 | action="store_true") 1218 | parser.add_argument('-b', '--write-binary', 1219 | help='Use binary STL format for output.', 1220 | action="store_true") 1221 | parser.add_argument('-o', '--outfile', 1222 | help='Write normalized STL to file.') 1223 | parser.add_argument('-g', '--gui-display', 1224 | help='Show non-manifold edges in GUI.', 1225 | action="store_true") 1226 | parser.add_argument('-f', '--show-facets', 1227 | help='Show facet edges in GUI.', 1228 | action="store_true") 1229 | parser.add_argument('-w', '--wireframe-only', 1230 | help='Display wireframe only in GUI.', 1231 | action="store_true") 1232 | parser.add_argument('infile', help='Input STL filename.') 1233 | args = parser.parse_args() 1234 | 1235 | stl = StlData() 1236 | stl.read_file(args.infile) 1237 | if args.verbose: 1238 | print("Read {0} ({1:.1f} x {2:.1f} x {3:.1f})".format( 1239 | args.infile, 1240 | stl.points.maxx - stl.points.minx, 1241 | stl.points.maxy - stl.points.miny, 1242 | stl.points.maxz - stl.points.minz, 1243 | )) 1244 | print("Bounds {0} ({1:.1f} x {2:.1f} x {3:.1f}) to ({4:.1f} x {5:.1f} x {6:.1f})".format( 1245 | args.infile, 1246 | stl.points.minx, 1247 | stl.points.miny, 1248 | stl.points.minz, 1249 | stl.points.maxx, 1250 | stl.points.maxy, 1251 | stl.points.maxz, 1252 | )) 1253 | 1254 | manifold = True 1255 | if args.check_manifold: 1256 | manifold = stl.check_manifold(verbose=args.verbose) 1257 | if manifold and (args.verbose or args.gui_display): 1258 | print("{0} is manifold.".format(args.infile)) 1259 | if args.gui_display: 1260 | stl.gui_show(wireframe=args.wireframe_only, show_facets=args.show_facets) 1261 | if not manifold: 1262 | sys.exit(-1) 1263 | 1264 | if args.outfile: 1265 | stl.write_file(args.outfile, binary=args.write_binary) 1266 | if args.verbose: 1267 | print("Wrote {0} ({1})".format( 1268 | args.outfile, 1269 | ("binary" if args.write_binary else "ASCII"), 1270 | )) 1271 | 1272 | sys.exit(0) 1273 | 1274 | 1275 | if __name__ == "__main__": 1276 | main() 1277 | 1278 | 1279 | # vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap 1280 | -------------------------------------------------------------------------------- /test_stls/binary_cube.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/revarbat/stl_normalize/9f60064ed329a90bc0f38caf270f19e438eace85/test_stls/binary_cube.stl -------------------------------------------------------------------------------- /test_stls/cube.stl: -------------------------------------------------------------------------------- 1 | solid Model 2 | facet normal -1 0 0 3 | outer loop 4 | vertex -10 -10 0 5 | vertex -10 10 20 6 | vertex -10 10 0 7 | endloop 8 | endfacet 9 | facet normal 0 0 1 10 | outer loop 11 | vertex -10 -10 20 12 | vertex 10 -10 20 13 | vertex 10 10 20 14 | endloop 15 | endfacet 16 | facet normal 0 0 1 17 | outer loop 18 | vertex -10 -10 20 19 | vertex 10 10 20 20 | vertex -10 10 20 21 | endloop 22 | endfacet 23 | facet normal 1 0 0 24 | outer loop 25 | vertex 10 -10 0 26 | vertex 10 10 0 27 | vertex 10 -10 20 28 | endloop 29 | endfacet 30 | facet normal 0 1 0 31 | outer loop 32 | vertex -10 10 0 33 | vertex 10 10 20 34 | vertex 10 10 0 35 | endloop 36 | endfacet 37 | facet normal 0 0 -1 38 | outer loop 39 | vertex -10 -10 0 40 | vertex -10 10 0 41 | vertex 10 10 0 42 | endloop 43 | endfacet 44 | facet normal -1 0 0 45 | outer loop 46 | vertex -10 -10 0 47 | vertex -10 -10 20 48 | vertex -10 10 20 49 | endloop 50 | endfacet 51 | facet normal 0 -1 0 52 | outer loop 53 | vertex -10 -10 0 54 | vertex 10 -10 0 55 | vertex -10 -10 20 56 | endloop 57 | endfacet 58 | facet normal 0 -1 0 59 | outer loop 60 | vertex 10 -10 0 61 | vertex 10 -10 20 62 | vertex -10 -10 20 63 | endloop 64 | endfacet 65 | facet normal 0 0 -1 66 | outer loop 67 | vertex -10 -10 0 68 | vertex 10 10 0 69 | vertex 10 -10 0 70 | endloop 71 | endfacet 72 | facet normal 1 0 0 73 | outer loop 74 | vertex 10 10 0 75 | vertex 10 10 20 76 | vertex 10 -10 20 77 | endloop 78 | endfacet 79 | facet normal 0 1 0 80 | outer loop 81 | vertex -10 10 0 82 | vertex -10 10 20 83 | vertex 10 10 20 84 | endloop 85 | endfacet 86 | endsolid Model 87 | -------------------------------------------------------------------------------- /test_stls/nonmanifold_cube.stl: -------------------------------------------------------------------------------- 1 | solid Model 2 | facet normal -1 0 0 3 | outer loop 4 | vertex -10 -10 0 5 | vertex -10 10 20 6 | vertex -10 10 0 7 | endloop 8 | endfacet 9 | facet normal 0 0 1 10 | outer loop 11 | vertex -10 -10 20 12 | vertex 10 -10 20 13 | vertex 10 10 20 14 | endloop 15 | endfacet 16 | facet normal 1 0 0 17 | outer loop 18 | vertex 10 -10 0 19 | vertex 10 10 0 20 | vertex 10 -10 20 21 | endloop 22 | endfacet 23 | facet normal 0 1 0 24 | outer loop 25 | vertex -10 10 0 26 | vertex 10 10 20 27 | vertex 10 10 0 28 | endloop 29 | endfacet 30 | facet normal 0 0 -1 31 | outer loop 32 | vertex -10 -10 0 33 | vertex -10 10 0 34 | vertex 10 10 0 35 | endloop 36 | endfacet 37 | facet normal -1 0 0 38 | outer loop 39 | vertex -10 -10 0 40 | vertex -10 -10 20 41 | vertex -10 10 20 42 | endloop 43 | endfacet 44 | facet normal 0 -1 0 45 | outer loop 46 | vertex -10 -10 0 47 | vertex 10 -10 0 48 | vertex -10 -10 20 49 | endloop 50 | endfacet 51 | facet normal 0 -1 0 52 | outer loop 53 | vertex 10 -10 0 54 | vertex 10 -10 20 55 | vertex -10 -10 20 56 | endloop 57 | endfacet 58 | facet normal 0 0 -1 59 | outer loop 60 | vertex -10 -10 0 61 | vertex 10 10 0 62 | vertex 10 -10 0 63 | endloop 64 | endfacet 65 | facet normal 1 0 0 66 | outer loop 67 | vertex 10 10 0 68 | vertex 10 10 20 69 | vertex 10 -10 20 70 | endloop 71 | endfacet 72 | facet normal 0 1 0 73 | outer loop 74 | vertex -10 10 0 75 | vertex -10 10 20 76 | vertex 10 10 20 77 | endloop 78 | endfacet 79 | endsolid Model 80 | -------------------------------------------------------------------------------- /test_stls/nonmanifold_cube2.stl: -------------------------------------------------------------------------------- 1 | solid Model 2 | facet normal -1 0 0 3 | outer loop 4 | vertex -10 -10 0 5 | vertex -10 10 20 6 | vertex -10 10 0 7 | endloop 8 | endfacet 9 | facet normal 0 0 1 10 | outer loop 11 | vertex -10 -10 20 12 | vertex 10 -10 20 13 | vertex 10 10 20 14 | endloop 15 | endfacet 16 | facet normal 0 0 1 17 | outer loop 18 | vertex -10 -10 20 19 | vertex 10 10 20 20 | vertex -10 10 20 21 | endloop 22 | endfacet 23 | facet normal 1 0 0 24 | outer loop 25 | vertex 10 -10 0 26 | vertex 10 10 0 27 | vertex 10 -10 20 28 | endloop 29 | endfacet 30 | facet normal 0 1 0 31 | outer loop 32 | vertex -10 10 0 33 | vertex 10 10 20 34 | vertex 10 10 0 35 | endloop 36 | endfacet 37 | facet normal 0 0 -1 38 | outer loop 39 | vertex -10 -10 0 40 | vertex 10 10 0 41 | vertex 10 -10 0 42 | endloop 43 | endfacet 44 | facet normal 0 0 -1 45 | outer loop 46 | vertex -10 -10 0 47 | vertex -10 10 0 48 | vertex 10 10 0 49 | endloop 50 | endfacet 51 | facet normal -1 0 0 52 | outer loop 53 | vertex -10 -10 0 54 | vertex -10 -10 20 55 | vertex -10 10 20 56 | endloop 57 | endfacet 58 | facet normal 0 0 -1 59 | outer loop 60 | vertex -10 -10 0 61 | vertex 10 10 0 62 | vertex 10 -10 0 63 | endloop 64 | endfacet 65 | facet normal 0 -1 0 66 | outer loop 67 | vertex -10 -10 0 68 | vertex 10 -10 0 69 | vertex -10 -10 20 70 | endloop 71 | endfacet 72 | facet normal 0 -1 0 73 | outer loop 74 | vertex 10 -10 0 75 | vertex 10 -10 20 76 | vertex -10 -10 20 77 | endloop 78 | endfacet 79 | facet normal 0 0 -1 80 | outer loop 81 | vertex -10 -10 0 82 | vertex 10 10 0 83 | vertex 10 -10 0 84 | endloop 85 | endfacet 86 | facet normal 1 0 0 87 | outer loop 88 | vertex 10 10 0 89 | vertex 10 10 20 90 | vertex 10 -10 20 91 | endloop 92 | endfacet 93 | facet normal 0 1 0 94 | outer loop 95 | vertex -10 10 0 96 | vertex -10 10 20 97 | vertex 10 10 20 98 | endloop 99 | endfacet 100 | endsolid Model 101 | -------------------------------------------------------------------------------- /test_stls/nonmanifold_cube3.stl: -------------------------------------------------------------------------------- 1 | solid Model 2 | facet normal -1 0 0 3 | outer loop 4 | vertex -10 -10 0 5 | vertex -10 10 20 6 | vertex -10 10 0 7 | endloop 8 | endfacet 9 | facet normal 0 0 1 10 | outer loop 11 | vertex -10 -10 20 12 | vertex 10 -10 20 13 | vertex 10 10 20 14 | endloop 15 | endfacet 16 | facet normal 0 0 1 17 | outer loop 18 | vertex -10 -10 20 19 | vertex 10 10 20 20 | vertex -10 10 20 21 | endloop 22 | endfacet 23 | facet normal 1 0 0 24 | outer loop 25 | vertex 10 -10 0 26 | vertex 10 10 0 27 | vertex 10 -10 20 28 | endloop 29 | endfacet 30 | facet normal 0 1 0 31 | outer loop 32 | vertex -10 10 0 33 | vertex 10 10 20 34 | vertex 10 10 0 35 | endloop 36 | endfacet 37 | facet normal 0 0 -1 38 | outer loop 39 | vertex -10 -10 0 40 | vertex -10 10 0 41 | vertex 10 10 0 42 | endloop 43 | endfacet 44 | facet normal -1 0 0 45 | outer loop 46 | vertex -10 -10 0 47 | vertex -10 -10 20 48 | vertex -10 10 20 49 | endloop 50 | endfacet 51 | facet normal 0 -1 0 52 | outer loop 53 | vertex -10 -10 0 54 | vertex 10 -10 0 55 | vertex -10 -10 20 56 | endloop 57 | endfacet 58 | facet normal 0 -1 0 59 | outer loop 60 | vertex 10 -10 0 61 | vertex 10 -10 20 62 | vertex -10 -10 20 63 | endloop 64 | endfacet 65 | facet normal 0 0 -1 66 | outer loop 67 | vertex -10 -10 0 68 | vertex 10 10 0 69 | vertex 10 -10 0 70 | endloop 71 | endfacet 72 | facet normal 1 0 0 73 | outer loop 74 | vertex 10 10 0 75 | vertex 10 10 20 76 | vertex 10 -10 20 77 | endloop 78 | endfacet 79 | facet normal 0 1 0 80 | outer loop 81 | vertex -10 10 0 82 | vertex -10 10 20 83 | vertex 10 10 20 84 | endloop 85 | endfacet 86 | facet normal -0.7071 0.7071 0 87 | outer loop 88 | vertex -10 -10 0 89 | vertex 10 10 20 90 | vertex 10 10 0 91 | endloop 92 | endfacet 93 | facet normal 0.7071 -0.7071 0 94 | outer loop 95 | vertex -10 -10 0 96 | vertex 10 10 0 97 | vertex 10 10 20 98 | endloop 99 | endfacet 100 | endsolid Model 101 | -------------------------------------------------------------------------------- /test_stls/normalized_binary_cube.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/revarbat/stl_normalize/9f60064ed329a90bc0f38caf270f19e438eace85/test_stls/normalized_binary_cube.stl -------------------------------------------------------------------------------- /test_stls/normalized_cube.stl: -------------------------------------------------------------------------------- 1 | solid Model 2 | facet normal 0 0 -1 3 | outer loop 4 | vertex -10 -10 0 5 | vertex 10 10 0 6 | vertex 10 -10 0 7 | endloop 8 | endfacet 9 | facet normal 0 0 -1 10 | outer loop 11 | vertex -10 -10 0 12 | vertex -10 10 0 13 | vertex 10 10 0 14 | endloop 15 | endfacet 16 | facet normal 0 -1 0 17 | outer loop 18 | vertex -10 -10 0 19 | vertex 10 -10 0 20 | vertex -10 -10 20 21 | endloop 22 | endfacet 23 | facet normal 1 0 0 24 | outer loop 25 | vertex 10 -10 0 26 | vertex 10 10 0 27 | vertex 10 -10 20 28 | endloop 29 | endfacet 30 | facet normal -1 0 0 31 | outer loop 32 | vertex -10 -10 0 33 | vertex -10 10 20 34 | vertex -10 10 0 35 | endloop 36 | endfacet 37 | facet normal 0 1 0 38 | outer loop 39 | vertex -10 10 0 40 | vertex 10 10 20 41 | vertex 10 10 0 42 | endloop 43 | endfacet 44 | facet normal 0 -1 0 45 | outer loop 46 | vertex 10 -10 0 47 | vertex 10 -10 20 48 | vertex -10 -10 20 49 | endloop 50 | endfacet 51 | facet normal -1 0 0 52 | outer loop 53 | vertex -10 -10 0 54 | vertex -10 -10 20 55 | vertex -10 10 20 56 | endloop 57 | endfacet 58 | facet normal 1 0 0 59 | outer loop 60 | vertex 10 10 0 61 | vertex 10 10 20 62 | vertex 10 -10 20 63 | endloop 64 | endfacet 65 | facet normal 0 1 0 66 | outer loop 67 | vertex -10 10 0 68 | vertex -10 10 20 69 | vertex 10 10 20 70 | endloop 71 | endfacet 72 | facet normal 0 0 1 73 | outer loop 74 | vertex -10 -10 20 75 | vertex 10 -10 20 76 | vertex 10 10 20 77 | endloop 78 | endfacet 79 | facet normal 0 0 1 80 | outer loop 81 | vertex -10 -10 20 82 | vertex 10 10 20 83 | vertex -10 10 20 84 | endloop 85 | endfacet 86 | endsolid Model 87 | --------------------------------------------------------------------------------