mirror of
				https://github.com/revarbat/snappy-reprap.git
				synced 2025-10-31 07:36:44 -07:00 
			
		
		
		
	
		
			
				
	
	
		
			510 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			510 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
| #!/usr/bin/env python
 | |
| 
 | |
| import os
 | |
| import os.path
 | |
| import sys
 | |
| import math
 | |
| import time
 | |
| import struct
 | |
| import argparse
 | |
| import platform
 | |
| import itertools
 | |
| import subprocess
 | |
| 
 | |
| 
 | |
| guiscad_template = """\
 | |
| module showlines(clr, lines) {{
 | |
|     for (line = lines) {{
 | |
|         delta = line[1]-line[0];
 | |
|         dist = norm(delta);
 | |
|         theta = atan2(delta[1],delta[0]);
 | |
|         phi = atan2(delta[2],norm([delta[0],delta[1]]));
 | |
|         translate(line[0]) {{
 | |
|             rotate([0, 90-phi, theta]) {{
 | |
|                 color(clr) cylinder(d=0.5, h=dist);
 | |
|             }}
 | |
|         }}
 | |
|     }}
 | |
| }}
 | |
| module showfaces(clr, faces) {{
 | |
|     color(clr) {{
 | |
|         for (face = faces) {{
 | |
|             polyhedron(points=face, faces=[[0, 1, 2], [0, 2, 1]], convexity=2);
 | |
|         }}
 | |
|     }}
 | |
| }}
 | |
| showlines([1.0, 0.0, 1.0], [
 | |
| {dupe_edges}
 | |
| ]);
 | |
| showlines([1.0, 0.0, 0.0], [
 | |
| {hole_edges}
 | |
| ]);
 | |
| showfaces([1.0, 0.0, 1.0], [
 | |
| {dupe_faces}
 | |
| ]);
 | |
| color([0.0, 1.0, 0.0, 0.2]) import("{filename}", convexity=100);
 | |
| 
 | |
| """
 | |
| 
 | |
| 
 | |
| def dot(a, b):
 | |
|     return sum(p*q for p, q in zip(a, b))
 | |
| 
 | |
| 
 | |
| def cross(a, b):
 | |
|     return [
 | |
|         a[1]*b[2] - a[2]*b[1],
 | |
|         a[2]*b[0] - a[0]*b[2],
 | |
|         a[0]*b[1] - a[1]*b[0]
 | |
|     ]
 | |
| 
 | |
| 
 | |
| def vsub(a, b):
 | |
|     return [i - j for i, j in zip(a, b)]
 | |
| 
 | |
| 
 | |
| def vsdiv(v, s):
 | |
|     return [x / s for x in v]
 | |
| 
 | |
| 
 | |
| def dist(v):
 | |
|     return math.sqrt(sum([x*x for x in v]))
 | |
| 
 | |
| 
 | |
| def normalize(v):
 | |
|     return vsdiv(v, dist(v))
 | |
| 
 | |
| 
 | |
| def is_clockwise(a, b, c, n):
 | |
|     return dot(n, cross(vsub(b, a), vsub(c, a))) < 0
 | |
| 
 | |
| 
 | |
| def point_cmp(p1, p2):
 | |
|     for i in [2, 1, 0]:
 | |
|         val = cmp(p1[i], p2[i])
 | |
|         if val != 0:
 | |
|             return val
 | |
|     return 0
 | |
| 
 | |
| 
 | |
| def facet_cmp(f1, f2):
 | |
|     cl1 = [sorted([p[i] for p in f1]) for i in range(3)]
 | |
|     cl2 = [sorted([p[i] for p in f2]) for i in range(3)]
 | |
|     for i in [2, 1, 0]:
 | |
|         for c1, c2 in itertools.izip_longest(cl1[i], cl2[i]):
 | |
|             if c1 is None:
 | |
|                 return -1
 | |
|             val = cmp(c1, c2)
 | |
|             if val != 0:
 | |
|                 return val
 | |
|     return 0
 | |
| 
 | |
| 
 | |
| def float_fmt(val):
 | |
|     s = "%.3f" % val
 | |
|     while len(s) > 1 and s[-1:] in '0.':
 | |
|         if s[-1:] == '.':
 | |
|             s = s[:-1]
 | |
|             break
 | |
|         s = s[:-1]
 | |
|     if (s == '-0'):
 | |
|         s = '0'
 | |
|     return s
 | |
| 
 | |
| 
 | |
| def vertex_fmt(vals):
 | |
|     return " ".join([float_fmt(v) for v in vals])
 | |
| 
 | |
