コード例 #1
0
 def lineBetweenTwoPlanes(line, planeInf, planeSup):
     """\
     returns a tuple that contains :
      - the case encountered (aa, ab, ac, bb, bc, cc)
      - the line between the two planes (cases ab-c et bb-c) else None
     """
     p1, p2 = (line.p1, line.p2) if line.p1.z <= line.p2.z else (line.p2,
                                                                 line.p1)
     direction = p2.sub(p1)
     if p1.z <= planeInf.p.z:
         if p2.z <= planeInf.p.z:
             return ('aa', None)
         elif p2.z < planeSup.p.z:
             sec, l = planeInf.intersect_point(direction, p1)
             return ('ab', Line(sec, p2))
         elif p2.z >= planeSup.p.z:
             sec1, l1 = planeInf.intersect_point(direction, p1)
             sec2, l2 = planeSup.intersect_point(direction, p2)
             return ('ac', Line(sec1, sec2))
     elif p1.z < planeSup.p.z:
         if p2.z < planeSup.p.z:
             return ('bb', line)
         elif p2.z >= planeSup.p.z:
             sec, l = planeSup.intersect_point(direction, p2)
             return ('bc', Line(p2, sec))
     elif p1.z >= planeSup.p.z:
         return ('cc', None)
コード例 #2
0
ファイル: Triangle.py プロジェクト: yummyburger/pycam
 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)
コード例 #3
0
 def test_line(self):
     """Circle->Line collisions"""
     func = pycam.Geometry.intersection.intersect_circle_line
     func_args = [
         self._circle["center"], self._circle["axis"],
         self._circle["radius"], self._circle["radius"]**2
     ]
     # additional arguments: direction, edge
     """
     edge = Line((-1, -1, 4), (5, 5, 4))
     coll = func(*(func_args + [(0, 0, -1)] + [edge]))
     # The collision point seems to be the middle of the line.
     # This is technically not necessary, but the current algorithm does it this way.
     self.assert_collision_equal(((1.5, 1.5, 10), (1.5, 1.5, 4), 6), coll)
     """
     """
     # line dips into circle
     edge = Line((4, 1, 3), (10, 1, 5))
     coll = func(*(func_args + [(0, 0, -1)] + [edge]))
     #self.assert_collision_equal(((-1, 1, 10), (-1, 1, 2), 8), coll)
     self.assert_collision_equal(((5, 1, 10), (5, 1, 3.3333333333), 6.666666666), coll)
     # horizontally skewed line
     edge = Line((2, 1, 3), (8, 1, 5))
     coll = func(*(func_args + [(0, 0, -1)] + [edge]))
     #self.assert_collision_equal(((-1, 1, 10), (-1, 1, 2), 8), coll)
     self.assert_collision_equal(((5, 1, 10), (5, 1, 4), 6), coll)
     """
     # line touches circle
     edge = Line((10, 10, 4), (5, 1, 4))
     coll = func(*(func_args + [(0, 0, -1)] + [edge]))
     self.assert_collision_equal(((5, 1, 10), (5, 1, 4), 6), coll)
     # no collision
     edge = Line((10, 10, 4), (5.001, 1, 4))
     coll = func(*(func_args + [(0, 0, -1)] + [edge]))
     self.assert_collision_equal((None, None, INFINITE), coll)
コード例 #4
0
ファイル: MotionGrid.py プロジェクト: TurBoss/pycam
def get_lines_layer(lines,
                    z,
                    last_z=None,
                    step_width=None,
                    milling_style=MillingStyle.CONVENTIONAL):
    get_proj_point = lambda proj_point: (proj_point[0], proj_point[1], z)
    projected_lines = []
    for line in lines:
        if (last_z is not None) and (last_z < line.minz):
            # the line was processed before
            continue
        elif line.minz < z < line.maxz:
            # Split the line at the point at z level and do the calculation
            # for both point pairs.
            factor = (z - line.p1[2]) / (line.p2[2] - line.p1[2])
            plane_point = padd(line.p1, pmul(line.vector, factor))
            if line.p1[2] < z:
                p1 = get_proj_point(line.p1)
                p2 = line.p2
            else:
                p1 = line.p1
                p2 = get_proj_point(line.p2)
            projected_lines.append(Line(p1, plane_point))
            yield Line(plane_point, p2)
        elif line.minz < last_z < line.maxz:
            plane = Plane((0, 0, last_z), (0, 0, 1, 'v'))
            cp = plane.intersect_point(line.dir, line.p1)[0]
            # we can be sure that there is an intersection
            if line.p1[2] > last_z:
                p1, p2 = cp, line.p2
            else:
                p1, p2 = line.p1, cp
            projected_lines.append(Line(p1, p2))
        else:
            if line.maxz <= z:
                # the line is completely below z
                projected_lines.append(
                    Line(get_proj_point(line.p1), get_proj_point(line.p2)))
            elif line.minz >= z:
                projected_lines.append(line)
            else:
                _log.warn(
                    "Unexpected condition 'get_lines_layer': %s / %s / %s / %s",
                    line.p1, line.p2, z, last_z)
    # process all projected lines
    for line in projected_lines:
        points = []
        if step_width is None:
            points.append(line.p1)
            points.append(line.p2)
        else:
            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)
        yield points
