Beispiel #1
0
 def _get_polygon_rays(self, vertices, clip_rect):
     """Return rays that emanate from convex vertices to the outside
     clipping rectangle.
     """
     rays = []
     for A, B, C in vertices:
         # Calculate the interior angle bisector segment
         # using the angle bisector theorem:
         # https://en.wikipedia.org/wiki/Angle_bisector_theorem
         AC = geom.Line(A, C)
         d1 = B.distance(C)
         d2 = B.distance(A)
         mu = d2 / (d1 + d2)
         D = AC.point_at(mu)
         bisector = geom.Line(D, B)
         # find the intersection with the clip rectangle
         dx = bisector.p2.x - bisector.p1.x
         dy = bisector.p2.y - bisector.p1.y
         # if dx is zero the line is vertical
         if geom.float_eq(dx, 0.0):
             y = clip_rect.ymax if dy > 0 else clip_rect.ymin
             x = bisector.p1.x
         else:
             # if slope is zero the line is horizontal
             m = dy / dx
             b = (m * -bisector.p1.x) + bisector.p1.y
             if dx > 0:
                 if geom.float_eq(m, 0.0):
                     y = b
                     x = clip_rect.xmax
                 else:
                     y = clip_rect.xmax * m + b
                     if m > 0:
                         y = min(clip_rect.ymax, y)
                     else:
                         y = max(clip_rect.ymin, y)
                     x = (y - b) / m
             else:
                 if geom.float_eq(m, 0.0):
                     y = b
                     x = self.clip_rect.xmin
                 else:
                     y = self.clip_rect.xmin * m + b
                     if m < 0:
                         y = min(clip_rect.ymax, y)
                     else:
                         y = max(clip_rect.ymin, y)
                     x = (y - b) / m
         clip_pt = geom.P(x, y)
         rays.append(geom.Line(bisector.p2, clip_pt))
     return rays
Beispiel #2
0
    def _cutpath_process_corners(self, cutpath):
        if len(cutpath) <= 1:
            return [cutpath,]
        cutpath_list = []
        new_cutpath = paths.Path(name=getattr(cutpath, 'name'))
        firstline = cutpath[0]
        line1 = firstline
        for line2 in cutpath[1:]:
            # TODO: also process curves using tangent lines
            if isinstance(line1, geom.Line) and isinstance(line2, geom.Line):
                corner_angle = line1.p2.angle2(line1.p1, line2.p2)
                new_segments = ()
                if self.allow_corner_reversals and abs(corner_angle) <= self.min_corner_angle:
                    new_segments = self._corner_reversal(line1, line2, corner_angle)
                elif abs(corner_angle) > self.min_corner_angle and not geom.float_eq(abs(corner_angle), math.pi):
                    new_segments = self._corner_turn(line1, line2, corner_angle)
                if new_segments:
                    new_cutpath.extend(new_segments[:-1])
                    line2 = new_segments[-1]
                else:
                    new_cutpath.append(line1)
                    if not geom.float_eq(abs(corner_angle), math.pi):
                        # Split this path if the corner can't be processed
                        cutpath_list.append(new_cutpath)
                        new_cutpath = paths.Path()
            else:
                new_cutpath.append(line1)
            line1 = line2
        new_cutpath.append(line2)
        # process last segment of polygon
        if self.close_polygons and new_cutpath.is_closed() \
        and isinstance(new_cutpath[-1], geom.Line) \
        and isinstance(firstline, geom.Line):
#        and geom.float_eq(new_cutpath[0].p1, new_cutpath[-1].p2):
            line1 = new_cutpath[-1]
            line2 = firstline
            corner_angle = line1.p2.angle2(line1.p1, line2.p2)
            new_segments = ()
            if self.allow_corner_reversals and abs(corner_angle) <= self.min_corner_angle:
                new_segments = self._corner_reversal(line1, line2, corner_angle)
            elif abs(corner_angle) > self.min_corner_angle and not geom.float_eq(abs(corner_angle), math.pi):
                new_segments = self._corner_turn(line1, line2, corner_angle)
            if new_segments:
                new_cutpath[0] = new_segments[-1]
                del new_cutpath[-1]
                new_cutpath.extend(new_segments[0:-1])
        if len(new_cutpath) > 0:
            cutpath_list.append(new_cutpath)
        return cutpath_list
