示例#1
0
    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)
示例#2
0
 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
示例#3
0
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)
示例#4
0
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)
示例#5
0
 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"
示例#6
0
 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"
示例#7
0
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)
示例#8
0
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)
示例#9
0
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)
示例#10
0
 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
示例#11
0
文件: Plane.py 项目: zancas/pycam
 def reset_cache(self):
     # we need to prevent the "normal" from growing
     norm = pnormalized(self.n)
     if norm:
         self.n = norm
示例#12
0
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)
示例#13
0
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
示例#14
0
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
示例#15
0
 def dir(self):
     return pnormalized(self.vector)
示例#16
0
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
示例#17
0
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
示例#18
0
 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()
示例#19
0
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]
示例#20
0
    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
示例#21
0
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
示例#22
0
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