コード例 #5
0
ファイル: ContourFollow.py プロジェクト: patyork/pycam
    def extend_shifted_lines(self):
        # TODO: improve the code below to handle "holes" properly (neighbours
        # that disappear due to a negative collision distance - use the example
        # "SampleScene.stl" as a reference)
        def get_right_neighbour(group, ref):
            group_len = len(group)
            # limit the search for a neighbour for non-closed groups
            if self.waterlines[group[0]].p1 == self.waterlines[group[-1]].p2:
                index_range = range(ref + 1, ref + group_len)
            else:
                index_range = range(ref + 1, group_len)
            for index in index_range:
                line_id = group[index % group_len]
                if not self.shifted_lines[line_id] is None:
                    return line_id
            else:
                return None

        groups = self._get_groups()
        for group in groups:
            index = 0
            while index < len(group):
                current = group[index]
                current_shifted = self.shifted_lines[current]
                if current_shifted is None:
                    index += 1
                    continue
                neighbour = get_right_neighbour(group, index)
                if neighbour is None:
                    # no right neighbour available
                    break
                neighbour_shifted = self.shifted_lines[neighbour]
                if current_shifted.p2 == neighbour_shifted.p1:
                    index += 1
                    continue
                cp, dist = current_shifted.get_intersection(
                    neighbour_shifted, infinite_lines=True)
                cp2, dist2 = neighbour_shifted.get_intersection(
                    current_shifted, infinite_lines=True)
                # TODO: add an arc (composed of lines) for a soft corner (not
                # required, but nicer)
                if dist < epsilon:
                    self.shifted_lines[current] = None
                    index -= 1
                elif dist2 > 1 - epsilon:
                    self.shifted_lines[neighbour] = None
                else:
                    self.shifted_lines[current] = Line(current_shifted.p1, cp)
                    self.shifted_lines[neighbour] = Line(
                        cp, neighbour_shifted.p2)
                    index += 1
コード例 #6
0
ファイル: EngraveCutter.py プロジェクト: weeberp/MakerDroid
 def GenerateToolPathLinePush(self, pa, line, z, previous_z,
         draw_callback=None):
     if previous_z <= line.minz:
         # the line is completely above the previous level
         pass
     elif line.minz < z < line.maxz:
         # Split the line at the point at z level and do the calculation
         # for both point pairs.
         factor = (z - line.p1.z) / (line.p2.z - line.p1.z)
         plane_point = line.p1.add(line.vector.mul(factor))
         self.GenerateToolPathLinePush(pa, Line(line.p1, plane_point), z,
                 previous_z, draw_callback=draw_callback)
         self.GenerateToolPathLinePush(pa, Line(plane_point, line.p2), z,
                 previous_z, draw_callback=draw_callback)
     elif line.minz < previous_z < line.maxz:
         plane = Plane(Point(0, 0, previous_z), Vector(0, 0, 1))
         cp = plane.intersect_point(line.dir, line.p1)[0]
         # we can be sure that there is an intersection
         if line.p1.z > previous_z:
             p1, p2 = cp, line.p2
         else:
             p1, p2 = line.p1, cp
         self.GenerateToolPathLinePush(pa, Line(p1, p2), z, previous_z,
                 draw_callback=draw_callback)
     else:
         if line.maxz <= z:
             # the line is completely below z
             p1 = Point(line.p1.x, line.p1.y, z)
             p2 = Point(line.p2.x, line.p2.y, z)
         elif line.minz >= z:
             p1 = line.p1
             p2 = line.p2
         else:
             log.warn("Unexpected condition EC_GTPLP: %s / %s / %s / %s" % \
                     (line.p1, line.p2, z, previous_z))
             return
         # no model -> no possible obstacles
         # model is completely below z (e.g. support bridges) -> no obstacles
         relevant_models = [m for m in self.models if m.maxz >= z]
         if not relevant_models:
             points = [p1, p2]
         elif self.physics:
             points = get_free_paths_ode(self.physics, p1, p2)
         else:
             points = get_free_paths_triangles(relevant_models, self.cutter,
                     p1, p2)
         if points:
             for point in points:
                 pa.append(point)
             if draw_callback:
                 draw_callback(tool_position=points[-1], toolpath=pa.paths)
コード例 #7
0
ファイル: Polygon.py プロジェクト: valeriob01/pycam
 def get_lines(self):
     """ Caching is necessary to avoid constant recalculation for visualization. """
     if self._lines_cache is None:
         # recalculate the line cache
         lines = []
         for index in range(len(self._points) - 1):
             lines.append(Line(self._points[index],
                               self._points[index + 1]))
         # Connect the last point with the first only if the polygon is
         # closed.
         if self.is_closed:
             lines.append(Line(self._points[-1], self._points[0]))
         self._lines_cache = lines
     return self._lines_cache[:]