| 
 | |
| def vertex_fmt2(vals):
 | |
|     return "[" + (", ".join([float_fmt(v) for v in vals])) + "]"
 | |
| 
 | |
| 
 | |
| class PointCloud(object):
 | |
|     points = []
 | |
|     pointhash = {}
 | |
| 
 | |
|     def __init__(self):
 | |
|         self.points = []
 | |
|         self.pointhash = {}
 | |
|         self.minx = 9e9
 | |
|         self.miny = 9e9
 | |
|         self.minz = 9e9
 | |
|         self.maxx = -9e9
 | |
|         self.maxy = -9e9
 | |
|         self.maxz = -9e9
 | |
| 
 | |
|     def update_volume(self, x, y, z):
 | |
|         if x < self.minx:
 | |
|             self.minx = x
 | |
|         if x > self.maxx:
 | |
|             self.maxx = x
 | |
|         if y < self.miny:
 | |
|             self.miny = y
 | |
|         if y > self.maxy:
 | |
|             self.maxy = y
 | |
|         if z < self.minz:
 | |
|             self.minz = z
 | |
|         if z > self.maxz:
 | |
|             self.maxz = z
 | |
| 
 | |
|     def add_or_get_point(self, x, y, z):
 | |
|         pt = (
 | |
|             round(x, 4),
 | |
|             round(y, 4),
 | |
|             round(z, 4),
 | |
|         )
 | |
|         key = "%.3f %.3f %.3f" % pt
 | |
|         if key in self.pointhash:
 | |
|             return self.pointhash[key]
 | |
|         idx = len(self.points)
 | |
|         self.pointhash[key] = idx
 | |
|         self.points.append(pt)
 | |
|         self.update_volume(x, y, z)
 | |
|         return idx
 | |
| 
 | |
|     def point_coords(self, idx):
 | |
|         return self.points[idx]
 | |
| 
 | |
|     def facet_coords(self, facet):
 | |
|         return (
 | |
|             self.point_coords(facet[0]),
 | |
|             self.point_coords(facet[1]),
 | |
|             self.point_coords(facet[2]),
 | |
|         )
 | |
| 
 | |
| 
 | |
| class StlEndOfFileException(Exception):
 | |
|     pass
 | |
| 
 | |
| 
 | |
| class StlMalformedLineException(Exception):
 | |
|     pass
 | |
| 
 | |
| 
 | |
| class StlData(object):
 | |
|     def __init__(self):
 | |
|         self.points = PointCloud()
 | |
|         self.facets = []
 | |
|         self.edgehash = {}
 | |
|         self.facehash = {}
 | |
|         self.filename = ""
 | |
| 
 | |
|     def _mark_edge(self, vertex1, vertex2):
 | |
|         edge = [vertex1, vertex2]
 | |
|         edge.sort()
 | |
|         edge = tuple(edge)
 | |
|         if edge not in self.edgehash:
 | |
|             self.edgehash[edge] = 0
 | |
|         self.edgehash[edge] += 1
 | |
|         return self.edgehash[edge]
 | |
| 
 | |
|     def _mark_face(self, vertex1, vertex2, vertex3):
 | |
|         self._mark_edge(vertex1, vertex2)
 | |
|         self._mark_edge(vertex2, vertex3)
 | |
|         self._mark_edge(vertex3, vertex1)
 | |
|         face = [vertex1, vertex2, vertex3]
 | |
|         face.sort()
 | |
|         face = tuple(face)
 | |
|         if face not in self.facehash:
 | |
|             self.facehash[face] = 0
 | |
|         self.facehash[face] += 1
 | |
|         return self.facehash[face]
 | |
| 
 | |
|     def _read_ascii_line(self, f, watchwords=None):
 | |
|         line = f.readline(1024)
 | |
|         if line == "":
 | |
|             raise StlEndOfFileException()
 | |
|         words = line.strip(' \t\n\r').lower().split()
 | |
|         if words[0] == 'endsolid':
 | |
|             raise StlEndOfFileException()
 | |
|         argstart = 0
 | |
|         if watchwords:
 | |
|             watchwords = watchwords.lower().split()
 | |
|             argstart = len(watchwords)
 | |
|             for i in xrange(argstart):
 | |
|                 if words[i] != watchwords[i]:
 | |
|                     raise StlMalformedLineException()
 | |
|         return [float(val) for val in words[argstart:]]
 | |
| 
 | |
|     def _read_ascii_vertex(self, f):
 | |
|         point = self._read_ascii_line(f, watchwords='vertex')
 | |
|         return self.points.add_or_get_point(*point)
 | |