Beispiel #3
0
    def _calc_rotation3(self, new_angle):
        """"""
        if geom.float_eq(self.current_angle, new_angle):
            return 0.0
        prev_angle = math.fmod(self.current_angle, 2*math.pi)
        new_angle = math.fmod(new_angle, 2*math.pi)
        rotation_angle = new_angle - prev_angle
#        logger.debug('rotation: %d, %d, %d' % (math.degrees(self.current_angle), math.degrees(new_angle), math.degrees(rotation_angle)))
        return rotation_angle
Beispiel #4
0
    def _cutpath_process_pathends(self, cutpath):
        # Brush landing
        segment = cutpath[0]
        if not isinstance(segment, geom.Line) and not isinstance(segment, geom.Arc):
            logging.debug('segment type=%s' % segment.__name__)
        assert(isinstance(segment, geom.Line) or isinstance(segment, geom.Arc))
        d = max(self.brush_trail_down, 0.01)
        if segment.length() > d:
            seg1, seg2 = segment.subdivide(d / segment.length())
            cutpath[0] = seg1
            cutpath.insert(1, seg2)
        cutpath[0].inline_z = self.brush_depth        
        # Brush liftoff
        segment = cutpath[-1]
        assert(isinstance(segment, geom.Line) or isinstance(segment, geom.Arc))
        brush_direction = getattr(segment, 'inline_end_angle', segment.end_tangent_angle())
        # Default overshoot distance works fine for 90d angles
        overshoot_dist = self.brush_trail_down#self.tool_width/2
        # Closed path?
        if geom.float_eq(segment.p2, cutpath[0].p1):
            # tangent to reverse brush direction
            t1 = geom.P.from_polar(1.0, brush_direction + math.pi)
            corner_angle = abs(cutpath[0].p1.angle2(cutpath[0].p1 + t1, cutpath[0].p2))
            if not geom.float_eq(corner_angle, math.pi):
#                overshoot_dist = self.brush_trail_down
#            else:
                # Calculate overshoot based on corner angle
                d = (math.pi - (corner_angle % math.pi)) / math.pi
                overshoot_dist *= d
                logger.debug('overshoot dist: %.4f, %.4f, %.4f' % (math.degrees(corner_angle), d, overshoot_dist))
#        logger.debug('brush_direction= %.4f [%.4f]' % (math.degrees(brush_direction), math.degrees(segment.end_tangent_angle())))
        delta = geom.P(math.cos(brush_direction), math.sin(brush_direction))
        overshoot_endp = segment.p2 + (delta * overshoot_dist)
        overshoot_line = geom.Line(segment.p2, overshoot_endp)
        liftoff_dist = self.brush_trail_down
        liftoff_endp = overshoot_endp + (delta * liftoff_dist)
        liftoff_line = geom.Line(overshoot_endp, liftoff_endp)
        liftoff_line.inline_z = 0.0
        cutpath.append(overshoot_line)
        cutpath.append(liftoff_line)
        return cutpath
Beispiel #5
0
    def _calc_rotation2(self, new_angle):
        """:new_angle: -PI <= new_angle <= PI"""
        if geom.float_eq(self.current_angle, new_angle):
            return 0.0
        prev_angle = self._normalize_angle(self.current_angle)
        new_angle = self._normalize_angle(new_angle)
        rotation_angle = new_angle - prev_angle
        if rotation_angle < -math.pi:
            rotation_angle += 2*math.pi
        elif rotation_angle > math.pi:
            rotation_angle -= 2*math.pi
#        logger.debug('rotation: %d, %d, %d' % (math.degrees(self.current_angle), math.degrees(new_angle), math.degrees(rotation_angle)))
        return rotation_angle
