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
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
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
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
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
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
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
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
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