Wed, 20 Jan 2021 11:37:03 +0100
reimplemented lasercutter changes
15 | 1 | # coding: utf-8 |
2 | ||
3 | # This file is part of the Printrun suite. | |
4 | # | |
5 | # Printrun is free software: you can redistribute it and/or modify | |
6 | # it under the terms of the GNU General Public License as published by | |
7 | # the Free Software Foundation, either version 3 of the License, or | |
8 | # (at your option) any later version. | |
9 | # | |
10 | # Printrun is distributed in the hope that it will be useful, | |
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
13 | # GNU General Public License for more details. | |
14 | # | |
15 | # You should have received a copy of the GNU General Public License | |
16 | # along with Printrun. If not, see <http://www.gnu.org/licenses/>. | |
17 | ||
18 | import sys | |
19 | import struct | |
20 | import math | |
21 | import logging | |
22 | ||
23 | import numpy | |
24 | import numpy.linalg | |
25 | ||
26 | def normalize(v): | |
27 | return v / numpy.linalg.norm(v) | |
28 | ||
29 | def genfacet(v): | |
30 | veca = v[1] - v[0] | |
31 | vecb = v[2] - v[1] | |
32 | vecx = numpy.cross(veca, vecb) | |
33 | vlen = numpy.linalg.norm(vecx) | |
34 | if vlen == 0: | |
35 | vlen = 1 | |
36 | normal = vecx / vlen | |
37 | return (normal, v) | |
38 | ||
39 | I = numpy.identity(4) | |
40 | ||
41 | def homogeneous(v, w = 1): | |
42 | return numpy.append(v, w) | |
43 | ||
44 | def applymatrix(facet, matrix = I): | |
46 | 45 | return genfacet([matrix.dot(homogeneous(x))[:3] for x in facet[1]]) |
15 | 46 | |
46 | 47 | def ray_triangle_intersection(ray_near, ray_dir, v123): |
15 | 48 | """ |
49 | Möller–Trumbore intersection algorithm in pure python | |
50 | Based on http://en.wikipedia.org/wiki/M%C3%B6ller%E2%80%93Trumbore_intersection_algorithm | |
51 | """ | |
46 | 52 | v1, v2, v3 = v123 |
15 | 53 | eps = 0.000001 |
54 | edge1 = v2 - v1 | |
55 | edge2 = v3 - v1 | |
56 | pvec = numpy.cross(ray_dir, edge2) | |
57 | det = edge1.dot(pvec) | |
58 | if abs(det) < eps: | |
59 | return False, None | |
60 | inv_det = 1. / det | |
61 | tvec = ray_near - v1 | |
62 | u = tvec.dot(pvec) * inv_det | |
63 | if u < 0. or u > 1.: | |
64 | return False, None | |
65 | qvec = numpy.cross(tvec, edge1) | |
66 | v = ray_dir.dot(qvec) * inv_det | |
67 | if v < 0. or u + v > 1.: | |
68 | return False, None | |
69 | ||
70 | t = edge2.dot(qvec) * inv_det | |
71 | if t < eps: | |
72 | return False, None | |
73 | ||
74 | return True, t | |
75 | ||
76 | def ray_rectangle_intersection(ray_near, ray_dir, p0, p1, p2, p3): | |
77 | match1, _ = ray_triangle_intersection(ray_near, ray_dir, (p0, p1, p2)) | |
78 | match2, _ = ray_triangle_intersection(ray_near, ray_dir, (p0, p2, p3)) | |
79 | return match1 or match2 | |
80 | ||
81 | def ray_box_intersection(ray_near, ray_dir, p0, p1): | |
82 | x0, y0, z0 = p0[:] | |
83 | x1, y1, z1 = p1[:] | |
84 | rectangles = [((x0, y0, z0), (x1, y0, z0), (x1, y1, z0), (x0, y1, z0)), | |
85 | ((x0, y0, z1), (x1, y0, z1), (x1, y1, z1), (x0, y1, z1)), | |
86 | ((x0, y0, z0), (x1, y0, z0), (x1, y0, z1), (x0, y0, z1)), | |
87 | ((x0, y1, z0), (x1, y1, z0), (x1, y1, z1), (x0, y1, z1)), | |
88 | ((x0, y0, z0), (x0, y1, z0), (x0, y1, z1), (x0, y0, z1)), | |
89 | ((x1, y0, z0), (x1, y1, z0), (x1, y1, z1), (x1, y0, z1)), | |
90 | ] | |
91 | rectangles = [(numpy.array(p) for p in rect) | |
92 | for rect in rectangles] | |
93 | for rect in rectangles: | |
94 | if ray_rectangle_intersection(ray_near, ray_dir, *rect): | |
95 | return True | |
96 | return False | |
97 | ||
98 | def emitstl(filename, facets = [], objname = "stltool_export", binary = True): | |
99 | if filename is None: | |
100 | return | |
101 | if binary: | |
102 | with open(filename, "wb") as f: | |
46 | 103 | buf = b"".join([b"\0"] * 80) |
15 | 104 | buf += struct.pack("<I", len(facets)) |
105 | facetformat = struct.Struct("<ffffffffffffH") | |
106 | for facet in facets: | |
107 | l = list(facet[0][:]) | |
108 | for vertex in facet[1]: | |
109 | l += list(vertex[:]) | |
110 | l.append(0) | |
111 | buf += facetformat.pack(*l) | |
112 | f.write(buf) | |
113 | else: | |
114 | with open(filename, "w") as f: | |
115 | f.write("solid " + objname + "\n") | |
116 | for i in facets: | |
117 | f.write(" facet normal " + " ".join(map(str, i[0])) + "\n outer loop\n") | |
118 | for j in i[1]: | |
119 | f.write(" vertex " + " ".join(map(str, j)) + "\n") | |
120 | f.write(" endloop" + "\n") | |
121 | f.write(" endfacet" + "\n") | |
122 | f.write("endsolid " + objname + "\n") | |
123 | ||
46 | 124 | class stl: |
15 | 125 | |
126 | _dims = None | |
127 | ||
128 | def _get_dims(self): | |
129 | if self._dims is None: | |
130 | minx = float("inf") | |
131 | miny = float("inf") | |
132 | minz = float("inf") | |
133 | maxx = float("-inf") | |
134 | maxy = float("-inf") | |
135 | maxz = float("-inf") | |
136 | for normal, facet in self.facets: | |
137 | for vert in facet: | |
138 | if vert[0] < minx: | |
139 | minx = vert[0] | |
140 | if vert[1] < miny: | |
141 | miny = vert[1] | |
142 | if vert[2] < minz: | |
143 | minz = vert[2] | |
144 | if vert[0] > maxx: | |
145 | maxx = vert[0] | |
146 | if vert[1] > maxy: | |
147 | maxy = vert[1] | |
148 | if vert[2] > maxz: | |
149 | maxz = vert[2] | |
150 | self._dims = [minx, maxx, miny, maxy, minz, maxz] | |
151 | return self._dims | |
152 | dims = property(_get_dims) | |
153 | ||
154 | def __init__(self, filename = None): | |
155 | self.facet = (numpy.zeros(3), (numpy.zeros(3), numpy.zeros(3), numpy.zeros(3))) | |
156 | self.facets = [] | |
157 | self.facetsminz = [] | |
158 | self.facetsmaxz = [] | |
159 | ||
160 | self.name = "" | |
161 | self.insolid = 0 | |
162 | self.infacet = 0 | |
163 | self.inloop = 0 | |
164 | self.facetloc = 0 | |
165 | if filename is None: | |
166 | return | |
46 | 167 | with open(filename,encoding="ascii",errors="ignore") as f: |
15 | 168 | data = f.read() |
169 | if "facet normal" in data[1:300] and "outer loop" in data[1:300]: | |
170 | lines = data.split("\n") | |
171 | for line in lines: | |
172 | if not self.parseline(line): | |
173 | return | |
174 | else: | |
175 | logging.warning("Not an ascii stl solid - attempting to parse as binary") | |
176 | f = open(filename, "rb") | |
177 | buf = f.read(84) | |
178 | while len(buf) < 84: | |
179 | newdata = f.read(84 - len(buf)) | |
180 | if not len(newdata): | |
181 | break | |
182 | buf += newdata | |
183 | facetcount = struct.unpack_from("<I", buf, 80) | |
184 | facetformat = struct.Struct("<ffffffffffffH") | |
46 | 185 | for i in range(facetcount[0]): |
15 | 186 | buf = f.read(50) |
187 | while len(buf) < 50: | |
188 | newdata = f.read(50 - len(buf)) | |
189 | if not len(newdata): | |
190 | break | |
191 | buf += newdata | |
192 | fd = list(facetformat.unpack(buf)) | |
193 | self.name = "binary soloid" | |
194 | facet = [fd[:3], [fd[3:6], fd[6:9], fd[9:12]]] | |
195 | self.facets.append(facet) | |
46 | 196 | self.facetsminz.append((min(x[2] for x in facet[1]), facet)) |
197 | self.facetsmaxz.append((max(x[2] for x in facet[1]), facet)) | |
15 | 198 | f.close() |
199 | return | |
200 | ||
201 | def intersect_box(self, ray_near, ray_far): | |
202 | ray_near = numpy.array(ray_near) | |
203 | ray_far = numpy.array(ray_far) | |
204 | ray_dir = normalize(ray_far - ray_near) | |
205 | x0, x1, y0, y1, z0, z1 = self.dims | |
206 | p0 = numpy.array([x0, y0, z0]) | |
207 | p1 = numpy.array([x1, y1, z1]) | |
208 | return ray_box_intersection(ray_near, ray_dir, p0, p1) | |
209 | ||
210 | def intersect(self, ray_near, ray_far): | |
211 | ray_near = numpy.array(ray_near) | |
212 | ray_far = numpy.array(ray_far) | |
213 | ray_dir = normalize(ray_far - ray_near) | |
214 | best_facet = None | |
215 | best_dist = float("inf") | |
216 | for facet_i, (normal, facet) in enumerate(self.facets): | |
217 | match, dist = ray_triangle_intersection(ray_near, ray_dir, facet) | |
218 | if match and dist < best_dist: | |
219 | best_facet = facet_i | |
220 | best_dist = dist | |
221 | return best_facet, best_dist | |
222 | ||
223 | def rebase(self, facet_i): | |
224 | normal, facet = self.facets[facet_i] | |
225 | u1 = facet[1] - facet[0] | |
226 | v2 = facet[2] - facet[0] | |
227 | n1 = u1.dot(u1) | |
228 | e1 = u1 / math.sqrt(n1) | |
229 | u2 = v2 - u1 * v2.dot(u1) / n1 | |
230 | e2 = u2 / numpy.linalg.norm(u2) | |
231 | e3 = numpy.cross(e1, e2) | |
232 | # Ensure Z direction if opposed to the normal | |
233 | if normal.dot(e3) > 0: | |
234 | e2 = - e2 | |
235 | e3 = - e3 | |
236 | matrix = [[e1[0], e2[0], e3[0], 0], | |
237 | [e1[1], e2[1], e3[1], 0], | |
238 | [e1[2], e2[2], e3[2], 0], | |
239 | [0, 0, 0, 1]] | |
240 | matrix = numpy.array(matrix) | |
241 | # Inverse change of basis matrix | |
242 | matrix = numpy.linalg.inv(matrix) | |
243 | # Set first vertex of facet as origin | |
244 | neworig = matrix.dot(homogeneous(facet[0])) | |
245 | matrix[:3, 3] = -neworig[:3] | |
246 | newmodel = self.transform(matrix) | |
247 | return newmodel | |
248 | ||
249 | def cut(self, axis, direction, dist): | |
250 | s = stl() | |
251 | s.facets = [] | |
252 | f = min if direction == 1 else max | |
253 | for _, facet in self.facets: | |
254 | minval = f([vertex[axis] for vertex in facet]) | |
255 | if direction * minval > direction * dist: | |
256 | continue | |
257 | vertices = [] | |
258 | for vertex in facet: | |
259 | vertex = numpy.copy(vertex) | |
260 | if direction * (vertex[axis] - dist) > 0: | |
261 | vertex[axis] = dist | |
262 | vertices.append(vertex) | |
263 | s.facets.append(genfacet(vertices)) | |
264 | s.insolid = 0 | |
265 | s.infacet = 0 | |
266 | s.inloop = 0 | |
267 | s.facetloc = 0 | |
268 | s.name = self.name | |
269 | for facet in s.facets: | |
46 | 270 | s.facetsminz += [(min(x[2] for x in facet[1]), facet)] |
271 | s.facetsmaxz += [(max(x[2] for x in facet[1]), facet)] | |
15 | 272 | return s |
273 | ||
274 | def translation_matrix(self, v): | |
275 | matrix = [[1, 0, 0, v[0]], | |
276 | [0, 1, 0, v[1]], | |
277 | [0, 0, 1, v[2]], | |
278 | [0, 0, 0, 1] | |
279 | ] | |
280 | return numpy.array(matrix) | |
281 | ||
282 | def translate(self, v = [0, 0, 0]): | |
283 | return self.transform(self.translation_matrix(v)) | |
284 | ||
285 | def rotation_matrix(self, v): | |
286 | z = v[2] | |
287 | matrix1 = [[math.cos(math.radians(z)), -math.sin(math.radians(z)), 0, 0], | |
288 | [math.sin(math.radians(z)), math.cos(math.radians(z)), 0, 0], | |
289 | [0, 0, 1, 0], | |
290 | [0, 0, 0, 1] | |
291 | ] | |
292 | matrix1 = numpy.array(matrix1) | |
293 | y = v[0] | |
294 | matrix2 = [[1, 0, 0, 0], | |
295 | [0, math.cos(math.radians(y)), -math.sin(math.radians(y)), 0], | |
296 | [0, math.sin(math.radians(y)), math.cos(math.radians(y)), 0], | |
297 | [0, 0, 0, 1] | |
298 | ] | |
299 | matrix2 = numpy.array(matrix2) | |
300 | x = v[1] | |
301 | matrix3 = [[math.cos(math.radians(x)), 0, -math.sin(math.radians(x)), 0], | |
302 | [0, 1, 0, 0], | |
303 | [math.sin(math.radians(x)), 0, math.cos(math.radians(x)), 0], | |
304 | [0, 0, 0, 1] | |
305 | ] | |
306 | matrix3 = numpy.array(matrix3) | |
307 | return matrix3.dot(matrix2.dot(matrix1)) | |
308 | ||
309 | def rotate(self, v = [0, 0, 0]): | |
310 | return self.transform(self.rotation_matrix(v)) | |
311 | ||
312 | def scale_matrix(self, v): | |
313 | matrix = [[v[0], 0, 0, 0], | |
314 | [0, v[1], 0, 0], | |
315 | [0, 0, v[2], 0], | |
316 | [0, 0, 0, 1] | |
317 | ] | |
318 | return numpy.array(matrix) | |
319 | ||
320 | def scale(self, v = [0, 0, 0]): | |
321 | return self.transform(self.scale_matrix(v)) | |
322 | ||
323 | def transform(self, m = I): | |
324 | s = stl() | |
325 | s.facets = [applymatrix(i, m) for i in self.facets] | |
326 | s.insolid = 0 | |
327 | s.infacet = 0 | |
328 | s.inloop = 0 | |
329 | s.facetloc = 0 | |
330 | s.name = self.name | |
331 | for facet in s.facets: | |
46 | 332 | s.facetsminz += [(min(x[2] for x in facet[1]), facet)] |
333 | s.facetsmaxz += [(max(x[2] for x in facet[1]), facet)] | |
15 | 334 | return s |
335 | ||
336 | def export(self, f = sys.stdout): | |
337 | f.write("solid " + self.name + "\n") | |
338 | for i in self.facets: | |
339 | f.write(" facet normal " + " ".join(map(str, i[0])) + "\n") | |
340 | f.write(" outer loop" + "\n") | |
341 | for j in i[1]: | |
342 | f.write(" vertex " + " ".join(map(str, j)) + "\n") | |
343 | f.write(" endloop" + "\n") | |
344 | f.write(" endfacet" + "\n") | |
345 | f.write("endsolid " + self.name + "\n") | |
346 | f.flush() | |
347 | ||
348 | def parseline(self, l): | |
349 | l = l.strip() | |
350 | if l.startswith("solid"): | |
351 | self.insolid = 1 | |
352 | self.name = l[6:] | |
353 | elif l.startswith("endsolid"): | |
354 | self.insolid = 0 | |
355 | return 0 | |
356 | elif l.startswith("facet normal"): | |
357 | l = l.replace(", ", ".") | |
358 | self.infacet = 1 | |
359 | self.facetloc = 0 | |
46 | 360 | normal = numpy.array([float(f) for f in l.split()[2:]]) |
15 | 361 | self.facet = (normal, (numpy.zeros(3), numpy.zeros(3), numpy.zeros(3))) |
362 | elif l.startswith("endfacet"): | |
363 | self.infacet = 0 | |
364 | self.facets.append(self.facet) | |
365 | facet = self.facet | |
46 | 366 | self.facetsminz += [(min(x[2] for x in facet[1]), facet)] |
367 | self.facetsmaxz += [(max(x[2] for x in facet[1]), facet)] | |
15 | 368 | elif l.startswith("vertex"): |
369 | l = l.replace(", ", ".") | |
46 | 370 | self.facet[1][self.facetloc][:] = numpy.array([float(f) for f in l.split()[1:]]) |
15 | 371 | self.facetloc += 1 |
372 | return 1 | |
373 | ||
374 | if __name__ == "__main__": | |
375 | s = stl("../../Downloads/frame-vertex-neo-foot-x4.stl") | |
46 | 376 | for i in range(11, 11): |
15 | 377 | working = s.facets[:] |
378 | for j in reversed(sorted(s.facetsminz)): | |
379 | if j[0] > i: | |
380 | working.remove(j[1]) | |
381 | else: | |
382 | break | |
383 | for j in (sorted(s.facetsmaxz)): | |
384 | if j[0] < i: | |
385 | working.remove(j[1]) | |
386 | else: | |
387 | break | |
388 | ||
46 | 389 | print(i, len(working)) |
15 | 390 | emitstl("../../Downloads/frame-vertex-neo-foot-x4-a.stl", s.facets, "emitted_object") |
391 | # stl("../prusamendel/stl/mendelplate.stl") |