Beispiel #6
0
def split_path_g1(path):
    """Split the path at path vertices that connect
    non-tangential segments.

    Args:
        path: The path to split.

    Returns:
        A list of one or more paths.
    """
    path_list = []
    new_path = []
    seg1 = path[0]
    for seg2 in path[1:]:
        new_path.append(seg1)
        if (not geom.float_eq(seg1.end_tangent_angle(),
                              seg2.start_tangent_angle())
                or hasattr(seg1, 'ignore_g1') or hasattr(seg2, 'ignore_g1')):
            path_list.append(new_path)
            new_path = []
        seg1 = seg2
    new_path.append(seg1)
    path_list.append(new_path)
    return path_list
Beispiel #7
0
def split_path_g1(path):
    """Split the path at path vertices that connect
    non-tangential segments.

    Args:
        path: The path to split.

    Returns:
        A list of one or more paths.
    """
    path_list = []
    new_path = []
    seg1 = path[0]
    for seg2 in path[1:]:
        new_path.append(seg1)
        if (not geom.float_eq(seg1.end_tangent_angle(),
                             seg2.start_tangent_angle()) or
            hasattr(seg1, 'ignore_g1') or hasattr(seg2, 'ignore_g1')):
            path_list.append(new_path)
            new_path = []
        seg1 = seg2
    new_path.append(seg1)
    path_list.append(new_path)
    return path_list
Beispiel #8
0
def parse_path_geom(path_data, ellipse_to_bezier=False):
    """
    Parse SVG path data and convert to geometry objects.

    Args:
        path_data: The `d` attribute value of an SVG path element.
        ellipse_to_bezier: Convert elliptical arcs to bezier curves
            if True. Default is False.

    Returns:
        A list of zero or more subpaths.
        A subpath being a list of zero or more Line, Arc, EllipticalArc,
        or CubicBezier objects.
    """
    subpath = []
    subpath_list = []
    p1 = (0.0, 0.0)
    for cmd, params in svg.parse_path(path_data):
        p2 = (params[-2], params[-1])
        if cmd == 'M':
            # Start of path or sub-path
            if subpath:
                subpath_list.append(subpath)
                subpath = []
        elif cmd == 'L':
            subpath.append(geom.Line(p1, p2))
        elif cmd == 'A':
            rx = params[0]
            ry = params[1]
            phi = params[2]
            large_arc = params[3]
            sweep_flag = params[4]
            elliptical_arc = geom.ellipse.EllipticalArc.from_endpoints(
                p1, p2, rx, ry, large_arc, sweep_flag, phi)
            if elliptical_arc is None:
                # Parameters must be degenerate...
                # Try just making a line
                logger = logging.getLogger(__name__)
                logger.debug('Degenerate arc...')
                subpath.append(geom.Line(p1, p2))
            elif geom.float_eq(rx, ry):
                # If it's a circular arc then create one using
                # the previously computed ellipse parameters.
                segment = geom.Arc(p1, p2, rx, elliptical_arc.sweep_angle,
                                   elliptical_arc.center)
                subpath.append(segment)
            elif ellipse_to_bezier:
                # Convert the elliptical arc to cubic Beziers
                subpath.extend(bezier.bezier_ellipse(elliptical_arc))
            else:
                subpath.append(elliptical_arc)
        elif cmd == 'C':
            c1 = (params[0], params[1])
            c2 = (params[2], params[3])
            subpath.append(bezier.CubicBezier(p1, c1, c2, p2))
        elif cmd == 'Q':
            c1 = (params[0], params[1])
            subpath.append(bezier.CubicBezier.from_quadratic(p1, c1, p2))
        p1 = p2
    if subpath:
        subpath_list.append(subpath)
    return subpath_list