コード例 #8
0
ファイル: test_polygon.py プロジェクト: yummyburger/pycam
def test_get_offset_polygons_truncated_square_inside_small_offset():
    """This tests a "truncated square", which is a square with the
    corners shaved off, and an inside offset that's small compared to
    the corner chamfers."""

    # 'expected_inside_p' is the expected offset polygon with a *negative*
    # offset, so it's inside the input polygon.
    lines = (Line((1.4142135623730951, 1.0, 0.0),
                  (8.585786437626904, 1.0, 0.0)),
             Line((8.585786437626904, 1.0, 0.0),
                  (9.0, 1.4142135623730951, 0.0)),
             Line((9.0, 1.4142135623730951, 0.0),
                  (9.0, 8.585786437626904, 0.0)),
             Line((9.0, 8.585786437626904, 0.0),
                  (8.585786437626904, 9.0, 0.0)),
             Line((8.585786437626904, 9.0, 0.0),
                  (1.4142135623730951, 9.0, 0.0)),
             Line((1.4142135623730951, 9.0, 0.0),
                  (1.0, 8.585786437626904, 0.0)),
             Line((1.0, 8.585786437626904, 0.0),
                  (1.0, 1.4142135623730951, 0.0)),
             Line((1.0, 1.4142135623730951, 0.0),
                  (1.4142135623730951, 1.0, 0.0)))

    expected_inside_p = Polygon()
    for line in lines:
        expected_inside_p.append(line)

    output_p = truncated_square_p.get_offset_polygons(-1)
    print("get_offset_polygons() returned:")
    for p in output_p:
        print(str(p))
    assert (len(output_p) == 1)
    assert_polygons_are_identical(output_p[0], expected_inside_p)
コード例 #9
0
ファイル: Filters.py プロジェクト: patyork/pycam
 def filter_toolpath(self, toolpath):
     new_path = []
     last_pos = None
     optional_moves = []
     for move_type, args in toolpath:
         if move_type in (MOVE_STRAIGHT, MOVE_STRAIGHT_RAPID):
             if last_pos:
                 # find all remaining pieces of this line
                 inner_lines = []
                 for polygon in self.settings["polygons"]:
                     inner, outer = polygon.split_line(Line(last_pos, args))
                     inner_lines.extend(inner)
                 # turn these lines into moves
                 for line in inner_lines:
                     if pdist(line.p1, last_pos) > epsilon:
                         new_path.append((MOVE_SAFETY, None))
                         new_path.append((move_type, line.p1))
                     else:
                         # we continue were we left
                         if optional_moves:
                             new_path.extend(optional_moves)
                             optional_moves = []
                     new_path.append((move_type, line.p2))
                     last_pos = line.p2
                 optional_moves = []
                 # finish the line by moving to its end (if necessary)
                 if pdist(last_pos, args) > epsilon:
                     optional_moves.append((MOVE_SAFETY, None))
                     optional_moves.append((move_type, args))
             last_pos = args
         elif move_type == MOVE_SAFETY:
             optional_moves = []
         else:
             new_path.append((move_type, args))
     return new_path
コード例 #10
0
 def mergeRiddanceLines(lines):
     """\
     returns not-layered part of the lines
     lines must be aligned
     """
     if len(lines) <= 1: return lines
     points = {}
     for line in lines:
         if line.p1 > line.p2: line.p1, line.p2 = line.p2, line.p1
         if line.p1 not in points:
             points[line.p1] = 1
         else:
             points[line.p1] += 1
         if line.p2 not in points:
             points[line.p2] = -1
         else:
             points[line.p2] -= 1
     order = sorted(points.keys())
     counter = 0
     start = None
     uniques = []
     for point in order:
         counter += points[point]
         if counter == 1 and start is None:
             start = point
         elif counter != 1 and start is not None:
             if cmp(start, point):
                 uniques.append(Line(start, point))
             start = None
     return uniques
コード例 #11
0
 def get_outside_lines(poly1, poly2):
     result = []
     for line in poly1.get_lines():
         collisions = []
         for o_line in poly2.get_lines():
             cp, dist = o_line.get_intersection(line)
             if (cp is not None) and (0 < dist < 1):
                 collisions.append((cp, dist))
         # sort the collisions according to the distance
         collisions.append((line.p1, 0))
         collisions.append((line.p2, 1))
         collisions.sort(key=lambda collision: collision[1])
         for index in range(len(collisions) - 1):
             p1 = collisions[index][0]
             p2 = collisions[index + 1][0]
             if pdist(p1, p2) < epsilon:
                 # ignore zero-length lines
                 continue
             # Use the middle between p1 and p2 to check the
             # inner/outer state.
             p_middle = pdiv(padd(p1, p2), 2)
             p_inside = (poly2.is_point_inside(p_middle)
                         and not poly2.is_point_on_outline(p_middle))
             if not p_inside:
                 result.append(Line(p1, p2))
     return result
コード例 #12
0
 def inside(self) :
     p1 = self.get_center()
     p2 = Point(Box.grid.minx-1, p1.y, p1.z) # TODO: implement algo to find nearest border
     escaping_line = Line(p1, p2)
     cuts = 0
     box = self
     lines = set()
     while box.x != 0 :
         box = box.get_neighbour(-1, 0, 0)
         for line in box.lines :
             if line.id not in lines :
                 cut, d = escaping_line.get_intersection(line)
                 if cut is not None :
                     cuts += 1
                 lines.add(line.id)
     return cuts%2