| 
 | |
|     def _read_ascii_facet(self, f):
 | |
|         while True:
 | |
|             try:
 | |
|                 normal = self._read_ascii_line(f, watchwords='facet normal')
 | |
|                 self._read_ascii_line(f, watchwords='outer loop')
 | |
|                 vertex1 = self._read_ascii_vertex(f)
 | |
|                 vertex2 = self._read_ascii_vertex(f)
 | |
|                 vertex3 = self._read_ascii_vertex(f)
 | |
|                 self._read_ascii_line(f, watchwords='endloop')
 | |
|                 self._read_ascii_line(f, watchwords='endfacet')
 | |
|                 if vertex1 == vertex2:
 | |
|                     continue  # zero area facet.  Skip to next facet.
 | |
|                 if vertex2 == vertex3:
 | |
|                     continue  # zero area facet.  Skip to next facet.
 | |
|                 if vertex3 == vertex1:
 | |
|                     continue  # zero area facet.  Skip to next facet.
 | |
| 
 | |
|             except StlEndOfFileException:
 | |
|                 return None
 | |
| 
 | |
|             except StlMalformedLineException:
 | |
|                 continue  # Skip to next facet.
 | |
| 
 | |
|             return (vertex1, vertex2, vertex3, normal)
 | |
| 
 | |
|     def _read_binary_facet(self, f):
 | |
|         data = struct.unpack('<3f 3f 3f 3f H', f.read(4*4*3+2))
 | |
|         normal = data[0:3]
 | |
|         vertex1 = data[3:6]
 | |
|         vertex2 = data[6:9]
 | |
|         vertex3 = data[9:12]
 | |
|         v1 = self.points.add_or_get_point(*vertex1)
 | |
|         v2 = self.points.add_or_get_point(*vertex2)
 | |
|         v3 = self.points.add_or_get_point(*vertex3)
 | |
|         return (v1, v2, v3, normal)
 | |
| 
 | |
|     def sort_facet(self, facet):
 | |
|         v1, v2, v3, norm = facet
 | |
|         p1 = self.points.point_coords(v1)
 | |
|         p2 = self.points.point_coords(v2)
 | |
|         p3 = self.points.point_coords(v3)
 | |
|         if dist(norm) > 0:
 | |
|             # Make sure vertex ordering is counter-clockwise,
 | |
|             # relative to the outward facing normal.
 | |
|             if is_clockwise(p1, p2, p3, norm):
 | |
|                 v1, v3, v2 = (v1, v2, v3)
 | |
|                 p1, p3, p2 = (p1, p2, p3)
 | |
|         else:
 | |
|             # If no normal was specified, we should calculate it, relative
 | |
|             # to the counter-clockwise vertices (as seen from outside).
 | |
|             norm = cross(vsub(p3, p1), vsub(p2, p1))
 | |
|             if dist(norm) > 1e-6:
 | |
|                 norm = normalize(norm)
 | |
|         cmp23 = point_cmp(p2, p3)
 | |
|         if point_cmp(p1, p2) > 0 and cmp23 < 0:
 | |
|             return (v2, v3, v1, norm)
 | |
|         if point_cmp(p1, p3) > 0 and cmp23 > 0:
 | |
|             return (v3, v1, v2, norm)
 | |
|         return (v1, v2, v3, norm)
 | |
| 
 | |
|     def read_file(self, filename):
 | |
|         self.filename = filename
 | |
|         with open(filename, 'rb') as f:
 | |
|             line = f.readline(80)
 | |
|             if line == "":
 | |
|                 return  # End of file.
 | |
|             if line[0:6].lower() == "solid ":
 | |
|                 while True:
 | |
|                     facet = self._read_ascii_facet(f)
 | |
|                     if facet is None:
 | |
|                         break
 | |
|                     facet = self.sort_facet(facet)
 | |
|                     vertex1, vertex2, vertex3, normal = facet
 | |
|                     self.facets.append(facet)
 | |
|                     self._mark_face(vertex1, vertex2, vertex3)
 | |
|             else:
 | |
|                 chunk = f.read(4)
 | |
|                 facets = struct.unpack('<I', chunk)[0]
 | |
|                 while facets > 0:
 | |
|                     facets -= 1
 | |
|                     facet = self._read_binary_facet(f)
 | |
|                     if facet is None:
 | |
|                         break
 | |
|                     facet = self.sort_facet(facet)
 | |
|                     vertex1, vertex2, vertex3, normal = facet
 | |
|                     self.facets.append(facet)
 | |
|                     self._mark_face(vertex1, vertex2, vertex3)
 | |
