├── .gitignore ├── LICENSE ├── README.rst ├── mandoline ├── TextThermometer.py ├── __init__.py ├── facet3d.py ├── float_fmt.py ├── geometry2d.py ├── line_segment3d.py ├── point3d.py ├── slicer.py ├── stl_data.py └── vector.py ├── setup.cfg ├── setup.py └── test_models ├── AlunarM506FanShroud.stl ├── Ball_Bearing.A.3.STL ├── cube.stl └── sphere.stl /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | .*.swp 104 | *.gcode 105 | 106 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2017, Revar Desmera 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ############ 2 | Mandoline Py 3 | ############ 4 | 5 | This is a 3D printing STL-to-GCode slicer, written in Python, based 6 | on the Clipper geometry library. It will let you take STL files 7 | and generate a GCode path file that you can send to your RepRap 3D 8 | printer to print the object. 9 | 10 | 11 | Installation 12 | ============ 13 | 14 | Install using PyPi (NOT IMPLEMENTED YET):: 15 | 16 | unzip mandoline-py.zip 17 | cd mandoline-py 18 | pip3 install . 19 | 20 | Installing from sources:: 21 | 22 | python3 setup.py build install 23 | 24 | 25 | Usage 26 | ===== 27 | To just validate a model, checking it for manifold errors, just run 28 | ``mandoline`` with the name of the file:: 29 | 30 | mandoline testcube.stl 31 | 32 | Any error messages will be printed to ``STDERR``, and the return code 33 | will be non-zero if errors were found. 34 | 35 | To slice a file into GCode, you need to specify the file to write to 36 | with the -o OUTFILE arguments:: 37 | 38 | mandoline -o testcube.gcode testcube.stl 39 | 40 | If you want to force it to skip validation, then add the -n argument:: 41 | 42 | mandoline -o testcube.gcode -n testcube.stl 43 | 44 | To display all slicing config options, use the --show-configs argument:: 45 | 46 | mandoline --show-configs 47 | 48 | To get descriptions about all slicing config options, use the --help-configs argument:: 49 | 50 | mandoline --help-configs 51 | 52 | You can set slicing options on the command-line with -S NAME=VALUE args:: 53 | 54 | mandoline -S layer_height=0.3 -S skirt_loops=3 55 | 56 | You can write changed options to the persistent slicing configs file using 57 | the -w argument:: 58 | 59 | mandoline -S layer_height=0.3 -S brim_loops=3 -w 60 | 61 | You can query the value of a slicing config option with the -q OPTNAME argument:: 62 | 63 | mandoline -q layer_height -q brim_loops 64 | 65 | You can view the sliced output in a GUI window if you add the -g argument. 66 | In this window, up and down arrow keys will move through the slice layers, 67 | and the 'q' key will quit and close the window. The keys `1` - `4` or 68 | `-` and `=` will zoom the image. 69 | 70 | -------------------------------------------------------------------------------- /mandoline/TextThermometer.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import sys 4 | import time 5 | 6 | class TextThermometer(object): 7 | def __init__(self, target=100, value=0, update_period=0.5): 8 | self.value = value 9 | self.target = target 10 | self.last_time = time.time() 11 | self.update_period = update_period 12 | self.spincnt = 0 13 | self.spinchars = r'/-\|' 14 | 15 | def set_target(self, target): 16 | self.target = target 17 | self.last_time = time.time() 18 | self.spincnt = 0 19 | 20 | def update(self, value): 21 | self.value = value 22 | now = time.time() 23 | if now - self.last_time >= self.update_period: 24 | self.last_time = now 25 | pct = 100.0 * self.value / self.target 26 | self.spincnt = (self.spincnt + 1) % len(self.spinchars) 27 | spinchar = "" if pct >= 100.0 else self.spinchars[self.spincnt] 28 | print("\r [{:50s}] {:.1f}%".format("="*int(pct/2) + spinchar, pct), end="") 29 | sys.stdout.flush() 30 | 31 | def clear(self): 32 | print("\r{:78s}".format(""), end="\r") 33 | 34 | 35 | -------------------------------------------------------------------------------- /mandoline/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os.path 3 | import argparse 4 | 5 | # import pyximport; pyximport.install() 6 | 7 | from .stl_data import StlData 8 | from mandoline.slicer import Slicer 9 | 10 | 11 | def main(): 12 | parser = argparse.ArgumentParser(prog='myprogram') 13 | parser.add_argument('-o', '--outfile', 14 | help='Slices STL and write GCode to file.') 15 | parser.add_argument('-n', '--no-validation', action="store_true", 16 | help='Skip performing model validation.') 17 | parser.add_argument('-g', '--gui-display', action="store_true", 18 | help='Show sliced paths output in GUI.') 19 | parser.add_argument('-v', '--verbose', action="store_true", 20 | help='Show verbose output.') 21 | 22 | parser.add_argument('--no-raft', dest="set_option", action="append_const", 23 | const="adhesion_type=None", help='Force adhesion to not be generated.') 24 | parser.add_argument('--raft', dest="set_option", action="append_const", 25 | const="adhesion_type=Raft", help='Force raft generation.') 26 | parser.add_argument('--brim', dest="set_option", action="append_const", 27 | const="adhesion_type=Brim", help='Force brim generation.') 28 | 29 | parser.add_argument('--no-support', dest="set_option", action="append_const", 30 | const="support_type=None", help='Force external support structure generation.') 31 | parser.add_argument('--support', dest="set_option", action="append_const", 32 | const="support_type=External", help='Force external support structure generation.') 33 | parser.add_argument('--support-all', dest="set_option", action="append_const", 34 | const="support_type=Everywhere", help='Force external support structure generation.') 35 | 36 | parser.add_argument('-f', '--filament', metavar="MATERIAL,...", 37 | help='Configures extruder(s) for given materials, in order. Ex: -f PLA,TPU,PVA') 38 | 39 | parser.add_argument('-S', '--set-option', action="append", metavar="OPTNAME=VALUE", 40 | help='Set a slicing config option.') 41 | parser.add_argument('-Q', '--query-option', action="append", metavar="OPTNAME", 42 | help='Display a slicing config option value.') 43 | parser.add_argument('-w', '--write-configs', action="store_true", 44 | help='Save any changed slicing config options.') 45 | parser.add_argument('--help-configs', action="store_true", 46 | help='Display help for all slicing options.') 47 | parser.add_argument('--show-configs', action="store_true", 48 | help='Display values of all slicing options.') 49 | parser.add_argument('infile', nargs="?", help='Input STL filename.') 50 | args = parser.parse_args() 51 | 52 | stl = StlData() 53 | if args.infile: 54 | stl.read_file(args.infile) 55 | if args.verbose: 56 | print("Read {0} ({4} facets, {1:.1f} x {2:.1f} x {3:.1f})".format( 57 | args.infile, 58 | stl.points.maxx - stl.points.minx, 59 | stl.points.maxy - stl.points.miny, 60 | stl.points.maxz - stl.points.minz, 61 | len(stl.facets), 62 | )) 63 | 64 | if not args.no_validation: 65 | manifold = True 66 | manifold = stl.check_manifold(verbose=args.verbose) 67 | if manifold and (args.verbose or args.gui_display): 68 | print("{} is manifold.".format(args.infile)) 69 | if not manifold: 70 | sys.exit(-1) 71 | 72 | slicer = Slicer([stl]) 73 | 74 | slicer.load_configs() 75 | if args.set_option: 76 | for opt in args.set_option: 77 | key, val = opt.split('=', 1) 78 | slicer.set_config(key,val) 79 | if args.filament: 80 | materials = args.filament.lower().split(",") 81 | for extnum,material in enumerate(materials): 82 | if '{}_hotend_temp'.format(material) not in slicer.conf: 83 | print("Unknown material: {}".format(material)) 84 | sys.exit(-1) 85 | newbedtemp = max(slicer.conf['{}_bed_temp'.format(material)] for material in materials) 86 | slicer.set_config("bed_temp", str(newbedtemp)) 87 | for extnum,material in enumerate(materials): 88 | print("Configuring extruder{} for {}".format(extnum, material)) 89 | slicer.set_config("nozzle_{}_temp".format(extnum), str(slicer.conf['{}_hotend_temp'.format(material)])) 90 | slicer.set_config("nozzle_{}_max_speed".format(extnum), str(slicer.conf['{}_max_speed'.format(material)])) 91 | if args.write_configs: 92 | slicer.save_configs() 93 | if args.query_option: 94 | for opt in args.query_option: 95 | slicer.display_configs_help(key=opt, vals_only=True) 96 | if args.help_configs: 97 | slicer.display_configs_help() 98 | if args.show_configs: 99 | slicer.display_configs_help(vals_only=True) 100 | 101 | if args.infile: 102 | if args.outfile: 103 | outfile = args.outfile 104 | else: 105 | outfile = os.path.splitext(args.infile)[0] + ".gcode" 106 | slicer.slice_to_file(outfile, showgui=args.gui_display) 107 | 108 | sys.exit(0) 109 | 110 | 111 | # vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap 112 | -------------------------------------------------------------------------------- /mandoline/facet3d.py: -------------------------------------------------------------------------------- 1 | import math 2 | import numbers 3 | 4 | try: 5 | from itertools import zip_longest as ziplong 6 | except ImportError: 7 | from itertools import izip_longest as ziplong 8 | 9 | from .vector import Vector 10 | from .point3d import Point3D 11 | from .line_segment3d import LineSegment3D 12 | 13 | 14 | class Facet3D(object): 15 | """Class to represent a 3D triangular face.""" 16 | 17 | def __init__(self, v1, v2, v3, norm): 18 | for x in [v1, v2, v3, norm]: 19 | try: 20 | n = len(x) 21 | except: 22 | n = 0 23 | if n != 3: 24 | raise TypeError('Expected 3D vector.') 25 | for y in x: 26 | if not isinstance(y, numbers.Real): 27 | raise TypeError('Expected 3D vector.') 28 | verts = [ 29 | Point3D(v1), 30 | Point3D(v2), 31 | Point3D(v3) 32 | ] 33 | # Re-order vertices in a normalized order. 34 | while verts[0] > verts[1] or verts[0] > verts[2]: 35 | verts = verts[1:] + verts[:1] 36 | self.vertices = verts 37 | self.norm = Vector(norm) 38 | self.count = 1 39 | self.fixup_normal() 40 | 41 | def __len__(self): 42 | """Length of sequence. Three vertices and a normal.""" 43 | return 4 44 | 45 | def __getitem__(self, idx): 46 | """Get vertices and normal by index.""" 47 | lst = self.vertices + [self.norm] 48 | return lst[idx] 49 | 50 | def __hash__(self): 51 | """Returns hash value for facet""" 52 | return hash((self.verts, self.norm)) 53 | 54 | def __lt__(self, other): 55 | return self.__cmp__(other) < 0 56 | 57 | def __cmp__(self, other): 58 | """Compare faces for sorting in an arbitrary heirarchy.""" 59 | cl1 = [sorted(v[i] for v in self.vertices) for i in range(3)] 60 | cl2 = [sorted(v[i] for v in other.vertices) for i in range(3)] 61 | for i in reversed(range(3)): 62 | for c1, c2 in ziplong(cl1[i], cl2[i]): 63 | if c1 is None: 64 | return -1 65 | val = (c1 > c2) - (c1 < c2) 66 | if val != 0: 67 | return val 68 | return 0 69 | 70 | def __format__(self, fmt): 71 | """Provides .format() support.""" 72 | pfx = "" 73 | sep = " - " 74 | sfx = "" 75 | if "a" in fmt: 76 | pfx = "[" 77 | sep = ", " 78 | sfx = "]" 79 | elif "s" in fmt: 80 | pfx = "" 81 | sep = " " 82 | sfx = "" 83 | ifx = sep.join(n.__format__(fmt) for n in list(self)[0:3]) 84 | return pfx + ifx + sfx 85 | 86 | def _side_of_line(self, line, pt): 87 | return (line[1][0] - line[0][0]) * (pt[1] - line[0][1]) - (line[1][1] - line[0][1]) * (pt[0] - line[0][0]) 88 | 89 | def _clockwise_line(self, line, pt): 90 | if self._side_of_line(line, pt) < 0: 91 | return (line[1], line[0]) 92 | return (line[0], line[1]) 93 | 94 | def _shoestring_algorithm(self, path): 95 | if path[0] == path[-1]: 96 | path = path[1:] 97 | out = 0 98 | for p1, p2 in zip(path, path[1:] + path[0:1]): 99 | out += p1[0] * p2[1] 100 | out -= p2[0] * p1[1] 101 | return out 102 | 103 | def _z_intercept(self,p1,p2,z): 104 | if p1[2] > z and p2[2] > z: 105 | return None 106 | if p1[2] < z and p2[2] < z: 107 | return None 108 | if p1[2] == z and p2[2] == z: 109 | return None 110 | u = (0.0+z-p1[2])/(p2[2]-p1[2]) 111 | delta = [p2[a]-p1[a] for a in range(3)] 112 | return [delta[a]*u+p1[a] for a in range(3)] 113 | 114 | def translate(self, offset): 115 | for a in range(3): 116 | for v in self.vertices: 117 | v[a] += offset[a] 118 | 119 | def get_footprint(self, z=None): 120 | if z is None: 121 | path = [v[0:2] for v in self.vertices] 122 | else: 123 | opath = list(self.vertices) + [self.vertices[0]] 124 | path = [] 125 | zed = zip(opath[:-1], opath[1:]) 126 | for v1,v2 in zed: 127 | if v1[2] > z: 128 | path.append(v1[0:2]) 129 | if (v1[2] > z and v2[2] < z) or (v1[2] < z and v2[2] > z): 130 | icept = self._z_intercept(v1,v2,z) 131 | if icept: 132 | path.append(icept[0:2]) 133 | if not path: 134 | return None 135 | a = self._shoestring_algorithm(path) 136 | if a == 0: 137 | return None 138 | if a > 0: # counter-clockwise 139 | path = list(reversed(path)) 140 | return path 141 | 142 | def overhang_angle(self): 143 | vert = Vector([0.0, 0.0, -1.0]) 144 | ang = vert.angle(self.norm) * 180.0 / math.pi 145 | return (90.0 - ang) 146 | 147 | def intersects_z(self, z): 148 | minz = min([v[2] for v in self.vertices]) 149 | maxz = max([v[2] for v in self.vertices]) 150 | return z >= minz and z <= maxz 151 | 152 | def z_range(self): 153 | allz = [v[2] for v in self.vertices] 154 | return (min(allz), max(allz)) 155 | 156 | def slice_at_z(self, z, quanta=1e-3): 157 | z = math.floor(z / quanta + 0.5) * quanta + quanta/2 158 | minz, maxz = self.z_range() 159 | if z < minz: 160 | return None 161 | if z > maxz: 162 | return None 163 | if math.hypot(self.norm[0], self.norm[1]) < 1e-6: 164 | return None 165 | norm2d = self.norm[0:2] 166 | vl = self.vertices 167 | vl2 = vl[1:] + vl[0:1] 168 | for v1, v2 in zip(vl, vl2): 169 | if v1[2] == z and v2[2] == z: 170 | line = ((v1[0], v1[1]), (v2[0], v2[1])) 171 | pt = (v1[0] + norm2d[0], v1[1] + norm2d[1]) 172 | line = self._clockwise_line(line, pt) 173 | return line 174 | if z == minz or z == maxz: 175 | return None 176 | vl3 = vl2[1:] + vl2[0:1] 177 | for v1, v2, v3 in zip(vl, vl2, vl3): 178 | if v2[2] == z: 179 | u = (z-v1[2])/(v3[2]-v1[2]) 180 | px = v1[0]+u*(v3[0]-v1[0]) 181 | py = v1[1]+u*(v3[1]-v1[1]) 182 | line = ((v2[0], v2[1]), (px, py)) 183 | pt = (v2[0] + norm2d[0], v2[1] + norm2d[1]) 184 | line = self._clockwise_line(line, pt) 185 | return line 186 | isects = [] 187 | for v1, v2 in zip(vl, vl2): 188 | if v1[2] == v2[2]: 189 | continue 190 | u = (z-v1[2])/(v2[2]-v1[2]) 191 | if u >= 0.0 and u <= 1.0: 192 | isects.append((v1, v2)) 193 | p1, p2 = isects[0] 194 | p3, p4 = isects[1] 195 | u1 = (z-p1[2])/(p2[2]-p1[2]) 196 | u2 = (z-p3[2])/(p4[2]-p3[2]) 197 | px = p1[0]+u1*(p2[0]-p1[0]) 198 | py = p1[1]+u1*(p2[1]-p1[1]) 199 | qx = p3[0]+u2*(p4[0]-p3[0]) 200 | qy = p3[1]+u2*(p4[1]-p3[1]) 201 | line = ((px, py), (qx, qy)) 202 | pt = (px + norm2d[0], py + norm2d[1]) 203 | line = self._clockwise_line(line, pt) 204 | return line 205 | 206 | def is_clockwise(self): 207 | """ 208 | Returns true if the three vertices of the face are in clockwise 209 | order with respect to the normal vector. 210 | """ 211 | v1 = Vector(self.vertices[1]-self.vertices[0]) 212 | v2 = Vector(self.vertices[2]-self.vertices[0]) 213 | return self.norm.dot(v1.cross(v2)) < 0 214 | 215 | def fixup_normal(self): 216 | if self.norm.length() > 0: 217 | # Make sure vertex ordering is counter-clockwise, 218 | # relative to the outward facing normal. 219 | if self.is_clockwise(): 220 | self.vertices = [ 221 | self.vertices[0], 222 | self.vertices[2], 223 | self.vertices[1] 224 | ] 225 | else: 226 | # If no normal was specified, we should calculate it, relative 227 | # to the counter-clockwise vertices (as seen from outside). 228 | v1 = Vector(self.vertices[2] - self.vertices[0]) 229 | v2 = Vector(self.vertices[1] - self.vertices[0]) 230 | self.norm = v1.cross(v2) 231 | if self.norm.length() > 1e-6: 232 | self.norm = self.norm.normalize() 233 | 234 | 235 | class Facet3DCache(object): 236 | """Cache class for 3D Facets.""" 237 | 238 | def __init__(self): 239 | """Initialize as an empty cache.""" 240 | self.vertex_hash = {} 241 | self.edge_hash = {} 242 | self.facet_hash = {} 243 | 244 | def rehash(self): 245 | """Rebuild the facet caches.""" 246 | oldhash = self.facet_hash 247 | self.vertex_hash = {} 248 | self.edge_hash = {} 249 | self.facet_hash = {} 250 | for facet in oldhash.values(): 251 | self._rehash_facet(facet) 252 | 253 | def _rehash_facet(self, facet): 254 | """Re-adds a facet to the caches.""" 255 | pts = tuple(facet[a] for a in range(3)) 256 | self.facet_hash[pts] = facet 257 | self._add_edge(pts[0], pts[1], facet) 258 | self._add_edge(pts[1], pts[2], facet) 259 | self._add_edge(pts[2], pts[0], facet) 260 | self._add_vertex(pts[0], facet) 261 | self._add_vertex(pts[1], facet) 262 | self._add_vertex(pts[2], facet) 263 | 264 | def translate(self, offset): 265 | """Translates vertices of all facets in the facet cache.""" 266 | for facet in self.facet_hash.values(): 267 | facet.translate(offset) 268 | self.rehash() 269 | 270 | def _add_vertex(self, pt, facet): 271 | """Remember that a given vertex touches a given facet.""" 272 | if pt not in self.vertex_hash: 273 | self.vertex_hash[pt] = [] 274 | self.vertex_hash[pt].append(facet) 275 | 276 | def _add_edge(self, p1, p2, facet): 277 | """Remember that a given edge touches a given facet.""" 278 | if p1 > p2: 279 | edge = (p1, p2) 280 | else: 281 | edge = (p2, p1) 282 | if edge not in self.edge_hash: 283 | self.edge_hash[edge] = [] 284 | self.edge_hash[edge].append(facet) 285 | 286 | def vertex_facets(self, pt): 287 | """Returns the facets that have a given facet.""" 288 | if pt not in self.vertex_hash: 289 | return [] 290 | return self.vertex_hash[pt] 291 | 292 | def edge_facets(self, p1, p2): 293 | """Returns the facets that have a given edge.""" 294 | if p1 > p2: 295 | edge = (p1, p2) 296 | else: 297 | edge = (p2, p1) 298 | if edge not in self.edge_hash: 299 | return [] 300 | return self.edge_hash[edge] 301 | 302 | def get(self, p1, p2, p3): 303 | """Given 3 vertices, return the cached Facet3D instance, if any.""" 304 | key = (p1, p2, p3) 305 | if key not in self.facet_hash: 306 | return None 307 | return self.facet_hash[key] 308 | 309 | def add(self, p1, p2, p3, norm): 310 | """ 311 | Given 3 vertices and a norm, return the (new or cached) Facet3d inst. 312 | """ 313 | key = (p1, p2, p3) 314 | if key in self.facet_hash: 315 | facet = self.facet_hash[key] 316 | facet.count += 1 317 | return facet 318 | facet = Facet3D(p1, p2, p3, norm) 319 | self.facet_hash[key] = facet 320 | self._add_edge(p1, p2, facet) 321 | self._add_edge(p2, p3, facet) 322 | self._add_edge(p3, p1, facet) 323 | self._add_vertex(p1, facet) 324 | self._add_vertex(p2, facet) 325 | self._add_vertex(p3, facet) 326 | return facet 327 | 328 | def sorted(self): 329 | """Returns a sorted iterator.""" 330 | vals = self.facet_hash.values() 331 | for pt in sorted(vals): 332 | yield pt 333 | 334 | def __iter__(self): 335 | """Creates an iterator for the facets in the cache.""" 336 | for pt in self.facet_hash.values(): 337 | yield pt 338 | 339 | def __len__(self): 340 | """Length of sequence.""" 341 | return len(self.facet_hash) 342 | 343 | 344 | # vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap 345 | -------------------------------------------------------------------------------- /mandoline/float_fmt.py: -------------------------------------------------------------------------------- 1 | def float_fmt(val): 2 | """ 3 | Returns a short, clean floating point string representation. 4 | Unnecessary trailing zeroes and decimal points are trimmed off. 5 | """ 6 | s = "{0:.6f}".format(val).rstrip('0').rstrip('.') 7 | return s if s != '-0' else '0' 8 | 9 | 10 | # vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap 11 | -------------------------------------------------------------------------------- /mandoline/geometry2d.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import pyclipper 4 | 5 | 6 | SCALING_FACTOR = 1000 7 | 8 | 9 | def offset(paths, amount): 10 | pco = pyclipper.PyclipperOffset() 11 | pco.ArcTolerance = SCALING_FACTOR / 40 12 | paths = pyclipper.scale_to_clipper(paths, SCALING_FACTOR) 13 | pco.AddPaths(paths, pyclipper.JT_SQUARE, pyclipper.ET_CLOSEDPOLYGON) 14 | outpaths = pco.Execute(amount * SCALING_FACTOR) 15 | outpaths = pyclipper.scale_from_clipper(outpaths, SCALING_FACTOR) 16 | return outpaths 17 | 18 | 19 | def union(paths1, paths2): 20 | if not paths1: 21 | return paths2 22 | if not paths2: 23 | return paths1 24 | pc = pyclipper.Pyclipper() 25 | if paths1: 26 | if paths1[0][0] in (int, float): 27 | raise pyclipper.ClipperException() 28 | paths1 = pyclipper.scale_to_clipper(paths1, SCALING_FACTOR) 29 | pc.AddPaths(paths1, pyclipper.PT_SUBJECT, True) 30 | if paths2: 31 | if paths2[0][0] in (int, float): 32 | raise pyclipper.ClipperException() 33 | paths2 = pyclipper.scale_to_clipper(paths2, SCALING_FACTOR) 34 | pc.AddPaths(paths2, pyclipper.PT_CLIP, True) 35 | try: 36 | outpaths = pc.Execute(pyclipper.CT_UNION, pyclipper.PFT_EVENODD, pyclipper.PFT_EVENODD) 37 | except: 38 | print("paths1={}".format(paths1)) 39 | print("paths2={}".format(paths2)) 40 | outpaths = pyclipper.scale_from_clipper(outpaths, SCALING_FACTOR) 41 | return outpaths 42 | 43 | 44 | def diff(subj, clip_paths, subj_closed=True): 45 | if not subj: 46 | return [] 47 | if not clip_paths: 48 | return subj 49 | pc = pyclipper.Pyclipper() 50 | if subj: 51 | subj = pyclipper.scale_to_clipper(subj, SCALING_FACTOR) 52 | pc.AddPaths(subj, pyclipper.PT_SUBJECT, subj_closed) 53 | if clip_paths: 54 | clip_paths = pyclipper.scale_to_clipper(clip_paths, SCALING_FACTOR) 55 | pc.AddPaths(clip_paths, pyclipper.PT_CLIP, True) 56 | outpaths = pc.Execute(pyclipper.CT_DIFFERENCE, pyclipper.PFT_EVENODD, pyclipper.PFT_EVENODD) 57 | outpaths = pyclipper.scale_from_clipper(outpaths, SCALING_FACTOR) 58 | return outpaths 59 | 60 | 61 | def clip(subj, clip_paths, subj_closed=True): 62 | if not subj: 63 | return [] 64 | if not clip_paths: 65 | return [] 66 | pc = pyclipper.Pyclipper() 67 | if subj: 68 | subj = pyclipper.scale_to_clipper(subj, SCALING_FACTOR) 69 | pc.AddPaths(subj, pyclipper.PT_SUBJECT, subj_closed) 70 | if clip_paths: 71 | clip_paths = pyclipper.scale_to_clipper(clip_paths, SCALING_FACTOR) 72 | pc.AddPaths(clip_paths, pyclipper.PT_CLIP, True) 73 | out_tree = pc.Execute2(pyclipper.CT_INTERSECTION, pyclipper.PFT_EVENODD, pyclipper.PFT_EVENODD) 74 | outpaths = pyclipper.PolyTreeToPaths(out_tree) 75 | outpaths = pyclipper.scale_from_clipper(outpaths, SCALING_FACTOR) 76 | return outpaths 77 | 78 | 79 | def paths_contain(pt, paths): 80 | cnt = 0 81 | pt = pyclipper.scale_to_clipper([pt], SCALING_FACTOR)[0] 82 | for path in paths: 83 | path = pyclipper.scale_to_clipper(path, SCALING_FACTOR) 84 | if pyclipper.PointInPolygon(pt, path): 85 | cnt = 1 - cnt 86 | return cnt % 2 != 0 87 | 88 | 89 | def orient_path(path, dir): 90 | orient = pyclipper.Orientation(path) 91 | path = pyclipper.scale_to_clipper(path, SCALING_FACTOR) 92 | if orient != dir: 93 | path = pyclipper.ReversePath(path) 94 | path = pyclipper.scale_from_clipper(path, SCALING_FACTOR) 95 | return path 96 | 97 | 98 | def orient_paths(paths): 99 | out = [] 100 | while paths: 101 | path = paths.pop(0) 102 | path = orient_path(path, not paths_contain(path[0], paths)) 103 | out.append(path) 104 | return out 105 | 106 | 107 | def paths_bounds(paths): 108 | if not paths: 109 | return (0, 0, 0, 0) 110 | minx, miny = (None, None) 111 | maxx, maxy = (None, None) 112 | for path in paths: 113 | for x, y in path: 114 | if minx is None or x < minx: 115 | minx = x 116 | if maxx is None or x > maxx: 117 | maxx = x 118 | if miny is None or y < miny: 119 | miny = y 120 | if maxy is None or y > maxy: 121 | maxy = y 122 | bounds = (minx, miny, maxx, maxy) 123 | return bounds 124 | 125 | 126 | def close_path(path): 127 | if not path: 128 | return path 129 | if path[0] == path[-1]: 130 | return path 131 | return path + path[0:1] 132 | 133 | 134 | def close_paths(paths): 135 | return [close_path(path) for path in paths] 136 | 137 | 138 | ############################################################ 139 | 140 | 141 | def make_infill_pat(rect, baseang, spacing, rots): 142 | minx, miny, maxx, maxy = rect 143 | w = maxx - minx 144 | h = maxy - miny 145 | cx = math.floor((maxx + minx)/2.0/spacing)*spacing 146 | cy = math.floor((maxy + miny)/2.0/spacing)*spacing 147 | r = math.hypot(w, h) / math.sqrt(2) 148 | n = int(math.ceil(r / spacing)) 149 | out = [] 150 | for rot in rots: 151 | c1 = math.cos((baseang+rot)*math.pi/180.0) 152 | s1 = math.sin((baseang+rot)*math.pi/180.0) 153 | c2 = math.cos((baseang+rot+90)*math.pi/180.0) * spacing 154 | s2 = math.sin((baseang+rot+90)*math.pi/180.0) * spacing 155 | for i in range(1-n, n): 156 | cp = (cx + c2 * i, cy + s2 * i) 157 | line = [ 158 | (cp[0] + r * c1, cp[1] + r * s1), 159 | (cp[0] - r * c1, cp[1] - r * s1) 160 | ] 161 | out.append( line ) 162 | return out 163 | 164 | 165 | def make_infill_lines(rect, base_ang, density, ewidth): 166 | if density <= 0.0: 167 | return [] 168 | if density > 1.0: 169 | density = 1.0 170 | spacing = ewidth / density 171 | return make_infill_pat(rect, base_ang, spacing, [0]) 172 | 173 | 174 | def make_infill_triangles(rect, base_ang, density, ewidth): 175 | if density <= 0.0: 176 | return [] 177 | if density > 1.0: 178 | density = 1.0 179 | spacing = 3.0 * ewidth / density 180 | return make_infill_pat(rect, base_ang, spacing, [0, 60, 120]) 181 | 182 | 183 | def make_infill_grid(rect, base_ang, density, ewidth): 184 | if density <= 0.0: 185 | return [] 186 | if density > 1.0: 187 | density = 1.0 188 | spacing = 2.0 * ewidth / density 189 | return make_infill_pat(rect, base_ang, spacing, [0, 90]) 190 | 191 | 192 | def make_infill_hexagons(rect, base_ang, density, ewidth): 193 | if density <= 0.0: 194 | return [] 195 | if density > 1.0: 196 | density = 1.0 197 | ext = 0.5 * ewidth / math.tan(60.0*math.pi/180.0) 198 | aspect = 3.0 / math.sin(60.0*math.pi/180.0) 199 | col_spacing = ewidth * 4./3. / density 200 | row_spacing = col_spacing * aspect 201 | minx, maxx, miny, maxy = rect 202 | w = maxx - minx 203 | h = maxy - miny 204 | cx = (maxx + minx)/2.0 205 | cy = (maxy + miny)/2.0 206 | r = max(w, h) * math.sqrt(2.0) 207 | n_col = math.ceil(r / col_spacing) 208 | n_row = math.ceil(r / row_spacing) 209 | out = [] 210 | s = math.sin(base_ang*math.pi/180.0) 211 | c = math.cos(base_ang*math.pi/180.0) 212 | for col in range(-n_col, n_col): 213 | path = [] 214 | base_x = col * col_spacing 215 | for row in range(-n_row, n_row): 216 | base_y = row * row_spacing 217 | x1 = base_x + ewidth/2.0 218 | x2 = base_x + col_spacing - ewidth/2.0 219 | if col % 2 != 0: 220 | x1, x2 = x2, x1 221 | path.append((x1, base_y+ext)) 222 | path.append((x2, base_y+row_spacing/6-ext)) 223 | path.append((x2, base_y+row_spacing/2+ext)) 224 | path.append((x1, base_y+row_spacing*2/3-ext)) 225 | path = [(x*c - y*s, x*s + y*c) for x, y in path] 226 | out.append(path) 227 | return out 228 | 229 | 230 | # vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap 231 | -------------------------------------------------------------------------------- /mandoline/line_segment3d.py: -------------------------------------------------------------------------------- 1 | 2 | class LineSegment3D(object): 3 | """A class to represent a 3D line segment.""" 4 | 5 | def __init__(self, p1, p2): 6 | """Initialize with twwo endpoints.""" 7 | if p1 > p2: 8 | p1, p2 = (p2, p1) 9 | self.p1 = p1 10 | self.p2 = p2 11 | self.count = 1 12 | 13 | def __len__(self): 14 | """Line segment always has two endpoints.""" 15 | return 2 16 | 17 | def __iter__(self): 18 | """Iterator generator for endpoints.""" 19 | yield self.p1 20 | yield self.p2 21 | 22 | def __getitem__(self, idx): 23 | """Given a vertex number, returns a vertex coordinate vector.""" 24 | if idx == 0: 25 | return self.p1 26 | if idx == 1: 27 | return self.p2 28 | raise LookupError() 29 | 30 | def __hash__(self): 31 | """Returns hash value for endpoints""" 32 | return hash((self.p1, self.p2)) 33 | 34 | def __lt__(self, p): 35 | return self < p 36 | 37 | def __cmp__(self, p): 38 | """Compare points for sort ordering in an arbitrary heirarchy.""" 39 | val = self[0].__cmp__(p[0]) 40 | if val != 0: 41 | return val 42 | return self[1].__cmp__(p[1]) 43 | 44 | def __format__(self, fmt): 45 | """Provides .format() support.""" 46 | pfx = "" 47 | sep = " - " 48 | sfx = "" 49 | if "a" in fmt: 50 | pfx = "[" 51 | sep = ", " 52 | sfx = "]" 53 | elif "s" in fmt: 54 | pfx = "" 55 | sep = " " 56 | sfx = "" 57 | p1 = self.p1.__format__(fmt) 58 | p2 = self.p2.__format__(fmt) 59 | return pfx + p1 + sep + p2 + sfx 60 | 61 | def __repr__(self): 62 | """Standard string representation.""" 63 | return "".format(self) 64 | 65 | def __str__(self): 66 | """Returns a human readable coordinate string.""" 67 | return "{0:a}".format(self) 68 | 69 | def translate(self,offset): 70 | """Translate the endpoint's vertices""" 71 | self.p1 = (self.p1[a] + offset[a] for a in range(3)) 72 | self.p2 = (self.p2[a] + offset[a] for a in range(3)) 73 | 74 | def length(self): 75 | """Returns the length of the line.""" 76 | return self.p1.distFromPoint(self.p2) 77 | 78 | 79 | class LineSegment3DCache(object): 80 | """Cache class for 3D Line Segments.""" 81 | 82 | def __init__(self): 83 | """Initialize as an empty cache.""" 84 | self.endhash = {} 85 | self.seghash = {} 86 | 87 | def _add_endpoint(self, p, seg): 88 | """Remember that this segment has a given endpoint""" 89 | if p not in self.endhash: 90 | self.endhash[p] = [] 91 | self.endhash[p].append(seg) 92 | 93 | def rehash(self): 94 | """Reset the hashes for changed edge vertices""" 95 | oldseghash = self.seghash 96 | self.seghash = { 97 | (v[0], v[1]): v 98 | for v in oldseghash.values() 99 | } 100 | oldendhash = self.endhash 101 | self.endhash = { 102 | k: v 103 | for v in oldendhash.values() 104 | for k in v 105 | } 106 | 107 | def translate(self,offset): 108 | """Translate vertices of all edges.""" 109 | for v in self.seghash.values(): 110 | v.translate(offset) 111 | self.rehash() 112 | 113 | def endpoint_segments(self, p): 114 | """get list of edges that end at point p""" 115 | if p not in self.endhash: 116 | return [] 117 | return self.endhash[p] 118 | 119 | def get(self, p1, p2): 120 | """Given 2 endpoints, return the cached LineSegment3D inst, if any.""" 121 | key = (p1, p2) if p1 < p2 else (p2, p1) 122 | if key not in self.seghash: 123 | return None 124 | return self.seghash[key] 125 | 126 | def add(self, p1, p2): 127 | """Given 2 endpoints, return the (new or cached) LineSegment3D inst.""" 128 | key = (p1, p2) if p1 < p2 else (p2, p1) 129 | if key in self.seghash: 130 | seg = self.seghash[key] 131 | seg.count += 1 132 | return seg 133 | seg = LineSegment3D(p1, p2) 134 | self.seghash[key] = seg 135 | self._add_endpoint(p1, seg) 136 | self._add_endpoint(p2, seg) 137 | return seg 138 | 139 | def __iter__(self): 140 | """Creates an iterator for the line segments in the cache.""" 141 | for pt in self.seghash.values(): 142 | yield pt 143 | 144 | def __len__(self): 145 | """Length of sequence.""" 146 | return len(self.seghash) 147 | 148 | 149 | # vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap 150 | -------------------------------------------------------------------------------- /mandoline/point3d.py: -------------------------------------------------------------------------------- 1 | import math 2 | import struct 3 | import numbers 4 | 5 | try: 6 | from itertools import zip_longest as ziplong 7 | except ImportError: 8 | from itertools import izip_longest as ziplong 9 | 10 | from .float_fmt import float_fmt 11 | 12 | 13 | class Point3D(object): 14 | """Class to represent a 3D Point.""" 15 | 16 | def __init__(self, *args): 17 | self._values = [0.0, 0.0, 0.0] 18 | if len(args) == 1: 19 | val = args[0] 20 | if isinstance(val, numbers.Real): 21 | self._values = [val, 0.0, 0.0] 22 | return 23 | elif isinstance(val, numbers.Complex): 24 | self._values = [val.real, val.imag, 0.0] 25 | return 26 | else: 27 | val = args 28 | try: 29 | for i, x in enumerate(val): 30 | if not isinstance(x, numbers.Real): 31 | raise TypeError('Expected sequence of real numbers.') 32 | self._values[i] = x 33 | except: 34 | pass 35 | 36 | def __iter__(self): 37 | """Iterator generator for point values.""" 38 | for idx in range(3): 39 | yield self[idx] 40 | 41 | def __len__(self): 42 | return 3 43 | 44 | def __setitem__(self, idx, val): 45 | self._values[idx] = val 46 | 47 | def __getitem__(self, idx): 48 | """Given a vertex number, returns a vertex coordinate vector.""" 49 | if type(idx) is not slice and idx >= len(self._values): 50 | return 0.0 51 | return self._values[idx] 52 | 53 | def __hash__(self): 54 | """Returns hash value for point coords""" 55 | return hash(tuple(self._values)) 56 | 57 | def __cmp__(self, p): 58 | """Compare points for sort ordering in an arbitrary heirarchy.""" 59 | longzip = ziplong(self._values, p, fillvalue=0.0) 60 | for v1, v2 in reversed(list(longzip)): 61 | val = v1 - v2 62 | if val != 0: 63 | val /= abs(val) 64 | return val 65 | return 0 66 | 67 | def __eq__(self, other): 68 | """Equality comparison for points.""" 69 | return self._values == other._values 70 | 71 | def __lt__(self, other): 72 | return self.__cmp__(other) < 0 73 | 74 | def __gt__(self, other): 75 | return self.__cmp__(other) > 0 76 | 77 | def __sub__(self, v): 78 | return Point3D(self[i] - v[i] for i in range(3)) 79 | 80 | def __rsub__(self, v): 81 | return Point3D(v[i] - self[i] for i in range(3)) 82 | 83 | def __add__(self, v): 84 | return Vector(i + j for i, j in zip(self._values, v)) 85 | 86 | def __radd__(self, v): 87 | return Vector(i + j for i, j in zip(v, self._values)) 88 | 89 | def __div__(self, s): 90 | """Divide each element in a vector by a scalar.""" 91 | return Vector(x / s for x in self._values) 92 | 93 | def __format__(self, fmt): 94 | vals = [float_fmt(x) for x in self._values] 95 | if "a" in fmt: 96 | return "[{0}]".format(", ".join(vals)) 97 | if "s" in fmt: 98 | return " ".join(vals) 99 | if "b" in fmt: 100 | return struct.pack('<3f', *self._values) 101 | return "({0})".format(", ".join(vals)) 102 | 103 | def __repr__(self): 104 | return "".format(self) 105 | 106 | def __str__(self): 107 | """Returns a standard array syntax string of the coordinates.""" 108 | return "{0:a}".format(self) 109 | 110 | def translate(self, offset): 111 | """Translates the coordinates of this point.""" 112 | self._values = [i + j for i, j in zip(offset, self._values)] 113 | 114 | def distFromPoint(self, v): 115 | """Returns the distance from another point.""" 116 | return math.sqrt(sum(math.pow(x1-x2, 2.0) for x1, x2 in zip(v, self))) 117 | 118 | def distFromLine(self, pt, line): 119 | """ 120 | Returns the distance of a 3d point from a line defined by a sequence 121 | of two 3d points. 122 | """ 123 | w = Vector(pt - line[0]) 124 | v = Vector(line[1]-line[0]) 125 | return v.normalize().cross(w).length() 126 | 127 | 128 | class Point3DCache(object): 129 | """Cache class for 3D Points.""" 130 | 131 | def __init__(self): 132 | """Initialize as an empty cache.""" 133 | self.point_hash = {} 134 | self.minx = 9e99 135 | self.miny = 9e99 136 | self.minz = 9e99 137 | self.maxx = -9e99 138 | self.maxy = -9e99 139 | self.maxz = -9e99 140 | 141 | def __len__(self): 142 | """Length of sequence.""" 143 | return len(self.point_hash) 144 | 145 | def _update_volume(self, p): 146 | """Update the volume cube that contains all the points.""" 147 | if p[0] < self.minx: 148 | self.minx = p[0] 149 | if p[0] > self.maxx: 150 | self.maxx = p[0] 151 | if p[1] < self.miny: 152 | self.miny = p[1] 153 | if p[1] > self.maxy: 154 | self.maxy = p[1] 155 | if p[2] < self.minz: 156 | self.minz = p[2] 157 | if p[2] > self.maxz: 158 | self.maxz = p[2] 159 | 160 | def rehash(self): 161 | """Rebuild the point cache.""" 162 | oldpthash = self.point_hash 163 | self.point_hash = { 164 | tuple(round(n, 4) for n in pt): pt 165 | for pt in oldpthash.values() 166 | } 167 | 168 | def translate(self, offset): 169 | """Translates all cached points.""" 170 | self.minx += offset[0] 171 | self.maxx += offset[0] 172 | self.miny += offset[1] 173 | self.maxy += offset[1] 174 | self.minz += offset[2] 175 | self.maxz += offset[2] 176 | for pt in self.point_hash.values(): 177 | pt.translate(offset) 178 | self.rehash() 179 | 180 | def get_volume(self): 181 | """Returns the 3D volume that contains all the points in the cache.""" 182 | return ( 183 | self.minx, self.miny, self.minz, 184 | self.maxx, self.maxy, self.maxz 185 | ) 186 | 187 | def add(self, x, y, z): 188 | """Given XYZ coords, returns the (new or cached) Point3D instance.""" 189 | key = tuple(round(n, 4) for n in [x, y, z]) 190 | if key in self.point_hash: 191 | return self.point_hash[key] 192 | pt = Point3D(key) 193 | self.point_hash[key] = pt 194 | self._update_volume(pt) 195 | return pt 196 | 197 | def __iter__(self): 198 | """Creates an iterator for the points in the cache.""" 199 | for pt in self.point_hash.values(): 200 | yield pt 201 | 202 | 203 | # vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap 204 | -------------------------------------------------------------------------------- /mandoline/slicer.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import sys 4 | import math 5 | import time 6 | import random 7 | import os.path 8 | import platform 9 | from collections import OrderedDict 10 | from appdirs import user_config_dir 11 | 12 | import mandoline.geometry2d as geom 13 | from .TextThermometer import TextThermometer 14 | 15 | 16 | slicer_configs = OrderedDict([ 17 | ('Quality', ( 18 | ('layer_height', float, 0.2, (0.01, 0.5), "Slice layer height in mm."), 19 | ('shell_count', int, 2, (1, 10), "Number of outer shells to print."), 20 | ('random_starts', bool, True, None, "Enable randomizing of perimeter starts."), 21 | ('top_layers', int, 3, (0, 10), "Number of layers to print on the top side of the object."), 22 | ('bottom_layers', int, 3, (0, 10), "Number of layers to print on the bottom side of the object."), 23 | ('infill_type', list, 'Grid', ['Lines', 'Triangles', 'Grid', 'Hexagons'], "Pattern that the infill will be printed in."), 24 | ('infill_density', float, 30., (0., 100.), "Infill density in percent."), 25 | ('infill_overlap', float, 0.15, (0.0, 1.0), "Amount, in mm that infill will overlap with perimeter extrusions."), 26 | ('feed_rate', int, 60, (1, 300), "Speed while extruding. (mm/s)"), 27 | ('travel_rate_xy', int, 100, (1, 300), "Travel motion speed (mm/s)"), 28 | ('travel_rate_z', float, 5., (0.1, 30.), "Z-axis motion speed (mm/s)"), 29 | )), 30 | ('Support', ( 31 | ('support_type', list, 'External', ('None', 'External', 'Everywhere'), "What kind of support structure to add."), 32 | ('support_outset', float, 0.5, (0., 2.), "How far support structures should be printed away from model, horizontally."), 33 | ('support_density', float, 33.0, (0., 100.), "Density of support structure internals."), 34 | ('overhang_angle', int, 45, (0, 90), "Angle from vertical that support structures should be printed for."), 35 | )), 36 | ('Adhesion', ( 37 | ('adhesion_type', list, 'None', ('None', 'Brim', 'Raft'), "What kind of base adhesion structure to add."), 38 | ('brim_width', float, 3.0, (0., 20.), "Width of brim to print on first layer to help with part adhesion."), 39 | ('raft_layers', int, 3, (1, 5), "Number of layers to use in making the raft."), 40 | ('raft_outset', float, 3.0, (0., 50.), "How much bigger raft should be than the model footprint."), 41 | ('skirt_outset', float, 0.0, (0., 20.), "How far the skirt should be printed away from model."), 42 | ('skirt_layers', int, 0, (0, 1000), "Number of layers to print print the skirt on."), 43 | ('prime_length', float, 10.0, (0., 1000.), "Length of filament to extrude when priming hotends."), 44 | )), 45 | ('Retraction', ( 46 | ('retract_enable', bool, True, None, "Enable filament retraction."), 47 | ('retract_speed', float, 30.0, (0., 200.), "Speed to retract filament at. (mm/s)"), 48 | ('retract_dist', float, 3.0, (0., 20.), "Distance to retract filament between extrusion moves. (mm)"), 49 | ('retract_extruder', float, 3.0, (0., 50.), "Distance to retract filament on extruder change. (mm)"), 50 | ('retract_lift', float, 0.0, (0., 10.), "Distance to lift the extruder head during retracted moves. (mm)"), 51 | )), 52 | ('Materials', ( 53 | ('abs_bed_temp', int, 90, ( 0, 150), "The bed temperature to use for ABS filament. (C)"), 54 | ('abs_hotend_temp', int, 230, (150, 300), "The extruder temperature to use for ABS filament. (C)"), 55 | ('abs_max_speed', float, 75.0, ( 0, 150), "The maximum speed when extruding ABS filament. (mm/s)"), 56 | ('hips_bed_temp', int, 100, ( 0, 150), "The bed temperature to use for dissolvable HIPS filament. (C)"), 57 | ('hips_hotend_temp', int, 230, (150, 300), "The extruder temperature to use for dissolvable HIPS filament. (C)"), 58 | ('hips_max_speed', float, 30.0, ( 0, 150), "The maximum speed when extruding dissolvable HIPS filament. (mm/s)"), 59 | ('nylon_bed_temp', int, 70, ( 0, 150), "The bed temperature to use for Nylon filament. (C)"), 60 | ('nylon_hotend_temp', int, 255, (150, 300), "The extruder temperature to use for Nylon filament. (C)"), 61 | ('nylon_max_speed', float, 75.0, ( 0, 150), "The maximum speed when extruding Nylon filament. (mm/s)"), 62 | ('pc_bed_temp', int, 130, ( 0, 150), "The bed temperature to use for Polycarbonate filament. (C)"), 63 | ('pc_hotend_temp', int, 290, (150, 300), "The extruder temperature to use for Polycarbonate filament. (C)"), 64 | ('pc_max_speed', float, 75.0, ( 0, 150), "The maximum speed when extruding Polycarbonate filament. (mm/s)"), 65 | ('pet_bed_temp', int, 70, ( 0, 150), "The bed temperature to use for PETG/PETT filament. (C)"), 66 | ('pet_hotend_temp', int, 230, (150, 300), "The extruder temperature to use for PETG/PETT filament. (C)"), 67 | ('pet_max_speed', float, 75.0, ( 0, 150), "The maximum speed when extruding PETG/PETT filament. (mm/s)"), 68 | ('pla_bed_temp', int, 45, ( 0, 150), "The bed temperature to use for PLA filament. (C)"), 69 | ('pla_hotend_temp', int, 205, (150, 300), "The extruder temperature to use for PLA filament. (C)"), 70 | ('pla_max_speed', float, 75.0, ( 0, 150), "The maximum speed when extruding PLA filament. (mm/s)"), 71 | ('pp_bed_temp', int, 110, ( 0, 150), "The bed temperature to use for Polypropylene filament. (C)"), 72 | ('pp_hotend_temp', int, 250, (150, 300), "The extruder temperature to use for Polypropylene filament. (C)"), 73 | ('pp_max_speed', float, 75.0, ( 0, 150), "The maximum speed when extruding Polypropylene filament. (mm/s)"), 74 | ('pva_bed_temp', int, 60, ( 0, 150), "The bed temperature to use for dissolvable PVA filament. (C)"), 75 | ('pva_hotend_temp', int, 220, (150, 300), "The extruder temperature to use for dissolvable PVA filament. (C)"), 76 | ('pva_max_speed', float, 30.0, ( 0, 150), "The maximum speed when extruding dissolvable PVA filament. (mm/s)"), 77 | ('softpla_bed_temp', int, 30, ( 0, 150), "The bed temperature to use for flexible SoftPLA filament. (C)"), 78 | ('softpla_hotend_temp', int, 230, (150, 300), "The extruder temperature to use for flexible SoftPLA filament. (C)"), 79 | ('softpla_max_speed', float, 30.0, ( 0, 150), "The maximum speed when extruding flexible SoftPLA filament. (mm/s)"), 80 | ('tpe_bed_temp', int, 30, ( 0, 150), "The bed temperature to use for flexible TPE filament. (C)"), 81 | ('tpe_hotend_temp', int, 220, (150, 300), "The extruder temperature to use for flexible TPE filament. (C)"), 82 | ('tpe_max_speed', float, 30.0, ( 0, 150), "The maximum speed when extruding flexible TPE filament. (mm/s)"), 83 | ('tpu_bed_temp', int, 50, ( 0, 150), "The bed temperature to use for flexible TPU filament. (C)"), 84 | ('tpu_hotend_temp', int, 250, (150, 300), "The extruder temperature to use for flexible TPU filament. (C)"), 85 | ('tpu_max_speed', float, 30.0, ( 0, 150), "The maximum speed when extruding flexible TPU filament. (mm/s)"), 86 | )), 87 | ('Machine', ( 88 | ('bed_geometry', list, 'Rectangular', ('Rectangular', 'Cylindrical'), "The shape of the build volume cross-section."), 89 | ('bed_size_x', float, 200, (0,1000), "The X-axis size of the build platform bed."), 90 | ('bed_size_y', float, 200, (0,1000), "The Y-axis size of the build platform bed."), 91 | ('bed_center_x', float, 100, (-500,500), "The X coordinate of the center of the bed."), 92 | ('bed_center_y', float, 100, (-500,500), "The Y coordinate of the center of the bed."), 93 | ('bed_temp', int, 70, (0, 150), "The temperature to set the heated bed to."), 94 | 95 | ('extruder_count', int, 1, (1, 4), "The number of extruders this machine has."), 96 | ('default_nozzle', int, 0, (0, 4), "The default extruder used for printing."), 97 | ('infill_nozzle', int, -1, (-1, 4), "The extruder used for infill material. -1 means use default nozzle."), 98 | ('support_nozzle', int, -1, (-1, 4), "The extruder used for support material. -1 means use default nozzle."), 99 | 100 | ('nozzle_0_temp', int, 190, (150, 250), "The temperature of the nozzle for extruder 0. (C)"), 101 | ('nozzle_0_filament', float, 1.75, (1.0, 3.5), "The diameter of the filament for extruder 0. (mm)"), 102 | ('nozzle_0_diam', float, 0.4, (0.1, 1.5), "The diameter of the nozzle for extruder 0. (mm)"), 103 | ('nozzle_0_xoff', float, 0.0, (-100, 100), "The X positional offset for extruder 0. (mm)"), 104 | ('nozzle_0_yoff', float, 0.0, (-100, 100), "The Y positional offset for extruder 0. (mm)"), 105 | ('nozzle_0_max_speed', float, 75.0, (0., 200.), "The maximum speed when using extruder 0. (mm/s)"), 106 | 107 | ('nozzle_1_temp', int, 190, (150, 250), "The temperature of the nozzle for extruder 1. (C)"), 108 | ('nozzle_1_filament', float, 1.75, (1.0, 3.5), "The diameter of the filament for extruder 1. (mm)"), 109 | ('nozzle_1_diam', float, 0.4, (0.1, 1.5), "The diameter of the nozzle for extruder 1. (mm)"), 110 | ('nozzle_1_xoff', float, 25.0, (-100, 100), "The X positional offset for extruder 1. (mm)"), 111 | ('nozzle_1_yoff', float, 0.0, (-100, 100), "The Y positional offset for extruder 1. (mm)"), 112 | ('nozzle_1_max_speed', float, 75.0, (0., 200.), "The maximum speed when using extruder 1. (mm/s)"), 113 | 114 | ('nozzle_2_temp', int, 190, (150, 250), "The temperature of the nozzle for extruder 2. (C)"), 115 | ('nozzle_2_filament', float, 1.75, (1.0, 3.5), "The diameter of the filament for extruder 2. (mm)"), 116 | ('nozzle_2_diam', float, 0.4, (0.1, 1.5), "The diameter of the nozzle for extruder 2. (mm)"), 117 | ('nozzle_2_xoff', float, -25., (-100, 100), "The X positional offset for extruder 2. (mm)"), 118 | ('nozzle_2_yoff', float, 0.0, (-100, 100), "The Y positional offset for extruder 2. (mm)"), 119 | ('nozzle_2_max_speed', float, 75.0, (0., 200.), "The maximum speed when using extruder 2. (mm/s)"), 120 | 121 | ('nozzle_3_temp', int, 190, (150, 250), "The temperature of the nozzle for extruder 3. (C)"), 122 | ('nozzle_3_filament', float, 1.75, (1.0, 3.5), "The diameter of the filament for extruder 3. (mm)"), 123 | ('nozzle_3_diam', float, 0.4, (0.1, 1.5), "The diameter of the nozzle for extruder 3. (mm)"), 124 | ('nozzle_3_xoff', float, 0.0, (-100, 100), "The X positional offset for extruder 3. (mm)"), 125 | ('nozzle_3_yoff', float, 25.0, (-100, 100), "The Y positional offset for extruder 3. (mm)"), 126 | ('nozzle_3_max_speed', float, 75.0, (0., 200.), "The maximum speed when using extruder 3. (mm/s)"), 127 | )), 128 | ]) 129 | 130 | 131 | ############################################################ 132 | 133 | 134 | class Slicer(object): 135 | def __init__(self, models, **kwargs): 136 | self.models = models 137 | self.conf = {} 138 | self.conf_metadata = {} 139 | for key, opts in slicer_configs.items(): 140 | for name, typ, dflt, rng, desc in opts: 141 | self.conf[name] = dflt 142 | self.conf_metadata[name] = { 143 | "type": typ, 144 | "default": dflt, 145 | "range": rng, 146 | "descr": desc 147 | } 148 | self.raw_layer_paths = {} 149 | self.last_pos = (0.0, 0.0, 0.0) 150 | self.last_e = 0.0 151 | self.last_nozl = 0 152 | self.total_build_time = 0.0 153 | self.mag = 4 154 | self.layer = 0 155 | self.config(**kwargs) 156 | 157 | def config(self, **kwargs): 158 | for key, val in kwargs.items(): 159 | if key in self.conf: 160 | self.conf[key] = val 161 | 162 | def get_conf_filename(self): 163 | return user_config_dir("Mandoline") 164 | 165 | def set_config(self, key, valstr): 166 | key = key.strip() 167 | valstr = valstr.strip() 168 | if key not in self.conf_metadata: 169 | print("Ignoring unknown config option: {}".format(key)) 170 | return 171 | typ = self.conf_metadata[key]["type"] 172 | rng = self.conf_metadata[key]["range"] 173 | badval = True 174 | typestr = "" 175 | errmsg = "" 176 | if typ is bool: 177 | typestr = "boolean " 178 | errmsg = "Value should be either True or False" 179 | if valstr in ["True", "False"]: 180 | self.conf[key] = valstr == "True" 181 | badval = False 182 | elif typ is int: 183 | typestr = "integer " 184 | errmsg = "Value should be between {} and {}, inclusive.".format(*rng) 185 | try: 186 | if int(valstr) >= rng[0] and int(valstr) <= rng[1]: 187 | self.conf[key] = int(valstr) 188 | badval = False 189 | except(ValueError): 190 | pass 191 | elif typ is float: 192 | typestr = "float " 193 | errmsg = "Value should be between {} and {}, inclusive.".format(*rng) 194 | try: 195 | if float(valstr) >= rng[0] and float(valstr) <= rng[1]: 196 | self.conf[key] = float(valstr) 197 | badval = False 198 | except(ValueError): 199 | pass 200 | elif typ is list: 201 | typestr = "" 202 | errmsg = "Valid options are: {}".format(", ".join(rng)) 203 | if valstr in rng: 204 | self.conf[key] = str(valstr) 205 | badval = False 206 | if badval: 207 | print("Ignoring bad {0}configuration value: {1}={2}".format(typestr,key,valstr)) 208 | print(errmsg) 209 | 210 | def load_configs(self): 211 | conffile = self.get_conf_filename() 212 | if not os.path.exists(conffile): 213 | return 214 | if not os.path.isfile(conffile): 215 | return 216 | print("Loading configs from {}".format(conffile)) 217 | with open(conffile, "r") as f: 218 | for line in f.readlines(): 219 | line = line.strip() 220 | if not line or line.startswith("#"): 221 | continue 222 | key, val = line.split("=") 223 | self.set_config(key,val) 224 | 225 | def save_configs(self): 226 | conffile = self.get_conf_filename() 227 | confdir = os.path.dirname(conffile) 228 | if not os.path.exists(confdir): 229 | os.makedirs(confdir) 230 | with open(conffile, "w") as f: 231 | for sect, opts in slicer_configs.items(): 232 | f.write("# {}\n".format(sect)) 233 | for name, typ, dflt, rng, desc in opts: 234 | f.write("{}={}\n".format(name, self.conf[name])) 235 | f.write("\n\n") 236 | print("Saving configs to {}".format(conffile)) 237 | 238 | def display_configs_help(self, key=None, vals_only=False): 239 | if key: 240 | key = key.strip() 241 | if key not in self.conf_metadata: 242 | print("Unknown config option: {}".format(key)) 243 | return 244 | for sect, opts in slicer_configs.items(): 245 | if not vals_only and not key: 246 | print("{}:".format(sect)) 247 | for name, typ, dflt, rng, desc in opts: 248 | if key and key != name: 249 | continue 250 | if typ is bool: 251 | typename = "bool" 252 | rngstr = "True/False" 253 | elif typ is int: 254 | typename = "int" 255 | rngstr = "{} ... {}".format(*rng) 256 | elif typ is float: 257 | typename = "float" 258 | rngstr = "{} ... {}".format(*rng) 259 | elif typ is list: 260 | typename = "opt" 261 | rngstr = ", ".join(rng) 262 | print(" {} = {}".format(name, self.conf[name])) 263 | if not vals_only: 264 | print(" Type: {} ({})".format(typename, rngstr)) 265 | print(" {}".format(desc)) 266 | 267 | def slice_to_file(self, filename, showgui=False): 268 | print("Slicing start") 269 | self.dflt_nozl = self.conf['default_nozzle'] 270 | self.infl_nozl = self.conf['infill_nozzle'] 271 | self.supp_nozl = self.conf['support_nozzle'] 272 | self.center_point = (self.conf['bed_center_x'], self.conf['bed_center_y']) 273 | if self.infl_nozl == -1: 274 | self.infl_nozl = self.dflt_nozl 275 | if self.supp_nozl == -1: 276 | self.supp_nozl = self.dflt_nozl 277 | dflt_nozl_d = self.conf['nozzle_{0}_diam'.format(self.dflt_nozl)] 278 | infl_nozl_d = self.conf['nozzle_{0}_diam'.format(self.infl_nozl)] 279 | supp_nozl_d = self.conf['nozzle_{0}_diam'.format(self.supp_nozl)] 280 | 281 | self.layer_h = self.conf['layer_height'] 282 | self.raft_layers = self.conf['raft_layers'] if self.conf['adhesion_type'] == "Raft" else 0 283 | self.extrusion_ratio = 1.25 284 | self.extrusion_width = dflt_nozl_d * self.extrusion_ratio 285 | self.infill_width = infl_nozl_d * self.extrusion_ratio 286 | self.support_width = supp_nozl_d * self.extrusion_ratio 287 | for model in self.models: 288 | model.center( (self.center_point[0], self.center_point[1], (model.points.maxz-model.points.minz)/2.0) ) 289 | model.assign_layers(self.layer_h) 290 | height = max([model.points.maxz - model.points.minz for model in self.models]) 291 | self.layers = int(height / self.layer_h) 292 | self.layer_zs = [ 293 | self.layer_h * (layer + 1) 294 | for layer in range(self.layers + self.raft_layers) 295 | ] 296 | self.thermo = TextThermometer(self.layers) 297 | 298 | print("Perimeters") 299 | self._slicer_task_perimeters() 300 | 301 | print("Support") 302 | self._slicer_task_support() 303 | 304 | print("Raft, Brim, and Skirt") 305 | self._slicer_task_adhesion() 306 | 307 | print("Infill") 308 | self._slicer_task_fill() 309 | 310 | print("Pathing") 311 | self._slicer_task_pathing() 312 | 313 | print("Writing GCode to {}".format(filename)) 314 | self._slicer_task_gcode(filename) 315 | 316 | print( 317 | "Slicing complete. Estimated build time: {:d}h {:02d}m".format( 318 | int(self.total_build_time/3600), 319 | int((self.total_build_time%3600)/60) 320 | ) 321 | ) 322 | 323 | if showgui: 324 | print("Launching slice viewer") 325 | self._display_paths() 326 | 327 | # TODO: Enable multi-model loading/placement/rotation 328 | # TODO: Verify models fit inside build volume. 329 | # TODO: Interior solid infill perimeter paths 330 | # TODO: Pathing type prioritization 331 | # TODO: Optimize route paths 332 | # TODO: Skip retraction for short motions 333 | # TODO: Smooth top surfacing for non-flat surfaces 334 | # TODO: G-Code custom startup/shutdown/toolchange scripts. 335 | # TODO: G-Code flavors 336 | # TODO: G-Code volumetric extrusion 337 | # TODO: Relative E motions. 338 | # TODO: Better Bridging 339 | 340 | ############################################################ 341 | 342 | def _slicer_task_perimeters(self): 343 | self.thermo.set_target(2*self.layers) 344 | self.layer_paths = [] 345 | self.perimeter_paths = [] 346 | self.skirt_bounds = [] 347 | random_starts = self.conf['random_starts'] 348 | self.dead_paths = [] 349 | for layer in range(self.layers): 350 | self.thermo.update(layer) 351 | 352 | # Layer Slicing 353 | z = self.layer_zs[layer] 354 | paths = [] 355 | layer_dead_paths = [] 356 | for model in self.models: 357 | model_paths, dead_paths = model.slice_at_z(z - self.layer_h/2, self.layer_h) 358 | layer_dead_paths.extend(dead_paths) 359 | model_paths = geom.orient_paths(model_paths) 360 | paths = geom.union(paths, model_paths) 361 | self.layer_paths.append(paths) 362 | self.dead_paths.append(layer_dead_paths) 363 | 364 | # Perimeters 365 | perims = [] 366 | randpos = random.random() 367 | for i in range(self.conf['shell_count']): 368 | shell = geom.offset(paths, -(i+0.5) * self.extrusion_width) 369 | shell = geom.close_paths(shell) 370 | if self.conf['random_starts']: 371 | shell = [ 372 | ( path if i == 0 else (path[i:] + path[1:i+1]) ) 373 | for path in shell 374 | for i in [ int(randpos * (len(path)-1)) ] 375 | ] 376 | perims.insert(0, shell) 377 | self.perimeter_paths.append(perims) 378 | 379 | # Calculate horizontal bounding path 380 | if layer < self.conf['skirt_layers']: 381 | self.skirt_bounds = geom.union(self.skirt_bounds, paths) 382 | 383 | self.top_masks = [] 384 | self.bot_masks = [] 385 | for layer in range(self.layers): 386 | self.thermo.update(self.layers+layer) 387 | 388 | # Top and Bottom masks 389 | below = [] if layer < 1 else self.perimeter_paths[layer-1][0] 390 | perim = self.perimeter_paths[layer][0] 391 | above = [] if layer >= self.layers-1 else self.perimeter_paths[layer+1][0] 392 | self.top_masks.append(geom.diff(perim, above)) 393 | self.bot_masks.append(geom.diff(perim, below)) 394 | self.thermo.clear() 395 | 396 | def _slicer_task_support(self): 397 | self.thermo.set_target(5.0) 398 | 399 | self.support_outline = [] 400 | self.support_infill = [] 401 | supp_type = self.conf['support_type'] 402 | if supp_type == 'None': 403 | return 404 | supp_ang = self.conf['overhang_angle'] 405 | outset = self.conf['support_outset'] 406 | layer_height = self.conf['layer_height'] 407 | 408 | facets = [facet for model in self.models for facet in model.get_facets()] 409 | facet_cnt = len(facets) 410 | layer_facets = [[] for layer in range(self.layers)] 411 | for fnum, facet in enumerate(facets): 412 | self.thermo.update(0 + float(fnum)/facet_cnt) 413 | minz, maxz = facet.z_range() 414 | minl = int(math.ceil(minz/layer_height)) 415 | maxl = int(math.floor(maxz/layer_height)) 416 | for layer in range(minl, maxl): 417 | layer_facets[layer].append(facet) 418 | 419 | drop_mask = [] 420 | drop_paths = [[] for layer in range(self.layers)] 421 | for layer in reversed(range(self.layers)): 422 | self.thermo.update(1 + float(self.layers-1-layer)/self.layers) 423 | adds = [] 424 | diffs = [] 425 | for facet in layer_facets[layer]: 426 | footprint = facet.get_footprint() 427 | if not footprint: 428 | continue 429 | if facet.overhang_angle() < supp_ang: 430 | diffs.append(footprint) 431 | else: 432 | adds.append(footprint) 433 | drop_mask = geom.union(drop_mask, adds) 434 | drop_mask = geom.diff(drop_mask, diffs) 435 | drop_paths[layer] = drop_mask 436 | 437 | cumm_mask = [] 438 | for layer in range(self.layers): 439 | self.thermo.update(2 + float(layer)/self.layers) 440 | 441 | # Remove areas too close to model 442 | mask = geom.offset(self.layer_paths[layer], outset) 443 | if layer > 0 and supp_type == "Everywhere": 444 | mask = geom.union(mask, self.layer_paths[layer-1]) 445 | if layer < self.layers - 1: 446 | mask = geom.union(mask, self.layer_paths[layer+1]) 447 | if supp_type == "External": 448 | cumm_mask = geom.union(cumm_mask, mask) 449 | mask = cumm_mask 450 | overhang = geom.diff(drop_paths[layer], mask) 451 | 452 | # Clean up overhang paths 453 | overhang = geom.offset(overhang, self.extrusion_width) 454 | overhang = geom.offset(overhang, -self.extrusion_width*2) 455 | overhang = geom.offset(overhang, self.extrusion_width) 456 | drop_paths[layer] = geom.close_paths(overhang) 457 | 458 | for layer in range(self.layers): 459 | self.thermo.update(3 + float(layer)/self.layers) 460 | 461 | # Generate support infill 462 | outline = [] 463 | infill = [] 464 | overhangs = drop_paths[layer] 465 | density = self.conf['support_density'] / 100.0 466 | if density > 0.0: 467 | outline = geom.offset(overhangs, -self.extrusion_width/2.0) 468 | outline = geom.close_paths(outline) 469 | mask = geom.offset(outline, self.conf['infill_overlap']-self.extrusion_width) 470 | bounds = geom.paths_bounds(mask) 471 | lines = geom.make_infill_lines(bounds, 0, density, self.extrusion_width) 472 | infill = geom.clip(lines, mask, subj_closed=False) 473 | self.support_outline.append(outline) 474 | self.support_infill.append(infill) 475 | 476 | self.thermo.clear() 477 | 478 | def _slicer_task_adhesion(self): 479 | adhesion = self.conf['adhesion_type'] 480 | skirt_w = self.conf['skirt_outset'] 481 | brim_w = self.conf['brim_width'] 482 | raft_w = self.conf['raft_outset'] 483 | 484 | # Skirt 485 | if self.support_outline: 486 | skirt_mask = geom.offset(geom.union(self.skirt_bounds, self.support_outline[0]), skirt_w) 487 | else: 488 | skirt_mask = geom.offset(self.skirt_bounds, skirt_w) 489 | skirt = geom.offset(skirt_mask, brim_w + skirt_w + self.extrusion_width/2.0) 490 | self.skirt_paths = geom.close_paths(skirt) 491 | 492 | # Brim 493 | brim = [] 494 | if adhesion == "Brim": 495 | rings = int(math.ceil(brim_w/self.extrusion_width)) 496 | for i in range(rings): 497 | for path in geom.offset(self.layer_paths[0], (i+0.5)*self.extrusion_width): 498 | brim.append(path) 499 | self.brim_paths = geom.close_paths(brim) 500 | 501 | # Raft 502 | raft_outline = [] 503 | raft_infill = [] 504 | if adhesion == "Raft": 505 | rings = int(math.ceil(brim_w/self.extrusion_width)) 506 | outset = raft_w + max( 507 | skirt_w + self.extrusion_width, 508 | self.conf['raft_outset'] + self.extrusion_width 509 | ) 510 | paths = geom.union(self.layer_paths[0], self.support_outline[0]) 511 | raft_outline = geom.offset(paths, outset) 512 | bounds = geom.paths_bounds(raft_outline) 513 | mask = geom.offset(raft_outline, self.conf['infill_overlap']-self.extrusion_width) 514 | lines = geom.make_infill_lines(bounds, 0, 0.75, self.extrusion_width) 515 | raft_infill.append(geom.clip(lines, mask, subj_closed=False)) 516 | for layer in range(self.raft_layers-1): 517 | base_ang = 90 * ((layer+1) % 2) 518 | lines = geom.make_infill_lines(bounds, base_ang, 1.0, self.extrusion_width) 519 | raft_infill.append(geom.clip(lines, raft_outline, subj_closed=False)) 520 | self.raft_outline = geom.close_paths(raft_outline) 521 | self.raft_infill = raft_infill 522 | self.thermo.clear() 523 | 524 | def _slicer_task_fill(self): 525 | self.thermo.set_target(self.layers) 526 | 527 | self.solid_infill = [] 528 | self.sparse_infill = [] 529 | for layer in range(self.layers): 530 | self.thermo.update(layer) 531 | # Solid Mask 532 | top_cnt = self.conf['top_layers'] 533 | bot_cnt = self.conf['bottom_layers'] 534 | top_masks = self.top_masks[layer : layer+top_cnt] 535 | perims = self.perimeter_paths[layer] 536 | bot_masks = self.bot_masks[max(0, layer-bot_cnt+1) : layer+1] 537 | outmask = [] 538 | for mask in top_masks: 539 | outmask = geom.union(outmask, geom.close_paths(mask)) 540 | for mask in bot_masks: 541 | outmask = geom.union(outmask, geom.close_paths(mask)) 542 | solid_mask = geom.clip(outmask, perims[0]) 543 | bounds = geom.paths_bounds(perims[0]) 544 | 545 | # Solid Infill 546 | solid_infill = [] 547 | base_ang = 45 if layer % 2 == 0 else -45 548 | solid_mask = geom.offset(solid_mask, self.conf['infill_overlap']-self.extrusion_width) 549 | lines = geom.make_infill_lines(bounds, base_ang, 1.0, self.extrusion_width) 550 | for line in lines: 551 | lines = [line] 552 | lines = geom.clip(lines, solid_mask, subj_closed=False) 553 | solid_infill.extend(lines) 554 | self.solid_infill.append(solid_infill) 555 | 556 | # Sparse Infill 557 | sparse_infill = [] 558 | infill_type = self.conf['infill_type'] 559 | density = self.conf['infill_density'] / 100.0 560 | if density > 0.0: 561 | if density >= 0.99: 562 | infill_type = "Lines" 563 | mask = geom.offset(perims[0], self.conf['infill_overlap']-self.infill_width) 564 | mask = geom.diff(mask, solid_mask) 565 | if infill_type == "Lines": 566 | base_ang = 90 * (layer % 2) + 45 567 | lines = geom.make_infill_lines(bounds, base_ang, density, self.infill_width) 568 | elif infill_type == "Triangles": 569 | base_ang = 60 * (layer % 3) 570 | lines = geom.make_infill_triangles(bounds, base_ang, density, self.infill_width) 571 | elif infill_type == "Grid": 572 | base_ang = 90 * (layer % 2) + 45 573 | lines = geom.make_infill_grid(bounds, base_ang, density, self.infill_width) 574 | elif infill_type == "Hexagons": 575 | base_ang = 120 * (layer % 3) 576 | lines = geom.make_infill_hexagons(bounds, base_ang, density, self.infill_width) 577 | else: 578 | lines = [] 579 | lines = geom.clip(lines, mask, subj_closed=False) 580 | sparse_infill.extend(lines) 581 | self.sparse_infill.append(sparse_infill) 582 | self.thermo.clear() 583 | 584 | def _slicer_task_pathing(self): 585 | prime_nozls = [self.conf['default_nozzle']]; 586 | if self.conf['infill_nozzle'] != -1: 587 | prime_nozls.append(self.conf['infill_nozzle']); 588 | if self.conf['support_nozzle'] != -1: 589 | prime_nozls.append(self.conf['support_nozzle']); 590 | center_x = self.conf['bed_center_x'] 591 | center_y = self.conf['bed_center_y'] 592 | size_x = self.conf['bed_size_x'] 593 | size_y = self.conf['bed_size_y'] 594 | minx = center_x - size_x/2 595 | maxx = center_x + size_x/2 596 | miny = center_y - size_y/2 597 | maxy = center_y + size_y/2 598 | bed_geom = self.conf['bed_geometry'] 599 | rect_bed = bed_geom == 'Rectangular' 600 | cyl_bed = bed_geom == 'Cylindrical' 601 | maxlen = (maxy-miny-20) if rect_bed else (2*math.pi*math.sqrt((size_x*size_x)/2)-20) 602 | reps = self.conf['prime_length'] / maxlen 603 | ireps = int(math.ceil(reps)) 604 | for noznum, nozl in enumerate(prime_nozls): 605 | ewidth = self.extrusion_width * 1.25 606 | nozl_path = [] 607 | for rep in range(ireps): 608 | if rect_bed: 609 | x = minx + 5 + (noznum*reps+rep+1) * ewidth 610 | if rep%2 == 0: 611 | y1 = miny+10 612 | y2 = maxy-10 613 | else: 614 | y1 = maxy-10 615 | y2 = miny+10 616 | nozl_path.append([x, y1]) 617 | if rep == ireps-1: 618 | part = reps-math.floor(reps) 619 | nozl_path.append([x, y1 + (y2-y1)*part]) 620 | else: 621 | nozl_path.append([x, y2]) 622 | elif cyl_bed: 623 | r = maxx - 5 - (noznum*reps+rep+1) * ewidth 624 | if rep == ireps-1: 625 | part = float(reps) - math.floor(reps) 626 | else: 627 | part = 1.0 628 | steps = math.floor(2.0 * math.pi * r * part / 4.0) 629 | stepang = 2 * math.pi / steps 630 | for i in range(int(steps)): 631 | nozl_path.append( [r*math.cos(i*stepang), r*math.sin(i*stepang)] ) 632 | self._add_raw_layer_paths(0, [nozl_path], ewidth, noznum) 633 | 634 | if self.brim_paths: 635 | paths = geom.close_paths(self.brim_paths) 636 | self._add_raw_layer_paths(0, paths, self.support_width, self.supp_nozl) 637 | if self.raft_outline: 638 | outline = geom.close_paths(self.raft_outline) 639 | self._add_raw_layer_paths(0, outline, self.support_width, self.supp_nozl) 640 | if self.raft_infill: 641 | for layer in range(self.raft_layers): 642 | paths = self.raft_infill[layer] 643 | self._add_raw_layer_paths(layer, paths, self.support_width, self.supp_nozl) 644 | 645 | for slicenum in range(len(self.perimeter_paths)): 646 | self.thermo.update(slicenum) 647 | layer = self.raft_layers + slicenum 648 | 649 | if self.skirt_paths: 650 | paths = geom.close_paths(self.skirt_paths) 651 | if layer < self.conf['skirt_layers'] + self.raft_layers: 652 | self._add_raw_layer_paths(layer, paths, self.support_width, self.supp_nozl) 653 | 654 | if slicenum < len(self.support_outline): 655 | outline = geom.close_paths(self.support_outline[slicenum]) 656 | self._add_raw_layer_paths(layer, outline, self.support_width, self.supp_nozl) 657 | self._add_raw_layer_paths(layer, self.support_infill[slicenum], self.support_width, self.supp_nozl) 658 | 659 | for paths in self.perimeter_paths[slicenum]: 660 | paths = geom.close_paths(paths) 661 | self._add_raw_layer_paths(layer, paths, self.extrusion_width, self.dflt_nozl) 662 | self._add_raw_layer_paths(layer, self.solid_infill[slicenum], self.extrusion_width, self.dflt_nozl) 663 | 664 | self._add_raw_layer_paths(layer, self.sparse_infill[slicenum], self.infill_width, self.infl_nozl) 665 | self.thermo.clear() 666 | 667 | def _slicer_task_gcode(self, filename): 668 | self.thermo.set_target(self.layers) 669 | 670 | total_layers = self.layers + self.raft_layers 671 | with open(filename, "w") as f: 672 | f.write(";FLAVOR:Marlin\n") 673 | f.write(";Layer height: {:.2f}\n".format(self.conf['layer_height'])) 674 | f.write("M82 ;absolute extrusion mode\n") 675 | f.write("G21 ;metric values\n") 676 | f.write("G90 ;absolute positioning\n") 677 | f.write("M107 ;Fan off\n") 678 | if self.conf['bed_temp'] > 0: 679 | f.write("M140 S{:d} ;set bed temp\n".format(self.conf['bed_temp'])) 680 | f.write("M190 S{:d} ;wait for bed temp\n".format(self.conf['bed_temp'])) 681 | f.write("M104 S{:d} ;set extruder0 temp\n".format(self.conf['nozzle_0_temp'])) 682 | f.write("M109 S{:d} ;wait for extruder0 temp\n".format(self.conf['nozzle_0_temp'])) 683 | f.write("G28 X0 Y0 ;auto-home all axes\n") 684 | f.write("G28 Z0 ;auto-home all axes\n") 685 | f.write("G1 Z15 F6000 ;raise extruder\n") 686 | f.write("G92 E0 ;Zero extruder\n") 687 | f.write("M117 Printing...\n") 688 | f.write(";LAYER_COUNT:{}\n".format(total_layers)) 689 | 690 | self.thermo.set_target(total_layers) 691 | for layer in range(total_layers): 692 | self.thermo.update(layer) 693 | f.write(";LAYER:{}\n".format(layer)) 694 | for nozl in range(4): 695 | if layer in self.raw_layer_paths and self.raw_layer_paths[layer][nozl] != []: 696 | f.write("( Nozzle {} )\n".format(nozl)) 697 | for paths, width in self.raw_layer_paths[layer][nozl]: 698 | for line in self._paths_gcode(paths, width, nozl, self.layer_zs[layer]): 699 | f.write(line) 700 | self.thermo.clear() 701 | 702 | ############################################################ 703 | 704 | def _vdist(self,a,b): 705 | delta = [x-y for x,y in zip(a,b)] 706 | dist = math.sqrt(sum([float(x)*float(x) for x in delta])) 707 | return dist 708 | 709 | def _add_raw_layer_paths(self, layer, paths, width, nozl, do_not_cross=[]): 710 | maxdist = 2.0 711 | joined = [] 712 | if paths: 713 | path = paths.pop(0) 714 | while paths: 715 | mindist = 1e9 716 | minidx = None 717 | enda = False 718 | endb = False 719 | dists = [ 720 | [i, self._vdist(path[a], paths[i][b]), a==-1, b==-1] 721 | for a in [0,-1] 722 | for b in [0,-1] 723 | for i in range(len(paths)) 724 | ] 725 | for i, dist, ea, eb in dists: 726 | if dist < mindist: 727 | minidx, mindist, enda, endb = (i, dist, ea, eb) 728 | if mindist <= maxdist: 729 | path2 = paths.pop(minidx) 730 | if enda: 731 | path = path + (list(reversed(path2)) if endb else path2) 732 | else: 733 | path = (path2 if endb else list(reversed(path2))) + path 734 | else: 735 | if minidx is not None: 736 | if enda == endb: 737 | paths.insert(0, list(reversed(paths.pop(minidx)))) 738 | else: 739 | paths.insert(0, paths.pop(minidx)) 740 | joined.append(path) 741 | path = paths.pop(0) 742 | joined.append(path) 743 | if layer not in self.raw_layer_paths: 744 | self.raw_layer_paths[layer] = [[] for i in range(4)] 745 | self.raw_layer_paths[layer][nozl].append( (joined, width) ) 746 | 747 | def _tool_change_gcode(self, newnozl): 748 | retract_ext_dist = self.conf['retract_extruder'] 749 | retract_speed = self.conf['retract_speed'] 750 | if self.last_nozl == newnozl: 751 | return [] 752 | gcode_lines = [] 753 | gcode_lines.append("G1 E{e:.3f} F{f:g}\n".format(e=-retract_ext_dist, f=retract_speed*60.0)) 754 | gcode_lines.append("T{t:d}\n".format(t=newnozl)) 755 | gcode_lines.append("G1 E{e:.3f} F{f:g}\n".format(e=retract_ext_dist, f=retract_speed*60.0)) 756 | return gcode_lines 757 | 758 | def _paths_gcode(self, paths, ewidth, nozl, z): 759 | fil_diam = self.conf['nozzle_{0:d}_filament'.format(nozl)] 760 | nozl_diam = self.conf['nozzle_{0:d}_filament'.format(nozl)] 761 | max_speed = self.conf['nozzle_{0:d}_max_speed'.format(nozl)] 762 | layer_height = self.conf['layer_height'] 763 | retract_dist = self.conf['retract_dist'] 764 | retract_speed = self.conf['retract_speed'] 765 | retract_lift = self.conf['retract_lift'] 766 | feed_rate = self.conf['feed_rate'] 767 | travel_rate_xy = self.conf['travel_rate_xy'] 768 | travel_rate_z = self.conf['travel_rate_z'] 769 | ewidth = nozl_diam * self.extrusion_ratio 770 | xsect = math.pi * ewidth/2 * layer_height/2 771 | fil_xsect = math.pi * fil_diam/2 * fil_diam/2 772 | gcode_lines = [] 773 | for line in self._tool_change_gcode(nozl): 774 | gcode_lines.append(line) 775 | for path in paths: 776 | ox, oy = path[0][0:2] 777 | if retract_lift > 0 or self.last_pos[2] != z: 778 | self.total_build_time += abs(retract_lift) / travel_rate_z 779 | gcode_lines.append("G1 Z{z:.2f} F{f:g}\n".format(z=z+retract_lift, f=travel_rate_z*60.0)) 780 | dist = math.hypot(self.last_pos[1]-oy, self.last_pos[0]-ox) 781 | self.total_build_time += dist / travel_rate_xy 782 | gcode_lines.append("G0 X{x:.2f} Y{y:.2f} F{f:g}\n".format(x=ox, y=oy, f=travel_rate_xy*60.0)) 783 | if retract_lift > 0: 784 | self.total_build_time += abs(retract_lift) / travel_rate_z 785 | gcode_lines.append("G1 Z{z:.2f} F{f:g}\n".format(z=z, f=travel_rate_z*60.0)) 786 | if retract_dist > 0: 787 | self.total_build_time += abs(retract_dist) / retract_speed 788 | gcode_lines.append("G1 E{e:.3f} F{f:g}\n".format(e=self.last_e+retract_dist, f=retract_speed*60.0)) 789 | self.last_e += retract_dist 790 | for x, y in path[1:]: 791 | dist = math.hypot(y-oy, x-ox) 792 | fil_dist = dist * xsect / fil_xsect 793 | speed = min(feed_rate, max_speed) * 60.0 794 | self.total_build_time += dist / feed_rate 795 | self.last_e += fil_dist 796 | gcode_lines.append("G1 X{x:.2f} Y{y:.2f} E{e:.3f} F{f:g}\n".format(x=x, y=y, e=self.last_e, f=speed)) 797 | self.last_pos = (x, y, z) 798 | ox, oy = x, y 799 | if retract_dist > 0: 800 | self.total_build_time += abs(retract_dist) / retract_speed 801 | gcode_lines.append("G1 E{e:.3f} F{f:g}\n".format(e=self.last_e-retract_dist, f=retract_speed*60.0)) 802 | self.last_e -= retract_dist 803 | return gcode_lines 804 | 805 | ############################################################ 806 | 807 | def _display_paths(self): 808 | try: # Python 2 809 | from Tkinter import (Tk, Canvas, Label, Frame, Scrollbar, mainloop) 810 | from ttk import Progressbar, Style 811 | except ImportError: # Python 3 812 | from tkinter import (Tk, Canvas, Label, Frame, Scrollbar, mainloop) 813 | from tkinter.ttk import Progressbar, Style 814 | self.layer = 0 815 | self.mag = 5.0 816 | self.master = Tk() 817 | self.master.title("Mandoline - Layer Paths") 818 | self.info_fr = Frame(self.master, bd=2, relief="flat", bg="#ccc") 819 | self.info_fr.pack(side="top", fill="x", expand=False) 820 | self.zoom_lbl = Label(self.info_fr, anchor="w", width=16, bg="#ccc") 821 | self.zoom_lbl.pack(side="left") 822 | self.layer_lbl = Label(self.info_fr, anchor="w", width=16, bg="#ccc") 823 | self.layer_lbl.pack(side="left") 824 | self.zed_lbl = Label(self.info_fr, anchor="w", width=16, bg="#ccc") 825 | self.zed_lbl.pack(side="left") 826 | self.progbar = Progressbar(self.info_fr, orient="horizontal", length=200, value=0, maximum=100, mode="determinate") 827 | self.progbar.pack(side="left", fill="y", pady=5) 828 | st = Style() 829 | st.theme_use("default") 830 | st.configure("bar.Horizontal.TProgressbar", troughcolor="white", foreground="blue", background="white") 831 | self.fr = Frame(self.master) 832 | self.fr.pack(fill="both", expand=True) 833 | self.fr.grid_rowconfigure(0, weight=1) 834 | self.fr.grid_columnconfigure(0, weight=1) 835 | self.canvas = Canvas(self.fr, width=800, height=600, scrollregion=(0,0,1000,1000)) 836 | self.hbar = Scrollbar(self.fr, orient="horizontal", command=self.canvas.xview) 837 | self.vbar = Scrollbar(self.fr, orient="vertical", command=self.canvas.yview) 838 | self.canvas.config(xscrollcommand=self.hbar.set, yscrollcommand=self.vbar.set) 839 | self.hbar.grid(row=1, column=0, sticky="ew") 840 | self.vbar.grid(row=0, column=1, sticky="ns") 841 | self.canvas.grid(row=0, column=0, sticky="nsew") 842 | self.canvas.focus() 843 | self.canvas.bind_all('', lambda event: self.canvas.xview_scroll(int(-abs(event.delta)/event.delta), "units")) 844 | self.canvas.bind_all('', lambda event: self.canvas.yview_scroll(int(-abs(event.delta)/event.delta), "units")) 845 | self.canvas.bind_all('', lambda event: self._zoom(incdec=int(abs(event.delta)/event.delta))) 846 | self.master.bind("", lambda e: self._redraw_paths(incdec=10)) 847 | self.master.bind("", lambda e: self._redraw_paths(incdec=1)) 848 | self.master.bind("", lambda e: self._redraw_paths(incdec=-1)) 849 | self.master.bind("", lambda e: self._redraw_paths(incdec=-10)) 850 | self.master.bind("", lambda e: self._zoom(incdec=1)) 851 | self.master.bind("", lambda e: self._zoom(incdec=-1)) 852 | self.master.bind("", lambda e: self._zoom(val= 5.0)) 853 | self.master.bind("", lambda e: self._zoom(val=10.0)) 854 | self.master.bind("", lambda e: self._zoom(val=15.0)) 855 | self.master.bind("", lambda e: self._zoom(val=20.0)) 856 | self.master.bind("", lambda e: sys.exit(0)) 857 | self.master.bind("", lambda e: sys.exit(0)) 858 | self._redraw_paths() 859 | size_x = self.conf['bed_size_x'] 860 | size_y = self.conf['bed_size_y'] 861 | cx = self.conf['bed_center_x'] 862 | cy = self.conf['bed_center_y'] 863 | neww = (cx-400/self.mag)/size_x 864 | newh = (cy-300/self.mag)/size_y 865 | self.canvas.xview("moveto", neww) 866 | self.canvas.yview("moveto", newh) 867 | if platform.system() == "Darwin": 868 | os.system('''/usr/bin/osascript -e 'tell app "Finder" to set frontmost of process "Python" to true' ''') 869 | mainloop() 870 | 871 | def _zoom(self, incdec=0, val=None): 872 | self.master.update() 873 | winx = self.canvas.winfo_width() 874 | winy = self.canvas.winfo_height() 875 | cx = self.canvas.canvasx(int(winx/2))/self.mag 876 | cy = self.canvas.canvasy(int(winy/2))/self.mag 877 | size_x = self.conf['bed_size_x'] 878 | size_y = self.conf['bed_size_y'] 879 | if val is None: 880 | self.mag = max(1, self.mag+incdec) 881 | else: 882 | self.mag = val 883 | self._redraw_paths() 884 | neww = (cx-winx/2/self.mag)/size_x 885 | newh = (cy-winy/2/self.mag)/size_y 886 | self.canvas.xview("moveto", neww) 887 | self.canvas.yview("moveto", newh) 888 | 889 | def _redraw_paths(self, incdec=0): 890 | self.layer = min(max(0, self.layer + incdec), len(self.perimeter_paths)-1+self.raft_layers) 891 | layernum = self.layer 892 | layers = self.raft_layers + self.layers 893 | center_x = self.conf['bed_center_x'] 894 | center_y = self.conf['bed_center_y'] 895 | size_x = self.conf['bed_size_x'] 896 | size_y = self.conf['bed_size_y'] 897 | minx = (center_x - size_x/2) * self.mag 898 | maxx = (center_x + size_x/2) * self.mag 899 | miny = (center_y - size_y/2) * self.mag 900 | maxy = (center_y + size_y/2) * self.mag 901 | self.zoom_lbl.config(text="Zoom: {}%".format(int(self.mag*100/5.0))) 902 | self.layer_lbl.config(text="Layer: {}/{}".format(layernum, layers-1)) 903 | self.zed_lbl.config(text="Z: {:.3f}".format(self.layer_zs[layernum])) 904 | self.canvas.delete("all") 905 | self.canvas.config(scrollregion=(minx,miny,maxx,maxy)) 906 | self.progbar['value'] = layernum 907 | self.progbar['maximum'] = layers-1 908 | 909 | grid_colors = ["#ccf", "#eef"] 910 | for x in range(int(size_x/10)): 911 | for y in range(int(size_y/10)): 912 | rect = [val*10*self.mag for val in (x, y, x+1, y+1)] 913 | self.canvas.create_rectangle(rect, fill=grid_colors[(x+y)%2]) 914 | # nozl_colors = [ 915 | # ["#070", "#0c0", "#0f0", "#7f7"], 916 | # ["#770", "#aa0", "#dd0", "#ff0"], 917 | # ["#007", "#00c", "#00f", "#77f"], 918 | # ["#700", "#c00", "#f00", "#f77"], 919 | # ] 920 | nozl_colors = [ ["#0c0"], ["#aa0"], ["#00c"], ["#c00"] ] 921 | for nozl in range(4): 922 | if layernum in self.raw_layer_paths and self.raw_layer_paths[layernum][nozl]: 923 | for paths, width in self.raw_layer_paths[layernum][nozl]: 924 | self._draw_line(paths, colors=nozl_colors[nozl], ewidth=width) 925 | self._draw_line(self.layer_paths[self.layer], colors=["#cc0"], ewidth=self.extrusion_width/8.0) 926 | self._draw_line(self.dead_paths[self.layer], colors=["red"], ewidth=self.extrusion_width/8.0) 927 | 928 | def _draw_line(self, paths, offset=0, colors=["red", "green", "blue"], ewidth=0.5): 929 | center_x = self.conf['bed_center_x'] 930 | center_y = self.conf['bed_center_y'] 931 | size_x = self.conf['bed_size_x'] 932 | size_y = self.conf['bed_size_y'] 933 | minx = (center_x - size_x/2) * self.mag 934 | maxx = (center_x + size_x/2) * self.mag 935 | miny = (center_y - size_y/2) * self.mag 936 | maxy = (center_y + size_y/2) * self.mag 937 | for pathnum, path in enumerate(paths): 938 | path = [(x*self.mag, maxy-y*self.mag) for x, y in path] 939 | color = colors[(pathnum + offset) % len(colors)] 940 | self.canvas.create_line(path, fill=color, width=self.mag*ewidth, capstyle="round", joinstyle="round") 941 | self.canvas.create_line([path[0],path[0]], fill="blue", width=self.mag*ewidth, capstyle="round", joinstyle="round") 942 | self.canvas.create_line([path[-1],path[-1]], fill="cyan", width=self.mag*ewidth, capstyle="round", joinstyle="round") 943 | 944 | 945 | # vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap 946 | -------------------------------------------------------------------------------- /mandoline/stl_data.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import os.path 4 | import sys 5 | import math 6 | import time 7 | import struct 8 | from pyquaternion import Quaternion 9 | 10 | from .TextThermometer import TextThermometer 11 | from .point3d import Point3DCache 12 | from .vector import Vector 13 | from .facet3d import Facet3DCache 14 | from .line_segment3d import LineSegment3DCache 15 | 16 | 17 | class StlEndOfFileException(Exception): 18 | """Exception class for reaching the end of the STL file while reading.""" 19 | pass 20 | 21 | 22 | class StlMalformedLineException(Exception): 23 | """Exception class for malformed lines in the STL file being read.""" 24 | pass 25 | 26 | 27 | class StlData(object): 28 | """Class to read, write, and validate STL file data.""" 29 | 30 | def __init__(self): 31 | """Initialize with empty data set.""" 32 | self.points = Point3DCache() 33 | self.edges = LineSegment3DCache() 34 | self.facets = Facet3DCache() 35 | self.filename = "" 36 | self.dupe_faces = [] 37 | self.dupe_edges = [] 38 | self.hole_edges = [] 39 | self.layer_facets = {} 40 | 41 | def _read_ascii_line(self, f, watchwords=None): 42 | line = f.readline(1024).decode('utf-8') 43 | if line == "": 44 | raise StlEndOfFileException() 45 | words = line.strip(' \t\n\r').lower().split() 46 | if not words: 47 | return [] 48 | if words[0] == 'endsolid': 49 | raise StlEndOfFileException() 50 | argstart = 0 51 | if watchwords: 52 | watchwords = watchwords.lower().split() 53 | argstart = len(watchwords) 54 | for i in range(argstart): 55 | if words[i] != watchwords[i]: 56 | raise StlMalformedLineException() 57 | return [float(val) for val in words[argstart:]] 58 | 59 | def _read_ascii_vertex(self, f): 60 | point = self._read_ascii_line(f, watchwords='vertex') 61 | return self.points.add(*point) 62 | 63 | def quantz(self, pt, quanta=1e-3): 64 | """Quantize the Z coordinate of the given point so that it won't be exactly on a layer.""" 65 | x, y, z = pt 66 | z = math.floor(z / quanta + 0.5) * quanta 67 | return (x, y, z) 68 | 69 | def _read_ascii_facet(self, f, quanta=1e-3): 70 | while True: 71 | try: 72 | normal = self._read_ascii_line(f, watchwords='facet normal') 73 | self._read_ascii_line(f, watchwords='outer loop') 74 | vertex1 = self._read_ascii_vertex(f) 75 | vertex2 = self._read_ascii_vertex(f) 76 | vertex3 = self._read_ascii_vertex(f) 77 | self._read_ascii_line(f, watchwords='endloop') 78 | self._read_ascii_line(f, watchwords='endfacet') 79 | if quanta > 0.0: 80 | vertex1 = self.quantz(vertex1, quanta) 81 | vertex2 = self.quantz(vertex2, quanta) 82 | vertex3 = self.quantz(vertex3, quanta) 83 | if vertex1 == vertex2 or vertex2 == vertex3 or vertex3 == vertex1: 84 | continue # zero area facet. Skip to next facet. 85 | vec1 = Vector(vertex1) - Vector(vertex2) 86 | vec2 = Vector(vertex3) - Vector(vertex2) 87 | if vec1.angle(vec2) < 1e-8: 88 | continue # zero area facet. Skip to next facet. 89 | except StlEndOfFileException: 90 | return None 91 | except StlMalformedLineException: 92 | continue # Skip to next facet. 93 | self.edges.add(vertex1, vertex2) 94 | self.edges.add(vertex2, vertex3) 95 | self.edges.add(vertex3, vertex1) 96 | return self.facets.add(vertex1, vertex2, vertex3, normal) 97 | 98 | def _read_binary_facet(self, f, quanta=1e-3): 99 | data = struct.unpack('<3f 3f 3f 3f H', f.read(4*4*3+2)) 100 | normal = data[0:3] 101 | vertex1 = data[3:6] 102 | vertex2 = data[6:9] 103 | vertex3 = data[9:12] 104 | if quanta > 0.0: 105 | vertex1 = self.quantz(vertex1, quanta) 106 | vertex2 = self.quantz(vertex2, quanta) 107 | vertex3 = self.quantz(vertex3, quanta) 108 | if vertex1 == vertex2 or vertex2 == vertex3 or vertex3 == vertex1: 109 | return None 110 | vec1 = Vector(vertex1) - Vector(vertex2) 111 | vec2 = Vector(vertex3) - Vector(vertex2) 112 | if vec1.angle(vec2) < 1e-8: 113 | return None 114 | v1 = self.points.add(*vertex1) 115 | v2 = self.points.add(*vertex2) 116 | v3 = self.points.add(*vertex3) 117 | self.edges.add(v1, v2) 118 | self.edges.add(v2, v3) 119 | self.edges.add(v3, v1) 120 | return self.facets.add(v1, v2, v3, normal) 121 | 122 | def read_file(self, filename): 123 | """Read the model data from the given STL file.""" 124 | self.filename = filename 125 | print("Loading model") 126 | file_size = os.path.getsize(filename) 127 | with open(filename, 'rb') as f: 128 | line = f.readline(80) 129 | if line == "": 130 | return # End of file. 131 | if line[0:6].lower() == b"solid " and len(line) < 80: 132 | # Reading ASCII STL file. 133 | thermo = TextThermometer(file_size) 134 | while self._read_ascii_facet(f) is not None: 135 | thermo.update(f.tell()) 136 | thermo.clear() 137 | else: 138 | # Reading Binary STL file. 139 | chunk = f.read(4) 140 | facets = struct.unpack(' 2] 199 | 200 | def check_manifold(self, verbose=False): 201 | """Validate if the model is manifold, and therefore printable.""" 202 | is_manifold = True 203 | self.dupe_faces = self._check_manifold_duplicate_faces() 204 | for face in self.dupe_faces: 205 | is_manifold = False 206 | print("NON-MANIFOLD DUPLICATE FACE! {0}: {1}" 207 | .format(self.filename, face)) 208 | self.hole_edges = self._check_manifold_hole_edges() 209 | for edge in self.hole_edges: 210 | is_manifold = False 211 | print("NON-MANIFOLD HOLE EDGE! {0}: {1}" 212 | .format(self.filename, edge)) 213 | self.dupe_edges = self._check_manifold_excess_edges() 214 | for edge in self.dupe_edges: 215 | is_manifold = False 216 | print("NON-MANIFOLD DUPLICATE EDGE! {0}: {1}" 217 | .format(self.filename, edge)) 218 | return is_manifold 219 | 220 | def get_facets(self): 221 | return self.facets 222 | 223 | def get_edges(self): 224 | return self.edges 225 | 226 | def center(self, cp): 227 | """Centers the model at the given centerpoint cp.""" 228 | cx = (self.points.minx + self.points.maxx)/2.0 229 | cy = (self.points.miny + self.points.maxy)/2.0 230 | cz = (self.points.minz + self.points.maxz)/2.0 231 | self.translate((cp[0]-cx, cp[1]-cy, cp[2]-cz)) 232 | 233 | def translate(self, offset): 234 | """Translates vertices of all facets in the STL model.""" 235 | self.points.translate(offset) 236 | self.edges.translate(offset) 237 | self.facets.translate(offset) 238 | 239 | def assign_layers(self, layer_height): 240 | """Calculate which layers intersect which facets, for faster lookup.""" 241 | self.layer_facets = {} 242 | for facet in self.facets: 243 | minz, maxz = facet.z_range() 244 | minl = int(math.floor(minz / layer_height + 0.01)) 245 | maxl = int(math.ceil(maxz / layer_height - 0.01)) 246 | for layer in range(minl, maxl + 1): 247 | if layer not in self.layer_facets: 248 | self.layer_facets[layer] = [] 249 | self.layer_facets[layer].append(facet) 250 | 251 | def get_layer_facets(self, layer): 252 | """Get all facets that intersect the given layer.""" 253 | if layer not in self.layer_facets: 254 | return [] 255 | return self.layer_facets[layer] 256 | 257 | def slice_at_z(self, z, layer_h): 258 | """Get paths outlines of where this model intersects the given Z level.""" 259 | 260 | def ptkey(pt): 261 | return "{0:.3f}, {1:.3f}".format(pt[0], pt[1]) 262 | 263 | layer = math.floor(z / layer_h + 0.5) 264 | paths = {} 265 | for facet in self.get_layer_facets(layer): 266 | line = facet.slice_at_z(z) 267 | if line is None: 268 | continue 269 | path = list(line) 270 | key1 = ptkey(path[0]) 271 | key2 = ptkey(path[-1]) 272 | if key2 in paths and paths[key2][-1] == path[0]: 273 | continue 274 | if key1 not in paths: 275 | paths[key1] = [] 276 | paths[key1].append(path) 277 | 278 | outpaths = [] 279 | deadpaths = [] 280 | while paths: 281 | path = paths[next(iter(paths))][0] 282 | key1 = ptkey(path[0]) 283 | key2 = ptkey(path[-1]) 284 | del paths[key1][0] 285 | if not paths[key1]: 286 | del paths[key1] 287 | if key1 == key2: 288 | outpaths.append(path) 289 | continue 290 | elif key2 in paths: 291 | opath = paths[key2][0] 292 | del paths[key2][0] 293 | if not paths[key2]: 294 | del paths[key2] 295 | path.extend(opath[1:]) 296 | elif key1 in paths: 297 | opath = paths[key1][0] 298 | del paths[key1][0] 299 | if not paths[key1]: 300 | del paths[key1] 301 | opath = list(reversed(opath)) 302 | opath.extend(path[1:]) 303 | path = opath 304 | else: 305 | deadpaths.append(path) 306 | continue 307 | key1 = ptkey(path[0]) 308 | if key1 not in paths: 309 | paths[key1] = [] 310 | paths[key1].append(path) 311 | if deadpaths: 312 | print("\nIncomplete Polygon at z=%s" % z) 313 | return (outpaths, deadpaths) 314 | 315 | 316 | # vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap 317 | -------------------------------------------------------------------------------- /mandoline/vector.py: -------------------------------------------------------------------------------- 1 | import math 2 | import struct 3 | import numbers 4 | 5 | try: 6 | from itertools import zip_longest as ziplong 7 | except ImportError: 8 | from itertools import izip_longest as ziplong 9 | 10 | from .float_fmt import float_fmt 11 | 12 | 13 | class Vector(object): 14 | """Class to represent an N dimentional vector.""" 15 | 16 | def __init__(self, *args): 17 | self._values = [] 18 | if len(args) == 1: 19 | val = args[0] 20 | if isinstance(val, numbers.Real): 21 | self._values = [val] 22 | return 23 | elif isinstance(val, numbers.Complex): 24 | self._values = [val.real, val.imag] 25 | return 26 | else: 27 | val = args 28 | try: 29 | for x in val: 30 | if not isinstance(x, numbers.Real): 31 | raise TypeError('Expected sequence of real numbers.') 32 | self._values.append(x) 33 | except: 34 | pass 35 | 36 | def __iter__(self): 37 | """Iterator generator for vector values.""" 38 | for idx in self._values: 39 | yield idx 40 | 41 | def __len__(self): 42 | return len(self._values) 43 | 44 | def __getitem__(self, idx): 45 | """Given a vertex number, returns a vertex coordinate vector.""" 46 | return self._values[idx] 47 | 48 | def __hash__(self): 49 | """Returns hash value for vector coords""" 50 | return hash(tuple(self._values)) 51 | 52 | def __eq__(self, other): 53 | """Equality comparison for points.""" 54 | return self._values == other._values 55 | 56 | def __cmp__(self, other): 57 | """Compare points for sort ordering in an arbitrary heirarchy.""" 58 | longzip = ziplong(self._values, other, fillvalue=0.0) 59 | for v1, v2 in reversed(list(longzip)): 60 | val = v1 - v2 61 | if val != 0: 62 | val /= abs(val) 63 | return val 64 | return 0 65 | 66 | def __sub__(self, v): 67 | return Vector(i - j for i, j in zip(self._values, v)) 68 | 69 | def __rsub__(self, v): 70 | return Vector(i - j for i, j in zip(v, self._values)) 71 | 72 | def __add__(self, v): 73 | return Vector(i + j for i, j in zip(self._values, v)) 74 | 75 | def __radd__(self, v): 76 | return Vector(i + j for i, j in zip(v, self._values)) 77 | 78 | def __div__(self, s): 79 | """Divide each element in a vector by a scalar.""" 80 | return Vector(x / (s+0.0) for x in self._values) 81 | 82 | def __mul__(self, s): 83 | """Multiplies each element in a vector by a scalar.""" 84 | return Vector(x * s for x in self._values) 85 | 86 | def __format__(self, fmt): 87 | vals = [float_fmt(x) for x in self._values] 88 | if "a" in fmt: 89 | return "[{0}]".format(", ".join(vals)) 90 | if "s" in fmt: 91 | return " ".join(vals) 92 | if "b" in fmt: 93 | return struct.pack('<{0:d)f'.format(len(self._values)), *self._values) 94 | return "({0})".format(", ".join(vals)) 95 | 96 | def __repr__(self): 97 | return "".format(self) 98 | 99 | def __str__(self): 100 | """Returns a standard array syntax string of the coordinates.""" 101 | return "{0:a}".format(self) 102 | 103 | def dot(self, v): 104 | """Dot (scalar) product of two vectors.""" 105 | return sum(p*q for p, q in zip(self, v)) 106 | 107 | def cross(self, v): 108 | """ 109 | Cross (vector) product against another 3D Vector. 110 | Returned 3D Vector will be perpendicular to both original 3D Vectors. 111 | """ 112 | return Vector( 113 | self._values[1]*v[2] - self._values[2]*v[1], 114 | self._values[2]*v[0] - self._values[0]*v[2], 115 | self._values[0]*v[1] - self._values[1]*v[0] 116 | ) 117 | 118 | def length(self): 119 | """Returns the length of the vector.""" 120 | return math.sqrt(sum(x*x for x in self._values)) 121 | 122 | def normalize(self): 123 | """Normalizes the given vector to be unit-length.""" 124 | return self / self.length() 125 | 126 | def angle(self, other): 127 | """Returns angle in radians between this and another vector.""" 128 | l = self.length() * other.length() 129 | if l == 0: 130 | return 0.0 131 | return math.acos(self.dot(other) / l) 132 | 133 | 134 | # vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap 135 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import glob 5 | from setuptools import setup 6 | 7 | VERSION = "0.8.2" 8 | 9 | 10 | def find_data_files(source, target, patterns): 11 | """ 12 | Locates the specified data-files and returns the matches 13 | in a data_files compatible format. 14 | 15 | source is the root of the source data tree. 16 | Use '' or '.' for current directory. 17 | target is the root of the target data tree. 18 | Use '' or '.' for the distribution directory. 19 | patterns is a sequence of glob-patterns for the 20 | files you want to copy. 21 | """ 22 | if glob.has_magic(source) or glob.has_magic(target): 23 | raise ValueError("Magic not allowed in src, target") 24 | ret = {} 25 | for pattern in patterns: 26 | pattern = os.path.join(source, pattern) 27 | for filename in glob.glob(pattern): 28 | if os.path.isfile(filename): 29 | targetpath = os.path.join( 30 | target, os.path.relpath(filename, source) 31 | ) 32 | path = os.path.dirname(targetpath) 33 | ret.setdefault(path, []).append(filename) 34 | return sorted(ret.items()) 35 | 36 | 37 | with open('README.rst') as f: 38 | LONG_DESCR = f.read() 39 | 40 | 41 | setup( 42 | name='mandoline-py', 43 | version=VERSION, 44 | description='An STL to GCode slicer for 3D printing, using the clipper libraries.', 45 | long_description=LONG_DESCR, 46 | author='Revar Desmera', 47 | author_email='revarbat@gmail.com', 48 | url='https://github.com/revarbat/mandoline-py', 49 | download_url='https://github.com/revarbat/mandoline-py/archive/master.zip', 50 | packages=['mandoline'], 51 | license='MIT License', 52 | classifiers=[ 53 | 'Development Status :: 3 - Alpha', 54 | 'Environment :: Console', 55 | 'Environment :: Console', 56 | 'Intended Audience :: End Users/Desktop', 57 | 'Intended Audience :: Manufacturing', 58 | 'Intended Audience :: Science/Research', 59 | 'License :: OSI Approved :: MIT License', 60 | 'Natural Language :: English', 61 | 'Operating System :: MacOS :: MacOS X', 62 | 'Operating System :: Microsoft :: Windows', 63 | 'Operating System :: POSIX', 64 | 'Programming Language :: Python :: 2', 65 | 'Programming Language :: Python :: 3', 66 | 'Topic :: Multimedia :: Graphics :: 3D Modeling', 67 | ], 68 | keywords='stl gcode slicer 3dprinting', 69 | entry_points={ 70 | 'console_scripts': ['mandoline=mandoline:main'], 71 | }, 72 | install_requires=[ 73 | 'setuptools', 74 | 'six>=1.10.0', 75 | 'pyquaternion>=0.9.5', 76 | 'pyclipper>=1.1.0', 77 | 'appdirs>=1.4.3', 78 | ], 79 | ) 80 | -------------------------------------------------------------------------------- /test_models/AlunarM506FanShroud.stl: -------------------------------------------------------------------------------- 1 | solid OpenSCAD_Model 2 | facet normal 0 0 -1 3 | outer loop 4 | vertex -7.375 -4 0 5 | vertex -6.375 0 0 6 | vertex -6.375 -4 0 7 | endloop 8 | endfacet 9 | facet normal -0 0 -1 10 | outer loop 11 | vertex -6.375 0 0 12 | vertex -7.375 -4 0 13 | vertex -7.375 0 0 14 | endloop 15 | endfacet 16 | facet normal 0 0 -1 17 | outer loop 18 | vertex 14.5499 -33.3456 0 19 | vertex 10.242 -28.2005 0 20 | vertex 14.9639 -28.6136 0 21 | endloop 22 | endfacet 23 | facet normal 0 0 -1 24 | outer loop 25 | vertex 14.5499 -33.3456 0 26 | vertex 9.81888 -32.9216 0 27 | vertex 10.242 -28.2005 0 28 | endloop 29 | endfacet 30 | facet normal 0 -0 -1 31 | outer loop 32 | vertex 9.81888 -32.9216 0 33 | vertex 14.5499 -33.3456 0 34 | vertex 9.81801 -32.9316 0 35 | endloop 36 | endfacet 37 | facet normal 0 0 -1 38 | outer loop 39 | vertex 9.51962 -32.9028 0 40 | vertex 10.242 -28.2005 0 41 | vertex 9.81888 -32.9216 0 42 | endloop 43 | endfacet 44 | facet normal 0 0 -1 45 | outer loop 46 | vertex 8.049 -32.6856 0 47 | vertex 10.242 -28.2005 0 48 | vertex 9.51962 -32.9028 0 49 | endloop 50 | endfacet 51 | facet normal 0 -0 -1 52 | outer loop 53 | vertex 8.87498 -27.5801 0 54 | vertex 10.242 -28.2005 0 55 | vertex 8.049 -32.6856 0 56 | endloop 57 | endfacet 58 | facet normal 0 0 -1 59 | outer loop 60 | vertex 10.242 -28.2005 0 61 | vertex 8.87498 -27.5801 0 62 | vertex 10.2491 -28.1194 0 63 | endloop 64 | endfacet 65 | facet normal 0 0 -1 66 | outer loop 67 | vertex 6.60168 -32.3461 0 68 | vertex 8.87498 -27.5801 0 69 | vertex 8.049 -32.6856 0 70 | endloop 71 | endfacet 72 | facet normal 0 0 -1 73 | outer loop 74 | vertex 5.18785 -31.8867 0 75 | vertex 8.87498 -27.5801 0 76 | vertex 6.60168 -32.3461 0 77 | endloop 78 | endfacet 79 | facet normal -0 0 -1 80 | outer loop 81 | vertex 8.87498 -27.5801 0 82 | vertex 5.18785 -31.8867 0 83 | vertex 7.70973 -26.6509 0 84 | endloop 85 | endfacet 86 | facet normal 0 0 -1 87 | outer loop 88 | vertex 3.81743 -31.3107 0 89 | vertex 7.70973 -26.6509 0 90 | vertex 5.18785 -31.8867 0 91 | endloop 92 | endfacet 93 | facet normal 0 0 -1 94 | outer loop 95 | vertex 6.43083 -23.9952 0 96 | vertex 6.375 -15.25 0 97 | vertex 6.43083 -15.25 0 98 | endloop 99 | endfacet 100 | facet normal 0 0 -1 101 | outer loop 102 | vertex -6.375 -4 0 103 | vertex 6.375 -4 0 104 | vertex 6.375 -15.25 0 105 | endloop 106 | endfacet 107 | facet normal 0 0 -1 108 | outer loop 109 | vertex 6.375 -4 0 110 | vertex -6.375 -4 0 111 | vertex 6.375 0 0 112 | endloop 113 | endfacet 114 | facet normal 0 0 -1 115 | outer loop 116 | vertex 2.50002 -30.6219 0 117 | vertex 7.70973 -26.6509 0 118 | vertex 3.81743 -31.3107 0 119 | endloop 120 | endfacet 121 | facet normal -0 0 -1 122 | outer loop 123 | vertex 7.70973 -26.6509 0 124 | vertex 2.50002 -30.6219 0 125 | vertex 6.87015 -25.4194 0 126 | endloop 127 | endfacet 128 | facet normal 0 0 -1 129 | outer loop 130 | vertex 1.24484 -29.8254 0 131 | vertex 6.87015 -25.4194 0 132 | vertex 2.50002 -30.6219 0 133 | endloop 134 | endfacet 135 | facet normal 0 0 -1 136 | outer loop 137 | vertex 0.06073 -28.9266 0 138 | vertex 6.87015 -25.4194 0 139 | vertex 1.24484 -29.8254 0 140 | endloop 141 | endfacet 142 | facet normal -0 0 -1 143 | outer loop 144 | vertex 6.87015 -25.4194 0 145 | vertex 0.06073 -28.9266 0 146 | vertex 6.43083 -23.9952 0 147 | endloop 148 | endfacet 149 | facet normal 0 0 -1 150 | outer loop 151 | vertex -1.04402 -27.9319 0 152 | vertex 6.43083 -23.9952 0 153 | vertex 0.06073 -28.9266 0 154 | endloop 155 | endfacet 156 | facet normal 0 0 -1 157 | outer loop 158 | vertex -2.06166 -26.8482 0 159 | vertex 6.43083 -23.9952 0 160 | vertex -1.04402 -27.9319 0 161 | endloop 162 | endfacet 163 | facet normal 0 0 -1 164 | outer loop 165 | vertex -2.98505 -25.6832 0 166 | vertex 6.43083 -23.9952 0 167 | vertex -2.06166 -26.8482 0 168 | endloop 169 | endfacet 170 | facet normal 0 0 -1 171 | outer loop 172 | vertex -3.80771 -24.445 0 173 | vertex 6.43083 -23.9952 0 174 | vertex -2.98505 -25.6832 0 175 | endloop 176 | endfacet 177 | facet normal 0 0 -1 178 | outer loop 179 | vertex -4.52388 -23.1423 0 180 | vertex 6.43083 -23.9952 0 181 | vertex -3.80771 -24.445 0 182 | endloop 183 | endfacet 184 | facet normal 0 0 -1 185 | outer loop 186 | vertex -5.12852 -21.7842 0 187 | vertex 6.43083 -23.9952 0 188 | vertex -4.52388 -23.1423 0 189 | endloop 190 | endfacet 191 | facet normal 0 0 -1 192 | outer loop 193 | vertex 6.43083 -23.9952 0 194 | vertex -5.12852 -21.7842 0 195 | vertex 6.375 -15.25 0 196 | endloop 197 | endfacet 198 | facet normal 0 0 -1 199 | outer loop 200 | vertex -5.61742 -20.3803 0 201 | vertex 6.375 -15.25 0 202 | vertex -5.12852 -21.7842 0 203 | endloop 204 | endfacet 205 | facet normal 0 0 -1 206 | outer loop 207 | vertex -5.98711 -18.9404 0 208 | vertex 6.375 -15.25 0 209 | vertex -5.61742 -20.3803 0 210 | endloop 211 | endfacet 212 | facet normal 0 0 -1 213 | outer loop 214 | vertex -6.23503 -17.4747 0 215 | vertex 6.375 -15.25 0 216 | vertex -5.98711 -18.9404 0 217 | endloop 218 | endfacet 219 | facet normal 0 0 -1 220 | outer loop 221 | vertex -6.35942 -15.9933 0 222 | vertex 6.375 -15.25 0 223 | vertex -6.23503 -17.4747 0 224 | endloop 225 | endfacet 226 | facet normal -0 0 -1 227 | outer loop 228 | vertex 6.375 -15.25 0 229 | vertex -6.35942 -15.9933 0 230 | vertex -6.35942 -15.25 0 231 | endloop 232 | endfacet 233 | facet normal 0 0 -1 234 | outer loop 235 | vertex -6.375 -4 0 236 | vertex 6.375 -15.25 0 237 | vertex -6.35942 -15.25 0 238 | endloop 239 | endfacet 240 | facet normal 0 0 -1 241 | outer loop 242 | vertex -6.375 -4 0 243 | vertex -6.35942 -15.25 0 244 | vertex -6.375 -15.25 0 245 | endloop 246 | endfacet 247 | facet normal 0 0 -1 248 | outer loop 249 | vertex -6.375 0 0 250 | vertex 6.375 0 0 251 | vertex -6.375 -4 0 252 | endloop 253 | endfacet 254 | facet normal 0 0 -1 255 | outer loop 256 | vertex 6.375 0 0 257 | vertex -6.375 0 0 258 | vertex 6.375 5 0 259 | endloop 260 | endfacet 261 | facet normal -0 0 -1 262 | outer loop 263 | vertex 6.375 5 0 264 | vertex -6.375 0 0 265 | vertex -6.375 5 0 266 | endloop 267 | endfacet 268 | facet normal 0 0 -1 269 | outer loop 270 | vertex 6.375 -4 0 271 | vertex 7.375 0 0 272 | vertex 7.375 -4 0 273 | endloop 274 | endfacet 275 | facet normal -0 0 -1 276 | outer loop 277 | vertex 7.375 0 0 278 | vertex 6.375 -4 0 279 | vertex 6.375 0 0 280 | endloop 281 | endfacet 282 | facet normal -0 0 1 283 | outer loop 284 | vertex -4.85815 -15.25 1.5 285 | vertex 4.875 -23.25 1.5 286 | vertex 4.875 5 1.5 287 | endloop 288 | endfacet 289 | facet normal -0 0 1 290 | outer loop 291 | vertex -4.85815 -15.9896 1.5 292 | vertex 4.875 -23.25 1.5 293 | vertex -4.85815 -15.25 1.5 294 | endloop 295 | endfacet 296 | facet normal 0 0 1 297 | outer loop 298 | vertex 4.875 -23.25 1.5 299 | vertex 3.25 -29.3229 1.5 300 | vertex 4.56339 -30.0034 1.5 301 | endloop 302 | endfacet 303 | facet normal 0 0 1 304 | outer loop 305 | vertex 4.875 -23.25 1.5 306 | vertex 2.00395 -28.5258 1.5 307 | vertex 3.25 -29.3229 1.5 308 | endloop 309 | endfacet 310 | facet normal 0 0 1 311 | outer loop 312 | vertex 4.875 -23.25 1.5 313 | vertex 0.835556 -27.6186 1.5 314 | vertex 2.00395 -28.5258 1.5 315 | endloop 316 | endfacet 317 | facet normal 0 0 1 318 | outer loop 319 | vertex 4.875 -23.25 1.5 320 | vertex -0.245529 -26.6089 1.5 321 | vertex 0.835556 -27.6186 1.5 322 | endloop 323 | endfacet 324 | facet normal 0 0 1 325 | outer loop 326 | vertex 4.875 -23.25 1.5 327 | vertex -1.2303 -25.5052 1.5 328 | vertex -0.245529 -26.6089 1.5 329 | endloop 330 | endfacet 331 | facet normal 0 0 1 332 | outer loop 333 | vertex 4.875 -23.25 1.5 334 | vertex -2.11063 -24.3164 1.5 335 | vertex -1.2303 -25.5052 1.5 336 | endloop 337 | endfacet 338 | facet normal 0 0 1 339 | outer loop 340 | vertex 4.875 -23.25 1.5 341 | vertex -2.87921 -23.0525 1.5 342 | vertex -2.11063 -24.3164 1.5 343 | endloop 344 | endfacet 345 | facet normal 0 0 1 346 | outer loop 347 | vertex 4.875 -23.25 1.5 348 | vertex -3.52968 -21.724 1.5 349 | vertex -2.87921 -23.0525 1.5 350 | endloop 351 | endfacet 352 | facet normal 0 0 1 353 | outer loop 354 | vertex 4.875 -23.25 1.5 355 | vertex -4.05664 -20.3418 1.5 356 | vertex -3.52968 -21.724 1.5 357 | endloop 358 | endfacet 359 | facet normal 0 0 1 360 | outer loop 361 | vertex 4.875 -23.25 1.5 362 | vertex -4.45573 -18.9175 1.5 363 | vertex -4.05664 -20.3418 1.5 364 | endloop 365 | endfacet 366 | facet normal 0 0 1 367 | outer loop 368 | vertex 4.875 -23.25 1.5 369 | vertex -4.72363 -17.4627 1.5 370 | vertex -4.45573 -18.9175 1.5 371 | endloop 372 | endfacet 373 | facet normal -0 0 1 374 | outer loop 375 | vertex -4.875 5 1.5 376 | vertex -4.85815 -15.25 1.5 377 | vertex 4.875 5 1.5 378 | endloop 379 | endfacet 380 | facet normal 0 0 1 381 | outer loop 382 | vertex -4.85815 -15.25 1.5 383 | vertex -4.875 5 1.5 384 | vertex -4.875 -15.25 1.5 385 | endloop 386 | endfacet 387 | facet normal 0 0 1 388 | outer loop 389 | vertex 9.9286 -29.587 1.5 390 | vertex 10.1112 -29.6948 1.5 391 | vertex 10.1188 -29.6084 1.5 392 | endloop 393 | endfacet 394 | facet normal 0 0 1 395 | outer loop 396 | vertex 10.1112 -29.6948 1.5 397 | vertex 9.9286 -29.587 1.5 398 | vertex 9.94974 -31.4259 1.5 399 | endloop 400 | endfacet 401 | facet normal 0 0 1 402 | outer loop 403 | vertex 9.9286 -29.587 1.5 404 | vertex 8.79642 -31.2941 1.5 405 | vertex 9.94974 -31.4259 1.5 406 | endloop 407 | endfacet 408 | facet normal 0 0 1 409 | outer loop 410 | vertex 8.55475 -29.1063 1.5 411 | vertex 8.79642 -31.2941 1.5 412 | vertex 9.9286 -29.587 1.5 413 | endloop 414 | endfacet 415 | facet normal 0 0 1 416 | outer loop 417 | vertex 8.55475 -29.1063 1.5 418 | vertex 7.34813 -30.9931 1.5 419 | vertex 8.79642 -31.2941 1.5 420 | endloop 421 | endfacet 422 | facet normal 0 0 1 423 | outer loop 424 | vertex 7.32231 -28.3319 1.5 425 | vertex 7.34813 -30.9931 1.5 426 | vertex 8.55475 -29.1063 1.5 427 | endloop 428 | endfacet 429 | facet normal 0 0 1 430 | outer loop 431 | vertex 4.56339 -30.0034 1.5 432 | vertex 7.32231 -28.3319 1.5 433 | vertex 6.29309 -27.3027 1.5 434 | endloop 435 | endfacet 436 | facet normal 0 0 1 437 | outer loop 438 | vertex 7.32231 -28.3319 1.5 439 | vertex 5.93321 -30.5617 1.5 440 | vertex 7.34813 -30.9931 1.5 441 | endloop 442 | endfacet 443 | facet normal 0 0 1 444 | outer loop 445 | vertex 4.56339 -30.0034 1.5 446 | vertex 6.29309 -27.3027 1.5 447 | vertex 5.51869 -26.0702 1.5 448 | endloop 449 | endfacet 450 | facet normal 0 0 1 451 | outer loop 452 | vertex 4.56339 -30.0034 1.5 453 | vertex 5.51869 -26.0702 1.5 454 | vertex 5.03796 -24.6964 1.5 455 | endloop 456 | endfacet 457 | facet normal 0 0 1 458 | outer loop 459 | vertex 7.32231 -28.3319 1.5 460 | vertex 4.56339 -30.0034 1.5 461 | vertex 5.93321 -30.5617 1.5 462 | endloop 463 | endfacet 464 | facet normal 0 0 1 465 | outer loop 466 | vertex 4.56339 -30.0034 1.5 467 | vertex 5.03796 -24.6964 1.5 468 | vertex 4.875 -23.25 1.5 469 | endloop 470 | endfacet 471 | facet normal -0 0 1 472 | outer loop 473 | vertex 14.8332 -30.1079 1.5 474 | vertex 14.6807 -31.8513 1.5 475 | vertex 14.9639 -28.6136 1.5 476 | endloop 477 | endfacet 478 | facet normal 0 0 1 479 | outer loop 480 | vertex 10.1112 -29.6948 1.5 481 | vertex 14.6807 -31.8513 1.5 482 | vertex 14.8332 -30.1079 1.5 483 | endloop 484 | endfacet 485 | facet normal -0 0 1 486 | outer loop 487 | vertex 9.94974 -31.4259 1.5 488 | vertex 14.6807 -31.8513 1.5 489 | vertex 10.1112 -29.6948 1.5 490 | endloop 491 | endfacet 492 | facet normal 0 0 1 493 | outer loop 494 | vertex 14.6807 -31.8513 1.5 495 | vertex 9.94974 -31.4259 1.5 496 | vertex 9.94875 -31.4373 1.5 497 | endloop 498 | endfacet 499 | facet normal 0 0 1 500 | outer loop 501 | vertex 4.875 -23.25 1.5 502 | vertex -4.85815 -15.9896 1.5 503 | vertex -4.72363 -17.4627 1.5 504 | endloop 505 | endfacet 506 | facet normal 0 -1 0 507 | outer loop 508 | vertex -7.375 -4 0 509 | vertex -6.375 -4 17 510 | vertex -7.375 -4 17 511 | endloop 512 | endfacet 513 | facet normal 0 -1 -0 514 | outer loop 515 | vertex -6.375 -4 17 516 | vertex -7.375 -4 0 517 | vertex -6.375 -4 0 518 | endloop 519 | endfacet 520 | facet normal -1 0 0 521 | outer loop 522 | vertex -6.375 -15.25 0 523 | vertex -6.375 -4 17 524 | vertex -6.375 -4 0 525 | endloop 526 | endfacet 527 | facet normal -1 -0 0 528 | outer loop 529 | vertex -6.375 -4 17 530 | vertex -6.375 -15.25 0 531 | vertex -6.375 -15.25 17 532 | endloop 533 | endfacet 534 | facet normal 0 -1 0 535 | outer loop 536 | vertex -6.375 -15.25 0 537 | vertex -6.35942 -15.25 17 538 | vertex -6.375 -15.25 17 539 | endloop 540 | endfacet 541 | facet normal 0 -1 -0 542 | outer loop 543 | vertex -6.35942 -15.25 17 544 | vertex -6.375 -15.25 0 545 | vertex -6.35942 -15.25 0 546 | endloop 547 | endfacet 548 | facet normal -1 0 0 549 | outer loop 550 | vertex -6.35942 -15.9933 0 551 | vertex -6.35942 -15.25 17 552 | vertex -6.35942 -15.25 0 553 | endloop 554 | endfacet 555 | facet normal -1 -0 0 556 | outer loop 557 | vertex -6.35942 -15.25 17 558 | vertex -6.35942 -15.9933 0 559 | vertex -6.35942 -15.9933 17 560 | endloop 561 | endfacet 562 | facet normal -0.996493 -0.0836749 0 563 | outer loop 564 | vertex -6.23503 -17.4747 0 565 | vertex -6.35942 -15.9933 17 566 | vertex -6.35942 -15.9933 0 567 | endloop 568 | endfacet 569 | facet normal -0.996493 -0.0836749 0 570 | outer loop 571 | vertex -6.35942 -15.9933 17 572 | vertex -6.23503 -17.4747 0 573 | vertex -6.23503 -17.4747 17 574 | endloop 575 | endfacet 576 | facet normal -0.985995 -0.166774 0 577 | outer loop 578 | vertex -5.98711 -18.9404 0 579 | vertex -6.23503 -17.4747 17 580 | vertex -6.23503 -17.4747 0 581 | endloop 582 | endfacet 583 | facet normal -0.985995 -0.166774 0 584 | outer loop 585 | vertex -6.23503 -17.4747 17 586 | vertex -5.98711 -18.9404 0 587 | vertex -5.98711 -18.9404 17 588 | endloop 589 | endfacet 590 | facet normal -0.968585 -0.248685 0 591 | outer loop 592 | vertex -5.61742 -20.3803 0 593 | vertex -5.98711 -18.9404 17 594 | vertex -5.98711 -18.9404 0 595 | endloop 596 | endfacet 597 | facet normal -0.968585 -0.248685 0 598 | outer loop 599 | vertex -5.98711 -18.9404 17 600 | vertex -5.61742 -20.3803 0 601 | vertex -5.61742 -20.3803 17 602 | endloop 603 | endfacet 604 | facet normal -0.944376 -0.328868 0 605 | outer loop 606 | vertex -5.12852 -21.7842 0 607 | vertex -5.61742 -20.3803 17 608 | vertex -5.61742 -20.3803 0 609 | endloop 610 | endfacet 611 | facet normal -0.944376 -0.328868 0 612 | outer loop 613 | vertex -5.61742 -20.3803 17 614 | vertex -5.12852 -21.7842 0 615 | vertex -5.12852 -21.7842 17 616 | endloop 617 | endfacet 618 | facet normal -0.913546 -0.406735 0 619 | outer loop 620 | vertex -4.52388 -23.1423 0 621 | vertex -5.12852 -21.7842 17 622 | vertex -5.12852 -21.7842 0 623 | endloop 624 | endfacet 625 | facet normal -0.913546 -0.406735 0 626 | outer loop 627 | vertex -5.12852 -21.7842 17 628 | vertex -4.52388 -23.1423 0 629 | vertex -4.52388 -23.1423 17 630 | endloop 631 | endfacet 632 | facet normal -0.876306 -0.481756 0 633 | outer loop 634 | vertex -3.80771 -24.445 0 635 | vertex -4.52388 -23.1423 17 636 | vertex -4.52388 -23.1423 0 637 | endloop 638 | endfacet 639 | facet normal -0.876306 -0.481756 0 640 | outer loop 641 | vertex -4.52388 -23.1423 17 642 | vertex -3.80771 -24.445 0 643 | vertex -3.80771 -24.445 17 644 | endloop 645 | endfacet 646 | facet normal -0.832921 -0.553392 0 647 | outer loop 648 | vertex -2.98505 -25.6832 0 649 | vertex -3.80771 -24.445 17 650 | vertex -3.80771 -24.445 0 651 | endloop 652 | endfacet 653 | facet normal -0.832921 -0.553392 0 654 | outer loop 655 | vertex -3.80771 -24.445 17 656 | vertex -2.98505 -25.6832 0 657 | vertex -2.98505 -25.6832 17 658 | endloop 659 | endfacet 660 | facet normal -0.783694 -0.621147 0 661 | outer loop 662 | vertex -2.06166 -26.8482 0 663 | vertex -2.98505 -25.6832 17 664 | vertex -2.98505 -25.6832 0 665 | endloop 666 | endfacet 667 | facet normal -0.783694 -0.621147 0 668 | outer loop 669 | vertex -2.98505 -25.6832 17 670 | vertex -2.06166 -26.8482 0 671 | vertex -2.06166 -26.8482 17 672 | endloop 673 | endfacet 674 | facet normal -0.72897 -0.684546 0 675 | outer loop 676 | vertex -1.04402 -27.9319 0 677 | vertex -2.06166 -26.8482 17 678 | vertex -2.06166 -26.8482 0 679 | endloop 680 | endfacet 681 | facet normal -0.72897 -0.684546 0 682 | outer loop 683 | vertex -2.06166 -26.8482 17 684 | vertex -1.04402 -27.9319 0 685 | vertex -1.04402 -27.9319 17 686 | endloop 687 | endfacet 688 | facet normal -0.66913 -0.743146 0 689 | outer loop 690 | vertex -1.04402 -27.9319 0 691 | vertex 0.06073 -28.9266 17 692 | vertex -1.04402 -27.9319 17 693 | endloop 694 | endfacet 695 | facet normal -0.66913 -0.743146 -0 696 | outer loop 697 | vertex 0.06073 -28.9266 17 698 | vertex -1.04402 -27.9319 0 699 | vertex 0.06073 -28.9266 0 700 | endloop 701 | endfacet 702 | facet normal -0.604598 -0.796531 0 703 | outer loop 704 | vertex 0.06073 -28.9266 0 705 | vertex 1.24484 -29.8254 17 706 | vertex 0.06073 -28.9266 17 707 | endloop 708 | endfacet 709 | facet normal -0.604598 -0.796531 -0 710 | outer loop 711 | vertex 1.24484 -29.8254 17 712 | vertex 0.06073 -28.9266 0 713 | vertex 1.24484 -29.8254 0 714 | endloop 715 | endfacet 716 | facet normal -0.535826 -0.844329 0 717 | outer loop 718 | vertex 1.24484 -29.8254 0 719 | vertex 2.50002 -30.6219 17 720 | vertex 1.24484 -29.8254 17 721 | endloop 722 | endfacet 723 | facet normal -0.535826 -0.844329 -0 724 | outer loop 725 | vertex 2.50002 -30.6219 17 726 | vertex 1.24484 -29.8254 0 727 | vertex 2.50002 -30.6219 0 728 | endloop 729 | endfacet 730 | facet normal -0.463293 -0.886205 0 731 | outer loop 732 | vertex 2.50002 -30.6219 0 733 | vertex 3.81743 -31.3107 17 734 | vertex 2.50002 -30.6219 17 735 | endloop 736 | endfacet 737 | facet normal -0.463293 -0.886205 -0 738 | outer loop 739 | vertex 3.81743 -31.3107 17 740 | vertex 2.50002 -30.6219 0 741 | vertex 3.81743 -31.3107 0 742 | endloop 743 | endfacet 744 | facet normal -0.38752 -0.921861 0 745 | outer loop 746 | vertex 3.81743 -31.3107 0 747 | vertex 5.18785 -31.8867 17 748 | vertex 3.81743 -31.3107 17 749 | endloop 750 | endfacet 751 | facet normal -0.38752 -0.921861 -0 752 | outer loop 753 | vertex 5.18785 -31.8867 17 754 | vertex 3.81743 -31.3107 0 755 | vertex 5.18785 -31.8867 0 756 | endloop 757 | endfacet 758 | facet normal -0.309016 -0.951057 0 759 | outer loop 760 | vertex 5.18785 -31.8867 0 761 | vertex 6.60168 -32.3461 17 762 | vertex 5.18785 -31.8867 17 763 | endloop 764 | endfacet 765 | facet normal -0.309016 -0.951057 -0 766 | outer loop 767 | vertex 6.60168 -32.3461 17 768 | vertex 5.18785 -31.8867 0 769 | vertex 6.60168 -32.3461 0 770 | endloop 771 | endfacet 772 | facet normal -0.22835 -0.973579 0 773 | outer loop 774 | vertex 6.60168 -32.3461 0 775 | vertex 8.049 -32.6856 17 776 | vertex 6.60168 -32.3461 17 777 | endloop 778 | endfacet 779 | facet normal -0.22835 -0.973579 -0 780 | outer loop 781 | vertex 8.049 -32.6856 17 782 | vertex 6.60168 -32.3461 0 783 | vertex 8.049 -32.6856 0 784 | endloop 785 | endfacet 786 | facet normal -0.146083 -0.989272 0 787 | outer loop 788 | vertex 8.049 -32.6856 0 789 | vertex 9.51962 -32.9028 17 790 | vertex 8.049 -32.6856 17 791 | endloop 792 | endfacet 793 | facet normal -0.146083 -0.989272 -0 794 | outer loop 795 | vertex 9.51962 -32.9028 17 796 | vertex 8.049 -32.6856 0 797 | vertex 9.51962 -32.9028 0 798 | endloop 799 | endfacet 800 | facet normal -0.0627965 -0.998026 0 801 | outer loop 802 | vertex 9.51962 -32.9028 0 803 | vertex 9.81888 -32.9216 17 804 | vertex 9.51962 -32.9028 17 805 | endloop 806 | endfacet 807 | facet normal -0.0627965 -0.998026 -0 808 | outer loop 809 | vertex 9.81888 -32.9216 17 810 | vertex 9.51962 -32.9028 0 811 | vertex 9.81888 -32.9216 0 812 | endloop 813 | endfacet 814 | facet normal -0.996235 0.0866952 0 815 | outer loop 816 | vertex 9.81801 -32.9316 0 817 | vertex 9.81888 -32.9216 17 818 | vertex 9.81888 -32.9216 0 819 | endloop 820 | endfacet 821 | facet normal -0.996235 0.0866952 0 822 | outer loop 823 | vertex 9.81888 -32.9216 17 824 | vertex 9.81801 -32.9316 0 825 | vertex 9.81801 -32.9316 17 826 | endloop 827 | endfacet 828 | facet normal -0.0871548 -0.996195 0 829 | outer loop 830 | vertex 9.81801 -32.9316 17 831 | vertex 14.5499 -33.3456 15.51 832 | vertex 14.5499 -33.3456 17 833 | endloop 834 | endfacet 835 | facet normal -0.0871548 -0.996195 0 836 | outer loop 837 | vertex 9.81801 -32.9316 17 838 | vertex 14.5499 -33.3456 1.49 839 | vertex 14.5499 -33.3456 15.51 840 | endloop 841 | endfacet 842 | facet normal -0.0871548 -0.996195 0 843 | outer loop 844 | vertex 9.81801 -32.9316 0 845 | vertex 14.5499 -33.3456 1.49 846 | vertex 9.81801 -32.9316 17 847 | endloop 848 | endfacet 849 | facet normal -0.0871548 -0.996195 -0 850 | outer loop 851 | vertex 14.5499 -33.3456 1.49 852 | vertex 9.81801 -32.9316 0 853 | vertex 14.5499 -33.3456 0 854 | endloop 855 | endfacet 856 | facet normal 0.996195 -0.0871552 0 857 | outer loop 858 | vertex 14.6807 -31.8513 1.49 859 | vertex 14.9639 -28.6136 1.5 860 | vertex 14.6807 -31.8513 1.5 861 | endloop 862 | endfacet 863 | facet normal 0.996195 -0.0871552 0 864 | outer loop 865 | vertex 14.9639 -28.6136 1.5 866 | vertex 14.6807 -31.8513 1.49 867 | vertex 14.9639 -28.6136 0 868 | endloop 869 | endfacet 870 | facet normal 0.996195 -0.087155 0 871 | outer loop 872 | vertex 14.5499 -33.3456 0 873 | vertex 14.6807 -31.8513 1.49 874 | vertex 14.5499 -33.3456 1.49 875 | endloop 876 | endfacet 877 | facet normal 0.996195 -0.0871551 1.54344e-07 878 | outer loop 879 | vertex 14.6807 -31.8513 1.49 880 | vertex 14.5499 -33.3456 0 881 | vertex 14.9639 -28.6136 0 882 | endloop 883 | endfacet 884 | facet normal 0.0871587 0.996194 -0 885 | outer loop 886 | vertex 14.9639 -28.6136 15.5 887 | vertex 10.242 -28.2005 17 888 | vertex 14.9639 -28.6136 17 889 | endloop 890 | endfacet 891 | facet normal 0.0871587 0.996194 -0 892 | outer loop 893 | vertex 14.9639 -28.6136 1.5 894 | vertex 10.242 -28.2005 17 895 | vertex 14.9639 -28.6136 15.5 896 | endloop 897 | endfacet 898 | facet normal 0.0871587 0.996194 0 899 | outer loop 900 | vertex 10.242 -28.2005 0 901 | vertex 14.9639 -28.6136 1.5 902 | vertex 14.9639 -28.6136 0 903 | endloop 904 | endfacet 905 | facet normal 0.0871587 0.996194 0 906 | outer loop 907 | vertex 14.9639 -28.6136 1.5 908 | vertex 10.242 -28.2005 0 909 | vertex 10.242 -28.2005 17 910 | endloop 911 | endfacet 912 | facet normal 0.996196 -0.0871391 0 913 | outer loop 914 | vertex 10.242 -28.2005 17 915 | vertex 10.2491 -28.1194 0 916 | vertex 10.2491 -28.1194 17 917 | endloop 918 | endfacet 919 | facet normal 0.996196 -0.0871391 0 920 | outer loop 921 | vertex 10.2491 -28.1194 0 922 | vertex 10.242 -28.2005 17 923 | vertex 10.242 -28.2005 0 924 | endloop 925 | endfacet 926 | facet normal 0.365334 0.930877 -0 927 | outer loop 928 | vertex 10.2491 -28.1194 0 929 | vertex 8.87498 -27.5801 17 930 | vertex 10.2491 -28.1194 17 931 | endloop 932 | endfacet 933 | facet normal 0.365334 0.930877 0 934 | outer loop 935 | vertex 8.87498 -27.5801 17 936 | vertex 10.2491 -28.1194 0 937 | vertex 8.87498 -27.5801 0 938 | endloop 939 | endfacet 940 | facet normal 0.623491 0.781831 -0 941 | outer loop 942 | vertex 8.87498 -27.5801 0 943 | vertex 7.70973 -26.6509 17 944 | vertex 8.87498 -27.5801 17 945 | endloop 946 | endfacet 947 | facet normal 0.623491 0.781831 0 948 | outer loop 949 | vertex 7.70973 -26.6509 17 950 | vertex 8.87498 -27.5801 0 951 | vertex 7.70973 -26.6509 0 952 | endloop 953 | endfacet 954 | facet normal 0.826239 0.56332 0 955 | outer loop 956 | vertex 7.70973 -26.6509 17 957 | vertex 6.87015 -25.4194 0 958 | vertex 6.87015 -25.4194 17 959 | endloop 960 | endfacet 961 | facet normal 0.826239 0.56332 0 962 | outer loop 963 | vertex 6.87015 -25.4194 0 964 | vertex 7.70973 -26.6509 17 965 | vertex 7.70973 -26.6509 0 966 | endloop 967 | endfacet 968 | facet normal 0.955572 0.294758 0 969 | outer loop 970 | vertex 6.87015 -25.4194 17 971 | vertex 6.43083 -23.9952 0 972 | vertex 6.43083 -23.9952 17 973 | endloop 974 | endfacet 975 | facet normal 0.955572 0.294758 0 976 | outer loop 977 | vertex 6.43083 -23.9952 0 978 | vertex 6.87015 -25.4194 17 979 | vertex 6.87015 -25.4194 0 980 | endloop 981 | endfacet 982 | facet normal 1 -0 0 983 | outer loop 984 | vertex 6.43083 -23.9952 17 985 | vertex 6.43083 -15.25 0 986 | vertex 6.43083 -15.25 17 987 | endloop 988 | endfacet 989 | facet normal 1 0 0 990 | outer loop 991 | vertex 6.43083 -15.25 0 992 | vertex 6.43083 -23.9952 17 993 | vertex 6.43083 -23.9952 0 994 | endloop 995 | endfacet 996 | facet normal 0 1 -0 997 | outer loop 998 | vertex 6.43083 -15.25 0 999 | vertex 6.375 -15.25 17 1000 | vertex 6.43083 -15.25 17 1001 | endloop 1002 | endfacet 1003 | facet normal 0 1 0 1004 | outer loop 1005 | vertex 6.375 -15.25 17 1006 | vertex 6.43083 -15.25 0 1007 | vertex 6.375 -15.25 0 1008 | endloop 1009 | endfacet 1010 | facet normal 1 -0 0 1011 | outer loop 1012 | vertex 6.375 -15.25 17 1013 | vertex 6.375 -4 0 1014 | vertex 6.375 -4 17 1015 | endloop 1016 | endfacet 1017 | facet normal 1 0 0 1018 | outer loop 1019 | vertex 6.375 -4 0 1020 | vertex 6.375 -15.25 17 1021 | vertex 6.375 -15.25 0 1022 | endloop 1023 | endfacet 1024 | facet normal 0 -1 0 1025 | outer loop 1026 | vertex 6.375 -4 0 1027 | vertex 7.375 -4 17 1028 | vertex 6.375 -4 17 1029 | endloop 1030 | endfacet 1031 | facet normal 0 -1 -0 1032 | outer loop 1033 | vertex 7.375 -4 17 1034 | vertex 6.375 -4 0 1035 | vertex 7.375 -4 0 1036 | endloop 1037 | endfacet 1038 | facet normal 1 -0 0 1039 | outer loop 1040 | vertex 7.375 -4 17 1041 | vertex 7.375 0 0 1042 | vertex 7.375 0 17 1043 | endloop 1044 | endfacet 1045 | facet normal 1 0 0 1046 | outer loop 1047 | vertex 7.375 0 0 1048 | vertex 7.375 -4 17 1049 | vertex 7.375 -4 0 1050 | endloop 1051 | endfacet 1052 | facet normal 0 1 -0 1053 | outer loop 1054 | vertex 7.375 0 0 1055 | vertex 6.375 0 17 1056 | vertex 7.375 0 17 1057 | endloop 1058 | endfacet 1059 | facet normal 0 1 0 1060 | outer loop 1061 | vertex 6.375 0 17 1062 | vertex 7.375 0 0 1063 | vertex 6.375 0 0 1064 | endloop 1065 | endfacet 1066 | facet normal 1 -0 0 1067 | outer loop 1068 | vertex 6.375 0 17 1069 | vertex 6.375 5 0 1070 | vertex 6.375 5 17 1071 | endloop 1072 | endfacet 1073 | facet normal 1 0 0 1074 | outer loop 1075 | vertex 6.375 5 0 1076 | vertex 6.375 0 17 1077 | vertex 6.375 0 0 1078 | endloop 1079 | endfacet 1080 | facet normal 0 1 0 1081 | outer loop 1082 | vertex 6.375 5 17 1083 | vertex 4.875 5 15.5 1084 | vertex -6.375 5 17 1085 | endloop 1086 | endfacet 1087 | facet normal 0 1 0 1088 | outer loop 1089 | vertex 6.375 5 17 1090 | vertex 4.875 5 1.5 1091 | vertex 4.875 5 15.5 1092 | endloop 1093 | endfacet 1094 | facet normal 0 1 0 1095 | outer loop 1096 | vertex 4.875 5 1.5 1097 | vertex 6.375 5 0 1098 | vertex -4.875 5 1.5 1099 | endloop 1100 | endfacet 1101 | facet normal 0 1 -0 1102 | outer loop 1103 | vertex 6.375 5 0 1104 | vertex 4.875 5 1.5 1105 | vertex 6.375 5 17 1106 | endloop 1107 | endfacet 1108 | facet normal 0 1 -0 1109 | outer loop 1110 | vertex -4.875 5 15.5 1111 | vertex -6.375 5 17 1112 | vertex 4.875 5 15.5 1113 | endloop 1114 | endfacet 1115 | facet normal 0 1 -0 1116 | outer loop 1117 | vertex -4.875 5 1.5 1118 | vertex -6.375 5 17 1119 | vertex -4.875 5 15.5 1120 | endloop 1121 | endfacet 1122 | facet normal 0 1 0 1123 | outer loop 1124 | vertex -4.875 5 1.5 1125 | vertex -6.375 5 0 1126 | vertex -6.375 5 17 1127 | endloop 1128 | endfacet 1129 | facet normal 0 1 0 1130 | outer loop 1131 | vertex -6.375 5 0 1132 | vertex -4.875 5 1.5 1133 | vertex 6.375 5 0 1134 | endloop 1135 | endfacet 1136 | facet normal -1 0 0 1137 | outer loop 1138 | vertex -6.375 0 0 1139 | vertex -6.375 5 17 1140 | vertex -6.375 5 0 1141 | endloop 1142 | endfacet 1143 | facet normal -1 -0 0 1144 | outer loop 1145 | vertex -6.375 5 17 1146 | vertex -6.375 0 0 1147 | vertex -6.375 0 17 1148 | endloop 1149 | endfacet 1150 | facet normal 0 1 -0 1151 | outer loop 1152 | vertex -6.375 0 0 1153 | vertex -7.375 0 17 1154 | vertex -6.375 0 17 1155 | endloop 1156 | endfacet 1157 | facet normal 0 1 0 1158 | outer loop 1159 | vertex -7.375 0 17 1160 | vertex -6.375 0 0 1161 | vertex -7.375 0 0 1162 | endloop 1163 | endfacet 1164 | facet normal -1 0 0 1165 | outer loop 1166 | vertex -7.375 -4 0 1167 | vertex -7.375 0 17 1168 | vertex -7.375 0 0 1169 | endloop 1170 | endfacet 1171 | facet normal -1 -0 0 1172 | outer loop 1173 | vertex -7.375 0 17 1174 | vertex -7.375 -4 0 1175 | vertex -7.375 -4 17 1176 | endloop 1177 | endfacet 1178 | facet normal 0.0871549 0.996195 0 1179 | outer loop 1180 | vertex 14.6807 -31.8513 15.5 1181 | vertex 14.6807 -31.8513 1.5 1182 | vertex 9.94875 -31.4373 15.5 1183 | endloop 1184 | endfacet 1185 | facet normal 0 1 0 1186 | outer loop 1187 | vertex 14.6807 -31.8513 15.51 1188 | vertex 14.6807 -31.8513 1.5 1189 | vertex 14.6807 -31.8513 15.5 1190 | endloop 1191 | endfacet 1192 | facet normal 0 1 -0 1193 | outer loop 1194 | vertex 14.6807 -31.8513 1.49 1195 | vertex 14.6807 -31.8513 1.5 1196 | vertex 14.6807 -31.8513 15.51 1197 | endloop 1198 | endfacet 1199 | facet normal 0.0871549 0.996195 0 1200 | outer loop 1201 | vertex 9.94875 -31.4373 15.5 1202 | vertex 14.6807 -31.8513 1.5 1203 | vertex 9.94875 -31.4373 1.5 1204 | endloop 1205 | endfacet 1206 | facet normal 0.996195 -0.0871503 0 1207 | outer loop 1208 | vertex 9.94875 -31.4373 15.5 1209 | vertex 9.94974 -31.4259 1.5 1210 | vertex 9.94974 -31.4259 15.5 1211 | endloop 1212 | endfacet 1213 | facet normal 0.996195 -0.0871503 0 1214 | outer loop 1215 | vertex 9.94974 -31.4259 1.5 1216 | vertex 9.94875 -31.4373 15.5 1217 | vertex 9.94875 -31.4373 1.5 1218 | endloop 1219 | endfacet 1220 | facet normal 0.113583 0.993528 -0 1221 | outer loop 1222 | vertex 9.94974 -31.4259 1.5 1223 | vertex 8.79642 -31.2941 15.5 1224 | vertex 9.94974 -31.4259 15.5 1225 | endloop 1226 | endfacet 1227 | facet normal 0.113583 0.993528 0 1228 | outer loop 1229 | vertex 8.79642 -31.2941 15.5 1230 | vertex 9.94974 -31.4259 1.5 1231 | vertex 8.79642 -31.2941 1.5 1232 | endloop 1233 | endfacet 1234 | facet normal 0.20345 0.979085 -0 1235 | outer loop 1236 | vertex 8.79642 -31.2941 1.5 1237 | vertex 7.34813 -30.9931 15.5 1238 | vertex 8.79642 -31.2941 15.5 1239 | endloop 1240 | endfacet 1241 | facet normal 0.20345 0.979085 0 1242 | outer loop 1243 | vertex 7.34813 -30.9931 15.5 1244 | vertex 8.79642 -31.2941 1.5 1245 | vertex 7.34813 -30.9931 1.5 1246 | endloop 1247 | endfacet 1248 | facet normal 0.291647 0.956526 -0 1249 | outer loop 1250 | vertex 7.34813 -30.9931 1.5 1251 | vertex 5.93321 -30.5617 15.5 1252 | vertex 7.34813 -30.9931 15.5 1253 | endloop 1254 | endfacet 1255 | facet normal 0.291647 0.956526 0 1256 | outer loop 1257 | vertex 5.93321 -30.5617 15.5 1258 | vertex 7.34813 -30.9931 1.5 1259 | vertex 5.93321 -30.5617 1.5 1260 | endloop 1261 | endfacet 1262 | facet normal 0.377419 0.926043 -0 1263 | outer loop 1264 | vertex 5.93321 -30.5617 1.5 1265 | vertex 4.56339 -30.0034 15.5 1266 | vertex 5.93321 -30.5617 15.5 1267 | endloop 1268 | endfacet 1269 | facet normal 0.377419 0.926043 0 1270 | outer loop 1271 | vertex 4.56339 -30.0034 15.5 1272 | vertex 5.93321 -30.5617 1.5 1273 | vertex 4.56339 -30.0034 1.5 1274 | endloop 1275 | endfacet 1276 | facet normal 0.460065 0.887885 -0 1277 | outer loop 1278 | vertex 4.56339 -30.0034 1.5 1279 | vertex 3.25 -29.3229 15.5 1280 | vertex 4.56339 -30.0034 15.5 1281 | endloop 1282 | endfacet 1283 | facet normal 0.460065 0.887885 0 1284 | outer loop 1285 | vertex 3.25 -29.3229 15.5 1286 | vertex 4.56339 -30.0034 1.5 1287 | vertex 3.25 -29.3229 1.5 1288 | endloop 1289 | endfacet 1290 | facet normal 0.5389 0.84237 -0 1291 | outer loop 1292 | vertex 3.25 -29.3229 1.5 1293 | vertex 2.00395 -28.5258 15.5 1294 | vertex 3.25 -29.3229 15.5 1295 | endloop 1296 | endfacet 1297 | facet normal 0.5389 0.84237 0 1298 | outer loop 1299 | vertex 2.00395 -28.5258 15.5 1300 | vertex 3.25 -29.3229 1.5 1301 | vertex 2.00395 -28.5258 1.5 1302 | endloop 1303 | endfacet 1304 | facet normal 0.613272 0.789872 -0 1305 | outer loop 1306 | vertex 2.00395 -28.5258 1.5 1307 | vertex 0.835556 -27.6186 15.5 1308 | vertex 2.00395 -28.5258 15.5 1309 | endloop 1310 | endfacet 1311 | facet normal 0.613272 0.789872 0 1312 | outer loop 1313 | vertex 0.835556 -27.6186 15.5 1314 | vertex 2.00395 -28.5258 1.5 1315 | vertex 0.835556 -27.6186 1.5 1316 | endloop 1317 | endfacet 1318 | facet normal 0.682546 0.730843 -0 1319 | outer loop 1320 | vertex 0.835556 -27.6186 1.5 1321 | vertex -0.245529 -26.6089 15.5 1322 | vertex 0.835556 -27.6186 15.5 1323 | endloop 1324 | endfacet 1325 | facet normal 0.682546 0.730843 0 1326 | outer loop 1327 | vertex -0.245529 -26.6089 15.5 1328 | vertex 0.835556 -27.6186 1.5 1329 | vertex -0.245529 -26.6089 1.5 1330 | endloop 1331 | endfacet 1332 | facet normal 0.746187 0.665737 0 1333 | outer loop 1334 | vertex -0.245529 -26.6089 15.5 1335 | vertex -1.2303 -25.5052 1.5 1336 | vertex -1.2303 -25.5052 15.5 1337 | endloop 1338 | endfacet 1339 | facet normal 0.746187 0.665737 0 1340 | outer loop 1341 | vertex -1.2303 -25.5052 1.5 1342 | vertex -0.245529 -26.6089 15.5 1343 | vertex -0.245529 -26.6089 1.5 1344 | endloop 1345 | endfacet 1346 | facet normal 0.803632 0.595127 0 1347 | outer loop 1348 | vertex -1.2303 -25.5052 15.5 1349 | vertex -2.11063 -24.3164 1.5 1350 | vertex -2.11063 -24.3164 15.5 1351 | endloop 1352 | endfacet 1353 | facet normal 0.803632 0.595127 0 1354 | outer loop 1355 | vertex -2.11063 -24.3164 1.5 1356 | vertex -1.2303 -25.5052 15.5 1357 | vertex -1.2303 -25.5052 1.5 1358 | endloop 1359 | endfacet 1360 | facet normal 0.85442 0.519584 0 1361 | outer loop 1362 | vertex -2.11063 -24.3164 15.5 1363 | vertex -2.87921 -23.0525 1.5 1364 | vertex -2.87921 -23.0525 15.5 1365 | endloop 1366 | endfacet 1367 | facet normal 0.85442 0.519584 0 1368 | outer loop 1369 | vertex -2.87921 -23.0525 1.5 1370 | vertex -2.11063 -24.3164 15.5 1371 | vertex -2.11063 -24.3164 1.5 1372 | endloop 1373 | endfacet 1374 | facet normal 0.898126 0.439738 0 1375 | outer loop 1376 | vertex -2.87921 -23.0525 15.5 1377 | vertex -3.52968 -21.724 1.5 1378 | vertex -3.52968 -21.724 15.5 1379 | endloop 1380 | endfacet 1381 | facet normal 0.898126 0.439738 0 1382 | outer loop 1383 | vertex -3.52968 -21.724 1.5 1384 | vertex -2.87921 -23.0525 15.5 1385 | vertex -2.87921 -23.0525 1.5 1386 | endloop 1387 | endfacet 1388 | facet normal 0.934394 0.35624 0 1389 | outer loop 1390 | vertex -3.52968 -21.724 15.5 1391 | vertex -4.05664 -20.3418 1.5 1392 | vertex -4.05664 -20.3418 15.5 1393 | endloop 1394 | endfacet 1395 | facet normal 0.934394 0.35624 0 1396 | outer loop 1397 | vertex -4.05664 -20.3418 1.5 1398 | vertex -3.52968 -21.724 15.5 1399 | vertex -3.52968 -21.724 1.5 1400 | endloop 1401 | endfacet 1402 | facet normal 0.962916 0.269801 0 1403 | outer loop 1404 | vertex -4.05664 -20.3418 15.5 1405 | vertex -4.45573 -18.9175 1.5 1406 | vertex -4.45573 -18.9175 15.5 1407 | endloop 1408 | endfacet 1409 | facet normal 0.962916 0.269801 0 1410 | outer loop 1411 | vertex -4.45573 -18.9175 1.5 1412 | vertex -4.05664 -20.3418 15.5 1413 | vertex -4.05664 -20.3418 1.5 1414 | endloop 1415 | endfacet 1416 | facet normal 0.983463 0.181108 0 1417 | outer loop 1418 | vertex -4.45573 -18.9175 15.5 1419 | vertex -4.72363 -17.4627 1.5 1420 | vertex -4.72363 -17.4627 15.5 1421 | endloop 1422 | endfacet 1423 | facet normal 0.983463 0.181108 0 1424 | outer loop 1425 | vertex -4.72363 -17.4627 1.5 1426 | vertex -4.45573 -18.9175 15.5 1427 | vertex -4.45573 -18.9175 1.5 1428 | endloop 1429 | endfacet 1430 | facet normal 0.995856 0.0909403 0 1431 | outer loop 1432 | vertex -4.72363 -17.4627 15.5 1433 | vertex -4.85815 -15.9896 1.5 1434 | vertex -4.85815 -15.9896 15.5 1435 | endloop 1436 | endfacet 1437 | facet normal 0.995856 0.0909403 0 1438 | outer loop 1439 | vertex -4.85815 -15.9896 1.5 1440 | vertex -4.72363 -17.4627 15.5 1441 | vertex -4.72363 -17.4627 1.5 1442 | endloop 1443 | endfacet 1444 | facet normal 1 -0 0 1445 | outer loop 1446 | vertex -4.85815 -15.9896 15.5 1447 | vertex -4.85815 -15.25 1.5 1448 | vertex -4.85815 -15.25 15.5 1449 | endloop 1450 | endfacet 1451 | facet normal 1 0 0 1452 | outer loop 1453 | vertex -4.85815 -15.25 1.5 1454 | vertex -4.85815 -15.9896 15.5 1455 | vertex -4.85815 -15.9896 1.5 1456 | endloop 1457 | endfacet 1458 | facet normal 0 1 -0 1459 | outer loop 1460 | vertex -4.85815 -15.25 1.5 1461 | vertex -4.875 -15.25 15.5 1462 | vertex -4.85815 -15.25 15.5 1463 | endloop 1464 | endfacet 1465 | facet normal 0 1 0 1466 | outer loop 1467 | vertex -4.875 -15.25 15.5 1468 | vertex -4.85815 -15.25 1.5 1469 | vertex -4.875 -15.25 1.5 1470 | endloop 1471 | endfacet 1472 | facet normal 1 -0 0 1473 | outer loop 1474 | vertex -4.875 -15.25 15.5 1475 | vertex -4.875 5 1.5 1476 | vertex -4.875 5 15.5 1477 | endloop 1478 | endfacet 1479 | facet normal 1 0 0 1480 | outer loop 1481 | vertex -4.875 5 1.5 1482 | vertex -4.875 -15.25 15.5 1483 | vertex -4.875 -15.25 1.5 1484 | endloop 1485 | endfacet 1486 | facet normal 0.996194 -0.0871581 0 1487 | outer loop 1488 | vertex 14.5499 -33.3456 15.51 1489 | vertex 14.6807 -31.8513 1.49 1490 | vertex 14.6807 -31.8513 15.51 1491 | endloop 1492 | endfacet 1493 | facet normal 0.996194 -0.0871581 0 1494 | outer loop 1495 | vertex 14.6807 -31.8513 1.49 1496 | vertex 14.5499 -33.3456 15.51 1497 | vertex 14.5499 -33.3456 1.49 1498 | endloop 1499 | endfacet 1500 | facet normal -1 0 0 1501 | outer loop 1502 | vertex 4.875 -23.25 1.5 1503 | vertex 4.875 5 15.5 1504 | vertex 4.875 5 1.5 1505 | endloop 1506 | endfacet 1507 | facet normal -1 -0 0 1508 | outer loop 1509 | vertex 4.875 5 15.5 1510 | vertex 4.875 -23.25 1.5 1511 | vertex 4.875 -23.25 15.5 1512 | endloop 1513 | endfacet 1514 | facet normal -0.993713 -0.111962 0 1515 | outer loop 1516 | vertex 5.03796 -24.6964 1.5 1517 | vertex 4.875 -23.25 15.5 1518 | vertex 4.875 -23.25 1.5 1519 | endloop 1520 | endfacet 1521 | facet normal -0.993713 -0.111962 0 1522 | outer loop 1523 | vertex 4.875 -23.25 15.5 1524 | vertex 5.03796 -24.6964 1.5 1525 | vertex 5.03796 -24.6964 15.5 1526 | endloop 1527 | endfacet 1528 | facet normal -0.943884 -0.330276 0 1529 | outer loop 1530 | vertex 5.51869 -26.0702 1.5 1531 | vertex 5.03796 -24.6964 15.5 1532 | vertex 5.03796 -24.6964 1.5 1533 | endloop 1534 | endfacet 1535 | facet normal -0.943884 -0.330276 0 1536 | outer loop 1537 | vertex 5.03796 -24.6964 15.5 1538 | vertex 5.51869 -26.0702 1.5 1539 | vertex 5.51869 -26.0702 15.5 1540 | endloop 1541 | endfacet 1542 | facet normal -0.846722 -0.532036 0 1543 | outer loop 1544 | vertex 6.29309 -27.3027 1.5 1545 | vertex 5.51869 -26.0702 15.5 1546 | vertex 5.51869 -26.0702 1.5 1547 | endloop 1548 | endfacet 1549 | facet normal -0.846722 -0.532036 0 1550 | outer loop 1551 | vertex 5.51869 -26.0702 15.5 1552 | vertex 6.29309 -27.3027 1.5 1553 | vertex 6.29309 -27.3027 15.5 1554 | endloop 1555 | endfacet 1556 | facet normal -0.707107 -0.707107 0 1557 | outer loop 1558 | vertex 7.32231 -28.3319 1.5 1559 | vertex 6.29309 -27.3027 15.5 1560 | vertex 6.29309 -27.3027 1.5 1561 | endloop 1562 | endfacet 1563 | facet normal -0.707107 -0.707107 0 1564 | outer loop 1565 | vertex 6.29309 -27.3027 15.5 1566 | vertex 7.32231 -28.3319 1.5 1567 | vertex 7.32231 -28.3319 15.5 1568 | endloop 1569 | endfacet 1570 | facet normal -0.532036 -0.846722 0 1571 | outer loop 1572 | vertex 7.32231 -28.3319 1.5 1573 | vertex 8.55475 -29.1063 15.5 1574 | vertex 7.32231 -28.3319 15.5 1575 | endloop 1576 | endfacet 1577 | facet normal -0.532036 -0.846722 -0 1578 | outer loop 1579 | vertex 8.55475 -29.1063 15.5 1580 | vertex 7.32231 -28.3319 1.5 1581 | vertex 8.55475 -29.1063 1.5 1582 | endloop 1583 | endfacet 1584 | facet normal -0.330276 -0.943884 0 1585 | outer loop 1586 | vertex 8.55475 -29.1063 1.5 1587 | vertex 9.9286 -29.587 15.5 1588 | vertex 8.55475 -29.1063 15.5 1589 | endloop 1590 | endfacet 1591 | facet normal -0.330276 -0.943884 -0 1592 | outer loop 1593 | vertex 9.9286 -29.587 15.5 1594 | vertex 8.55475 -29.1063 1.5 1595 | vertex 9.9286 -29.587 1.5 1596 | endloop 1597 | endfacet 1598 | facet normal -0.111936 -0.993715 0 1599 | outer loop 1600 | vertex 9.9286 -29.587 1.5 1601 | vertex 10.1188 -29.6084 15.5 1602 | vertex 9.9286 -29.587 15.5 1603 | endloop 1604 | endfacet 1605 | facet normal -0.111936 -0.993715 -0 1606 | outer loop 1607 | vertex 10.1188 -29.6084 15.5 1608 | vertex 9.9286 -29.587 1.5 1609 | vertex 10.1188 -29.6084 1.5 1610 | endloop 1611 | endfacet 1612 | facet normal -0.996198 0.0871233 0 1613 | outer loop 1614 | vertex 10.1112 -29.6948 1.5 1615 | vertex 10.1188 -29.6084 15.5 1616 | vertex 10.1188 -29.6084 1.5 1617 | endloop 1618 | endfacet 1619 | facet normal -0.996198 0.0871233 0 1620 | outer loop 1621 | vertex 10.1188 -29.6084 15.5 1622 | vertex 10.1112 -29.6948 1.5 1623 | vertex 10.1112 -29.6948 15.5 1624 | endloop 1625 | endfacet 1626 | facet normal -0.0871555 -0.996195 0 1627 | outer loop 1628 | vertex 10.1112 -29.6948 1.5 1629 | vertex 14.8332 -30.1079 15.5 1630 | vertex 10.1112 -29.6948 15.5 1631 | endloop 1632 | endfacet 1633 | facet normal -0.0871555 -0.996195 -0 1634 | outer loop 1635 | vertex 14.8332 -30.1079 15.5 1636 | vertex 10.1112 -29.6948 1.5 1637 | vertex 14.8332 -30.1079 1.5 1638 | endloop 1639 | endfacet 1640 | facet normal 0.996194 -0.087159 0 1641 | outer loop 1642 | vertex 14.8332 -30.1079 15.5 1643 | vertex 14.9639 -28.6136 1.5 1644 | vertex 14.9639 -28.6136 15.5 1645 | endloop 1646 | endfacet 1647 | facet normal 0.996194 -0.087159 0 1648 | outer loop 1649 | vertex 14.9639 -28.6136 1.5 1650 | vertex 14.8332 -30.1079 15.5 1651 | vertex 14.8332 -30.1079 1.5 1652 | endloop 1653 | endfacet 1654 | facet normal 0 0 -1 1655 | outer loop 1656 | vertex 14.6807 -31.8513 15.5 1657 | vertex 14.8332 -30.1079 15.5 1658 | vertex 14.9639 -28.6136 15.5 1659 | endloop 1660 | endfacet 1661 | facet normal 0 0 -1 1662 | outer loop 1663 | vertex 14.6807 -31.8513 15.5 1664 | vertex 10.1112 -29.6948 15.5 1665 | vertex 14.8332 -30.1079 15.5 1666 | endloop 1667 | endfacet 1668 | facet normal 0 0 -1 1669 | outer loop 1670 | vertex 14.6807 -31.8513 15.5 1671 | vertex 9.94974 -31.4259 15.5 1672 | vertex 10.1112 -29.6948 15.5 1673 | endloop 1674 | endfacet 1675 | facet normal 0 -0 -1 1676 | outer loop 1677 | vertex 9.94974 -31.4259 15.5 1678 | vertex 14.6807 -31.8513 15.5 1679 | vertex 9.94875 -31.4373 15.5 1680 | endloop 1681 | endfacet 1682 | facet normal 0 0 -1 1683 | outer loop 1684 | vertex 9.9286 -29.587 15.5 1685 | vertex 10.1112 -29.6948 15.5 1686 | vertex 9.94974 -31.4259 15.5 1687 | endloop 1688 | endfacet 1689 | facet normal 0 0 -1 1690 | outer loop 1691 | vertex 10.1112 -29.6948 15.5 1692 | vertex 9.9286 -29.587 15.5 1693 | vertex 10.1188 -29.6084 15.5 1694 | endloop 1695 | endfacet 1696 | facet normal 0 0 -1 1697 | outer loop 1698 | vertex 8.79642 -31.2941 15.5 1699 | vertex 9.9286 -29.587 15.5 1700 | vertex 9.94974 -31.4259 15.5 1701 | endloop 1702 | endfacet 1703 | facet normal 0 0 -1 1704 | outer loop 1705 | vertex 8.79642 -31.2941 15.5 1706 | vertex 8.55475 -29.1063 15.5 1707 | vertex 9.9286 -29.587 15.5 1708 | endloop 1709 | endfacet 1710 | facet normal 0 0 -1 1711 | outer loop 1712 | vertex 7.34813 -30.9931 15.5 1713 | vertex 8.55475 -29.1063 15.5 1714 | vertex 8.79642 -31.2941 15.5 1715 | endloop 1716 | endfacet 1717 | facet normal 0 0 -1 1718 | outer loop 1719 | vertex 7.34813 -30.9931 15.5 1720 | vertex 7.32231 -28.3319 15.5 1721 | vertex 8.55475 -29.1063 15.5 1722 | endloop 1723 | endfacet 1724 | facet normal 0 0 -1 1725 | outer loop 1726 | vertex 5.93321 -30.5617 15.5 1727 | vertex 7.32231 -28.3319 15.5 1728 | vertex 7.34813 -30.9931 15.5 1729 | endloop 1730 | endfacet 1731 | facet normal 0 0 -1 1732 | outer loop 1733 | vertex 4.56339 -30.0034 15.5 1734 | vertex 7.32231 -28.3319 15.5 1735 | vertex 5.93321 -30.5617 15.5 1736 | endloop 1737 | endfacet 1738 | facet normal -0 0 -1 1739 | outer loop 1740 | vertex 7.32231 -28.3319 15.5 1741 | vertex 4.56339 -30.0034 15.5 1742 | vertex 6.29309 -27.3027 15.5 1743 | endloop 1744 | endfacet 1745 | facet normal 0 0 -1 1746 | outer loop 1747 | vertex 3.25 -29.3229 15.5 1748 | vertex 6.29309 -27.3027 15.5 1749 | vertex 4.56339 -30.0034 15.5 1750 | endloop 1751 | endfacet 1752 | facet normal -0 0 -1 1753 | outer loop 1754 | vertex 6.29309 -27.3027 15.5 1755 | vertex 3.25 -29.3229 15.5 1756 | vertex 5.51869 -26.0702 15.5 1757 | endloop 1758 | endfacet 1759 | facet normal 0 0 -1 1760 | outer loop 1761 | vertex 2.00395 -28.5258 15.5 1762 | vertex 5.51869 -26.0702 15.5 1763 | vertex 3.25 -29.3229 15.5 1764 | endloop 1765 | endfacet 1766 | facet normal -0 0 -1 1767 | outer loop 1768 | vertex 5.51869 -26.0702 15.5 1769 | vertex 2.00395 -28.5258 15.5 1770 | vertex 5.03796 -24.6964 15.5 1771 | endloop 1772 | endfacet 1773 | facet normal 0 0 -1 1774 | outer loop 1775 | vertex 0.835556 -27.6186 15.5 1776 | vertex 5.03796 -24.6964 15.5 1777 | vertex 2.00395 -28.5258 15.5 1778 | endloop 1779 | endfacet 1780 | facet normal 0 0 -1 1781 | outer loop 1782 | vertex -0.245529 -26.6089 15.5 1783 | vertex 5.03796 -24.6964 15.5 1784 | vertex 0.835556 -27.6186 15.5 1785 | endloop 1786 | endfacet 1787 | facet normal -0 0 -1 1788 | outer loop 1789 | vertex 5.03796 -24.6964 15.5 1790 | vertex -0.245529 -26.6089 15.5 1791 | vertex 4.875 -23.25 15.5 1792 | endloop 1793 | endfacet 1794 | facet normal 0 0 -1 1795 | outer loop 1796 | vertex -1.2303 -25.5052 15.5 1797 | vertex 4.875 -23.25 15.5 1798 | vertex -0.245529 -26.6089 15.5 1799 | endloop 1800 | endfacet 1801 | facet normal 0 0 -1 1802 | outer loop 1803 | vertex -2.11063 -24.3164 15.5 1804 | vertex 4.875 -23.25 15.5 1805 | vertex -1.2303 -25.5052 15.5 1806 | endloop 1807 | endfacet 1808 | facet normal 0 0 -1 1809 | outer loop 1810 | vertex -2.87921 -23.0525 15.5 1811 | vertex 4.875 -23.25 15.5 1812 | vertex -2.11063 -24.3164 15.5 1813 | endloop 1814 | endfacet 1815 | facet normal 0 0 -1 1816 | outer loop 1817 | vertex -3.52968 -21.724 15.5 1818 | vertex 4.875 -23.25 15.5 1819 | vertex -2.87921 -23.0525 15.5 1820 | endloop 1821 | endfacet 1822 | facet normal 0 0 -1 1823 | outer loop 1824 | vertex -4.05664 -20.3418 15.5 1825 | vertex 4.875 -23.25 15.5 1826 | vertex -3.52968 -21.724 15.5 1827 | endloop 1828 | endfacet 1829 | facet normal 0 0 -1 1830 | outer loop 1831 | vertex -4.45573 -18.9175 15.5 1832 | vertex 4.875 -23.25 15.5 1833 | vertex -4.05664 -20.3418 15.5 1834 | endloop 1835 | endfacet 1836 | facet normal 0 0 -1 1837 | outer loop 1838 | vertex -4.72363 -17.4627 15.5 1839 | vertex 4.875 -23.25 15.5 1840 | vertex -4.45573 -18.9175 15.5 1841 | endloop 1842 | endfacet 1843 | facet normal 0 0 -1 1844 | outer loop 1845 | vertex -4.85815 -15.9896 15.5 1846 | vertex 4.875 -23.25 15.5 1847 | vertex -4.72363 -17.4627 15.5 1848 | endloop 1849 | endfacet 1850 | facet normal 0 0 -1 1851 | outer loop 1852 | vertex 4.875 -23.25 15.5 1853 | vertex -4.85815 -15.9896 15.5 1854 | vertex -4.85815 -15.25 15.5 1855 | endloop 1856 | endfacet 1857 | facet normal 0 0 -1 1858 | outer loop 1859 | vertex 4.875 -23.25 15.5 1860 | vertex -4.85815 -15.25 15.5 1861 | vertex 4.875 5 15.5 1862 | endloop 1863 | endfacet 1864 | facet normal 0 0 -1 1865 | outer loop 1866 | vertex -4.875 5 15.5 1867 | vertex -4.85815 -15.25 15.5 1868 | vertex -4.875 -15.25 15.5 1869 | endloop 1870 | endfacet 1871 | facet normal 0 0 -1 1872 | outer loop 1873 | vertex -4.85815 -15.25 15.5 1874 | vertex -4.875 5 15.5 1875 | vertex 4.875 5 15.5 1876 | endloop 1877 | endfacet 1878 | facet normal 0 0 1 1879 | outer loop 1880 | vertex 6.43083 -15.25 17 1881 | vertex 6.375 -15.25 17 1882 | vertex 6.43083 -23.9952 17 1883 | endloop 1884 | endfacet 1885 | facet normal 0 0 1 1886 | outer loop 1887 | vertex 8.87498 -27.5801 17 1888 | vertex 10.242 -28.2005 17 1889 | vertex 10.2491 -28.1194 17 1890 | endloop 1891 | endfacet 1892 | facet normal 0 0 1 1893 | outer loop 1894 | vertex 10.242 -28.2005 17 1895 | vertex 9.51962 -32.9028 17 1896 | vertex 9.81888 -32.9216 17 1897 | endloop 1898 | endfacet 1899 | facet normal 0 0 1 1900 | outer loop 1901 | vertex 10.242 -28.2005 17 1902 | vertex 8.87498 -27.5801 17 1903 | vertex 8.049 -32.6856 17 1904 | endloop 1905 | endfacet 1906 | facet normal 0 0 1 1907 | outer loop 1908 | vertex 10.242 -28.2005 17 1909 | vertex 8.049 -32.6856 17 1910 | vertex 9.51962 -32.9028 17 1911 | endloop 1912 | endfacet 1913 | facet normal 0 0 1 1914 | outer loop 1915 | vertex 5.18785 -31.8867 17 1916 | vertex 8.87498 -27.5801 17 1917 | vertex 7.70973 -26.6509 17 1918 | endloop 1919 | endfacet 1920 | facet normal 0 0 1 1921 | outer loop 1922 | vertex 2.50002 -30.6219 17 1923 | vertex 7.70973 -26.6509 17 1924 | vertex 6.87015 -25.4194 17 1925 | endloop 1926 | endfacet 1927 | facet normal 0 0 1 1928 | outer loop 1929 | vertex 8.87498 -27.5801 17 1930 | vertex 6.60168 -32.3461 17 1931 | vertex 8.049 -32.6856 17 1932 | endloop 1933 | endfacet 1934 | facet normal 0 0 1 1935 | outer loop 1936 | vertex 0.06073 -28.9266 17 1937 | vertex 6.87015 -25.4194 17 1938 | vertex 6.43083 -23.9952 17 1939 | endloop 1940 | endfacet 1941 | facet normal 0 0 1 1942 | outer loop 1943 | vertex 8.87498 -27.5801 17 1944 | vertex 5.18785 -31.8867 17 1945 | vertex 6.60168 -32.3461 17 1946 | endloop 1947 | endfacet 1948 | facet normal -0 0 1 1949 | outer loop 1950 | vertex -5.12852 -21.7842 17 1951 | vertex 6.43083 -23.9952 17 1952 | vertex 6.375 -15.25 17 1953 | endloop 1954 | endfacet 1955 | facet normal 0 0 1 1956 | outer loop 1957 | vertex 10.242 -28.2005 17 1958 | vertex 14.5499 -33.3456 17 1959 | vertex 14.9639 -28.6136 17 1960 | endloop 1961 | endfacet 1962 | facet normal -0 0 1 1963 | outer loop 1964 | vertex 9.81888 -32.9216 17 1965 | vertex 14.5499 -33.3456 17 1966 | vertex 10.242 -28.2005 17 1967 | endloop 1968 | endfacet 1969 | facet normal 0 0 1 1970 | outer loop 1971 | vertex 14.5499 -33.3456 17 1972 | vertex 9.81888 -32.9216 17 1973 | vertex 9.81801 -32.9316 17 1974 | endloop 1975 | endfacet 1976 | facet normal -0 0 1 1977 | outer loop 1978 | vertex -7.375 0 17 1979 | vertex -6.375 -4 17 1980 | vertex -6.375 0 17 1981 | endloop 1982 | endfacet 1983 | facet normal 0 0 1 1984 | outer loop 1985 | vertex -6.375 -4 17 1986 | vertex -7.375 0 17 1987 | vertex -7.375 -4 17 1988 | endloop 1989 | endfacet 1990 | facet normal 0 0 1 1991 | outer loop 1992 | vertex -6.375 0 17 1993 | vertex 6.375 0 17 1994 | vertex 6.375 5 17 1995 | endloop 1996 | endfacet 1997 | facet normal 0 0 1 1998 | outer loop 1999 | vertex 6.375 0 17 2000 | vertex -6.375 0 17 2001 | vertex 6.375 -4 17 2002 | endloop 2003 | endfacet 2004 | facet normal 0 0 1 2005 | outer loop 2006 | vertex 6.375 -4 17 2007 | vertex -6.375 -4 17 2008 | vertex 6.375 -15.25 17 2009 | endloop 2010 | endfacet 2011 | facet normal 0 0 1 2012 | outer loop 2013 | vertex -6.35942 -15.9933 17 2014 | vertex 6.375 -15.25 17 2015 | vertex -6.35942 -15.25 17 2016 | endloop 2017 | endfacet 2018 | facet normal 0 0 1 2019 | outer loop 2020 | vertex 7.70973 -26.6509 17 2021 | vertex 3.81743 -31.3107 17 2022 | vertex 5.18785 -31.8867 17 2023 | endloop 2024 | endfacet 2025 | facet normal 0 0 1 2026 | outer loop 2027 | vertex 7.70973 -26.6509 17 2028 | vertex 2.50002 -30.6219 17 2029 | vertex 3.81743 -31.3107 17 2030 | endloop 2031 | endfacet 2032 | facet normal 0 0 1 2033 | outer loop 2034 | vertex 6.87015 -25.4194 17 2035 | vertex 1.24484 -29.8254 17 2036 | vertex 2.50002 -30.6219 17 2037 | endloop 2038 | endfacet 2039 | facet normal 0 0 1 2040 | outer loop 2041 | vertex 6.87015 -25.4194 17 2042 | vertex 0.06073 -28.9266 17 2043 | vertex 1.24484 -29.8254 17 2044 | endloop 2045 | endfacet 2046 | facet normal 0 0 1 2047 | outer loop 2048 | vertex 6.43083 -23.9952 17 2049 | vertex -1.04402 -27.9319 17 2050 | vertex 0.06073 -28.9266 17 2051 | endloop 2052 | endfacet 2053 | facet normal 0 0 1 2054 | outer loop 2055 | vertex 6.43083 -23.9952 17 2056 | vertex -2.06166 -26.8482 17 2057 | vertex -1.04402 -27.9319 17 2058 | endloop 2059 | endfacet 2060 | facet normal 0 0 1 2061 | outer loop 2062 | vertex 6.43083 -23.9952 17 2063 | vertex -2.98505 -25.6832 17 2064 | vertex -2.06166 -26.8482 17 2065 | endloop 2066 | endfacet 2067 | facet normal 0 0 1 2068 | outer loop 2069 | vertex 6.43083 -23.9952 17 2070 | vertex -3.80771 -24.445 17 2071 | vertex -2.98505 -25.6832 17 2072 | endloop 2073 | endfacet 2074 | facet normal 0 0 1 2075 | outer loop 2076 | vertex 6.43083 -23.9952 17 2077 | vertex -4.52388 -23.1423 17 2078 | vertex -3.80771 -24.445 17 2079 | endloop 2080 | endfacet 2081 | facet normal 0 0 1 2082 | outer loop 2083 | vertex 6.43083 -23.9952 17 2084 | vertex -5.12852 -21.7842 17 2085 | vertex -4.52388 -23.1423 17 2086 | endloop 2087 | endfacet 2088 | facet normal 0 0 1 2089 | outer loop 2090 | vertex 6.375 -15.25 17 2091 | vertex -5.61742 -20.3803 17 2092 | vertex -5.12852 -21.7842 17 2093 | endloop 2094 | endfacet 2095 | facet normal 0 0 1 2096 | outer loop 2097 | vertex 6.375 -15.25 17 2098 | vertex -5.98711 -18.9404 17 2099 | vertex -5.61742 -20.3803 17 2100 | endloop 2101 | endfacet 2102 | facet normal 0 0 1 2103 | outer loop 2104 | vertex 6.375 -15.25 17 2105 | vertex -6.23503 -17.4747 17 2106 | vertex -5.98711 -18.9404 17 2107 | endloop 2108 | endfacet 2109 | facet normal 0 0 1 2110 | outer loop 2111 | vertex -6.375 0 17 2112 | vertex 6.375 5 17 2113 | vertex -6.375 5 17 2114 | endloop 2115 | endfacet 2116 | facet normal 0 0 1 2117 | outer loop 2118 | vertex 6.375 -4 17 2119 | vertex -6.375 0 17 2120 | vertex -6.375 -4 17 2121 | endloop 2122 | endfacet 2123 | facet normal 0 0 1 2124 | outer loop 2125 | vertex 6.375 -15.25 17 2126 | vertex -6.375 -4 17 2127 | vertex -6.35942 -15.25 17 2128 | endloop 2129 | endfacet 2130 | facet normal 0 0 1 2131 | outer loop 2132 | vertex -6.35942 -15.25 17 2133 | vertex -6.375 -4 17 2134 | vertex -6.375 -15.25 17 2135 | endloop 2136 | endfacet 2137 | facet normal -0 0 1 2138 | outer loop 2139 | vertex 6.375 0 17 2140 | vertex 7.375 -4 17 2141 | vertex 7.375 0 17 2142 | endloop 2143 | endfacet 2144 | facet normal 0 0 1 2145 | outer loop 2146 | vertex 7.375 -4 17 2147 | vertex 6.375 0 17 2148 | vertex 6.375 -4 17 2149 | endloop 2150 | endfacet 2151 | facet normal 0 0 1 2152 | outer loop 2153 | vertex 6.375 -15.25 17 2154 | vertex -6.35942 -15.9933 17 2155 | vertex -6.23503 -17.4747 17 2156 | endloop 2157 | endfacet 2158 | facet normal 0.996195 -0.0871552 0 2159 | outer loop 2160 | vertex 14.9639 -28.6136 17 2161 | vertex 14.6807 -31.8513 15.51 2162 | vertex 14.9639 -28.6136 15.5 2163 | endloop 2164 | endfacet 2165 | facet normal 0.996195 -0.0871551 -1.54344e-07 2166 | outer loop 2167 | vertex 14.5499 -33.3456 17 2168 | vertex 14.6807 -31.8513 15.51 2169 | vertex 14.9639 -28.6136 17 2170 | endloop 2171 | endfacet 2172 | facet normal 0.996195 -0.087155 0 2173 | outer loop 2174 | vertex 14.6807 -31.8513 15.51 2175 | vertex 14.5499 -33.3456 17 2176 | vertex 14.5499 -33.3456 15.51 2177 | endloop 2178 | endfacet 2179 | facet normal 0.996195 -0.0871552 0 2180 | outer loop 2181 | vertex 14.9639 -28.6136 15.5 2182 | vertex 14.6807 -31.8513 15.51 2183 | vertex 14.6807 -31.8513 15.5 2184 | endloop 2185 | endfacet 2186 | endsolid OpenSCAD_Model 2187 | -------------------------------------------------------------------------------- /test_models/Ball_Bearing.A.3.STL: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/revarbat/mandoline-py/cc7209ea5670a634432d5d46125999068b46ca48/test_models/Ball_Bearing.A.3.STL -------------------------------------------------------------------------------- /test_models/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 | --------------------------------------------------------------------------------