コード例 #13
0
ファイル: test_polygon.py プロジェクト: yummyburger/pycam
def test_get_offset_polygons_square_inside():
    # 'expected_inside_p' is the expected offset polygon with a *negative*
    # offset, so it's inside the input polygon.
    lines = (Line((1, 1, 0), (9, 1, 0)), Line(
        (9, 1, 0), (9, 9, 0)), Line((9, 9, 0),
                                    (1, 9, 0)), Line((1, 9, 0), (1, 1, 0)))
    expected_inside_p = Polygon()
    for line in lines:
        expected_inside_p.append(line)

    output_p = square_p.get_offset_polygons(-1)
    print("get_offset_polygons() returned:")
    for p in output_p:
        print(str(p))
    assert (len(output_p) == 1)
    assert_polygons_are_identical(output_p[0], expected_inside_p)
コード例 #14
0
    def split_line(self, line):
        outer = []
        inner = []
        # project the line onto the polygon's plane
        proj_line = self.plane.get_line_projection(line)
        intersections = []
        for pline in self.get_lines():
            cp, d = proj_line.get_intersection(pline)
            if cp:
                intersections.append((cp, d))
        # sort the intersections by distance
        intersections.sort(key=lambda collision: collision[1])
        intersections.insert(0, (proj_line.p1, 0))
        intersections.append((proj_line.p2, 1))

        def get_original_point(d):
            return padd(line.p1, pmul(line.vector, d))

        for index in range(len(intersections) - 1):
            p1, d1 = intersections[index]
            p2, d2 = intersections[index + 1]
            if p1 != p2:
                middle = pdiv(padd(p1, p2), 2)
                new_line = Line(get_original_point(d1), get_original_point(d2))
                if self.is_point_inside(middle):
                    inner.append(new_line)
                else:
                    outer.append(new_line)
        return (inner, outer)
コード例 #15
0
ファイル: Model.py プロジェクト: zancas/pycam
 def get_line(i1, i2):
     a = list(coords[i1 % 4])
     b = list(coords[i2 % 4])
     # the contour points of the model will always be at level zero
     a[2] = self.z_level
     b[2] = self.z_level
     return Line(a, b)
コード例 #16
0
 def discretise_line(
     self, line
 ):  # TODO: OPTIMISER !!!!! (pas de boucle, un seul cas avec 2 passages, calcul de l'intervale sans list.index)
     # on discrétise les deux extrémités du segment
     c1, c2 = self.discretiser_point(ligne.p1), self.discretiser_point(
         ligne.p2)
     c1.lignes.append(ligne)
     c2.lignes.append(ligne)
     if ligne.p1.x == ligne.p2.x and ligne.p1.x in self.rangex:  # la ligne appartient au pavage vertical
         # alors on va ajouter toutes les paires de cases qu'elle rencontre
         x = self.rangex.index(ligne.p1.x)
         if c1.y > c2.y: c1, c2 = c2, c1
         for y in range(c1.y, c2.y + 1):
             self.get_case(c1.z, x, y).lignes.append(ligne)
     elif ligne.p1.y == ligne.p2.y and ligne.p1.y in self.rangey:  # la ligne appartient au pavage horizontal
         y = self.rangey.index(ligne.p1.y)
         if c1.x > c2.x: c1, c2 = c2, c1
         for x in range(c1.x, c2.x + 1):
             self.get_case(c1.z, x, y).lignes.append(ligne)
     else:  # sinon la ligne est simplement verticale, horizontale ou oblique
         # alors on va discrétiser toutes les cases traversées
         # d'abord les intersections verticales
         ordx = 1
         if c1.x > c2.x: c1, c2 = c2, c1  # pour itérer dans le bon sens
         for x in self.rangex[c1.x + 1:c2.x]:
             # pour cela on calcule l'intersection de la ligne avec la grille des X
             sec, d = ligne.get_intersection(Line(Point(x, self.miny, ligne.p1.z), \
                                             Point(x, self.maxy, ligne.p1.z)))
             if sec is None:  # il n'y a pas intersection : les deux lignes sont parallèles et distinctes
                 break  # la ligne sera traitée par l'itération verticale
             # puis on trouve à quelle colonne l'intersection appartient
             dsec = self.Case.get_emplacement(0, sec.y, 0)
             # et on discrétise alors la case correspondante
             self.get_case(c1.z, c1.x + ordx, dsec[1]).lignes.append(ligne)
             # le [1] correspond à la composante Y de la coordonnée
             ordx += 1
         # puis celles horizontales de la même manière
         ordy = 1
         if c1.y > c2.y: c1, c2 = c2, c1
         for y in self.rangey[c1.y + 1:c2.y]:
             sec, d = ligne.get_intersection(Line(Point(self.minx, y, ligne.p1.z), \
                                             Point(self.maxx, y, ligne.p1.z)))
             if sec is None:
                 break  # la ligne est horizontale, et donc déjà traitée
             dsec = self.Case.get_emplacement(sec.x, 0, 0)
             self.get_case(c1.z, dsec[0], c1.y + ordy).lignes.append(ligne)
             ordy += 1