| 
 | |
|     def write_file(self, filename, binary=False):
 | |
|         if binary:
 | |
|             self._write_binary_file(filename)
 | |
|         else:
 | |
|             self._write_ascii_file(filename)
 | |
| 
 | |
|     def _write_ascii_file(self, filename):
 | |
|         with open(filename, 'wb') as f:
 | |
|             f.write("solid Model\n")
 | |
|             for facet in self.facets:
 | |
|                 v1, v2, v3, norm = facet
 | |
|                 v1 = self.points.point_coords(v1)
 | |
|                 v2 = self.points.point_coords(v2)
 | |
|                 v3 = self.points.point_coords(v3)
 | |
|                 f.write("  facet normal %s\n" % vertex_fmt(norm))
 | |
|                 f.write("    outer loop\n")
 | |
|                 f.write("      vertex %s\n" % vertex_fmt(v1))
 | |
|                 f.write("      vertex %s\n" % vertex_fmt(v2))
 | |
|                 f.write("      vertex %s\n" % vertex_fmt(v3))
 | |
|                 f.write("    endloop\n")
 | |
|                 f.write("  endfacet\n")
 | |
|             f.write("endsolid Model\n")
 | |
| 
 | |
|     def _write_binary_file(self, filename):
 | |
|         with open(filename, 'wb') as f:
 | |
|             f.write('%-80s' % 'Binary STL Model')
 | |
|             f.write(struct.pack('<I', len(self.facets)))
 | |
|             for facet in self.facets:
 | |
|                 v1, v2, v3, norm = facet
 | |
|                 v1 = self.points.point_coords(v1)
 | |
|                 v2 = self.points.point_coords(v2)
 | |
|                 v3 = self.points.point_coords(v3)
 | |
|                 f.write(struct.pack('<3f', *norm))
 | |
|                 f.write(struct.pack('<3f', *v1))
 | |
|                 f.write(struct.pack('<3f', *v2))
 | |
|                 f.write(struct.pack('<3f', *v3))
 | |
|                 f.write(struct.pack('<H', 0))
 | |
| 
 | |
|     def _gui_display_manifold(self, hole_edges, dupe_edges, dupe_faces):
 | |
|         global guiscad_template
 | |
|         modulename = os.path.basename(self.filename)
 | |
|         if modulename.endswith('.stl'):
 | |
|             modulename = modulename[:-4]
 | |
|         tmpfile = "mani-{0}.scad".format(modulename)
 | |
|         with open(tmpfile, 'w') as f:
 | |
|             f.write(guiscad_template.format(
 | |
|                 hole_edges=hole_edges,
 | |
|                 dupe_edges=dupe_edges,
 | |
|                 dupe_faces=dupe_faces,
 | |
|                 modulename=modulename,
 | |
|                 filename=self.filename,
 | |
|             ))
 | |
|         if platform.system() == 'Darwin':
 | |
|             subprocess.call(['open', tmpfile])
 | |
|             time.sleep(5)
 | |
|         else:
 | |
|             subprocess.call(['openscad', tmpfile])
 | |
|             time.sleep(5)
 | |
|         os.remove(tmpfile)
 | |
| 
 | |
|     def _check_manifold_duplicate_faces(self):
 | |
|         found = []
 | |
|         for face, count in self.facehash.iteritems():
 | |
|             if count != 1:
 | |
|                 v1 = vertex_fmt2(self.points.point_coords(face[0]))
 | |
|                 v2 = vertex_fmt2(self.points.point_coords(face[1]))
 | |
|                 v3 = vertex_fmt2(self.points.point_coords(face[2]))
 | |
|                 found.append((v1, v2, v3))
 | |
|         return found
 | |
| 
 | |
|     def _check_manifold_hole_edges(self):
 | |
|         found = []
 | |
|         for edge, count in self.edgehash.iteritems():
 | |
|             if count == 1:
 | |
|                 v1 = vertex_fmt2(self.points.point_coords(edge[0]))
 | |
|                 v2 = vertex_fmt2(self.points.point_coords(edge[1]))
 | |
|                 found.append((v1, v2))
 | |
|         return found
 | |
| 
 | |
|     def _check_manifold_excess_edges(self):
 | |
|         found = []
 | |
|         for edge, count in self.edgehash.iteritems():
 | |
|             if count > 2:
 | |
|                 v1 = vertex_fmt2(self.points.point_coords(edge[0]))
 | |
|                 v2 = vertex_fmt2(self.points.point_coords(edge[1]))
 | |
|                 found.append((v1, v2))
 | |
|         return found
 | |
