-
Notifications
You must be signed in to change notification settings - Fork 0
/
stl-split
executable file
·207 lines (164 loc) · 7.19 KB
/
stl-split
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
#!/usr/bin/env python3
"""
Split an stl file.
The idea is to help post-processing stl files made with mapelia, so they
can be printed more easily. It does not modify the original file, but
creates two new files that end with "_N.stl" and "_S.stl"
(or "_head.stl" and "_tail.stl" if using the option --number).
"""
import sys
import os
import struct
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter as fmt
from maps import check_if_exists
red = lambda txt: '\x1b[31m%s\x1b[0m' % txt
def main():
args = get_args()
if not os.path.isfile(args.file):
sys.exit('File %s does not exist.' % args.file)
if not valid_stl(args.file):
if args.ignore_check:
print('Going ahead anyway as you used --ignore-check ...')
else:
sys.exit('Cancelling (use --ignore-check to force processing it).')
zcut = get_zcut(args.zcut, args.file)
find_class = class_selector(args.number, zcut)
triangles_per_class = extract_triangles(args.file, find_class, zcut,
args.discard_border)
name = args.name or args.file.rsplit('.', 1)[0]
for tclass, triangles in triangles_per_class.items():
output = '%s_%s.stl' % (name, tclass)
if not args.overwrite:
check_if_exists(output)
write_stl(output, triangles)
def get_args():
"Return the parsed command line arguments"
parser = ArgumentParser(description=__doc__, formatter_class=fmt)
add = parser.add_argument # shortcut
add('file', help='stl file')
add('-n', '--name', default='',
help='output file (if empty, it is generated from the image file name)')
add('--zcut', default='0', help='z value of the cutting xy-plane (or auto)')
add('--discard-border', action='store_true',
help='put triangles not cleanly cut in a "_discarded.stl" file')
add('--number', type=int, default=0,
help='split by leaving a given number of triangles in the first file')
add('--overwrite', action='store_true',
help='do not check if the output files already exist')
add('--ignore-check', action='store_true',
help='go ahead even if the input file does not look like an stl')
return parser.parse_args()
def valid_stl(fname):
"Return True if file looks like an stl, warn and return False otherwise"
if not fname.endswith('.stl'): # soft warning
print(red('File %s does not end in ".stl". Is it really an stl file?' %
fname))
ntriangles = struct.unpack('<I', open(fname, 'rb').read(84)[-4:])[0]
size_content = os.path.getsize(fname) - 84 # 84 = header + number
size_triangle = (1 * 3 + 3 * 3) * 4 + 2 # 4 bytes per float
# 1 normal vector, 3 vertices (with 3 components each), 2 dummy bytes
if size_content / size_triangle == ntriangles:
return True
else:
print(red('File %s does not look like an stl file:\n'
' It declares to have %d triangles but has capacity for %g.'
% (fname, ntriangles, size_content / size_triangle)))
return False
def get_zcut(zcut, fname):
"Return the z value where the hemispheres will be cut"
try:
return float(zcut)
except ValueError:
if zcut == 'auto':
return z_mean(fname)
else:
sys.exit('zcut can be "auto" or a float.')
def z_mean(fname):
"Return the mean z of all the triangles in the stl file fname"
z, n = 0, 0
for triangle in get_triangles(fname):
_, _, v1_z, _, _, v2_z, _, _, v3_z = unpack(triangle)
z = (n * z + v1_z + v2_z + v3_z) / (n + 3)
n += 3
return z
def get_triangles(fname):
"Yield raw-data triangles in file fname"
with open(fname, 'rb') as fin:
fin.read(84) # discard header + number of triangles
size_triangle = 12 * 4 + 2 # 12 floats and 2 dummy bytes
yield from iter(lambda: fin.read(size_triangle), b'')
def unpack(triangle):
"Return x, y, z from the 3 points that define the raw-data triangle"
return struct.unpack('<9f', triangle[12:48])
def class_selector(number, zcut):
"Return function that, given a number and a triangle, returns its class"
if number == 0:
def find_class(n, triangle):
_, _, v1_z, _, _, v2_z, _, _, v3_z = unpack(triangle)
if min(v1_z, v2_z, v3_z) >= zcut:
return 'N'
elif max(v1_z, v2_z, v3_z) <= zcut:
return 'S'
else:
return 'discarded'
else:
def find_class(n, triangle):
return 'head' if n < number else 'tail'
return find_class
def extract_triangles(fname, find_class, zcut=0, separate_discarded=False):
"Return dict with the triangles belonging to each class of stl file fname"
print('Processing file %s ...' % fname)
triangles_per_class = {}
for i, triangle in enumerate(get_triangles(fname)):
tclass = find_class(i, triangle) # N, S, head, tail...
if tclass != 'discarded' or separate_discarded:
triangles_per_class.setdefault(tclass, []).append(triangle)
else: # convert discarded triangle into a tiling
for pclass, piece in cut(triangle, zcut):
triangles_per_class.setdefault(pclass, []).append(piece)
return triangles_per_class
def cut(triangle, zcut):
"Yield pairs of (class, piece) that form a tiling of the given triangle"
points = []
v1_x, v1_y, v1_z, v2_x, v2_y, v2_z, v3_x, v3_y, v3_z = unpack(triangle)
hemisphere_last = None
p_last = None
for p in [(v1_x, v1_y, v1_z),
(v2_x, v2_y, v2_z),
(v3_x, v3_y, v3_z),
(v1_x, v1_y, v1_z)]:
hemisphere = 'N' if p[2] > zcut else 'S'
if hemisphere_last and hemisphere != hemisphere_last:
points.append((pq_at_zcut(p_last, p, zcut), 'x'))
if (p, hemisphere) not in points:
points.append((p, hemisphere))
hemisphere_last, p_last = hemisphere, p
for i in range(5): # we end up with the 3 points plus 2 at z=zcut
pclass = points[i][1]
if pclass != 'x':
il, ir, irr = (i - 1) % 5, (i + 1) % 5, (i + 2) % 5
i2 = ir if points[ir][1] == 'x' else irr
yield pclass, pack(points[i][0], points[i2][0], points[il][0])
def pq_at_zcut(p, q, zcut):
"Return point between p and q that has z=zcut"
pq = (q[0] - p[0], q[1] - p[1], q[2] - p[2])
a = (zcut - p[2]) / pq[2]
return (p[0] + a * pq[0], p[1] + a * pq[1], zcut)
def pack(p0, p1, p2):
"Return triangle formed by the given points, packed in stl-style"
sp = struct.pack # shortcut
return (sp('<3f', 0, 0, 0) + # normal vector (empty)
sp('<3f', *p0) + # vertex 1
sp('<3f', *p1) + # vertex 2
sp('<3f', *p2) + # vertex 3
sp('<H', 0)) # attribute byte count (empty)
def write_stl(fname, triangles):
"Write binary-encoded list of triangles as an stl file"
print('Writing file %s ...' % fname)
with open(fname, 'wb') as fout:
fout.write(b'\0' * 80) # header (empty)
fout.write(struct.pack('<I', len(triangles))) # number of triangles
for triangle in triangles:
fout.write(triangle)
if __name__ == '__main__':
main()