def reset_cache(self): self.minx = min(self.p1[0], self.p2[0], self.p3[0]) self.miny = min(self.p1[1], self.p2[1], self.p3[1]) self.minz = min(self.p1[2], self.p2[2], self.p3[2]) self.maxx = max(self.p1[0], self.p2[0], self.p3[0]) self.maxy = max(self.p1[1], self.p2[1], self.p3[1]) self.maxz = max(self.p1[2], self.p2[2], self.p3[2]) self.e1 = Line(self.p1, self.p2) self.e2 = Line(self.p2, self.p3) self.e3 = Line(self.p3, self.p1) # calculate normal, if p1-p2-pe are in clockwise order if self.normal is None: self.normal = pnormalized( pcross(psub(self.p3, self.p1), psub(self.p2, self.p1))) if not len(self.normal) > 3: self.normal = (self.normal[0], self.normal[1], self.normal[2], 'v') self.center = pdiv(padd(padd(self.p1, self.p2), self.p3), 3) self.plane = Plane(self.center, self.normal) # calculate circumcircle (resulting in radius and middle) denom = pnorm(pcross(psub(self.p2, self.p1), psub(self.p3, self.p2))) self.radius = (pdist(self.p2, self.p1) * pdist(self.p3, self.p2) * pdist(self.p3, self.p1)) / (2 * denom) self.radiussq = self.radius**2 denom2 = 2 * denom * denom alpha = pdist_sq(self.p3, self.p2) * pdot(psub( self.p1, self.p2), psub(self.p1, self.p3)) / denom2 beta = pdist_sq(self.p1, self.p3) * pdot(psub( self.p2, self.p1), psub(self.p2, self.p3)) / denom2 gamma = pdist_sq(self.p1, self.p2) * pdot(psub( self.p3, self.p1), psub(self.p3, self.p2)) / denom2 self.middle = (self.p1[0] * alpha + self.p2[0] * beta + self.p3[0] * gamma, self.p1[1] * alpha + self.p2[1] * beta + self.p3[1] * gamma, self.p1[2] * alpha + self.p2[2] * beta + self.p3[2] * gamma)
def intersect_sphere_line(center, radius, radiussq, direction, edge): # make a plane by sliding the line along the direction (1) d = edge.dir n = pcross(d, direction) if pnorm(n) == 0: # no contact point, but should check here if sphere *always* intersects # line... return (None, None, INFINITE) n = pnormalized(n) # calculate the distance from the sphere center to the plane dist = -pdot(center, n) + pdot(edge.p1, n) if abs(dist) > radius - epsilon: return (None, None, INFINITE) # this gives us the intersection circle on the sphere # now take a plane through the edge and perpendicular to the direction (2) # find the center on the circle closest to this plane # which means the other component is perpendicular to this plane (2) n2 = pnormalized(pcross(n, d)) # the contact point is on a big circle through the sphere... dist2 = sqrt(radiussq - dist * dist) # ... and it's on the plane (1) ccp = padd(center, padd(pmul(n, dist), pmul(n2, dist2))) # now intersect a line through this point with the plane (2) plane = Plane(edge.p1, n2) (cp, l) = plane.intersect_point(direction, ccp) return (ccp, cp, l)
def get_intersection(self, line, infinite_lines=False): """ Get the point of intersection between two lines. Intersections outside the length of these lines are ignored. Returns (None, None) if no valid intersection was found. Otherwise the result is (CollisionPoint, distance). Distance is between 0 and 1. """ x1, x2, x3, x4 = self.p1, self.p2, line.p1, line.p2 a = psub(x2, x1) b = psub(x4, x3) c = psub(x3, x1) # see http://mathworld.wolfram.com/Line-LineIntersection.html (24) try: factor = pdot(pcross(c, b), pcross(a, b)) / pnormsq(pcross(a, b)) except ZeroDivisionError: # lines are parallel # check if they are _one_ line if pnorm(pcross(a, c)) != 0: # the lines are parallel with a distance return None, None # the lines are on one straight candidates = [] if self.is_point_inside(x3): candidates.append((x3, pnorm(c) / pnorm(a))) elif self.is_point_inside(x4): candidates.append((x4, pdist(line.p2, self.p1) / pnorm(a))) elif line.is_point_inside(x1): candidates.append((x1, 0)) elif line.is_point_inside(x2): candidates.append((x2, 1)) else: return None, None # return the collision candidate with the lowest distance candidates.sort(key=lambda collision: collision[1]) return candidates[0] if infinite_lines or (-epsilon <= factor <= 1 + epsilon): intersec = padd(x1, pmul(a, factor)) # check if the intersection is between x3 and x4 if infinite_lines: return intersec, factor elif ((min(x3[0], x4[0]) - epsilon <= intersec[0] <= max(x3[0], x4[0]) + epsilon) and (min(x3[1], x4[1]) - epsilon <= intersec[1] <= max(x3[1], x4[1]) + epsilon) and (min(x3[2], x4[2]) - epsilon <= intersec[2] <= max(x3[2], x4[2]) + epsilon)): return intersec, factor else: # intersection outside of the length of line(x3, x4) return None, None else: # intersection outside of the length of line(x1, x2) return None, None
def calculate_point_height(self, x, y, func): point = (x, y, self.outer.minz) if not self.outer.is_point_inside(point): return None for poly in self.inner: if poly.is_point_inside(point): return None point = (x, y, self.outer.minz) line_distances = [] for line in self.lines: cross_product = pcross(line.dir, psub(point, line.p1)) if cross_product[2] > 0: close_points = [] close_point = line.closest_point(point) if not line.is_point_inside(close_point): close_points.append(line.p1) close_points.append(line.p2) else: close_points.append(close_point) for p in close_points: direction = psub(point, p) dist = pnorm(direction) line_distances.append(dist) elif cross_product[2] == 0: # the point is on the line line_distances.append(0.0) # no other line can get closer than this break else: # the point is in the left of this line pass line_distances.sort() return self.z_level + func(line_distances[0])
def intersect_torus_point(center, axis, majorradius, minorradius, majorradiussq, minorradiussq, direction, point): dist = 0 if (direction[0] == 0) and (direction[1] == 0): # drop minlsq = (majorradius - minorradius)**2 maxlsq = (majorradius + minorradius)**2 l_sq = (point[0] - center[0])**2 + (point[1] - center[1])**2 if (l_sq < minlsq + epsilon) or (l_sq > maxlsq - epsilon): return (None, None, INFINITE) l_len = sqrt(l_sq) z_sq = minorradiussq - (majorradius - l_len)**2 if z_sq < 0: return (None, None, INFINITE) z = sqrt(z_sq) ccp = (point[0], point[1], center[2] - z) dist = ccp[2] - point[2] elif direction[2] == 0: # push z = point[2] - center[2] if abs(z) > minorradius - epsilon: return (None, None, INFINITE) l_len = majorradius + sqrt(minorradiussq - z * z) n = pcross(axis, direction) d = pdot(n, point) - pdot(n, center) if abs(d) > l_len - epsilon: return (None, None, INFINITE) a = sqrt(l_len * l_len - d * d) ccp = padd(padd(center, pmul(n, d)), pmul(direction, a)) ccp = (ccp[0], ccp[1], point[2]) dist = pdot(psub(point, ccp), direction) else: # general case x = psub(point, center) v = pmul(direction, -1) x_x = pdot(x, x) x_v = pdot(x, v) x1 = (x[0], x[1], 0) v1 = (v[0], v[1], 0) x1_x1 = pdot(x1, x1) x1_v1 = pdot(x1, v1) v1_v1 = pdot(v1, v1) r2_major = majorradiussq r2_minor = minorradiussq a = 1.0 b = 4 * x_v c = 2 * (x_x + 2 * x_v**2 + (r2_major - r2_minor) - 2 * r2_major * v1_v1) d = 4 * (x_x * x_v + x_v * (r2_major - r2_minor) - 2 * r2_major * x1_v1) e = ((x_x)**2 + 2 * x_x * (r2_major - r2_minor) + (r2_major - r2_minor)**2 - 4 * r2_major * x1_x1) r = poly4_roots(a, b, c, d, e) if not r: return (None, None, INFINITE) else: l_len = min(r) ccp = padd(point, pmul(direction, -l_len)) dist = l_len return (ccp, point, dist)
def get_bisector(p1, p2, p3, up_vector): """ Calculate the bisector between p1, p2 and p3, whereas p2 is the origin of the angle. """ d1 = pnormalized(psub(p2, p1)) d2 = pnormalized(psub(p2, p3)) bisector_dir = pnormalized(padd(d1, d2)) if bisector_dir is None: # the two vectors pointed to opposite directions bisector_dir = pnormalized(pcross(d1, up_vector)) else: skel_up_vector = pcross(bisector_dir, psub(p2, p1)) if pdot(up_vector, skel_up_vector) < 0: # reverse the skeleton vector to point outwards bisector_dir = pmul(bisector_dir, -1) return bisector_dir
def _process_one_triangle(extra_args): model, cutter, up_vector, triangle, z = extra_args result = [] # ignore triangles below the z level if triangle.maxz < z: # Case 1a return result, None # ignore triangles pointing upwards or downwards if pnorm(pcross(triangle.normal, up_vector)) == 0: # Case 1b return result, None edge_collisions = get_collision_waterline_of_triangle( model, cutter, up_vector, triangle, z) if edge_collisions is None: # don't try to use this edge again return result, [id(triangle)] elif len(edge_collisions) == 0: return result, None else: for cutter_location, edge in edge_collisions: shifted_edge = get_shifted_waterline(up_vector, edge, cutter_location) if shifted_edge is not None: if _DEBUG_DISBALE_WATERLINE_SHIFT: result.append((edge, edge)) else: result.append((edge, shifted_edge)) return result, None
def get_x3d_cone(start: tuple, end: tuple, position: float, length: float, radius: float, color: tuple): # by default the X3D cone points along the y axis original_vector = (0, 1, 0) line_vector = psub(end, start) line_normalized = pnormalized(line_vector) # the cone position is at its bottom bottom_position = padd(start, pmul(line_vector, position)) # handling of corner cases if pnormsq(pcross(original_vector, line_normalized)) == 0: # Both vectors are aligned - but maybe point into different directions. rotation_axis = (1, 0, 0) rotation_angle = math.pi if original_vector != line_normalized else 0 else: # Both vectors are not aligned. Use a normal rotation. # Rotate around the vector in the vector in the middle by 180 degrees. rotation_axis = padd(original_vector, line_normalized) rotation_angle = math.pi yield ('<Transform translation="{:f} {:f} {:f}" rotation="{:f} {:f} {:f} {:f}">' .format(*bottom_position, *rotation_axis, rotation_angle)) yield "<Shape>" yield "<Appearance>" yield ('<Material diffuseColor="{:f} {:f} {:f}" transparency="{:f}" />' .format(color["red"], color["green"], color["blue"], 1 - color["alpha"])) yield "</Appearance>" yield '<Cone bottomRadius="{:f}" topRadius="0" height="{:f}" />'.format(radius, length) yield "</Shape>" yield "</Transform>"
def _get_edge_bridges(polygon, z_plane, min_bridges, average_distance, avoid_distance): def is_near_list(point_list, point, distance): for p in point_list: if pdist(p, point) <= distance: return True return False lines = polygon.get_lines() poly_lengths = polygon.get_lengths() num_of_bridges = max(min_bridges, int(round(sum(poly_lengths) / average_distance))) real_average_distance = sum(poly_lengths) / num_of_bridges max_line_index = poly_lengths.index(max(poly_lengths)) positions = [] current_line_index = max_line_index distance_processed = poly_lengths[current_line_index] / 2 positions.append(current_line_index) while len(positions) < num_of_bridges: current_line_index += 1 current_line_index %= len(poly_lengths) # skip lines that are not at least twice as long as the grid width while (distance_processed + poly_lengths[current_line_index] < real_average_distance): distance_processed += poly_lengths[current_line_index] current_line_index += 1 current_line_index %= len(poly_lengths) positions.append(current_line_index) distance_processed += poly_lengths[current_line_index] distance_processed %= real_average_distance result = [] bridge_positions = [] for line_index in positions: position = polygon.get_middle_of_line(line_index) # skip bridges that are close to another existing bridge if is_near_list(bridge_positions, position, avoid_distance): line = polygon.get_lines()[line_index] # calculate two alternative points on the same line position1 = pdiv(padd(position, line.p1), 2) position2 = pdiv(padd(position, line.p2), 2) if is_near_list(bridge_positions, position1, avoid_distance): if is_near_list(bridge_positions, position2, avoid_distance): # no valid alternative - we skip this bridge continue else: # position2 is OK position = position2 else: # position1 is OK position = position1 # append the original position (ignoring z_plane) bridge_positions.append(position) # move the point to z_plane position = (position[0], position[1], z_plane) bridge_dir = pnormalized(pcross(lines[line_index].dir, polygon.plane.n)) result.append((position, bridge_dir)) return result
def get_shifted_vertex(self, index, offset): p1 = self._points[index] p2 = self._points[(index + 1) % len(self._points)] cross_offset = pnormalized(pcross(psub(p2, p1), self.plane.n)) bisector_normalized = self.get_bisector(index) factor = pdot(cross_offset, bisector_normalized) if factor != 0: bisector_sized = pmul(bisector_normalized, offset / factor) return padd(p1, bisector_sized) else: return p2
def get_rotation_matrix_from_to(v_orig, v_dest): """ calculate the rotation matrix used to transform one vector into another The result is useful for modifying the rotation matrix of a 3d object. The simplest example is the following with the original vector pointing along the x axis, while the destination vectors goes along the y axis: get_rotation_matrix((1, 0, 0), (0, 1, 0)) Basically this describes a rotation around the z axis by 90 degrees. The resulting 3x3 matrix (tuple of tuple of floats) can be multiplied with any other vector to rotate it in the same way around the z axis. @type v_orig: tuple(float) | list(float) | pycam.Geometry.Point @value v_orig: the original 3d vector @type v_dest: tuple(float) | list(float) | pycam.Geometry.Point @value v_dest: the destination 3d vector @rtype: tuple(tuple(float)) @return: the rotation matrix (3x3) """ v_orig_length = get_length(v_orig) v_dest_length = get_length(v_dest) cross_product = get_length(pcross(v_orig, v_dest)) try: arcsin = cross_product / (v_orig_length * v_dest_length) except ZeroDivisionError: return None # prevent float inaccuracies to crash the calculation (within limits) if 1 < arcsin < 1 + epsilon: arcsin = 1.0 elif -1 - epsilon < arcsin < -1: arcsin = -1.0 rot_angle = math.asin(arcsin) # calculate the rotation axis # The rotation axis is equal to the cross product of the original and # destination vectors. rot_axis = pnormalized((v_orig[1] * v_dest[2] - v_orig[2] * v_dest[1], v_orig[2] * v_dest[0] - v_orig[0] * v_dest[2], v_orig[0] * v_dest[1] - v_orig[1] * v_dest[0])) if not rot_axis: return None # get the rotation matrix # see http://www.fastgraph.com/makegames/3drotation/ c = math.cos(rot_angle) s = math.sin(rot_angle) t = 1 - c return ((t * rot_axis[0] * rot_axis[0] + c, t * rot_axis[0] * rot_axis[1] - s * rot_axis[2], t * rot_axis[0] * rot_axis[2] + s * rot_axis[1]), (t * rot_axis[0] * rot_axis[1] + s * rot_axis[2], t * rot_axis[1] * rot_axis[1] + c, t * rot_axis[1] * rot_axis[2] - s * rot_axis[0]), (t * rot_axis[0] * rot_axis[2] - s * rot_axis[1], t * rot_axis[1] * rot_axis[2] + s * rot_axis[0], t * rot_axis[2] * rot_axis[2] + c))
def _get_axes_vectors(self): """calculate the model vectors along the screen's x and y axes""" # The "up" vector defines, in what proportion each axis of the model is # in line with the screen's y axis. v_up = self.view["up"] factors_y = (number(v_up[0]), number(v_up[1]), number(v_up[2])) # Calculate the proportion of each model axis according to the x axis of # the screen. distv = self.view["distance"] distv = pnormalized((distv[0], distv[1], distv[2])) factors_x = pnormalized(pcross(distv, (v_up[0], v_up[1], v_up[2]))) return (factors_x, factors_y)
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 intersect_triangle(self, triangle, counter_clockwise=False): """ Returns the line of intersection of a triangle with a plane. "None" is returned, if: - the triangle does not intersect with the plane - all vertices of the triangle are on the plane The line always runs clockwise through the triangle. """ # don't import Line in the header -> circular import from pycam.Geometry.Line import Line collisions = [] for edge, point in ((triangle.e1, triangle.p1), (triangle.e2, triangle.p2), (triangle.e3, triangle.p3)): cp, l = self.intersect_point(edge.dir, point) # filter all real collisions # We don't want to count vertices double -> thus we only accept # a distance that is lower than the length of the edge. if (cp is not None) and (-epsilon < l < edge.len - epsilon): collisions.append(cp) elif (cp is None) and (pdot(self.n, edge.dir) == 0): cp, dist = self.intersect_point(self.n, point) if abs(dist) < epsilon: # the edge is on the plane collisions.append(point) if len(collisions) == 3: # All points of the triangle are on the plane. # We don't return a waterline, as there should be another non-flat # triangle with the same waterline. return None if len(collisions) == 2: collision_line = Line(collisions[0], collisions[1]) # no further calculation, if the line is zero-sized if collision_line.len == 0: return collision_line cross = pcross(self.n, collision_line.dir) if (pdot(cross, triangle.normal) < 0) == bool(not counter_clockwise): # anti-clockwise direction -> revert the direction of the line collision_line = Line(collision_line.p2, collision_line.p1) return collision_line elif len(collisions) == 1: # only one point is on the plane # This waterline (with zero length) should be of no use. return None else: return None
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 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 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_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 get_area(self): cross = pcross(psub(self.p2, self.p1), psub(self.p3, self.p1)) return pnorm(cross) / 2
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 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 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