def draw_triangle_model(self, models): def calc_normal(main, normals): suitable = (0, 0, 0, 'v') for normal, weight in normals: dot = pdot(main, normal) if dot > 0: suitable = padd(suitable, pmul(normal, weight * dot)) return pnormalized(suitable) if not models: return GL = self._GL removal_list = [] for index, model in enumerate(models): if not hasattr(model, "triangles"): continue vertices = {} for t in model.triangles(): for p in (t.p1, t.p2, t.p3): if p not in vertices: vertices[p] = [] vertices[p].append((pnormalized(t.normal), t.get_area())) GL.glBegin(GL.GL_TRIANGLES) for t in model.triangles(): # The triangle's points are in clockwise order, but GL expects # counter-clockwise sorting. for p in (t.p1, t.p3, t.p2): normal = calc_normal(pnormalized(t.normal), vertices[p]) GL.glNormal3f(normal[0], normal[1], normal[2]) GL.glVertex3f(p[0], p[1], p[2]) GL.glEnd() removal_list.append(index) # remove all models that we processed removal_list.reverse() for index in removal_list: models.pop(index)
def auto_adjust_distance(self): v = self.view # adjust the distance to get a view of the whole object low_high = list(zip(*self._get_low_high_dims())) if (None, None) in low_high: return max_dim = max([high - low for low, high in low_high]) distv = pnormalized((v["distance"][0], v["distance"][1], v["distance"][2])) # The multiplier "1.25" is based on experiments. 1.414 (sqrt(2)) should # be roughly sufficient for showing the diagonal of any model. distv = pmul(distv, (max_dim * 1.25) / number(math.sin(v["fovy"] / 2))) self.view["distance"] = distv # Adjust the "far" distance for the camera to make sure, that huge # models (e.g. x=1000) are still visible. self.view["zfar"] = 100 * max_dim
def intersect_cylinder_line(center, axis, radius, radiussq, direction, edge): d = edge.dir # take a plane throught the line and along the cylinder axis (1) n = pcross(d, axis) if pnorm(n) == 0: # no contact point, but should check here if cylinder *always* # intersects line... return (None, None, INFINITE) n = pnormalized(n) # the contact line between the cylinder and this plane (1) # is where the surface normal is perpendicular to the plane # so line := ccl + \lambda * axis if pdot(n, direction) < 0: ccl = psub(center, pmul(n, radius)) else: ccl = padd(center, pmul(n, radius)) # now extrude the contact line along the direction, this is a plane (2) n2 = pcross(direction, axis) if pnorm(n2) == 0: # no contact point, but should check here if cylinder *always* # intersects line... return (None, None, INFINITE) n2 = pnormalized(n2) plane1 = Plane(ccl, n2) # intersect this plane with the line, this gives us the contact point (cp, l) = plane1.intersect_point(d, edge.p1) if not cp: return (None, None, INFINITE) # now take a plane through the contact line and perpendicular to the # direction (3) plane2 = Plane(ccl, direction) # the intersection of this plane (3) with the line through the contact point # gives us the cutter contact point (ccp, l) = plane2.intersect_point(direction, cp) cp = padd(ccp, pmul(direction, -l)) return (ccp, cp, -l)
def intersect_cylinder_point(center, axis, radius, radiussq, direction, point): # take a plane along direction and axis n = pnormalized(pcross(direction, axis)) # distance of the point to this plane d = pdot(n, point) - pdot(n, center) if abs(d) > radius - epsilon: return (None, None, INFINITE) # ccl is on cylinder d2 = sqrt(radiussq - d * d) ccl = padd(padd(center, pmul(n, d)), pmul(direction, d2)) # take plane through ccl and axis plane = Plane(ccl, direction) # intersect point with plane (ccp, l) = plane.intersect_point(direction, point) return (ccp, point, -l)
def get_output_lines(self): date = datetime.date.today().isoformat() yield """solid "%s"; Produced by %s (v%s), %s""" \ % (self.name, self.created_by, VERSION, date) for triangle in self.model.triangles(): norm = pnormalized(triangle.normal) yield "facet normal %f %f %f" % (norm[0], norm[1], norm[2]) yield " outer loop" # Triangle vertices are stored in clockwise order - thus we need # to reverse the order (STL expects counter-clockwise orientation). for point in (triangle.p1, triangle.p3, triangle.p2): yield " vertex %f %f %f" % (point[0], point[1], point[2]) yield " endloop" yield "endfacet" yield "endsolid"
def get_output_lines(self): date = datetime.date.today().isoformat() yield ("""solid "%s"; Produced by %s (v%s), %s""" % (self.name, self.created_by, VERSION, date)) for triangle in self.model.triangles(): norm = pnormalized(triangle.normal) yield "facet normal %f %f %f" % (norm[0], norm[1], norm[2]) yield " outer loop" # Triangle vertices are stored in clockwise order - thus we need # to reverse the order (STL expects counter-clockwise orientation). for point in (triangle.p1, triangle.p3, triangle.p2): yield " vertex %f %f %f" % (point[0], point[1], point[2]) yield " endloop" yield "endfacet" yield "endsolid"
def intersect_circle_plane(center, radius, direction, triangle): # let n be the normal to the plane n = triangle.normal if pdot(n, direction) == 0: return (None, None, INFINITE) # project onto z=0 n2 = (n[0], n[1], 0) if pnorm(n2) == 0: (cp, d) = triangle.plane.intersect_point(direction, center) ccp = psub(cp, pmul(direction, d)) return (ccp, cp, d) n2 = pnormalized(n2) # the cutter contact point is on the circle, where the surface normal is n ccp = padd(center, pmul(n2, -radius)) # intersect the plane with a line through the contact point (cp, d) = triangle.plane.intersect_point(direction, ccp) return (ccp, cp, d)
def _add_cuboid_to_model(model, start, direction, height, width): up = pmul((0, 0, 1, 'v'), height) ortho_dir = pnormalized(pcross(direction, up)) start1 = padd(start, pmul(ortho_dir, -width / 2)) start2 = padd(start1, up) start3 = padd(start2, pmul(ortho_dir, width)) start4 = psub(start3, up) end1 = padd(start1, direction) end2 = padd(start2, direction) end3 = padd(start3, direction) end4 = padd(start4, direction) faces = ((start1, start2, start3, start4), (start1, end1, end2, start2), (start2, end2, end3, start3), (start3, end3, end4, start4), (start4, end4, end1, start1), (end4, end3, end2, end1)) for face in faces: t1, t2 = _get_triangles_for_face(face) model.append(t1) model.append(t2)
def draw_direction_cone(p1, p2, position=0.5, precision=12, size=0.1): distance = psub(p2, p1) length = pnorm(distance) direction = pnormalized(distance) if direction is None: # zero-length line return cone_length = length * size cone_radius = cone_length / 3.0 # move the cone to the middle of the line GL.glTranslatef((p1[0] + p2[0]) * position, (p1[1] + p2[1]) * position, (p1[2] + p2[2]) * position) # rotate the cone according to the line direction # The cross product is a good rotation axis. cross = pcross(direction, (0, 0, -1)) if pnorm(cross) != 0: # The line direction is not in line with the z axis. try: angle = math.asin(sqrt(direction[0]**2 + direction[1]**2)) except ValueError: # invalid angle - just ignore this cone return # convert from radians to degree angle = angle / math.pi * 180 if direction[2] < 0: angle = 180 - angle GL.glRotatef(angle, cross[0], cross[1], cross[2]) elif direction[2] == -1: # The line goes down the z axis - turn it around. GL.glRotatef(180, 1, 0, 0) else: # The line goes up the z axis - nothing to be done. pass # center the cone GL.glTranslatef(0, 0, -cone_length * position) # draw the cone GLUT.glutSolidCone(cone_radius, cone_length, precision, 1)
def draw_direction_cone_mesh(self, p1, p2, position=0.5, precision=12, size=0.1): distance = psub(p2, p1) length = pnorm(distance) direction = pnormalized(distance) if direction is None or length < 0.5: # zero-length line return [] cone_length = length * size cone_radius = cone_length / 3.0 bottom = padd(p1, pmul(psub(p2, p1), position - size / 2)) top = padd(p1, pmul(psub(p2, p1), position + size / 2)) # generate a a line perpendicular to this line, cross product is good at this cross = pcross(direction, (0, 0, -1)) conepoints = [] if pnorm(cross) != 0: # The line direction is not in line with the z axis. conep1 = padd(bottom, pmul(cross, cone_radius)) conepoints = [ self._rotate_point(conep1, bottom, direction, x) for x in numpy.linspace(0, 2 * math.pi, precision) ] else: # Z axis # just add cone radius to the x axis and rotate the point conep1 = (bottom[0] + cone_radius, bottom[1], bottom[2]) conepoints = [ self._rotate_point(conep1, p1, direction, x) for x in numpy.linspace(0, 2 * math.pi, precision) ] triangles = [(top, conepoints[idx], conepoints[idx + 1]) for idx in range(len(conepoints) - 1)] return triangles
def reset_cache(self): # we need to prevent the "normal" from growing norm = pnormalized(self.n) if norm: self.n = norm
def intersect_circle_line(center, axis, radius, radiussq, direction, edge): # make a plane by sliding the line along the direction (1) d = edge.dir if pdot(d, axis) == 0: if pdot(direction, axis) == 0: return (None, None, INFINITE) plane = Plane(center, axis) (p1, l) = plane.intersect_point(direction, edge.p1) (p2, l) = plane.intersect_point(direction, edge.p2) pc = Line(p1, p2).closest_point(center) d_sq = pnormsq(psub(pc, center)) if d_sq >= radiussq: return (None, None, INFINITE) a = sqrt(radiussq - d_sq) d1 = pdot(psub(p1, pc), d) d2 = pdot(psub(p2, pc), d) ccp = None cp = None if abs(d1) < a - epsilon: ccp = p1 cp = psub(p1, pmul(direction, l)) elif abs(d2) < a - epsilon: ccp = p2 cp = psub(p2, pmul(direction, l)) elif ((d1 < -a + epsilon) and (d2 > a - epsilon)) \ or ((d2 < -a + epsilon) and (d1 > a - epsilon)): ccp = pc cp = psub(pc, pmul(direction, l)) return (ccp, cp, -l) n = pcross(d, direction) if pnorm(n) == 0: # no contact point, but should check here if circle *always* intersects # line... return (None, None, INFINITE) n = pnormalized(n) # take a plane through the base plane = Plane(center, axis) # intersect base with line (lp, l) = plane.intersect_point(d, edge.p1) if not lp: return (None, None, INFINITE) # intersection of 2 planes: lp + \lambda v v = pcross(axis, n) if pnorm(v) == 0: return (None, None, INFINITE) v = pnormalized(v) # take plane through intersection line and parallel to axis n2 = pcross(v, axis) if pnorm(n2) == 0: return (None, None, INFINITE) n2 = pnormalized(n2) # distance from center to this plane dist = pdot(n2, center) - pdot(n2, lp) distsq = dist * dist if distsq > radiussq - epsilon: return (None, None, INFINITE) # must be on circle dist2 = sqrt(radiussq - distsq) if pdot(d, axis) < 0: dist2 = -dist2 ccp = psub(center, psub(pmul(n2, dist), pmul(v, dist2))) plane = Plane(edge.p1, pcross(pcross(d, direction), d)) (cp, l) = plane.intersect_point(direction, ccp) return (ccp, cp, l)
def get_bezier_lines(points_with_bulge, segments=32): # TODO: add a recursive algorithm for more than two points if len(points_with_bulge) != 2: return [] else: result_points = [] p1, bulge1 = points_with_bulge[0] p2, bulge2 = points_with_bulge[1] if not bulge1 and not bulge2: # straight line return [Line(p1, p2)] straight_dir = pnormalized(psub(p2, p1)) bulge1 = math.atan(bulge1) rot_matrix = Matrix.get_rotation_matrix_axis_angle((0, 0, 1), -2 * bulge1, use_radians=True) dir1_mat = Matrix.multiply_vector_matrix( (straight_dir[0], straight_dir[1], straight_dir[2]), rot_matrix) dir1 = (dir1_mat[0], dir1_mat[1], dir1_mat[2], 'v') if bulge2 is None: bulge2 = bulge1 else: bulge2 = math.atan(bulge2) rot_matrix = Matrix.get_rotation_matrix_axis_angle((0, 0, 1), 2 * bulge2, use_radians=True) dir2_mat = Matrix.multiply_vector_matrix( (straight_dir[0], straight_dir[1], straight_dir[2]), rot_matrix) dir2 = (dir2_mat[0], dir2_mat[1], dir2_mat[2], 'v') # interpretation of bulge1 and bulge2: # /// taken from http://paulbourke.net/dataformats/dxf/dxf10.html /// # The bulge is the tangent of 1/4 the included angle for an arc # segment, made negative if the arc goes clockwise from the start # point to the end point; a bulge of 0 indicates a straight segment, # and a bulge of 1 is a semicircle. alpha = 2 * (abs(bulge1) + abs(bulge2)) dist = pdist(p2, p1) # calculate the radius of the circumcircle - avoiding divide-by-zero if (abs(alpha) < epsilon) or (abs(math.pi - alpha) < epsilon): radius = dist / 2.0 else: # see http://en.wikipedia.org/wiki/Law_of_sines radius = abs(dist / math.sin(alpha / 2.0)) / 2.0 # The calculation of "factor" is based on random guessing - but it # seems to work well. factor = 4 * radius * math.tan(alpha / 4.0) dir1 = pmul(dir1, factor) dir2 = pmul(dir2, factor) for index in range(segments + 1): # t: 0..1 t = float(index) / segments # see: http://en.wikipedia.org/wiki/Cubic_Hermite_spline p = padd( pmul(p1, 2 * t**3 - 3 * t**2 + 1), padd( pmul(dir1, t**3 - 2 * t**2 + t), padd(pmul(p2, -2 * t**3 + 3 * t**2), pmul(dir2, t**3 - t**2)))) result_points.append(p) # create lines result = [] for index in range(len(result_points) - 1): result.append(Line(result_points[index], result_points[index + 1])) return result
def get_spiral_layer(minx, maxx, miny, maxy, z, line_distance, step_width, grid_direction, start_position, rounded_corners, reverse): current_location = _get_position(minx, maxx, miny, maxy, z, start_position) if line_distance > 0: line_steps_x = math.ceil((float(maxx - minx) / line_distance)) line_steps_y = math.ceil((float(maxy - miny) / line_distance)) line_distance_x = (maxx - minx) / line_steps_x line_distance_y = (maxy - miny) / line_steps_y lines = get_spiral_layer_lines(minx, maxx, miny, maxy, z, line_distance_x, line_distance_y, grid_direction, start_position, current_location) if reverse: lines.reverse() # turn the lines into steps if rounded_corners: rounded_lines = [] previous = None for index, (start, end) in enumerate(lines): radius = 0.5 * min(line_distance_x, line_distance_y) edge_vector = psub(end, start) # TODO: ellipse would be better than arc offset = pmul(pnormalized(edge_vector), radius) if previous: start = padd(start, offset) center = padd(previous, offset) up_vector = pnormalized( pcross(psub(previous, center), psub(start, center))) north = padd(center, (1.0, 0.0, 0.0, 'v')) angle_start = get_angle_pi( north, center, previous, up_vector, pi_factor=True) * 180.0 angle_end = get_angle_pi( north, center, start, up_vector, pi_factor=True) * 180.0 # TODO: remove these exceptions based on up_vector.z (get_points_of_arc does # not respect the plane, yet) if up_vector[2] < 0: angle_start, angle_end = -angle_end, -angle_start arc_points = get_points_of_arc(center, radius, angle_start, angle_end) if up_vector[2] < 0: arc_points.reverse() for arc_index in range(len(arc_points) - 1): p1_coord = arc_points[arc_index] p2_coord = arc_points[arc_index + 1] p1 = (p1_coord[0], p1_coord[1], z) p2 = (p2_coord[0], p2_coord[1], z) rounded_lines.append((p1, p2)) if index != len(lines) - 1: end = psub(end, offset) previous = end rounded_lines.append((start, end)) lines = rounded_lines for start, end in lines: points = [] if step_width is None: points.append(start) points.append(end) else: line = Line(start, end) if isiterable(step_width): steps = step_width else: steps = floatrange(0.0, line.len, inc=step_width) for step in steps: next_point = padd(line.p1, pmul(line.dir, step)) points.append(next_point) if reverse: points.reverse() yield points
def dir(self): return pnormalized(self.vector)
def get_collision_waterline_of_triangle(model, cutter, up_vector, triangle, z): # TODO: there are problems with "material allowance > 0" plane = Plane((0, 0, z), up_vector) if triangle.minz >= z: # no point of the triangle is below z # try all edges # Case (4) proj_points = [] for p in triangle.get_points(): proj_p = plane.get_point_projection(p) if proj_p not in proj_points: proj_points.append(proj_p) if len(proj_points) == 3: edges = [] for index in range(3): edge = Line(proj_points[index - 1], proj_points[index]) # the edge should be clockwise around the model if pdot(pcross(edge.dir, triangle.normal), up_vector) < 0: edge = Line(edge.p2, edge.p1) edges.append((edge, proj_points[index - 2])) outer_edges = [] for edge, other_point in edges: # pick only edges, where the other point is on the right side if pdot(pcross(psub(other_point, edge.p1), edge.dir), up_vector) > 0: outer_edges.append(edge) if len(outer_edges) == 0: # the points seem to be an one line # pick the longest edge long_edge = edges[0][0] for edge, other_point in edges[1:]: if edge.len > long_edge.len: long_edge = edge outer_edges = [long_edge] else: edge = Line(proj_points[0], proj_points[1]) if pdot(pcross(edge.dir, triangle.normal), up_vector) < 0: edge = Line(edge.p2, edge.p1) outer_edges = [edge] else: # some parts of the triangle are above and some below the cutter level # Cases (2a), (2b), (3a) and (3b) points_above = [ plane.get_point_projection(p) for p in triangle.get_points() if p[2] > z ] waterline = plane.intersect_triangle(triangle) if waterline is None: if len(points_above) == 0: # the highest point of the triangle is at z outer_edges = [] else: if abs(triangle.minz - z) < epsilon: # This is just an accuracy issue (see the # "triangle.minz >= z" statement above). outer_edges = [] elif not [ p for p in triangle.get_points() if p[2] > z + epsilon ]: # same as above: fix for inaccurate floating calculations outer_edges = [] else: # this should not happen raise ValueError(( "Could not find a waterline, but there are points above z " "level (%f): %s / %s") % (z, triangle, points_above)) else: # remove points that are not part of the waterline points_above = [ p for p in points_above if (p != waterline.p1) and (p != waterline.p2) ] if len(points_above) == 0: # part of case (2a) outer_edges = [waterline] elif len(points_above) == 1: other_point = points_above[0] dot = pdot( pcross(psub(other_point, waterline.p1), waterline.dir), up_vector) if dot > 0: # Case (2b) outer_edges = [waterline] elif dot < 0: # Case (3b) edges = [] edges.append(Line(waterline.p1, other_point)) edges.append(Line(waterline.p2, other_point)) outer_edges = [] for edge in edges: if pdot(pcross(edge.dir, triangle.normal), up_vector) < 0: outer_edges.append(Line(edge.p2, edge.p1)) else: outer_edges.append(edge) else: # the three points are on one line # part of case (2a) edges = [] edges.append(waterline) edges.append(Line(waterline.p1, other_point)) edges.append(Line(waterline.p2, other_point)) edges.sort(key=lambda x: x.len) edge = edges[-1] if pdot(pcross(edge.dir, triangle.normal), up_vector) < 0: outer_edges = [Line(edge.p2, edge.p1)] else: outer_edges = [edge] else: # two points above other_point = points_above[0] dot = pdot( pcross(psub(other_point, waterline.p1), waterline.dir), up_vector) if dot > 0: # Case (2b) # the other two points are on the right side outer_edges = [waterline] elif dot < 0: # Case (3a) edge = Line(points_above[0], points_above[1]) if pdot(pcross(edge.dir, triangle.normal), up_vector) < 0: outer_edges = [Line(edge.p2, edge.p1)] else: outer_edges = [edge] else: edges = [] # pick the longest combination of two of these points # part of case (2a) # TODO: maybe we should use the waterline instead? # (otherweise the line could be too long and thus # connections to the adjacent waterlines are not discovered? # Test this with an appropriate test model.) points = [waterline.p1, waterline.p2] + points_above for p1 in points: for p2 in points: if p1 is not p2: edges.append(Line(p1, p2)) edges.sort(key=lambda x: x.len) edge = edges[-1] if pdot(pcross(edge.dir, triangle.normal), up_vector) < 0: outer_edges = [Line(edge.p2, edge.p1)] else: outer_edges = [edge] # calculate the maximum diagonal length within the model x_dim = abs(model.maxx - model.minx) y_dim = abs(model.maxy - model.miny) z_dim = abs(model.maxz - model.minz) max_length = sqrt(x_dim**2 + y_dim**2 + z_dim**2) result = [] for edge in outer_edges: direction = pnormalized(pcross(up_vector, edge.dir)) if direction is None: continue direction = pmul(direction, max_length) edge_dir = psub(edge.p2, edge.p1) # TODO: Adapt the number of potential starting positions to the length # of the line. Don't use 0.0 and 1.0 - this could result in ambiguous # collisions with triangles sharing these vertices. for factor in (0.5, epsilon, 1.0 - epsilon, 0.25, 0.75): start = padd(edge.p1, pmul(edge_dir, factor)) # We need to use the triangle collision algorithm here - because we # need the point of collision in the triangle. collisions = get_free_paths_triangles([model], cutter, start, padd(start, direction), return_triangles=True) for index, coll in enumerate(collisions): if ((index % 2 == 0) and (coll[1] is not None) and (coll[2] is not None) and (pdot(psub(coll[0], start), direction) > 0)): cl, hit_t, cp = coll break else: log.debug("Failed to detect any collision: %s / %s -> %s", edge, start, direction) continue proj_cp = plane.get_point_projection(cp) # e.g. the Spherical Cutter often does not collide exactly above # the potential collision line. # TODO: maybe an "is cp inside of the triangle" check would be good? if (triangle is hit_t) or (edge.is_point_inside(proj_cp)): result.append((cl, edge)) # continue with the next outer_edge break # Don't check triangles again that are completely above the z level and # did not return any collisions. if not result and (triangle.minz > z): # None indicates that the triangle needs no further evaluation return None return result
def _check_colinearity(p1, p2, p3): v1 = pnormalized(psub(p2, p1)) v2 = pnormalized(psub(p3, p2)) # compare if the normalized distances between p1-p2 and p2-p3 are equal return v1 == v2
def to_opengl(self, color=None, show_directions=False): if not GL_enabled: return if color is not None: GL.glColor4f(*color) GL.glBegin(GL.GL_TRIANGLES) # use normals to improve lighting (contributed by imyrek) normal_t = self.normal GL.glNormal3f(normal_t[0], normal_t[1], normal_t[2]) # The triangle's points are in clockwise order, but GL expects # counter-clockwise sorting. GL.glVertex3f(self.p1[0], self.p1[1], self.p1[2]) GL.glVertex3f(self.p3[0], self.p3[1], self.p3[2]) GL.glVertex3f(self.p2[0], self.p2[1], self.p2[2]) GL.glEnd() if show_directions: # display surface normals n = self.normal c = self.center d = 0.5 GL.glBegin(GL.GL_LINES) GL.glVertex3f(c[0], c[1], c[2]) GL.glVertex3f(c[0] + n[0] * d, c[1] + n[1] * d, c[2] + n[2] * d) GL.glEnd() if False: # display bounding sphere GL.glPushMatrix() middle = self.middle GL.glTranslate(middle[0], middle[1], middle[2]) if not hasattr(self, "_sphere"): self._sphere = GLU.gluNewQuadric() GLU.gluSphere(self._sphere, self.radius, 10, 10) GL.glPopMatrix() if pycam.Utils.log.is_debug(): # draw triangle id on triangle face GL.glPushMatrix() c = self.center GL.glTranslate(c[0], c[1], c[2]) p12 = pmul(padd(self.p1, self.p2), 0.5) p3_12 = pnormalized(psub(self.p3, p12)) p2_1 = pnormalized(psub(self.p1, self.p2)) pn = pcross(p2_1, p3_12) GL.glMultMatrixf((p2_1[0], p2_1[1], p2_1[2], 0, p3_12[0], p3_12[1], p3_12[2], 0, pn[0], pn[1], pn[2], 0, 0, 0, 0, 1)) n = pmul(self.normal, 0.01) GL.glTranslatef(n[0], n[1], n[2]) maxdim = max((self.maxx - self.minx), (self.maxy - self.miny), (self.maxz - self.minz)) factor = 0.001 GL.glScalef(factor * maxdim, factor * maxdim, factor * maxdim) w = 0 id_string = "%s." % str(self.id) for ch in id_string: w += GLUT.glutStrokeWidth(GLUT_STROKE_ROMAN, ord(ch)) GL.glTranslate(-w / 2, 0, 0) for ch in id_string: GLUT.glutStrokeCharacter(GLUT_STROKE_ROMAN, ord(ch)) GL.glPopMatrix() if False: # draw point id on triangle face c = self.center p12 = pmul(padd(self.p1, self.p2), 0.5) p3_12 = pnormalized(psub(self.p3, p12)) p2_1 = pnormalized(psub(self.p1, self.p2)) pn = pcross(p2_1, p3_12) n = pmul(self.normal, 0.01) for p in (self.p1, self.p2, self.p3): GL.glPushMatrix() pp = psub(p, pmul(psub(p, c), 0.3)) GL.glTranslate(pp[0], pp[1], pp[2]) GL.glMultMatrixf( (p2_1[0], p2_1[1], p2_1[2], 0, p3_12[0], p3_12[1], p3_12[2], 0, pn[0], pn[1], pn[2], 0, 0, 0, 0, 1)) GL.glTranslatef(n[0], n[1], n[2]) GL.glScalef(0.001, 0.001, 0.001) w = 0 for ch in str(p.id): w += GLUT.glutStrokeWidth(GLUT_STROKE_ROMAN, ord(ch)) GL.glTranslate(-w / 2, 0, 0) for ch in str(p.id): GLUT.glutStrokeCharacter(GLUT_STROKE_ROMAN, ord(ch)) GL.glPopMatrix()
def get_free_paths_triangles(models, cutter, p1, p2, return_triangles=False): if (len(models) == 0) or ((len(models) == 1) and (models[0] is None)): return (p1, p2) elif len(models) == 1: # only one model is left - just continue model = models[0] else: # multiple models were given - process them in layers result = get_free_paths_triangles(models[:1], cutter, p1, p2, return_triangles) # group the result into pairs of two points (start/end) point_pairs = [] while result: pair1 = result.pop(0) pair2 = result.pop(0) point_pairs.append((pair1, pair2)) all_results = [] for pair in point_pairs: one_result = get_free_paths_triangles(models[1:], cutter, pair[0], pair[1], return_triangles) all_results.extend(one_result) return all_results backward = pnormalized(psub(p1, p2)) forward = pnormalized(psub(p2, p1)) xyz_dist = pdist(p2, p1) minx = min(p1[0], p2[0]) maxx = max(p1[0], p2[0]) miny = min(p1[1], p2[1]) maxy = max(p1[1], p2[1]) minz = min(p1[2], p2[2]) # find all hits along scan line hits = [] triangles = model.triangles(minx - cutter.distance_radius, miny - cutter.distance_radius, minz, maxx + cutter.distance_radius, maxy + cutter.distance_radius, INFINITE) for t in triangles: (cl1, d1, cp1) = cutter.intersect(backward, t, start=p1) if cl1: hits.append(Hit(cl1, cp1, t, -d1, backward)) (cl2, d2, cp2) = cutter.intersect(forward, t, start=p1) if cl2: hits.append(Hit(cl2, cp2, t, d2, forward)) # sort along the scan direction hits.sort(key=lambda h: h.d) count = 0 points = [] for h in hits: if h.dir == forward: if count == 0: if -epsilon <= h.d <= xyz_dist + epsilon: if len(points) == 0: points.append((p1, None, None)) points.append((h.cl, h.t, h.cp)) count += 1 else: if count == 1: if -epsilon <= h.d <= xyz_dist + epsilon: points.append((h.cl, h.t, h.cp)) count -= 1 if len(points) % 2 == 1: points.append((p2, None, None)) if len(points) == 0: # check if the path is completely free or if we are inside of the model inside_counter = 0 for h in hits: if -epsilon <= h.d: # we reached the outer limit of the model break if h.dir == forward: inside_counter += 1 else: inside_counter -= 1 if inside_counter <= 0: # we are not inside of the model points.append((p1, None, None)) points.append((p2, None, None)) if return_triangles: return points else: # return only the cutter locations (without triangles) return [cut_info[0] for cut_info in points]
def get_offset_polygons_validated(self, offset): if self.is_outer(): inside_shifting = max(0, -offset) else: inside_shifting = max(0, offset) if inside_shifting * 2 >= self.get_max_inside_distance(): # no polygons will be left return [] points = [] for index in range(len(self._points)): points.append(self.get_shifted_vertex(index, offset)) max_dist = 1000 * epsilon def test_point_near(p, others): for o in others: if pdist(p, o) < max_dist: return True return False reverse_lines = [] shifted_lines = [] for index, p1 in enumerate(points): next_index = (index + 1) % len(points) p2 = points[next_index] diff = psub(p2, p1) old_dir = pnormalized( psub(self._points[next_index], self._points[index])) if pnormalized(diff) != old_dir: # the direction turned around if pnorm(diff) > max_dist: # the offset was too big return None else: reverse_lines.append(index) shifted_lines.append((True, Line(p1, p2))) else: shifted_lines.append((False, Line(p1, p2))) # look for reversed lines index = 0 while index < len(shifted_lines): line_reverse, line = shifted_lines[index] if line_reverse: prev_index = (index - 1) % len(shifted_lines) next_index = (index + 1) % len(shifted_lines) prev_reverse, prev_line = shifted_lines[prev_index] while prev_reverse and (prev_index != next_index): prev_index = (prev_index - 1) % len(shifted_lines) prev_reverse, prev_line = shifted_lines[prev_index] if prev_index == next_index: # no lines are left print("out 1") return [] next_reverse, next_line = shifted_lines[next_index] while next_reverse and (prev_index != next_index): next_index = (next_index + 1) % len(shifted_lines) next_reverse, next_line = shifted_lines[next_index] if prev_index == next_index: # no lines are left print("out 2") return [] if pdist(prev_line.p2, next_line.p1) > max_dist: cp, dist = prev_line.get_intersection(next_line) else: cp = prev_line.p2 if cp: shifted_lines[prev_index] = (False, Line(prev_line.p1, cp)) shifted_lines[next_index] = (False, Line(cp, next_line.p2)) else: cp, dist = prev_line.get_intersection(next_line, infinite_lines=True) raise BaseException( "Expected intersection not found: %s - %s - %s(%d) / %s(%d)" % (cp, shifted_lines[prev_index + 1:next_index], prev_line, prev_index, next_line, next_index)) if index > next_index: # we wrapped around the end of the list break else: index = next_index + 1 else: index += 1 non_reversed = [ one_line for rev, one_line in shifted_lines if not rev and one_line.len > 0 ] # split the list of lines into groups (based on intersections) split_points = [] index = 0 while index < len(non_reversed): other_index = 0 while other_index < len(non_reversed): other_line = non_reversed[other_index] if (other_index == index) \ or (other_index == ((index - 1) % len(non_reversed))) \ or (other_index == ((index + 1) % len(non_reversed))): # skip neighbours other_index += 1 continue line = non_reversed[index] cp, dist = line.get_intersection(other_line) if cp: if not test_point_near( cp, (line.p1, line.p2, other_line.p1, other_line.p2)): # the collision is not close to an end of the line return None elif (cp == line.p1) or (cp == line.p2): # maybe we have been here before if cp not in split_points: split_points.append(cp) elif (pdist(cp, line.p1) < max_dist) or (pdist( cp, line.p2) < max_dist): if pdist(cp, line.p1) < pdist(cp, line.p2): non_reversed[index] = Line(cp, line.p2) else: non_reversed[index] = Line(line.p1, cp) non_reversed.pop(other_index) non_reversed.insert(other_index, Line(other_line.p1, cp)) non_reversed.insert(other_index + 1, Line(cp, other_line.p2)) split_points.append(cp) if other_index < index: index += 1 # skip the second part of this line other_index += 1 else: # the split of 'other_line' will be handled later pass other_index += 1 index += 1 groups = [[]] current_group = 0 split_here = False for line in non_reversed: if line.p1 in split_points: split_here = True if split_here: split_here = False # check if any preceding group fits to the point for index, group in enumerate(groups): if not group: continue if index == current_group: continue if group[0].p1 == group[-1].p2: # the group is already closed continue if line.p1 == group[-1].p2: current_group = index groups[current_group].append(line) break else: current_group = len(groups) groups.append([line]) else: groups[current_group].append(line) if line.p2 in split_points: split_here = True # try to combine open groups for index1, group1 in enumerate(groups): if not group1: continue for index2, group2 in enumerate(groups): if not group2: continue if index2 <= index1: continue if (group1[-1].p2 == group2[0].p1) \ and (group1[0].p1 == group2[-1].p2): group1.extend(group2) groups[index2] = [] break result_polygons = [] print("********** GROUPS **************") for a in groups: print(a) for group in groups: if len(group) <= 2: continue poly = Polygon(self.plane) for line in group: try: poly.append(line) except ValueError: print("NON_REVERSED") for a in non_reversed: print(a) print(groups) print(split_points) print(poly) print(line) raise if self.is_closed and ((not poly.is_closed) or (self.is_outer() != poly.is_outer())): continue elif (not self.is_closed) and (poly.get_area() != 0): continue else: result_polygons.append(poly) return result_polygons
def ImportModel(filename, use_kdtree=True, callback=None, **kwargs): global vertices, edges, kdtree vertices = 0 edges = 0 kdtree = None normal_conflict_warning_seen = False if hasattr(filename, "read"): # make sure that the input stream can seek and has ".len" f = StringIO(filename.read()) # useful for later error messages filename = "input stream" else: try: url_file = pycam.Utils.URIHandler(filename).open() # urllib.urlopen objects do not support "seek" - so we need to read # the whole file at once. This is ugly - anyone with a better idea? f = StringIO(url_file.read()) # TODO: the above ".read" may be incomplete - this is ugly # see http://patrakov.blogspot.com/2011/03/case-of-non-raised-exception.html # and http://stackoverflow.com/questions/1824069/ url_file.close() except IOError as err_msg: log.error("STLImporter: Failed to read file (%s): %s", filename, err_msg) return None # Read the first two lines of (potentially non-binary) input - they should # contain "solid" and "facet". header_lines = [] while len(header_lines) < 2: line = f.readline(200) if len(line) == 0: # empty line (not even a line-feed) -> EOF log.error("STLImporter: No valid lines found in '%s'", filename) return None # ignore comment lines # note: partial comments (starting within a line) are not handled if not line.startswith(";"): header_lines.append(line) header = "".join(header_lines) # read byte 80 to 83 - they contain the "numfacets" value in binary format f.seek(80) numfacets = unpack("<I", f.read(4))[0] binary = False log.debug("STL import info: %s / %s / %s / %s", f.len, numfacets, header.find("solid"), header.find("facet")) if f.len == (84 + 50 * numfacets): binary = True elif header.find("solid") >= 0 and header.find("facet") >= 0: binary = False f.seek(0) else: log.error("STLImporter: STL binary/ascii detection failed") return None if use_kdtree: kdtree = PointKdtree([], 3, 1, epsilon) model = Model(use_kdtree) t = None p1 = None p2 = None p3 = None if binary: for i in range(1, numfacets + 1): if callback and callback(): log.warn("STLImporter: load model operation cancelled") return None a1 = unpack("<f", f.read(4))[0] a2 = unpack("<f", f.read(4))[0] a3 = unpack("<f", f.read(4))[0] n = (float(a1), float(a2), float(a3), 'v') v11 = unpack("<f", f.read(4))[0] v12 = unpack("<f", f.read(4))[0] v13 = unpack("<f", f.read(4))[0] p1 = UniqueVertex(float(v11), float(v12), float(v13)) v21 = unpack("<f", f.read(4))[0] v22 = unpack("<f", f.read(4))[0] v23 = unpack("<f", f.read(4))[0] p2 = UniqueVertex(float(v21), float(v22), float(v23)) v31 = unpack("<f", f.read(4))[0] v32 = unpack("<f", f.read(4))[0] v33 = unpack("<f", f.read(4))[0] p3 = UniqueVertex(float(v31), float(v32), float(v33)) # not used (additional attributes) f.read(2) dotcross = pdot(n, pcross(psub(p2, p1), psub(p3, p1))) if a1 == a2 == a3 == 0: dotcross = pcross(psub(p2, p1), psub(p3, p1))[2] n = None if dotcross > 0: # Triangle expects the vertices in clockwise order t = Triangle(p1, p3, p2) elif dotcross < 0: if not normal_conflict_warning_seen: log.warn( "Inconsistent normal/vertices found in facet definition %d of '%s'. " "Please validate the STL file!", i, filename) normal_conflict_warning_seen = True t = Triangle(p1, p2, p3) else: # the three points are in a line - or two points are identical # usually this is caused by points, that are too close together # check the tolerance value in pycam/Geometry/PointKdtree.py log.warn( "Skipping invalid triangle: %s / %s / %s (maybe the resolution of the " "model is too high?)", p1, p2, p3) continue if n: t.normal = n model.append(t) else: solid = re.compile(r"\s*solid\s+(\w+)\s+.*") endsolid = re.compile(r"\s*endsolid\s*") facet = re.compile(r"\s*facet\s*") normal = re.compile( r"\s*facet\s+normal" + r"\s+(?P<x>[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?)" + r"\s+(?P<y>[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?)" + r"\s+(?P<z>[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?)\s+") endfacet = re.compile(r"\s*endfacet\s+") loop = re.compile(r"\s*outer\s+loop\s+") endloop = re.compile(r"\s*endloop\s+") vertex = re.compile( r"\s*vertex" + r"\s+(?P<x>[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?)" + r"\s+(?P<y>[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?)" + r"\s+(?P<z>[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?)\s+") current_line = 0 for line in f: if callback and callback(): log.warn("STLImporter: load model operation cancelled") return None current_line += 1 m = solid.match(line) if m: model.name = m.group(1) continue m = facet.match(line) if m: m = normal.match(line) if m: n = (float(m.group('x')), float(m.group('y')), float(m.group('z')), 'v') else: n = None continue m = loop.match(line) if m: continue m = vertex.match(line) if m: p = UniqueVertex(float(m.group('x')), float(m.group('y')), float(m.group('z'))) if p1 is None: p1 = p elif p2 is None: p2 = p elif p3 is None: p3 = p else: log.error( "STLImporter: more then 3 points in facet (line %d)", current_line) continue m = endloop.match(line) if m: continue m = endfacet.match(line) if m: if None in (p1, p2, p3): log.warn( "Invalid facet definition in line %d of '%s'. Please validate the " "STL file!", current_line, filename) n, p1, p2, p3 = None, None, None, None continue if not n: n = pnormalized(pcross(psub(p2, p1), psub(p3, p1))) # validate the normal # The three vertices of a triangle in an STL file are supposed # to be in counter-clockwise order. This should match the # direction of the normal. if n is None: # invalid triangle (zero-length vector) dotcross = 0 else: # make sure the points are in ClockWise order dotcross = pdot(n, pcross(psub(p2, p1), psub(p3, p1))) if dotcross > 0: # Triangle expects the vertices in clockwise order t = Triangle(p1, p3, p2, n) elif dotcross < 0: if not normal_conflict_warning_seen: log.warn( "Inconsistent normal/vertices found in line %d of '%s'. Please " "validate the STL file!", current_line, filename) normal_conflict_warning_seen = True t = Triangle(p1, p2, p3, n) else: # The three points are in a line - or two points are # identical. Usually this is caused by points, that are too # close together. Check the tolerance value in # pycam/Geometry/PointKdtree.py. log.warn( "Skipping invalid triangle: %s / %s / %s (maybe the resolution of " "the model is too high?)", p1, p2, p3) n, p1, p2, p3 = (None, None, None, None) continue n, p1, p2, p3 = (None, None, None, None) model.append(t) continue m = endsolid.match(line) if m: continue log.info("Imported STL model: %d vertices, %d edges, %d triangles", vertices, edges, len(model.triangles())) vertices = 0 edges = 0 kdtree = None if not model: # no valid items added to the model return None else: return model
def import_model(filename, use_kdtree=True, callback=None, **kwargs): global vertices, edges, kdtree vertices = 0 edges = 0 kdtree = None normal_conflict_warning_seen = False if hasattr(filename, "read"): # make sure that the input stream can seek and has ".len" f = BufferedReader(filename) # useful for later error messages filename = "input stream" else: try: url_file = pycam.Utils.URIHandler(filename).open() # urllib.urlopen objects do not support "seek" - so we need a buffered reader # Is there a better approach than consuming the whole file at once? f = BufferedReader(BytesIO(url_file.read())) url_file.close() except IOError as exc: raise LoadFileError( "STLImporter: Failed to read file ({}): {}".format( filename, exc)) # the facet count is only available for the binary format facet_count = get_facet_count_if_binary_format(f) is_binary = (facet_count is not None) if use_kdtree: kdtree = PointKdtree([], 3, 1, epsilon) model = Model(use_kdtree) t = None p1 = None p2 = None p3 = None if is_binary: # Skip the header and count fields of binary stl file f.seek(HEADER_SIZE + COUNT_SIZE) for i in range(1, facet_count + 1): if callback and callback(): raise AbortOperationException( "STLImporter: load model operation cancelled") a1 = unpack("<f", f.read(4))[0] a2 = unpack("<f", f.read(4))[0] a3 = unpack("<f", f.read(4))[0] n = (float(a1), float(a2), float(a3), 'v') v11 = unpack("<f", f.read(4))[0] v12 = unpack("<f", f.read(4))[0] v13 = unpack("<f", f.read(4))[0] p1 = get_unique_vertex(float(v11), float(v12), float(v13)) v21 = unpack("<f", f.read(4))[0] v22 = unpack("<f", f.read(4))[0] v23 = unpack("<f", f.read(4))[0] p2 = get_unique_vertex(float(v21), float(v22), float(v23)) v31 = unpack("<f", f.read(4))[0] v32 = unpack("<f", f.read(4))[0] v33 = unpack("<f", f.read(4))[0] p3 = get_unique_vertex(float(v31), float(v32), float(v33)) # not used (additional attributes) f.read(2) dotcross = pdot(n, pcross(psub(p2, p1), psub(p3, p1))) if a1 == a2 == a3 == 0: dotcross = pcross(psub(p2, p1), psub(p3, p1))[2] n = None if dotcross > 0: # Triangle expects the vertices in clockwise order t = Triangle(p1, p3, p2) elif dotcross < 0: if not normal_conflict_warning_seen: log.warn( "Inconsistent normal/vertices found in facet definition %d of '%s'. " "Please validate the STL file!", i, filename) normal_conflict_warning_seen = True t = Triangle(p1, p2, p3) else: # the three points are in a line - or two points are identical # usually this is caused by points, that are too close together # check the tolerance value in pycam/Geometry/PointKdtree.py log.warn( "Skipping invalid triangle: %s / %s / %s (maybe the resolution of the " "model is too high?)", p1, p2, p3) continue if n: t.normal = n model.append(t) else: # from here on we want to use a text based input stream (not bytes) f = TextIOWrapper(f, encoding="utf-8") solid = re.compile(r"\s*solid\s+(\w+)\s+.*") endsolid = re.compile(r"\s*endsolid\s*") facet = re.compile(r"\s*facet\s*") normal = re.compile( r"\s*facet\s+normal" + r"\s+(?P<x>[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?)" + r"\s+(?P<y>[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?)" + r"\s+(?P<z>[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?)\s+") endfacet = re.compile(r"\s*endfacet\s+") loop = re.compile(r"\s*outer\s+loop\s+") endloop = re.compile(r"\s*endloop\s+") vertex = re.compile( r"\s*vertex" + r"\s+(?P<x>[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?)" + r"\s+(?P<y>[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?)" + r"\s+(?P<z>[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?)\s+") current_line = 0 for line in f: if callback and callback(): raise AbortOperationException( "STLImporter: load model operation cancelled") current_line += 1 m = solid.match(line) if m: model.name = m.group(1) continue m = facet.match(line) if m: m = normal.match(line) if m: n = (float(m.group('x')), float(m.group('y')), float(m.group('z')), 'v') else: n = None continue m = loop.match(line) if m: continue m = vertex.match(line) if m: p = get_unique_vertex(float(m.group('x')), float(m.group('y')), float(m.group('z'))) if p1 is None: p1 = p elif p2 is None: p2 = p elif p3 is None: p3 = p else: log.error( "STLImporter: more then 3 points in facet (line %d)", current_line) continue m = endloop.match(line) if m: continue m = endfacet.match(line) if m: if None in (p1, p2, p3): log.warn( "Invalid facet definition in line %d of '%s'. Please validate the " "STL file!", current_line, filename) n, p1, p2, p3 = None, None, None, None continue if not n: n = pnormalized(pcross(psub(p2, p1), psub(p3, p1))) # validate the normal # The three vertices of a triangle in an STL file are supposed # to be in counter-clockwise order. This should match the # direction of the normal. if n is None: # invalid triangle (zero-length vector) dotcross = 0 else: # make sure the points are in ClockWise order dotcross = pdot(n, pcross(psub(p2, p1), psub(p3, p1))) if dotcross > 0: # Triangle expects the vertices in clockwise order t = Triangle(p1, p3, p2, n) elif dotcross < 0: if not normal_conflict_warning_seen: log.warn( "Inconsistent normal/vertices found in line %d of '%s'. Please " "validate the STL file!", current_line, filename) normal_conflict_warning_seen = True t = Triangle(p1, p2, p3, n) else: # The three points are in a line - or two points are # identical. Usually this is caused by points, that are too # close together. Check the tolerance value in # pycam/Geometry/PointKdtree.py. log.warn( "Skipping invalid triangle: %s / %s / %s (maybe the resolution of " "the model is too high?)", p1, p2, p3) n, p1, p2, p3 = (None, None, None, None) continue n, p1, p2, p3 = (None, None, None, None) model.append(t) continue m = endsolid.match(line) if m: continue # TODO display unique vertices and edges count - currently not counted log.info("Imported STL model: %d triangles", len(model.triangles())) vertices = 0 edges = 0 kdtree = None if not model: # no valid items added to the model raise LoadFileError( "Failed to load model from STL file: no elements found") else: return model