Beispiel #9
0
def offset_path(path, offset, min_arc_dist, g1_tolerance=None):
    """Recalculate path to compensate for a trailing tangential offset.
    This will shift all of the segments by `offset` amount. Arcs will
    be recalculated to correct for the shift offset.

    Args:
        path: The path to recalculate.
        offset: The amount of tangential tool trail.
        min_arc_dist: The minimum distance between two connected
            segment end points that can be bridged with an arc.
            A line will be used if the distance is less than this.
        g1_tolerance: The angle tolerance to determine if two segments
            are g1 continuous.

    Returns:
        A new path

    Raises:
        :class:`cam.toolpath.ToolpathException`: if the path contains segment
            types other than Line or Arc.
    """
    if geom.float_eq(offset, 0.0):
        return path
    offset_path = []
    prev_seg = None
    prev_offset_seg = None
    for seg in path:
        if seg.p1 == seg.p2:
            # Skip zero length segments
            continue
        if isinstance(seg, geom.Line):
            # Line segments are easy - just shift them forward by offset
            offset_seg = seg.shift(offset)
        elif isinstance(seg, geom.Arc):
            offset_seg = offset_arc(seg, offset)
        else:
            raise toolpath.ToolpathException('Unrecognized path segment type.')
        # Fix discontinuities caused by offsetting non-G1 segments
        if prev_seg is not None:
            if prev_offset_seg.p2 != offset_seg.p1:
                seg_distance = prev_offset_seg.p2.distance(offset_seg.p1)
                # If the distance between the two segments is less than the
                # minimum arc distance or if the segments are G1 continuous
                # then just insert a connecting line.
                if (seg_distance < min_arc_dist or geom.segments_are_g1(
                        prev_offset_seg, offset_seg, g1_tolerance)):
                    connect_seg = geom.Line(prev_offset_seg.p2, offset_seg.p1)
                else:
                    # Insert an arc in tool path to rotate the tool to the next
                    # starting tangent when the segments are not G1 continuous.
                    # TODO: avoid creating tiny segments by extending
                    # offset segment.
                    p1 = prev_offset_seg.p2
                    p2 = offset_seg.p1
                    angle = prev_seg.p2.angle2(p1, p2)
                    # TODO: This should be a straight line if the arc is tiny
                    connect_seg = geom.Arc(p1, p2, offset, angle, prev_seg.p2)


#                    if connect_seg.length() < 0.01:
#                        logger.debug('tiny arc! length= %f, radius=%f, angle=%f', connect_seg.length(), connect_seg.radius, connect_seg.angle)
                connect_seg.inline_start_angle = prev_seg.end_tangent_angle()
                connect_seg.inline_end_angle = seg.start_tangent_angle()
                offset_path.append(connect_seg)
                prev_offset_seg = connect_seg
            elif (geom.segments_are_g1(prev_seg, seg, g1_tolerance)
                  and not hasattr(prev_seg, 'ignore_g1')
                  and not hasattr(seg, 'ignore_g1')):
                # Add hint for smoothing pass
                prev_offset_seg.g1 = True
        prev_seg = seg
        prev_offset_seg = offset_seg
        offset_path.append(offset_seg)
    # Compensate for starting angle
    start_angle = (offset_path[0].p1 - path[0].p1).angle()
    offset_path[0].inline_start_angle = start_angle
    return offset_path
Beispiel #10
0
    def _generate_segment_gcode(self, segment, depth):
        """Generate G code for Line and Arc path segments."""
        # If enabled and the tool has traveled a specified interval distance
        # then allow a subclass to generate some G code at this point.
        if self.feed_interval > 0.0 \
        and self.feed_interval_travel >= self.feed_interval:
            self.generate_feed_interval_gcode(self.current_angle, depth)
            self.preview_plotter.draw_interval_marker(self.last_point, depth)
            self.feed_interval_travel = 0.0
            
        seglen = segment.length()
        if seglen < geom.EPSILON:
            # This avoids an accumulated error if there are a string of very
            # tiny segments (where each segment length < EPSILON).
            self._tinyseg_accumulation += seglen
        if seglen > geom.EPSILON or self._tinyseg_accumulation > geom.EPSILON:
            self.feed_distance += seglen + self._tinyseg_accumulation
            self.feed_interval_travel += seglen + self._tinyseg_accumulation
            self._tinyseg_accumulation = 0.0
            previous_angle = self.current_angle
            # Amount needed to rotate to new start tangent
            rotation = self._calc_rotation2(segment.start_tangent_angle())
            inline_rotation = self._calc_rotation2(getattr(segment, 'inline_end_angle', self.current_angle))