コード例 #17
0
ファイル: Plane.py プロジェクト: patyork/pycam
 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 (not cp is 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
コード例 #18
0
 def inside(self):
     p1 = self.get_center()
     p2 = Point(Box.grid.minx - 1, p1.y,
                p1.z)  # TODO: implement algo to find nearest border
     escaping_line = Line(p1, p2)
     cuts = 0
     box = self
     lines = set()
     while box.x != 0:
         box = box.get_neighbour(-1, 0, 0)
         for line in box.lines:
             if line.id not in lines:
                 cut, d = escaping_line.get_intersection(line)
                 if cut is not None:
                     cuts += 1
                 lines.add(line.id)
     return cuts % 2
コード例 #19
0
def _offset_loops_to_polygons(offset_loops):
    model = pycam.Geometry.Model.ContourModel()
    before = None
    for n_loop, loop in enumerate(offset_loops):
        lines = []
        _log.info("loop #%d has %d lines/arcs", n_loop, len(loop))
        for n_segment, item in enumerate(loop):
            _log.info("%d -> %s", n_segment, item)
            point, radius = item[:2]
            point = (point.x, point.y, 0.0)
            if before is not None:
                if radius == -1:
                    lines.append(Line(before, point))
                    _log.info("%d line %s to %s", n_segment, before, point)
                else:
                    _log.info("%d arc %s to %s r=%f", n_segment, before, point,
                              radius)
                    center, clock_wise = item[2:4]
                    center = (center.x, center.y, 0.0)
                    direction_before = (before[0] - center[0],
                                        before[1] - center[1], 0.0)
                    direction_end = (point[0] - center[0],
                                     point[1] - center[1], 0.0)
                    angles = [
                        180.0 * get_angle_pi((1.0, 0.0, 0.0), (0, 0.0, 0.0),
                                             direction, (0.0, 0.0, 1.0),
                                             pi_factor=True)
                        for direction in (direction_before, direction_end)
                    ]
                    if clock_wise:
                        angles.reverse()
                    points = get_points_of_arc(center, radius, angles[0],
                                               angles[1])
                    last_p = before
                    for p in points:
                        lines.append(Line(last_p, p))
                        last_p = p
            before = point
        for line in lines:
            if line.len > epsilon:
                model.append(line)
    return model.get_polygons()
コード例 #20
0
 def parse_polyline(self, init):
     params = self._open_sequence_params
     if init:
         self._open_sequence = "POLYLINE"
         self._open_sequence_items = []
         key, value = self._read_key_value()
         while (key is not None) and (key != self.KEYS["MARKER"]):
             if key == self.KEYS["CURVE_TYPE"]:
                 if value == 8:
                     params["CURVE_TYPE"] = "BEZIER"
             elif key == self.KEYS["ENTITY_FLAGS"]:
                 if value == 1:
                     if "ENTITY_FLAGS" not in params:
                         params["ENTITY_FLAGS"] = set()
                     params["ENTITY_FLAGS"].add("IS_CLOSED")
             key, value = self._read_key_value()
         if key is not None:
             self._push_on_stack(key, value)
     else:
         # closing
         if ("CURVE_TYPE" in params) and (params["CURVE_TYPE"] == "BEZIER"):
             self.lines.extend(get_bezier_lines(self._open_sequence_items))
             if ("ENTITY_FLAGS" in params) and ("IS_CLOSED"
                                                in params["ENTITY_FLAGS"]):
                 # repeat the same polyline on the other side
                 self._open_sequence_items.reverse()
                 self.lines.extend(
                     get_bezier_lines(self._open_sequence_items))
         else:
             points = [p for p, bulge in self._open_sequence_items]
             for index in range(len(points) - 1):
                 point = points[index]
                 next_point = points[index + 1]
                 if point != next_point:
                     self.lines.append(Line(point, next_point))
             if ("ENTITY_FLAGS" in params) and ("IS_CLOSED"
                                                in params["ENTITY_FLAGS"]):
                 # repeat the same polyline on the other side
                 self.lines.append(Line(points[-1], points[0]))
         self._open_sequence_items = []
         self._open_sequence_params = {}
         self._open_sequence = None
コード例 #21
0
ファイル: DXFImporter.py プロジェクト: patyork/pycam
 def parse_polyline(self, init):
     start_line = self.line_number
     params = self._open_sequence_params
     if init:
         self._open_sequence = "POLYLINE"
         self._open_sequence_items = []
         key, value = self._read_key_value()
         while (not key is None) and (key != self.KEYS["MARKER"]):
             if key == self.KEYS["CURVE_TYPE"]:
                 if value == 8:
                     params["CURVE_TYPE"] = "BEZIER"
             elif key == self.KEYS["VERTEX_FLAGS"]:
                 if value == 1:
                     params["VERTEX_FLAGS"] = "EXTRA_VERTEX"
             key, value = self._read_key_value()
         if not key is None:
             self._push_on_stack(key, value)
     else:
         # closing
         if ("CURVE_TYPE" in params) and (params["CURVE_TYPE"] == "BEZIER"):
             self.lines.extend(
                 pycam.Geometry.get_bezier_lines(self._open_sequence_items))
             if ("VERTEX_FLAGS" in params) and \
                     (params["VERTEX_FLAGS"] == "EXTRA_VERTEX"):
                 # repeat the same polyline on the other side
                 self._open_sequence_items.reverse()
                 self.lines.extend(
                     pycam.Geometry.get_bezier_lines(
                         self._open_sequence_items))
         else:
             points = [p for p, bulge in self._open_sequence_items]
             for index in range(len(points) - 1):
                 point = points[index]
                 next_point = points[index + 1]
                 if point != next_point:
                     self.lines.append(Line(point, next_point))
             if ("VERTEX_FLAGS" in params) and (params["VERTEX_FLAGS"]
                                                == "EXTRA_VERTEX"):
                 self.lines.append(Line(points[-1], points[0]))
         self._open_sequence_items = []
         self._open_sequence_params = {}
         self._open_sequence = None
コード例 #22
0
ファイル: Letters.py プロジェクト: patyork/pycam
 def get_positioned_lines(self, base_point, skew=None):
     result = []
     get_skewed_point = lambda p: (base_point[0] + p[0] + (p[1] * skew / 100.0), base_point[1] + p[1], base_point[2])
     for line in self.lines:
         skewed_p1 = get_skewed_point(line.p1)
         skewed_p2 = get_skewed_point(line.p2)
         # Some triplex fonts contain zero-length lines
         # (e.g. "/" in italict.cxf). Ignore these.
         if skewed_p1 != skewed_p2:
             new_line = Line(skewed_p1, skewed_p2)
             result.append(new_line)
     return result
コード例 #23
0
ファイル: Triangle.py プロジェクト: weeberp/MakerDroid
 def reset_cache(self):
     self.minx = min(self.p1.x, self.p2.x, self.p3.x)
     self.miny = min(self.p1.y, self.p2.y, self.p3.y)
     self.minz = min(self.p1.z, self.p2.z, self.p3.z)
     self.maxx = max(self.p1.x, self.p2.x, self.p3.x)
     self.maxy = max(self.p1.y, self.p2.y, self.p3.y)
     self.maxz = max(self.p1.z, self.p2.z, self.p3.z)
     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 = self.p3.sub(self.p1).cross(self.p2.sub( \
                 self.p1)).normalized()
     if not isinstance(self.normal, Vector):
         self.normal = self.normal.get_vector()
     # make sure that the normal has always a unit length
     self.normal = self.normal.normalized()
     self.center = self.p1.add(self.p2).add(self.p3).div(3)
     self.plane = Plane(self.center, self.normal)
     # calculate circumcircle (resulting in radius and middle)
     denom = self.p2.sub(self.p1).cross(self.p3.sub(self.p2)).norm
     self.radius = (self.p2.sub(self.p1).norm \
             * self.p3.sub(self.p2).norm * self.p3.sub(self.p1).norm) \
             / (2 * denom)
     self.radiussq = self.radius**2
     denom2 = 2 * denom * denom
     alpha = self.p3.sub(self.p2).normsq \
             * self.p1.sub(self.p2).dot(self.p1.sub(self.p3)) / denom2
     beta  = self.p1.sub(self.p3).normsq \
             * self.p2.sub(self.p1).dot(self.p2.sub(self.p3)) / denom2
     gamma = self.p1.sub(self.p2).normsq \
             * self.p3.sub(self.p1).dot(self.p3.sub(self.p2)) / denom2
     self.middle = Point(
         self.p1.x * alpha + self.p2.x * beta + self.p3.x * gamma,
         self.p1.y * alpha + self.p2.y * beta + self.p3.y * gamma,
         self.p1.z * alpha + self.p2.z * beta + self.p3.z * gamma)
コード例 #24
0
ファイル: Polygon.py プロジェクト: yummyburger/pycam
 def get_plane_projection(self, plane):
     if plane == self.plane:
         return self
     elif pdot(plane.n, self.plane.n) == 0:
         log.warn("Polygon projection onto plane: orthogonal projection is not possible")
         return None
     else:
         result = Polygon(plane)
         for line in self.get_lines():
             p1 = plane.get_point_projection(line.p1)
             p2 = plane.get_point_projection(line.p2)
             result.append(Line(p1, p2))
         # check if the projection would revert the direction of the polygon
         if pdot(plane.n, self.plane.n) < 0:
             result.reverse_direction()
         return result
コード例 #25
0
def get_shifted_waterline(up_vector, waterline, cutter_location):
    # Project the waterline and the cutter location down to the slice plane.
    # This is necessary for calculating the horizontal distance between the
    # cutter and the triangle waterline.
    plane = Plane(cutter_location, up_vector)
    wl_proj = plane.get_line_projection(waterline)
    if wl_proj.len < epsilon:
        return None
    offset = wl_proj.dist_to_point(cutter_location)
    if offset < epsilon:
        return wl_proj
    # shift both ends of the waterline towards the cutter location
    shift = cutter_location.sub(wl_proj.closest_point(cutter_location))
    # increase the shift width slightly to avoid "touch" collisions
    shift = shift.mul(1.0 + epsilon)
    shifted_waterline = Line(wl_proj.p1.add(shift), wl_proj.p2.add(shift))
    return shifted_waterline
コード例 #26
0
ファイル: __init__.py プロジェクト: signotheque/pycam
 def crop(self, polygons, callback=None):
     # collect all existing toolpath lines
     open_lines = []
     for path in self.paths:
         if path:
             for index in range(len(path.points) - 1):
                 open_lines.append(
                     Line(path.points[index], path.points[index + 1]))
     # go through all polygons and add "inner" lines (or parts thereof) to
     # the final list of remaining lines
     inner_lines = []
     for polygon in polygons:
         new_open_lines = []
         for line in open_lines:
             if callback and callback():
                 return
             inner, outer = polygon.split_line(line)
             inner_lines.extend(inner)
             new_open_lines.extend(outer)
         open_lines = new_open_lines
     # turn all "inner_lines" into toolpath moves
     new_paths = []
     current_path = Path()
     if inner_lines:
         line = inner_lines.pop(0)
         current_path.append(line.p1)
         current_path.append(line.p2)
     while inner_lines:
         if callback and callback():
             return
         end = current_path.points[-1]
         # look for the next connected point
         for line in inner_lines:
             if line.p1 == end:
                 inner_lines.remove(line)
                 current_path.append(line.p2)
                 break
         else:
             new_paths.append(current_path)
             current_path = Path()
             line = inner_lines.pop(0)
             current_path.append(line.p1)
             current_path.append(line.p2)
     if current_path.points:
         new_paths.append(current_path)
     self.paths = new_paths
コード例 #27
0
 def parse_line(self):
     start_line = self.line_number
     # the z-level defaults to zero (for 2D models)
     p1 = [None, None, 0]
     p2 = [None, None, 0]
     color = None
     key, value = self._read_key_value()
     while (key is not None) and (key != self.KEYS["MARKER"]):
         if key == self.KEYS["P1_X"]:
             p1[0] = value
         elif key == self.KEYS["P1_Y"]:
             p1[1] = value
         elif key == self.KEYS["P1_Z"]:
             p1[2] = value
         elif key == self.KEYS["P2_X"]:
             p2[0] = value
         elif key == self.KEYS["P2_Y"]:
             p2[1] = value
         elif key == self.KEYS["P2_Z"]:
             p2[2] = value
         elif key == self.KEYS["COLOR"]:
             color = value
         else:
             pass
         key, value = self._read_key_value()
     end_line = self.line_number
     # The last lines were not used - they are just the marker for the next
     # item.
     if key is not None:
         self._push_on_stack(key, value)
     if (None in p1) or (None in p2):
         log.warn(
             "DXFImporter: Incomplete LINE definition between line %d and %d",
             start_line, end_line)
     else:
         if self._color_as_height and (color is not None):
             # use the color code as the z coordinate
             p1[2] = float(color) / 255
             p2[2] = float(color) / 255
         line = Line((p1[0], p1[1], p1[2]), (p2[0], p2[1], p2[2]))
         if line.p1 != line.p2:
             self.lines.append(line)
         else:
             log.warn(
                 "DXFImporter: Ignoring zero-length LINE (between input line %d and %d): "
                 "%s", start_line, end_line, line)
コード例 #28
0
ファイル: DXFImporter.py プロジェクト: weeberp/MakerDroid
 def parse_polyline(self, init):
     start_line = self.line_number
     if init:
         self._open_sequence = "POLYLINE"
         self._open_sequence_items = []
         key, value = self._read_key_value()
         while (not key is None) and (key != self.KEYS["MARKER"]):
             key, value = self._read_key_value()
         if not key is None:
             self._push_on_stack(key, value)
     else:
         # closing
         points = self._open_sequence_items
         for index in range(len(points) - 1):
             point = points[index]
             next_point = points[index + 1]
             if point != next_point:
                 self.lines.append(Line(point, next_point))
         self._open_sequence_items = []
         self._open_sequence = None
コード例 #29
0
ファイル: Filters.py プロジェクト: zancas/pycam
 def filter_toolpath(self, toolpath):
     new_path = []
     last_pos = None
     optional_moves = []
     for step in toolpath:
         if step.action in MOVES_LIST:
             if last_pos:
                 # find all remaining pieces of this line
                 inner_lines = []
                 for polygon in self.settings["polygons"]:
                     inner, outer = polygon.split_line(
                         Line(last_pos, step.position))
                     inner_lines.extend(inner)
                 # turn these lines into moves
                 for line in inner_lines:
                     if pdist(line.p1, last_pos) > epsilon:
                         new_path.append(ToolpathSteps.MoveSafety())
                         new_path.append(
                             ToolpathSteps.get_step_class_by_action(
                                 step.action)(line.p1))
                     else:
                         # we continue where we left
                         if optional_moves:
                             new_path.extend(optional_moves)
                             optional_moves = []
                     new_path.append(
                         ToolpathSteps.get_step_class_by_action(
                             step.action)(line.p2))
                     last_pos = line.p2
                 optional_moves = []
                 # finish the line by moving to its end (if necessary)
                 if pdist(last_pos, step.position) > epsilon:
                     optional_moves.append(ToolpathSteps.MoveSafety())
                     optional_moves.append(step)
             last_pos = step.position
         elif step.action == MOVE_SAFETY:
             optional_moves = []
         else:
             new_path.append(step)
     return new_path
コード例 #30
0
def get_collision_waterline_of_triangle(model, cutter, up_vector, triangle, z):
    # TODO: there are problems with "material allowance > 0"
    plane = Plane(Point(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 not proj_p 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 edge.dir.cross(triangle.normal).dot(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 other_point.sub(edge.p1).cross(edge.dir).dot(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 edge.dir.cross(triangle.normal).dot(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.z > 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.z > 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 = other_point.sub(waterline.p1).cross(waterline.dir).dot(
                        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 edge.dir.cross(triangle.normal).dot(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 edge.dir.cross(triangle.normal).dot(up_vector) < 0:
                        outer_edges = [Line(edge.p2, edge.p1)]
                    else:
                        outer_edges = [edge]
            else:
                # two points above
                other_point = points_above[0]
                dot = other_point.sub(waterline.p1).cross(waterline.dir).dot(
                        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 edge.dir.cross(triangle.normal).dot(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 not p1 is p2:
                                edges.append(Line(p1, p2))
                    edges.sort(key=lambda x: x.len)
                    edge = edges[-1]
                    if edge.dir.cross(triangle.normal).dot(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 = up_vector.cross(edge.dir).normalized()
        if direction is None:
            continue
        direction = direction.mul(max_length)
        edge_dir = edge.p2.sub(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 = edge.p1.add(edge_dir.mul(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,
                    start.add(direction), return_triangles=True)
            for index, coll in enumerate(collisions):
                if (index % 2 == 0) and (not coll[1] is None) \
                        and (not coll[2] is None) \
                        and (coll[0].sub(start).dot(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 (len(result) == 0) and (triangle.minz > z):
        # None indicates that the triangle needs no further evaluation
        return None
    return result
コード例 #31
0
if __name__ == "__main__":
    import sys
    if len(sys.argv) > 1:
        import pycam.Importers.DXFImporter as importer
        model = importer.import_model(sys.argv[1])
    else:
        model = pycam.Geometry.Model.ContourModel()
        # convert some points to a 2D model
        points = ((0.0, 0.0, 0.0), (0.5, 0.0, 0.0), (0.5, 0.5, 0.0), (0.0, 0.0,
                                                                      0.0))
        print("original points: ", points)
        before = None
        for p in points:
            if before:
                model.append(Line(before, p))
            before = p
    if len(sys.argv) > 2:
        offset = float(sys.argv[2])
    else:
        offset = 0.4
    # scale model within a range of -1..1
    maxdim = max(model.maxx - model.minx, model.maxy - model.miny)
    # stay well below sqrt(2)/2 in all directions
    scale_value = 1.4 / maxdim
    print("Scaling factor: %f" % scale_value)
    model.scale(scale_value)
    shift_x = -(model.minx + (model.maxx - model.minx) / 2.0)
    shift_y = -(model.miny + (model.maxy - model.miny) / 2.0)
    print("Shifting x: ", shift_x)
    print("Shifting y: ", shift_y)
コード例 #32
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 d.dot(axis) == 0:
        if direction.dot(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 = pc.sub(center).normsq
        if d_sq >= radiussq:
            return (None, None, INFINITE)
        a = sqrt(radiussq - d_sq)
        d1 = p1.sub(pc).dot(d)
        d2 = p2.sub(pc).dot(d)
        ccp = None
        cp = None
        if abs(d1) < a - epsilon:
            ccp = p1
            cp = p1.sub(direction.mul(l))
        elif abs(d2) < a - epsilon:
            ccp = p2
            cp = p2.sub(direction.mul(l))
        elif ((d1 < -a + epsilon) and (d2 > a - epsilon)) \
                or ((d2 < -a + epsilon) and (d1 > a - epsilon)):
            ccp = pc
            cp = pc.sub(direction.mul(l))
        return (ccp, cp, -l)
    n = d.cross(direction)
    if n.norm == 0:
        # no contact point, but should check here if circle *always* intersects
        # line...
        return (None, None, INFINITE)
    n = n.normalized()
    # 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 = axis.cross(n)
    if v.norm == 0:
        return (None, None, INFINITE)
    v = v.normalized()
    # take plane through intersection line and parallel to axis
    n2 = v.cross(axis)
    if n2.norm == 0:
        return (None, None, INFINITE)
    n2 = n2.normalized()
    # distance from center to this plane
    dist = n2.dot(center) - n2.dot(lp)
    distsq = dist * dist
    if distsq > radiussq - epsilon:
        return (None, None, INFINITE)
    # must be on circle
    dist2 = sqrt(radiussq - distsq)
    if d.dot(axis) < 0:
        dist2 = -dist2
    ccp = center.sub(n2.mul(dist)).sub(v.mul(dist2))
    plane = Plane(edge.p1, d.cross(direction).cross(d))
    (cp, l) = plane.intersect_point(direction, ccp)
    return (ccp, cp, l)