| 
 | |
|     def check_manifold(self, verbose=False, gui=False):
 | |
|         is_manifold = True
 | |
| 
 | |
|         faces = self._check_manifold_duplicate_faces()
 | |
|         for v1, v2, v3 in faces:
 | |
|             is_manifold = False
 | |
|             print("NON-MANIFOLD DUPLICATE FACE! {3}: {0} - {1} - {2}"
 | |
|                   .format(v1, v2, v3, self.filename))
 | |
|         if gui:
 | |
|             dupe_faces = ",\n".join(
 | |
|                 ["  [{0}, {1}, {2}]".format(*coords) for coords in faces]
 | |
|             )
 | |
| 
 | |
|         edges = self._check_manifold_hole_edges()
 | |
|         for v1, v2 in edges:
 | |
|             is_manifold = False
 | |
|             print("NON-MANIFOLD HOLE EDGE! {2}: {0} - {1}"
 | |
|                   .format(v1, v2, self.filename))
 | |
|         if gui:
 | |
|             hole_edges = ",\n".join(
 | |
|                 ["  [{0}, {1}]".format(*coords) for coords in edges]
 | |
|             )
 | |
| 
 | |
|         edges = self._check_manifold_excess_edges()
 | |
|         for v1, v2 in edges:
 | |
|             is_manifold = False
 | |
|             print("NON-MANIFOLD DUPLICATE EDGE! {2}: {0} - {1}"
 | |
|                   .format(v1, v2, self.filename))
 | |
|         if gui:
 | |
|             dupe_edges = ",\n".join(
 | |
|                 ["  [{0}, {1}]".format(*coords) for coords in edges]
 | |
|             )
 | |
| 
 | |
|         if is_manifold:
 | |
|             if gui or verbose:
 | |
|                 print("%s is manifold." % self.filename)
 | |
|         elif gui:
 | |
|                 self._gui_display_manifold(hole_edges, dupe_edges, dupe_faces)
 | |
|         return is_manifold
 | |
| 
 | |
|     def sort_facets(self):
 | |
|         self.facets = sorted(
 | |
|             self.facets,
 | |
|             cmp=lambda x, y: facet_cmp(
 | |
|                 self.points.facet_coords(x),
 | |
|                 self.points.facet_coords(y)
 | |
|             )
 | |
|         )
 | |
| 
 | |
| 
 | |
| def main():
 | |
|     parser = argparse.ArgumentParser(prog='stl_normalize')
 | |
|     parser.add_argument('-v', '--verbose',
 | |
|                         help='Show verbose output.',
 | |
|                         action="store_true")
 | |
|     parser.add_argument('-c', '--check-manifold',
 | |
|                         help='Perform manifold validation of model.',
 | |
|                         action="store_true")
 | |
|     parser.add_argument('-g', '--gui-display',
 | |
|                         help='Show non-manifold edges in GUI.',
 | |
|                         action="store_true")
 | |
|     parser.add_argument('-b', '--write-binary',
 | |
|                         help='Use binary STL format for output.',
 | |
|                         action="store_true")
 | |
|     parser.add_argument('-o', '--outfile',
 | |
|                         help='Write normalized STL to file.')
 | |
|     parser.add_argument('infile', help='Input STL filename.')
 | |
|     args = parser.parse_args()
 | |
| 
 | |
|     stl = StlData()
 | |
|     stl.read_file(args.infile)
 | |
|     if args.verbose:
 | |
|         print("Read {0} ({1:.1f} x {2:.1f} x {3:.1f})".format(
 | |
|             args.infile,
 | |
|             (stl.points.maxx-stl.points.minx),
 | |
|             (stl.points.maxy-stl.points.miny),
 | |
|             (stl.points.maxz-stl.points.minz),
 | |
|         ))
 | |
| 
 | |
|     if args.check_manifold or args.gui_display:
 | |
|         if not stl.check_manifold(verbose=args.verbose, gui=args.gui_display):
 | |
|             sys.exit(-1)
 | |
| 
 | |
|     if args.outfile:
 | |
|         stl.sort_facets()
 | |
|         stl.write_file(args.outfile, binary=args.write_binary)
 | |
|         if args.verbose:
 | |
|             print("Wrote {0} ({1})".format(
 | |
|                 args.outfile,
 | |
|                 ("binary" if args.write_binary else "ASCII"),
 | |
|             ))
 | |
| 
 | |
|     sys.exit(0)
 | |
| 
 | |
| 
 | |
| if __name__ == "__main__":
 | |
|     main()
 | |
| 
 | |
| 
 | |
| # vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap
 |