#            logger.debug('length= %f, prev-angle= %f, angle= %f' % (seglen, previous_angle, self.current_angle))
            # Extract any rendering hints attached to the segment
            depth = getattr(segment, 'inline_z', depth)
            inline_delta_a = getattr(segment, 'inline_delta_a', 0.0)
            inline_flip_tool = getattr(segment, 'inline_flip_tool', False)
            inline_ignore_angle = getattr(segment, 'inline_ignore_angle', False)
            if not inline_flip_tool and not inline_ignore_angle:
                self.current_angle += rotation
                if not geom.float_eq(previous_angle, self.current_angle):
#                    self.gc.feed_rotate(self.current_angle)
                    self.gc.feed(a=self.current_angle)
                    self.preview_plotter.draw_feed_rotate(self.last_point,
                                                          previous_angle,
                                                          self.current_angle,
                                                          depth)
                    previous_angle = self.current_angle
            end_angle = self.current_angle + inline_rotation + inline_delta_a
            if isinstance(segment, geom.Line):
                self.gc.feed(segment.p2.x, segment.p2.y, a=end_angle, z=depth)
                self.preview_plotter.draw_feed_line(self.last_point,
                                                    segment.p2, previous_angle,
                                                    end_angle, depth)
            elif isinstance(segment, geom.Arc):
                arcv = segment.center - segment.p1
                if geom.float_eq(inline_rotation, 0.0):
                    end_angle += segment.angle
                self.gc.feed_arc(segment.is_clockwise(),
                                 segment.p2.x, segment.p2.y,
                                 arcv.x, arcv.y, a=end_angle, z=depth)
                self.preview_plotter.draw_feed_arc(segment, previous_angle,
                                                   end_angle, depth)
            self.current_angle = end_angle
            if not geom.float_eq(inline_delta_a, 0.0):
                self.preview_plotter.draw_angle_marker(segment.p2, end_angle,
                                                       depth)
            if inline_flip_tool:
                self.current_angle += rotation
                self.gc.axis_offset['A'] -= rotation
        self.last_point = segment.p2
Beispiel #11
0
def parse_path_geom(path_data, ellipse_to_bezier=False):
    """
    Parse SVG path data and convert to geometry objects.

    Args:
        path_data: The `d` attribute value of an SVG path element.
        ellipse_to_bezier: Convert elliptical arcs to bezier curves
            if True. Default is False.

    Returns:
        A list of zero or more subpaths.
        A subpath being a list of zero or more Line, Arc, EllipticalArc,
        or CubicBezier objects.
    """
    subpath = []
    subpath_list = []
    p1 = (0.0, 0.0)
    for cmd, params in svg.parse_path(path_data):
        p2 = (params[-2], params[-1])
        if cmd == 'M':
            # Start of path or sub-path
            if subpath:
                subpath_list.append(subpath)
                subpath = []
        elif cmd == 'L':
            subpath.append(geom.Line(p1, p2))
        elif cmd == 'A':
            rx = params[0]
            ry = params[1]
            phi = params[2]
            large_arc = params[3]
            sweep_flag = params[4]
            elliptical_arc = geom.ellipse.EllipticalArc.from_endpoints(
                p1, p2, rx, ry, large_arc, sweep_flag, phi)
            if elliptical_arc is None:
                # Parameters must be degenerate...
                # Try just making a line
                logger = logging.getLogger(__name__)
                logger.debug('Degenerate arc...')
                subpath.append(geom.Line(p1, p2))
            elif geom.float_eq(rx, ry):
                # If it's a circular arc then create one using
                # the previously computed ellipse parameters.
                segment = geom.Arc(p1, p2, rx, elliptical_arc.sweep_angle,
                                   elliptical_arc.center)
                subpath.append(segment)
            elif ellipse_to_bezier:
                # Convert the elliptical arc to cubic Beziers
                subpath.extend(bezier.bezier_ellipse(elliptical_arc))
            else:
                subpath.append(elliptical_arc)
        elif cmd == 'C':
            c1 = (params[0], params[1])
            c2 = (params[2], params[3])
            subpath.append(bezier.CubicBezier(p1, c1, c2, p2))
        elif cmd == 'Q':
            c1 = (params[0], params[1])
            subpath.append(bezier.CubicBezier.from_quadratic(p1, c1, p2))
        p1 = p2
    if subpath:
        subpath_list.append(subpath)
    return subpath_list