class MyExtension(inkext.InkscapeExtension): """Inkscape extension barebones template. """ OPTIONSPEC = ( inkext.ExtOption('--option-name1', type='docunits', default=1.0, help=_('Document unit option description')), inkext.ExtOption('--option-name2', type='inkbool', default=True, help=_('Boolean option description')), inkext.ExtOption('--option-name3', type='int', default=1, help=_('Integer option description')), ) def run(self): """Main entry point for Inkscape extension. """ # Initialize the debug SVG context for the geometry package geom.debug.set_svg_context(self.debug_svg) # Get a list of selected SVG shape elements and their transforms svg_elements = self.svg.get_shape_elements(self.get_elements()) if svg_elements: path_list = geomsvg.svg_to_geometry(svg_elements) else: # Nothing selected or document is empty path_list = ()
class Repeater(inkext.InkscapeExtension): """An Inkscape extension that duplicates paths along a straight line. """ # Command line options OPTIONSPEC = ( inkext.ExtOption('--copies', type='int', default=1, help=_('Number of copies')), inkext.ExtOption('--interval', type='docunits', default=1.0, help=_('Repeat interval')), inkext.ExtOption('--angle', type='degrees', default=0.0, help=_('Angle from horizontal')), inkext.ExtOption('--new-layer', type='inkbool', default=False, help=_('Create new layer for output.')), ) # Default layer name for output _LAYER_NAME = 'repeater' def run(self): """Main entry point for Inkscape extensions. """ # Set up debug SVG output context. geom.debug.set_svg_context(self.debug_svg) selected_elements = self.get_elements() if not len(selected_elements): # Nothing selected or document is empty return # Create a new layer for the SVG output. layer = None if self.options.new_layer: layer = self.svg.create_layer(self._LAYER_NAME, incr_suffix=True) for n in range(self.options.copies): for element in selected_elements: m_elem = self.svg.parse_transform_attr( element.get('transform')) v = geom.P.from_polar(self.options.interval * (n + 1), -self.options.angle) m_translate = transform2d.matrix_translate(v.x, v.y) m_transform = transform2d.compose_transform( m_elem, m_translate) transform_attr = svg.transform_attr(m_transform) elem_copy = deepcopy(element) elem_copy.set('transform', transform_attr) # elem_copy.set('id', element.get('id') + '_r') self.svg.add_elem(elem_copy, parent=layer)
class SineWave(inkext.InkscapeExtension): """An Inkscape extension that draws a sine wave using Bezier curves. """ # Command line options _OPTIONSPEC = ( inkext.ExtOption('--amplitude', '-a', type='docunits', default=1.0, help=_('Amplitude')), inkext.ExtOption('--wavelength', '-w', type='docunits', default=1.0, help=_('Wavelength')), inkext.ExtOption('--cycles', '-c', type='int', default=1, help=_('Number of cycles')), inkext.ExtOption('--origin_x', type='docunits', default=0.0, help=_('Origin X')), inkext.ExtOption('--origin_y', type='docunits', default=0.0, help=_('Origin X')), ) _LAYER_NAME = 'sine wave' _LINE_STYLE = 'fill:none;stroke:#000000;stroke-width:1px;stroke-opacity:1;' def run(self): """Main entry point for Inkscape extensions. """ # Create a new layer since there is currently no way for # an extension to determine the currently active layer... self.line_layer = self.svg.create_layer(self._LAYER_NAME, incr_suffix=True, flipy=True) # Approximate a sine wave using Bezier curves origin = (self.options.origin_x, self.options.origin_y) sine_path = bezier.bezier_sine_wave(self.options.amplitude, self.options.wavelength, cycles=self.options.cycles, origin=origin) # Draw the sine wave style = self.svg.scale_inline_style(self._LINE_STYLE) self.svg.create_polypath(sine_path, style=style, parent=self.line_layer)
class PipeLines(inkext.InkscapeExtension): """ """ OPTIONSPEC = ( inkext.ExtOption('--epsilon', type='docunits', default=0.00001, help='Epsilon'), inkext.ExtOption('--pipeline-count', type='int', default=3, help='Line count'), inkext.ExtOption('--pipeline-fillet', type='inkbool', default=False, help='Fillet lines'), inkext.ExtOption('--pipeline-fillet-radius', type='float', default=0, help='Fillet radius.'), inkext.ExtOption('--pipeline-maxspacing', type='float', default=0, help='Max spacing'), inkext.ExtOption('--pipeline-stroke', default='#000000', help='Line CSS stroke color'), inkext.ExtOption('--pipeline-stroke-width', default='3px', help='Line CSS stroke width'), ) _styles = { 'dot': 'fill:%s;stroke-width:1px;stroke:#000000;', 'pipeline': 'fill:none;stroke-opacity:1.0;stroke-linejoin:round;' 'stroke-width:${pipeline_stroke_width};stroke:${pipeline_stroke};', # 'segment': # 'fill:none;stroke-opacity:1.0;stroke-linejoin:round;' # 'stroke-width:${segment_stroke_width};stroke:${segment_stroke};', # 'segment1': # 'fill:none;stroke-opacity:1.0;stroke-linejoin:round;' # 'stroke-width:${segment1_stroke_width};stroke:${segment1_stroke};', } _style_defaults = { 'pipeline_stroke_width': '3pt', 'pipeline_stroke': '#505050', # 'segment_stroke_width': '3pt', # 'segment_stroke': '#00a000', # 'segment1_stroke_width': '3pt', # 'segment1_stroke': '#f00000', } def run(self): """Main entry point for Inkscape extension. """ random.seed() geom.set_epsilon(self.options.epsilon) geom.debug.set_svg_context(self.debug_svg) styles = self.svg.styles_from_templates(self._styles, self._style_defaults, self.options.__dict__) self._styles.update(styles) # Get a list of selected SVG shape elements and their transforms svg_elements = self.svg.get_shape_elements(self.get_elements()) if not svg_elements: # Nothing selected or document is empty return path_list = geomsvg.svg_to_geometry(svg_elements) # Path list should only have two sort of parallel paths if len(path_list) != 2: self.errormsg(_('Please select two polylines')) exit(1) layer = self.svg.create_layer('q_polylines', incr_suffix=True) # seglayer = self.svg.create_layer('q_segments', incr_suffix=True) segments = self._get_segments(path_list[0], path_list[1]) if segments: self._draw_segments(segments) polylines = self._get_polylines(path_list[0], path_list[1], segments) if polylines: self._draw_polylines(polylines, layer) def _get_segments(self, path1, path2): """ This is ridiculously non-optimal, but who cares... """ bbox = polygon.bounding_box([p1 for p1, _p2 in path1] + [p1 for p1, _p2 in path2]) # Make sure both paths are going more or less in the same direction # by checking if the distance between the starting points is less # than the distance between one path starting point and the other # path ending point. d1 = path1[0].p1.distance(path2[0].p1) d2 = path1[0].p1.distance(path2[-1].p2) if d1 > d2: geom.util.reverse_path(path2) rays = [geom.Line(path1[0].p1, path2[0].p1)] rayside = path1[0].which_side(path2[0].p1) p0 = path1[0].p1 for p1, p2 in path1[1:]: if abs(p1.angle2(p0, p2)) > geom.const.EPSILON: # The angle bisector at each vertex bisector = geom.Line(p1, p1.bisector(p0, p2)) # Make sure it's pointing the right way if geom.Line(p0, p1).which_side(bisector.p2) != rayside: bisector = bisector.flipped() # geom.debug.draw_line(bisector, width=11) bisector = bisector.extend(bbox.diagonal()) # See if it intersects a segment on the other path lx = self._linex_segment(bisector, path1, path2) if lx is not None: rays.append(lx) p0 = p1 # Do the same for the other path rayside = -rayside p0 = path2[0].p1 for p1, p2 in path2[1:]: bisector = geom.Line(p1, p1.bisector(p0, p2)) if geom.Line(p0, p1).which_side(bisector.p2) != rayside: bisector = bisector.flipped() # geom.debug.draw_line(bisector, width=11) bisector = bisector.extend(bbox.diagonal()) lx = self._linex_segment(bisector, path2, path1) if lx is not None: rays.append(lx.reversed()) p0 = p1 # Last line joining the path endpoints rays.append(geom.Line(path1[-1].p2, path2[-1].p2)) # Sort the segment endpoints on each path by distance from first # path endpoint. Then connect the sorted segment endpoints. p1list = [seg.p1 for seg in rays] p2list = [seg.p2 for seg in rays] p1list.sort(key=lambda p: self._pline_distance(p, path1)) p2list.sort(key=lambda p: self._pline_distance(p, path2)) rays = [geom.Line(p1, p2) for p1, p2 in zip(p1list, p2list)] # Sort the segments by distance on first path # rays.sort(key=lambda seg: self._pline_distance(seg.p1, path1)) # for i, ray in enumerate(rays): # logger.debug('ray[%d]: %.3f, %.3f', i, ray.length(), self._pline_distance(ray.p1, path1)) # geom.debug.draw_point(ray.p1, radius=7, color='#ffff00') # geom.debug.draw_point(rays[8].p1, radius=7, color='#ffff00') # geom.debug.draw_line(path1[5], color='#ffff00', width=7) # if path1[5].point_on_line(rays[8].p1): # logger.debug('yep') # logger.debug('d: %.3f', self._pline_distance(rays[25].p1, path1)) # return self._elim_crossings(rays, path1, path2) return rays def _num_polylines(self, segments): """ Get the number of polylines to draw """ if self.options.pipeline_maxspacing > 0: maxlen = 0 for seg in segments: seglen = seg.length() if seglen > maxlen: maxlen = seglen return int(round(maxlen / self.options.pipeline_maxspacing)) else: return self.options.pipeline_count def _get_polylines(self, path1, path2, segments): """ """ count = self._num_polylines(segments) polylines = [] for seg in segments: seglen = seg.length() mu = ((seglen / (count + 1)) / seglen) for n in range(count): mu_i = mu * (n + 1) p = seg.point_at(mu_i) if len(polylines) < (n + 1): polylines.append([]) polylines[n].append(p) # for i in range(count): # for seg in segments: # seglen = seg.length() # mu = ((seglen / (count + 1)) / seglen) * (i + 1) # p = seg.point_at(mu) # polylines[i].append(p) return polylines def _linex_segment(self, ray, path1, path2): """ Get the segment from the starting point of the ray (on the first path) to its intersection with the second path. """ ilines = [] # d1 = self._pline_distance(ray.p1, path1) # logger.debug('ray d1: %.3f', d1) for seg in path2: px = ray.intersection(seg, segment=True) if px is not None: # geom.debug.draw_point(px) lx = geom.Line(ray.p1, px) # geom.debug.draw_line(lx, color='#ff00ff') ilines.append(lx) # If multiple intersections, return the shortest. if ilines: # logger.debug('intersects: %d', len(ilines)) ilines.sort(key=lambda l: l.length()) # ilines.sort(key=lambda lx: abs(d1 - self._pline_distance(lx.p2, path2))) for lx in ilines: if lx.length() > 0.001: break # logger.debug('l: %f', lx.length()) # lx = ilines[0] # geom.debug.draw_line(lx, color='#ff00ff', width=15, opacity=.5) # d = abs(d1 - self._pline_distance(lx.p2, path2)) # logger.debug('lx d: %.3f', d) return lx return None def _draw_segments(self, segments): """ Draw guide segments on debug layer """ # Draw a point at the starting path endpoint. geom.debug.draw_point(segments[0].p1, radius=11, color='#00ff00') for seg in segments: geom.debug.draw_line(seg, color='#0080ff') def _draw_polylines(self, polylines, layer): """ """ for pline in polylines: self.svg.create_polygon(pline, close_polygon=False, style=self._styles['pipeline'], parent=layer) def _pline_distance(self, p, path): """ Distance from the starting point of a polyline to a point on the same polyline. """ d = 0.0 for seg in path: if seg.point_on_line(p, segment=True): # geom.debug.draw_point(p, radius=11, color='#ff0000') # geom.debug.draw_line(seg, color='#ffff00', width=7) d += seg.p1.distance(p) # logger.debug('d: %.4f', d) return d d += seg.length() # logger.debug('wtf?') # geom.debug.draw_point(p, radius=11, color='#ff0000') # geom.debug.draw_line(seg, color='#ffff00', width=7) # logger.debug('p: (%f, %f)', p.x, p.y) # geom.debug.draw_line(path[3], color='#ffff00', width=7) # logger.debug('seg: (%f, %f) (%f, %f)', path[3].p1.x, path[3].p1.y, path[3].p2.x, path[3].p2.y) # logger.debug('seg: (%f, %f) (%f, %f)', path[4].p1.x, path[4].p1.y, path[4].p2.x, path[4].p2.y) return d
class ExtBezier(inkext.InkscapeExtension): """An Inkscape extension that tests various CubicBezier properties and methods. """ # Command line options OPTIONSPEC = ( inkext.ExtOption('--tolerance', type='float', default=0.00001), inkext.ExtOption('--draw-inflections', type='inkbool', default=False), inkext.ExtOption('--draw-subdivide-inflections', type='inkbool', default=False), inkext.ExtOption('--draw-controlpoints', type='inkbool', default=False), inkext.ExtOption('--draw-biarcs', type='inkbool', default=False), inkext.ExtOption('--draw-normals', type='inkbool', default=False), inkext.ExtOption('--draw-t5', type='inkbool', default=False), inkext.ExtOption('--draw-extrema', type='inkbool', default=False), inkext.ExtOption('--draw-extrema-align', type='inkbool', default=False), inkext.ExtOption('--biarc-tolerance', type='docunits', default=0.01), inkext.ExtOption('--biarc-max-depth', type='int', default=4), inkext.ExtOption('--line-flatness', type='docunits', default=0.001), inkext.ExtOption('--test-arcbez', type='inkbool', default=False), inkext.ExtOption('--test-circbez', type='inkbool', default=False), ) _LAYER_NAME = 'bezier-test' _LINE_STYLE = 'fill:none;stroke:#000000;stroke-width:1px;stroke-opacity:1;' def run(self): """Main entry point for Inkscape extensions. """ geom.debug.set_svg_context(self.svg) self.bezier_layer = self.svg.create_layer(self._LAYER_NAME) # Get a list of selected SVG shape elements and their transforms svg_elements = self.svg.get_shape_elements(self.get_elements()) if not svg_elements: # Nothing selected or document is empty return # Convert SVG elements to path geometry path_list = geomsvg.svg_to_geometry(svg_elements) self.svg.set_default_parent(self.bezier_layer) if self.options.draw_controlpoints: self.controlpoint_layer = self.svg.create_layer('control points') if self.options.draw_inflections or self.options.draw_subdivide_inflections: self.inflection_layer = self.svg.create_layer('inflections') if self.options.draw_normals: self.normals_layer = self.svg.create_layer('normals') if self.options.draw_biarcs: self.biarc_layer = self.svg.create_layer('biarcs') for path in path_list: for segment in path: if isinstance(segment, bezier.CubicBezier): self.draw_curve_attributes(segment) if isinstance(segment, arc.Arc): self.test_arcbez(segment) def draw_curve_attributes(self, curve): """ """ if self.options.draw_t5: self.draw_t5(curve) if self.options.draw_extrema: extrema = curve.find_extrema_points() if extrema: for p in extrema: geom.debug.draw_point(p, color='#000000') extrema.append(curve.p1) extrema.append(curve.p2) bbox = geom.Box.from_points(extrema) geom.debug.draw_poly(bbox.vertices()) if self.options.draw_extrema_align: extrema, bbox = curve.find_extrema_align() for p in extrema: geom.debug.draw_point(p, color='#000000') if bbox is not None: geom.debug.draw_poly(bbox) if self.options.draw_controlpoints: self.draw_control_points(curve, self.controlpoint_layer) if self.options.draw_subdivide_inflections: self.draw_subdivide_inflections(curve, self.inflection_layer) elif self.options.draw_inflections: self.draw_inflections(curve, self.inflection_layer) if self.options.draw_normals: self.draw_normals(curve, self.normals_layer) if self.options.draw_biarcs: self.draw_biarcs(curve, self.options.biarc_tolerance, self.options.biarc_max_depth, self.options.line_flatness, self.biarc_layer) def draw_t5(self, curve, layer=None): """ """ p = curve.point_at(0.5) geom.debug.draw_point(p, color='#c00000', parent=layer) def draw_control_points(self, curve, layer): # Draw control points tseg1 = geom.Line(curve.p1, curve.c1) geom.debug.draw_line(tseg1, color='#606060', parent=layer) tseg2 = geom.Line(curve.p2, curve.c2) geom.debug.draw_line(tseg2, color='#606060', parent=layer) geom.debug.draw_point(curve.p1, color='#606060', parent=layer) geom.debug.draw_point(curve.p2, color='#606060', parent=layer) geom.debug.draw_point(curve.c1, color='#606060', parent=layer) geom.debug.draw_point(curve.c2, color='#606060', parent=layer) def draw_subdivide_inflections(self, curve, layer): # Draw inflection points if any subcurves = curve.subdivide_inflections() logger.debug('subcurves=%d', len(subcurves)) if len(subcurves) > 1: for subcurve in subcurves: geom.debug.draw_bezier(subcurve, color='#000000', parent=layer) self.draw_control_points(subcurve, layer=layer) def draw_inflections(self, curve, layer): # Draw inflection points if any t1, t2 = curve.find_inflections() if t1 > 0.0: ip1 = curve.point_at(t1) geom.debug.draw_point(ip1, color='#c00000', parent=layer) if t2 > 0.0: ip2 = curve.point_at(t2) geom.debug.draw_point(ip2, color='#c00000', parent=layer) def draw_normals(self, curve, layer): for i in range(101): t = i / 100.0 normal = curve.normal(t) pt = curve.point_at(t) normal_line = geom.Line(pt, pt + normal) geom.debug.draw_line(normal_line, parent=layer) def draw_biarcs(self, curve, tolerance, max_depth, line_flatness, layer): segments = curve.biarc_approximation(tolerance=tolerance, max_depth=max_depth, line_flatness=line_flatness) for segment in segments: if isinstance(segment, geom.Line): geom.debug.draw_line(segment, color='#00c000', parent=layer) elif isinstance(segment, geom.Arc): geom.debug.draw_arc(segment, color='#00c000', parent=layer) geom.debug.draw_point(segment.p1, color='#c000c0', parent=layer) geom.debug.draw_point(segment.p2, color='#c000c0', parent=layer) def test_arcbez(self, testarc): """ """ if self.options.test_circbez and geom.float_eq(testarc.angle, math.pi / 2): curves = bezier.bezier_circle(testarc.center, testarc.radius) for curve in curves: geom.debug.draw_bezier(curve) elif self.options.test_arcbez: curve = bezier.bezier_circular_arc(testarc) geom.debug.draw_bezier(curve)
clip_count += 1 if (self.clip_all and clip_count > 0) or clip_count > 3: return [] return xvertices def _update_bbox(self, points): """Update the bounding box with the given vertex point.""" for p in points: self._xmin = min(self._xmin, p.x) self._ymin = min(self._ymin, p.y) self._xmax = max(self._xmax, p.x) self._ymax = max(self._ymax, p.y) _OPTIONSPEC = ( inkext.ExtOption('--scale', '-s', type='docunits', default=5.0, help='Output scale.'), inkext.ExtOption('--rotate', '-r', type='degrees', default=0.0, help='Rotation.'), inkext.ExtOption('--symmetry', '-S', type='int', default=5, help='Degrees of symmetry.'), inkext.ExtOption('--numlines', '-n', type='int', default=30, help='Number of lines.'), inkext.ExtOption('--offset-x', type='docunits', default=0.0, help='X offset'), inkext.ExtOption('--offset-y', type='docunits', default=0.0, help='Y offset'), inkext.ExtOption('--salt-x', type='float', default=0.31416, help='X offset salt'), inkext.ExtOption('--salt-y', type='float', default=0.64159, help='Y offset salt'), inkext.ExtOption('--epsilon', type='docunits', default=0.00001, help='Epsilon'), inkext.ExtOption('--segment-draw', type='inkbool', default=False, help='Draw segments.'), inkext.ExtOption('--segtype-skinny', '-M', type='int', default=0, help='Midpoint type for skinny diamonds.'), inkext.ExtOption('--segtype-fat', '-N', type='int', default=0, help='Midpoint type for fat diamonds.'), inkext.ExtOption('--skinnyfat-ratio', type='float', default=0.5, help='Skinny/fat ratio'), inkext.ExtOption('--segment-ratio', type='float', default=0.5, help='Segment ratio'), inkext.ExtOption('--segment-scale', type='float', default=1.0, help='Segment scale.'),
class Voronoi(inkext.InkscapeExtension): """Inkscape plugin that creates Voronoi diagrams. """ _OPTIONSPEC = ( inkext.ExtOption('--epsilon', type='docunits', default=0.0001, help='Epsilon'), inkext.ExtOption('--jiggle-points', '-j', type='inkbool', default=True, help='Jiggle points.'), inkext.ExtOption('--delaunay-triangles', type='inkbool', default=False, help='Delaunay triangles.'), inkext.ExtOption('--delaunay-edges', type='inkbool', default=False, help='Delaunay edges.'), inkext.ExtOption('--clip-to-polygon', type='inkbool', default=False, help='Clip to hull polygon.'), ) _styles = { 'voronoi': 'fill:none;stroke-opacity:1.0;stroke-linejoin:round;' 'stroke-width:${voronoi_stroke_width};' 'stroke:${voronoi_stroke};', 'delaunay': 'fill:none;stroke-opacity:1.0;stroke-linejoin:round;' 'stroke-width:${delaunay_stroke_width};' 'stroke:${delaunay_stroke};', 'delaunay_triangle': 'stroke-opacity:1.0;stroke-linejoin:round;' 'fill:${delaunay_triangle_fill};' 'stroke-width:${delaunay_triangle_stroke_width};' 'stroke:${delaunay_triangle_stroke};', } _STYLE_DEFAULTS = { 'voronoi_stroke_width': '3pt', 'voronoi_stroke': '#000000', 'delaunay_stroke_width': '3pt', 'delaunay_stroke': '#000000', 'delaunay_triangle_fill': 'none', 'delaunay_triangle_stroke_width': '1pt', 'delaunay_triangle_stroke': '#000000', } def run(self): """Main entry point for Inkscape plugins. """ geom.set_epsilon(_GEOM_EPSILON) geom.debug.set_svg_context(self.debug_svg) self._styles.update( self.svg.styles_from_templates(self._styles, self._STYLE_DEFAULTS, self.options.__dict__)) # Get a list of selected SVG shape elements and their transforms svg_elements = self.svg.get_shape_elements(self.get_elements()) if not svg_elements: # Nothing selected or document is empty return # Convert the SVG elements to segment geometry path_list = geomsvg.svg_to_geometry(svg_elements) # Create a set of input points from the segment end points input_points = set() polygon_segment_graph = planargraph.Graph() for path in path_list: for segment in path: input_points.add(segment.p1) input_points.add(segment.p2) polygon_segment_graph.add_edge(segment) self.clip_rect = geom.box.Box((0, 0), self.svg.get_document_size()) clipping_hull = None if self.options.clip_to_polygon: clipping_hull = polygon_segment_graph.boundary_polygon() voronoi_diagram = voronoi.VoronoiDiagram( list(input_points), do_delaunay=True, jiggle_points=self.options.jiggle_points) self._draw_voronoi(voronoi_diagram, clipping_hull) def _draw_voronoi(self, voronoi_diagram, clipping_hull): # Voronoi segments clipped to document voronoi_segments = self._clipped_voronoi_segments( voronoi_diagram, self.clip_rect) # Voronoi segments clipped to polygon voronoi_clipped_segments = self._clipped_poly_voronoi_segments( voronoi_segments, clipping_hull) # Delaunay segments clipped to polygon delaunay_segments = self._clipped_delaunay_segments( voronoi_diagram, clipping_hull) layer = self.svg.create_layer('voronoi_diagram', incr_suffix=True) style = self._styles['voronoi'] for segment in voronoi_segments: self.svg.create_line(segment.p1, segment.p2, style=style, parent=layer) if clipping_hull is not None: layer = self.svg.create_layer('voronoi_clipped', incr_suffix=True) style = self._styles['voronoi'] for segment in voronoi_clipped_segments: self.svg.create_line(segment.p1, segment.p2, style=style, parent=layer) voronoi_graph = planargraph.Graph(voronoi_clipped_segments) voronoi_graph.cull_open_edges() layer = self.svg.create_layer('voronoi_closed', incr_suffix=True) style = self._styles['voronoi'] for segment in voronoi_graph.edges: self.svg.create_line(segment.p1, segment.p2, style=style, parent=layer) if self.options.delaunay_edges: layer = self.svg.create_layer('delaunay_edges', incr_suffix=True) style = self._styles['delaunay_triangle'] for segment in delaunay_segments: self.svg.create_line(segment.p1, segment.p2, style=style, parent=layer) if self.options.delaunay_triangles: layer = self.svg.create_layer('delaunay_triangles', incr_suffix=True) for triangle in voronoi_diagram.triangles: self.svg.create_polygon( triangle, close_polygon=True, style=self._styles['delaunay_triangle'], parent=layer) def _clipped_voronoi_segments(self, diagram, clip_rect): """Clip a voronoi diagram to a clipping rectangle. Args: diagram: A VoronoiDiagram. clip_rect. A Box. Clipping rectangle. Returns: A list of (possibly) clipped voronoi segments. """ voronoi_segments = [] for edge in diagram.edges: p1 = edge.p1 p2 = edge.p2 if p1 is None or p2 is None: # The segment is missing an end point which means it's # is infinitely long so create an end point clipped to # the clipping rect bounds. if p2 is None: # The line direction is right xclip = clip_rect.xmax else: # The line direction is left p1 = p2 xclip = clip_rect.xmin # Ignore start points outside of clip rect. if not clip_rect.point_inside(p1): continue a, b, c = edge.equation if geom.is_zero(b): #b == 0: logger.debug('vert: a=%f, b=%f, c=%f, p1=%s, p2=%s', a, b, c, str(p1), str(p2)) # vertical line x = c / a center_y = (clip_rect.ymin + clip_rect.ymax) / 2 if p1[0] > center_y: y = clip_rect.ymax else: y = clip_rect.ymin else: x = xclip y = (c - (x * a)) / b p2 = (x, y) line = clip_rect.clip_line(geom.Line(p1, p2)) if line is not None: voronoi_segments.append(line) return voronoi_segments def _clipped_poly_voronoi_segments(self, voronoi_segments, clip_polygon): voronoi_clipped_segments = [] for segment in voronoi_segments: if clip_polygon is not None: cliplines = polygon.intersect_line(clip_polygon, segment) for line in cliplines: voronoi_clipped_segments.append(line) return voronoi_clipped_segments def _clipped_delaunay_segments(self, voronoi_diagram, clip_polygon): delaunay_segments = [] for edge in voronoi_diagram.delaunay_edges: line = geom.Line(edge.p1, edge.p2) if (clip_polygon is None or self._line_inside_hull( clip_polygon, line, allow_hull=True)): delaunay_segments.append(line) return delaunay_segments def _line_inside_hull(self, points, line, allow_hull=False): """Test if line is inside or on the polygon defined by `points`. This is a special case.... basically the line segment will lie on the hull, have one endpoint on the hull, or lie completely within the hull, or be completely outside the hull. It will not intersect. This works for the Delaunay triangles and polygon segments... Args: points: polygon vertices. A list of 2-tuple (x, y) points. line: line segment to test. allow_hull: allow line segment to lie on hull Returns: True if line is inside or on the polygon defined by `points`. Otherwise False. """ if allow_hull: for i in range(len(points)): pp1 = geom.P(points[i]) pp2 = geom.P(points[i - 1]) if geom.Line(pp1, pp2) == line: return True if not polygon.point_inside(points, line.midpoint()): return False p1 = line.p1 p2 = line.p2 if not allow_hull: for i in range(len(points)): pp1 = geom.P(points[i]) pp2 = geom.P(points[i - 1]) if geom.Line(pp1, pp2) == line: return False for i in range(len(points)): pp1 = geom.P(points[i]) pp2 = geom.P(points[i - 1]) if p1 == pp1 or p1 == pp2 or p2 == pp1 or p2 == pp2: return True return (polygon.point_inside(points, p1) or polygon.point_inside(points, p2))
class PolySmooth(inkext.InkscapeExtension): """An Inkscape extension that smoothes polygons. """ # Command line options OPTIONSPEC = ( inkext.ExtOption('--simplify', type='inkbool', default=False, help=_('Simplify polylines first')), inkext.ExtOption('--simplify-tolerance', type='docunits', default=.01, help=_('Tolerance for simplification')), inkext.ExtOption('--smoothness', '-s', type='int', default=50, help=_('Smoothness in percent')), inkext.ExtOption('--new-layer', type='inkbool', default=False, help=_('Create new layer for output.')), inkext.ExtOption('--match-style', type='inkbool', default=True, help=_('Match style of input path.')), inkext.ExtOption('--polysmooth-stroke', help=_('CSS stroke color')), inkext.ExtOption('--polysmooth-stroke-width', help=_('CSS stroke width')), ) # SVG CSS inline style template _styles = { 'polysmooth': 'fill:none;stroke-opacity:1.0;stroke-linejoin:round;' 'stroke-width:${polysmooth_stroke_width};' 'stroke:${polysmooth_stroke};', } # Default style template values _style_defaults = { 'polysmooth_stroke_width': '1px', 'polysmooth_stroke': '#000', } # Default layer name for smoothed output _LAYER_NAME = 'polysmooth' def run(self): """Main entry point for Inkscape extensions. """ # Set up debug SVG output context. geom.debug.set_svg_context(self.debug_svg) # Update CSS inline styles from templates and/or options self._styles.update( self.svg.styles_from_templates(self._styles, self._style_defaults, self.options.__dict__)) # Get a list of selected SVG shape elements and their transforms parent_transform = None # if not self.options.new_layer: # # This will prevent the parent layer transform from being applied # # twice when the original element is replaced by the smoothed one. # parent_transform = transform2d.IDENTITY_MATRIX svg_elements = self.svg.get_shape_elements( self.get_elements(), parent_transform=parent_transform, accumulate_transform=self.options.new_layer) if not svg_elements: # Nothing selected or document is empty return # Create a new layer for the SVG output. if self.options.new_layer: new_layer = self.svg.create_layer(self._LAYER_NAME, incr_suffix=True) default_style = self._styles['polysmooth'] smoothness = self.options.smoothness / 100.0 for element, element_transform in svg_elements: # Convert the SVG element to Line/Arc/CubicBezier paths pathlist = geomsvg.svg_element_to_geometry( element, element_transform=element_transform) for path in pathlist: if self.options.simplify: path = self.simplify_polylines( path, self.options.simplify_tolerance) new_path = bezier.smooth_path(path, smoothness) if not new_path: # Ignore failures and keep going... # This should only happen if there are segments # that are neither arc nor line. continue if self.options.new_layer: parent = new_layer else: # Replace the original element with the smoothed one parent = element.getparent() if parent is not None: parent.remove(element) style = default_style if self.options.match_style: style = element.get('style') path_is_closed = (path[-1].p2 == path[0].p1) self.svg.create_polypath(new_path, style=style, close_path=path_is_closed, parent=parent) def simplify_polylines(self, path, tolerance): """Simplify any polylines in the path. """ new_path = [] # Find polylines polyline = [] for segment in path: if isinstance(segment, geom.Line): polyline.append(segment) elif len(polyline) > 1: # Simplify the polyline and then add it back to the path. polyline = self._simplify_polyline(polyline, tolerance) new_path.extend(polyline) # Reset accumulated polyline polyline = [] else: new_path.append(segment) if polyline: polyline = self._simplify_polyline(polyline, tolerance) new_path.extend(polyline) return new_path def _simplify_polyline(self, path, tolerance): points1, points2 = zip(*path) points1 = list(points1) # points2 = list(points2) # points1.append(points2[-1]) points1.append(path[-1][1]) points = polygon.simplify_polyline_rdp(points1, tolerance) new_path = [] prev_pt = points[0] for next_pt in points[1:]: next_line = geom.Line(prev_pt, next_pt) new_path.append(next_line) prev_pt = next_pt return new_path
class PolyPath(inkext.InkscapeExtension): """Inkscape plugin that traces paths on edge connected graphs. """ OPTIONSPEC = ( inkext.ExtOption('--epsilon', type='docunits', default=0.00001, help='Epsilon'), inkext.ExtOption('--polysegpath-draw', type='inkbool', default=True, help='Draw paths from polygon segments.'), inkext.ExtOption('--polysegpath-longest', type='inkbool', default=True, help='Draw longest paths.'), inkext.ExtOption('--polysegpath-min-length', type='int', default=1, help='Minimum number of path segments.'), inkext.ExtOption('--polysegpath-max', type='int', default=1, help='Number of paths.'), inkext.ExtOption('--polysegpath-type', type='int', default=0, help='Graph edge following strategy.'), inkext.ExtOption('--polysegpath-stroke', default='#000000', help='Polygon CSS stroke color.'), inkext.ExtOption('--polysegpath-stroke-width', default='3px', help='Polygon CSS stroke width.'), inkext.ExtOption('--polyoffset-draw', type='inkbool', default=True, help='Create offset polygons.'), inkext.ExtOption('--polyoffset-recurs', type='inkbool', default=True, help='Recursively offset polygons'), inkext.ExtOption('--polyoffset-jointype', type='int', default=0, help='Join type.'), inkext.ExtOption('--polyoffset-offset', type='float', default=0, help='Polygon offset.'), inkext.ExtOption('--polyoffset-fillet', type='inkbool', default=False, help='Fillet offset polygons.'), inkext.ExtOption('--polyoffset-fillet-radius', type='float', default=0, help='Offset polygon fillet radius.'), inkext.ExtOption('--convex-hull-draw', type='inkbool', default=True, help='Draw convex hull.'), inkext.ExtOption('--hull-draw', type='inkbool', default=True, help='Draw polyhull.'), inkext.ExtOption('--hull-inner-draw', type='inkbool', default=True, help='Draw inner polyhulls.'), inkext.ExtOption('--hull-stroke', default='#000000', help='Polygon CSS stroke color.'), inkext.ExtOption('--hull-stroke-width', default='3px', help='Polygon CSS stroke width.'), inkext.ExtOption('--hull2-draw', type='inkbool', default=True, help='Create expanded polyhull.'), inkext.ExtOption('--hull2-clip', type='inkbool', default=True, help='Use expanded polyhull to clip.'), inkext.ExtOption('--hull2-draw-rays', type='inkbool', default=True, help='Draw rays.'), inkext.ExtOption('--hull2-max-angle', type='degrees', default=180, help='Max angle'), ) _styles = { 'dot': 'fill:%s;stroke-width:1px;stroke:#000000;', 'polyhull': 'fill:none;stroke-opacity:1.0;stroke-linejoin:round;' 'stroke-width:${polyhull_stroke_width};stroke:${polyhull_stroke};', 'polychain': 'fill:none;stroke-opacity:0.8;stroke-linejoin:round;' 'stroke-width:${polychain_stroke_width};stroke:${polychain_stroke};', 'polypath0': 'fill:none;stroke-opacity:0.8;stroke-linejoin:round;' 'stroke-width:${polypath_stroke_width};stroke:${polypath_stroke};', 'polypath': 'fill:none;stroke-opacity:0.8;stroke-linejoin:round;' 'stroke-width:${polypath_stroke_width};stroke:${polypath_stroke};', 'convexhull': 'fill:none;stroke-opacity:1.0;stroke-linejoin:round;' 'stroke-width:${convexhull_stroke_width};stroke:${convexhull_stroke};', } _style_defaults = { 'polyhull_stroke_width': '3pt', 'polyhull_stroke': '#505050', 'polychain_stroke_width': '3pt', 'polychain_stroke': '#00ff00', 'polypath_stroke_width': '3pt', 'polypath_stroke': '#3090c0', 'convexhull_stroke_width': '3pt', 'convexhull_stroke': '#ff9030', } def run(self): """Main entry point for Inkscape extension. """ random.seed() geom.set_epsilon(self.options.epsilon) geom.debug.set_svg_context(self.debug_svg) styles = self.svg.styles_from_templates(self._styles, self._style_defaults, self.options.__dict__) self._styles.update(styles) # Get a list of selected SVG shape elements and their transforms svg_elements = self.svg.get_shape_elements(self.get_elements()) if not svg_elements: # Nothing selected or document is empty return path_list = geomsvg.svg_to_geometry(svg_elements) # Create graph from geometry segment_graph = planargraph.Graph() for path in path_list: for segment in path: segment_graph.add_edge(segment) self.clip_rect = geom.box.Box((0, 0), self.svg.get_document_size()) if self.options.polysegpath_draw or self.options.polysegpath_longest: path_builder = planargraph.GraphPathBuilder(segment_graph) if self.options.polysegpath_draw: self._draw_polypaths(path_builder) if self.options.polysegpath_longest: self._draw_longest_polypaths(path_builder) if self.options.polyoffset_draw: self._draw_offset_polygons(segment_graph) if self.options.convex_hull_draw: self._draw_convex_hull(segment_graph) if self.options.hull_draw: outer_hull = segment_graph.boundary_polygon() self._draw_polygon_hulls((outer_hull,)) if self.options.hull_inner_draw: inner_hulls = segment_graph.peel_boundary_polygon(outer_hull) if inner_hulls: self._draw_polygon_hulls(inner_hulls) def _draw_polypaths(self, path_builder): layer = self.svg.create_layer('q_polypath', incr_suffix=True) path_list = path_builder.build_paths( path_strategy=self.options.polysegpath_type) for path in path_list: if len(path) > self.options.polysegpath_min_length: self.svg.create_polygon(path, close_polygon=False, style=self._styles['polypath'], parent=layer) def _draw_longest_polypaths(self, path_builder): path_list = path_builder.build_longest_paths( path_strategy=self.options.polysegpath_type) for i, path in enumerate(path_list): if i == self.options.polysegpath_max: break layer = self.svg.create_layer('q_polypath_long_%d_' % i, incr_suffix=True) self.svg.create_polygon(path, close_polygon=False, style=self._styles['polypath'], parent=layer) def _draw_offset_polygons(self, graph): layer = self.svg.create_layer('q_cell_polygons', incr_suffix=True) polygons = graph.get_face_polygons() offset_polygons = self._offset_polys(polygons, self.options.polyoffset_offset, self.options.polyoffset_jointype, self.options.polyoffset_recurs) for poly in offset_polygons: if (self.options.polyoffset_fillet and self.options.polyoffset_fillet_radius > 0): offset_path = fillet.fillet_polygon(poly, self.options.polyoffset_fillet_radius) self.svg.create_polypath(offset_path, close_path=True, style=self._styles['polypath'], parent=layer) else: self.svg.create_polygon(poly, close_path=True, style=self._styles['polypath'], parent=layer) # faces = graph.get_face_polygons() # for face_poly in faces: # offset_polys = polygon.offset_polygons(face_poly, # self.options.polyoffset_offset) # for poly in offset_polys: # if (self.options.polyoffset_fillet # and self.options.polyoffset_fillet_radius > 0): # offset_path = fillet.fillet_polygon(poly, # self.options.polyoffset_fillet_radius) # self.svg.create_polypath(offset_path, close_path=True, # style=self._styles['polypath'], # parent=layer) # else: # self.svg.create_polygon(poly, close_path=True, # style=self._styles['polypath'], # parent=layer) def _offset_polys(self, polygons, offset, jointype, recurs=False): offset_polygons = [] for poly in polygons: offset_polys = polygon.offset_polygons(poly, offset, jointype) offset_polygons.extend(offset_polys) if recurs: sub_offset_polys = self._offset_polys(offset_polys, offset, jointype, True) offset_polygons.extend(sub_offset_polys) return offset_polygons def _draw_convex_hull(self, segment_graph): layer = self.svg.create_layer('q_convex_hull', incr_suffix=True) vertices = polygon.convex_hull(segment_graph.vertices()) style = self._styles['convexhull'] self.svg.create_polygon(vertices, style=style, parent=layer) def _draw_polygon_hulls(self, polygon_hulls): layer = self.svg.create_layer('q_polyhull', incr_suffix=True) for polyhull in polygon_hulls: self.svg.create_polygon(polyhull, style=self._styles['polyhull'], parent=layer) polyhull = polygon_hulls[0] # layer = self.svg.create_layer('q_polyhull2_triangles', incr_suffix=True) # concave_verts, polyhull2 = self._concave_vertices(polyhull, max_angle=math.pi/2) # for triangle in concave_verts: # self.svg.create_polygon(triangle, style=self._styles['polyhull'], parent=layer) # # layer = self.svg.create_layer('q_polyhull2', incr_suffix=True) # self.svg.create_polygon(polyhull2, # style=self._styles['polyhull'], parent=layer) # # layer = self.svg.create_layer('q_polyhull_rays', incr_suffix=True) # convex_verts = self._convex_vertices(polyhull) # rays = self._get_polygon_rays(convex_verts, self.clip_rect) # for ray in rays: # self.svg.create_line(ray.p1, ray.p2, style=self._styles['polyhull'], # parent=layer) # # layer = self.svg.create_layer('q_polyhull2_rays', incr_suffix=True) # convex_verts = self._convex_vertices(polyhull2) # rays = self._get_polygon_rays(convex_verts, self.clip_rect) # for ray in rays: # self.svg.create_line(ray.p1, ray.p2, style=self._styles['polyhull'], # parent=layer) 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 _convex_vertices(self, vertices): """ :param vertices: the polygon vertices. An iterable of 2-tuple (x, y) points. :return: A list of triplet vertices that are pointy towards the outside. """ pointy_verts = [] clockwise = polygon.area(vertices) < 0 i = -3 if vertices[-1] == vertices[0] else -2 vert1 = vertices[i] vert2 = vertices[i + 1] for vert3 in vertices: seg = geom.Line(vert1, vert2) side = seg.which_side(vert3, inline=True) if side != 0 and ((clockwise and side > 0) or (not clockwise and side < 0)): pointy_verts.append((vert1, vert2, vert3)) vert1 = vert2 vert2 = vert3 return pointy_verts def _concave_vertices(self, vertices, max_angle=math.pi): """ Args: vertices: the polygon vertices. An iterable of 2-tuple (x, y) points. max_angle: Maximum interior angle of the concave vertices. Only concave vertices with an interior angle less than this will be returned. Returns: A list of triplet vertices that are pointy towards the inside and a new, somewhat more convex, polygon with the concave vertices closed. """ concave_verts = [] new_polygon = [] clockwise = polygon.area(vertices) < 0 i = -3 if vertices[-1] == vertices[0] else -2 vert1 = vertices[i] vert2 = vertices[i + 1] for vert3 in vertices: seg = geom.Line(vert1, vert2) side = seg.which_side(vert3) angle = abs(vert2.angle2(vert1, vert3)) if angle < max_angle and ((clockwise and side < 0) or (not clockwise and side > 0)): concave_verts.append((vert1, vert2, vert3)) new_polygon.append(vert3) elif not new_polygon or vert2 != new_polygon[-1]: new_polygon.append(vert2) vert1 = vert2 vert2 = vert3 return (concave_verts, new_polygon)
class Lines(inkext.InkscapeExtension): """""" OPTIONSPEC = ( inkext.ExtOption('--hline-draw', type='inkbool', default=False, help=_('Draw horizontal lines')), inkext.ExtOption('--hline-spacing', type='docunits', default=1.0, help=_('Line spacing center to center')), inkext.ExtOption('--hline-spacing-jitter', type='int', default=0, help=_('Line spacing jitter (0-100% of spacing)')), inkext.ExtOption('--hline-angle-jitter', type='degrees', default=0, help=_('Maximum line angle jitter (degrees)')), inkext.ExtOption('--hline-angle-kappa', type='float', default=2, help=_('Angle jitter concentration (kappa)')), inkext.ExtOption('--hline-varspacing', type='inkbool', default=False, help=_('Enable variable spacing')), inkext.ExtOption('--hline-varspacing-min', type='docunits', default=0, help=_('Minimum line spacing')), inkext.ExtOption('--hline-varspacing-max', type='docunits', default=0, help=_('Maximum line spacing')), inkext.ExtOption('--hline-varspacing-cycles', type='float', default=1, help=_('Number of cycles')), inkext.ExtOption('--hline-varspacing-formula', default='', help=_('Spacing formula')), inkext.ExtOption('--hline-varspacing-invert', type='inkbool', default=False, help=_('Invert spacing order')), inkext.ExtOption('--hline-rotation', type='degrees', default=0.0, help=_('Line rotation')), inkext.ExtOption('--hline-reverse-path', type='inkbool', default=False, help=_('Draw left to right')), inkext.ExtOption('--hline-reverse-order', type='inkbool', default=True, help=_('Reverse line order')), inkext.ExtOption('--hline-double', type='inkbool', default=True, help=_('Double line')), inkext.ExtOption('--hline-alt', type='inkbool', default=False, help=_('Alternate line direction')), inkext.ExtOption('--hline-connect', type='inkbool', default=False, help=_('Connect lines')), inkext.ExtOption('--hline-skip', type='int', default=1, help=_('Skip lines')), inkext.ExtOption('--hline-start', type='int', default=2, help=_('Start lines at')), inkext.ExtOption('--vline-draw', type='inkbool', default=False, help=_('Draw vertical lines')), inkext.ExtOption('--vline-spacing', type='docunits', default=1.0, help=_('Line spacing center to center')), inkext.ExtOption('--vline-spacing-jitter', type='int', default=0, help=_('Line spacing jitter (0-100% of spacing)')), inkext.ExtOption('--vline-varspacing', type='inkbool', default=False, help=_('Enable variable spacing')), inkext.ExtOption('--vline-varspacing-min', type='docunits', default=0, help=_('Minimum line spacing')), inkext.ExtOption('--vline-varspacing-max', type='docunits', default=0, help=_('Maximum line spacing')), inkext.ExtOption('--vline-varspacing-cycles', type='float', default=1, help=_('Number of cycles')), inkext.ExtOption('--vline-varspacing-formula', default='', help=_('Spacing formula')), inkext.ExtOption('--vline-varspacing-invert', type='inkbool', default=True, help=_('Invert spacing order')), inkext.ExtOption('--vline-rotation', type='degrees', default=0.0, help=_('Line rotation')), inkext.ExtOption('--vline-angle-jitter', type='degrees', default=0, help=_('Maximum line angle jitter (degrees)')), inkext.ExtOption('--vline-angle-kappa', type='float', default=2, help=_('Angle jitter concentration (kappa)')), inkext.ExtOption('--vline-right2left', type='inkbool', default=False, help=_('Direction left to right')), inkext.ExtOption('--vline-reverse-path', type='inkbool', default=True, help=_('Order top to bottom')), inkext.ExtOption('--vline-reverse-order', type='inkbool', default=True, help=_('Reverse line order')), inkext.ExtOption('--vline-double', type='inkbool', default=True, help=_('Double line')), inkext.ExtOption('--vline-alt', type='inkbool', default=False, help=_('Alternate line direction')), inkext.ExtOption('--vline-connect', type='inkbool', default=False, help=_('Connect lines')), inkext.ExtOption('--vline-skip', type='int', default=1, help=_('Skip lines')), inkext.ExtOption('--vline-start', type='int', default=2, help=_('Start lines at')), inkext.ExtOption('--css-default', type='inkbool', default=False, help=_('Use default CSS (black, 1pt, 100%)')), inkext.ExtOption('--h-stroke-width', type='docunits', default=1.0, help=_('Line stroke width')), inkext.ExtOption('--h-stroke-opacity', type='float', default=1.0, help=_('Line stroke opacity')), inkext.ExtOption('--h-stroke', type='string', default='#000000', help=_('Line stroke color')), inkext.ExtOption('--vline-copycss', type='inkbool', default=False, help=_('Use same CSS settings as horizontal lines')), inkext.ExtOption('--v-stroke-width', type='docunits', default=1.0, help=_('Line stroke width')), inkext.ExtOption('--v-stroke-opacity', type='float', default=1.0, help=_('Line stroke opacity')), inkext.ExtOption('--v-stroke', type='string', default='#000000', help=_('Line stroke color')), inkext.ExtOption('--sine-waves', type='string', default='', help=_('Sine waves')), inkext.ExtOption('--sine-amplitude', type='docunits', default=1.0, help=_('Sine wave amplitude')), inkext.ExtOption('--sine-wavelength', type='docunits', default=1.0, help=_('Sine wavelength')), inkext.ExtOption('--grid-layers', type='inkbool', default=False, help=_('One layer per grid part (horizontal, vertical)')), inkext.ExtOption('--line-fillet', type='inkbool', default=False, help=_('Fillet connected lines')), inkext.ExtOption('--line-fillet-radius', type='docunits', default=0.0, help=_('Fillet radius')), inkext.ExtOption('--hline-vline', type='inkbool', default=False, help=_('Alternate horizontal and vertical lines')), inkext.ExtOption('--hv-shuffle', type='inkbool', default=False, help=_('Shuffle alternating lines')), # inkext.ExtOption('--enable-jitter', type='inkbool', default=False, # help=_('Disable jitter')), # inkext.ExtOption('--spacing-jitter', type='int', default=0, # help=_('Line spacing jitter (0-100% of spacing)')), # inkext.ExtOption('--angle-jitter', type='degrees', default=0, # help=_('Maximum line angle jitter (degrees)')), # inkext.ExtOption('--angle-kappa', type='float', default=2, # help=_('Angle jitter concentration (kappa)')), inkext.ExtOption('--gcode-write', type='inkbool', default=False, help=_('Write G code')), inkext.ExtOption('--gcode-no-z', type='inkbool', default=False, help=_('Don\'t lift brush for rapid moves (dangerous)')), inkext.ExtOption('--gcode-no-tangent', type='inkbool', default=False, help=_('Disable tangent rotation')), inkext.ExtOption('--gcode-speed', type='float', default=100, help=_('XY speed in units/minute')), inkext.ExtOption('--gcode-path', default='~/lines.ngc', help=_('Path to output file')), inkext.ExtOption('--sine-line', type='inkbool', default=False, help=_('Draw lines as sine waves')), inkext.ExtOption('--sine-start-wavelength', type='float', default=1, help=_('Starting wavelength')), inkext.ExtOption('--sine-end-wavelength', type='float', default=1, help=_('Ending wavelength')), inkext.ExtOption('--sine-start-amplitude', type='float', default=0, help=_('Starting amplitude')), inkext.ExtOption('--sine-end-amplitude', type='float', default=0, help=_('Ending amplitude')), inkext.ExtOption('--margin-left', type='docunits', default=0.0, help=_('Left margin')), inkext.ExtOption('--margin-right', type='docunits', default=0.0, help=_('Right margin')), inkext.ExtOption('--margin-top', type='docunits', default=0.0, help=_('Top margin')), inkext.ExtOption('--margin-bottom', type='docunits', default=0.0, help=_('Bottom margin')), ) _LAYER_NAME = 'Grid lines' _LAYER_NAME_H = 'Grid lines (H)' _LAYER_NAME_V = 'Grid lines (V)' _LINE_STYLE = 'fill:none;stroke:%s;stroke-width:%.3f;stroke-opacity:%.2f' _MIN_OPACITY = 0.1 _styles = { 'h_line': 'fill:none;stroke-linejoin:round;' 'stroke:$h_stroke;stroke-width:$h_stroke_width;' 'stroke-opacity:$h_stroke_opacity;', 'v_line': 'fill:none;stroke-linejoin:round;' 'stroke:$v_stroke;stroke-width:$v_stroke_width;' 'stroke-opacity:$v_stroke_opacity;', } _style_defaults = { 'h_stroke': '#c0c0c0', 'h_stroke_width': '1pt', 'h_stroke_opacity': '1', 'v_stroke': '#c0c0c0', 'v_stroke_width': '1pt', 'v_stroke_opacity': '1', } def run(self): """Main entry point for Inkscape plugins. """ geom.debug.set_svg_context(self.debug_svg) if not self.options.css_default: color = css.csscolor_to_cssrgb(self.options.h_stroke) self.options.h_stroke = color if self.options.h_stroke_width == 0: self.options.h_stroke_width = self.svg.unit2uu('1pt') if self.options.h_stroke_opacity == 0: self.options.h_stroke_opacity = self._MIN_OPACITY if self.options.vline_copycss: self.options.v_stroke = self.options.h_stroke self.options.v_stroke_width = self.options.h_stroke_width self.options.v_stroke_opacity = self.options.h_stroke_opacity else: color = css.csscolor_to_cssrgb(self.options.v_stroke) self.options.v_stroke = color if self.options.v_stroke_width == 0: self.options.v_stroke_width = self.svg.unit2uu('1pt') if self.options.h_stroke_opacity == 0: self.options.h_stroke_opacity = self._MIN_OPACITY option_styles = vars(self.options) else: option_styles = None # Update styles with any command line option values self._styles.update(self.svg.styles_from_templates( self._styles, self._style_defaults, option_styles)) self.cliprect = self.svg.margin_cliprect(self.options.margin_top, self.options.margin_right, self.options.margin_bottom, self.options.margin_left) # Jitter is expressed as a percentage of max jitter. # Max jitter is 50% of line spacing. self.options.hline_spacing_jitter /= 100 self.options.vline_spacing_jitter /= 100 # Create the grid lines hlines = [] vlines = [] if self.options.hline_draw: logger.debug('invert0: %s', self.options.hline_varspacing_invert) lineset = LineSet( self.cliprect, self.options.hline_spacing, self.options.hline_rotation, spacing_jitter=self.options.hline_spacing_jitter, angle_jitter=self.options.hline_angle_jitter, angle_jitter_kappa=self.options.hline_angle_kappa, spacing_formula=self.options.hline_varspacing_formula, varspace_min=self.options.hline_varspacing_min, varspace_max=self.options.hline_varspacing_max, varspace_cycles=self.options.hline_varspacing_cycles, varspace_invert=self.options.hline_varspacing_invert) hlines = lineset.lines if self.options.hline_reverse_order: hlines.reverse() hlines = self.insert_reversed_lines(hlines, self.options.hline_double, self.options.hline_reverse_path, self.options.hline_alt) if self.options.vline_draw: lineset = LineSet( self.cliprect, self.options.vline_spacing, self.options.vline_rotation + math.pi / 2, spacing_jitter=self.options.vline_spacing_jitter, angle_jitter=self.options.vline_angle_jitter, angle_jitter_kappa=self.options.vline_angle_kappa, spacing_formula=self.options.vline_varspacing_formula, varspace_min=self.options.vline_varspacing_min, varspace_max=self.options.vline_varspacing_max, varspace_cycles=self.options.vline_varspacing_cycles, varspace_invert=self.options.vline_varspacing_invert) vlines = lineset.lines if self.options.vline_reverse_order: vlines.reverse() vlines = self.insert_reversed_lines(vlines, self.options.vline_double, self.options.vline_reverse_path, self.options.vline_alt) if not self.options.hline_vline: # Connect the lines to create continuous paths if self.options.hline_connect: hlines = self.insert_connectors(hlines) if self.options.vline_connect: vlines = self.insert_connectors(vlines) # TODO: See if it makes sense to then connect the two paths # Create polypaths hpaths = self.connected_paths(hlines) vpaths = self.connected_paths(vlines) # Create SVG layer(s) if ((not self.options.grid_layers) or (self.options.hline_vline and hlines and vlines)): h_layer = self.svg.create_layer(self._LAYER_NAME, incr_suffix=True, flipy=True) v_layer = h_layer else: if hlines: h_layer = self.svg.create_layer(self._LAYER_NAME_H, incr_suffix=True, flipy=True) if vlines: v_layer = self.svg.create_layer(self._LAYER_NAME_V, incr_suffix=True, flipy=True) if self.options.hline_vline and hlines and vlines: # Optionally shuffle the path order. if self.options.hv_shuffle: random.shuffle(hpaths) random.shuffle(vpaths) # Draw horizontal alternating with vertical grid lines for hpath, vpath in itertools.izip_longest(hpaths, vpaths): if hpath is not None: self.svg.create_polypath(hpath, style=self._styles['h_line'], parent=h_layer) if vpath is not None: self.svg.create_polypath(vpath, style=self._styles['v_line'], parent=h_layer) else: if hlines: self.render_lines(hpaths, style=self._styles['h_line'], layer=h_layer) if vlines: self.render_lines(vpaths, style=self._styles['v_line'], layer=v_layer) def connected_paths(self, lines): """ Make paths from connected lines """ if not lines: return [] paths = [] path = [lines[0]] for line in lines[1:]: if path[-1].p2 == line.p1: path.append(line) else: paths.append(path) # logger.debug('pathlen: %d' % len(path)) path = [line] if path: paths.append(path) return paths def render_lines(self, paths, style, layer): """ Render line paths as SVG """ for path in paths: if len(path) == 1: self.svg.create_line(path[0].p1, path[0].p2, style=style, parent=layer) elif path: if self.options.line_fillet: radius = self.options.line_fillet_radius path = geom.fillet.fillet_path(path, radius, fillet_close=False) self.svg.create_polypath(path, style=style, parent=layer) def insert_reversed_lines(self, lines, doubled, reverse, alternate): """ """ if not doubled and not reverse and not alternate: return lines doubled_lines = [] linenum = 0 for line in lines: is_odd = (linenum % 2) != 0 # logger.debug('R: %s, A: %s, O: %s', str(reverse), str(alternate), str(is_odd)) if ((not reverse and alternate and is_odd) or (reverse and (not alternate or not is_odd))): doubled_lines.append(line.reversed()) if doubled: doubled_lines.append(line) else: doubled_lines.append(line) if doubled: doubled_lines.append(line.reversed()) linenum += 1 return doubled_lines def insert_connectors(self, lines): """ """ connected_lines = [] prev_line = None for line in lines: if prev_line is not None and prev_line.p2 != line.p1: connected_lines.append(geom.Line(prev_line.p2, line.p1)) prev_line = line connected_lines.append(line) return connected_lines
class BreakShuffle(inkext.InkscapeExtension): """An Inkscape extension that smoothes polygons. """ # Command line options OPTIONSPEC = ( inkext.ExtOption('--shuffle', type='inkbool', default=True, help=_('Shuffle paths')), inkext.ExtOption('--explode-paths', type='inkbool', default=False, help=_('Explode paths')), inkext.ExtOption('--reverse-order', type='inkbool', default=False, help=_('Reverse path order')), inkext.ExtOption('--method', default='shuffle', help=_('Path arrangement method')), # inkext.ExtOption('--shuffle-pathdir', default='', # help=_('Path direction shuffle method')), ) _LAYER_NAME = 'shuffled-paths' def run(self): """Main entry point for Inkscape extensions. """ if not self.options.shuffle and not self.options.explode_paths: return layer_elements = [] selected_elements = self.get_elements(selected_only=True) if selected_elements: layer_elements.append(selected_elements) else: layers = self.svg.get_visible_layers() for layer in layers: elements = self.svg.get_layer_elements(layer) if elements: layer_elements.append(elements) layer_paths = [] for elements in layer_elements: # Filter elements for just the path elements paths = [node for node in elements if node.tag == svg_ns('path')] logger.debug('ne=%d, np=%d', len(elements), len(paths)) if paths: layer_paths.append(paths) if not layer_paths: # Nothing to do - so bail. # TODO: Maybe let the user know... return # Create a new layer for the SVG output. new_layer = self.svg.create_layer(self._LAYER_NAME, incr_suffix=True) # Explode the paths first if self.options.explode_paths: exploded_layer_paths = [] for paths in layer_paths: exploded_paths = [] for path in paths: path_data = path.get('d') path_style = path.get('style') path_transform = self.svg.get_element_transform(path) transform_attr = None if not transform2d.is_identity_transform(path_transform): transform_attr = svg.svg.transform_attr(path_transform) dlist = svg.svg.explode_path(path_data) for d in dlist: attr = {'d': d} if path_style is not None and path_style: attr['style'] = path_style if transform_attr is not None: attr['transform'] = transform_attr element = etree.Element(svg_ns('path'), attr) exploded_paths.append(element) exploded_layer_paths.append(exploded_paths) layer_paths = exploded_layer_paths all_paths = list(itertools.chain(*layer_paths)) if self.options.method == 'shuffle': random.shuffle(all_paths) # for path in all_paths: # new_layer.append(path) elif self.options.method == 'reverse': all_paths.reverse() # else: # # Just add the exploded paths... # for paths in layer_paths: # for path in paths: # new_layer.append(path) for path in all_paths: new_layer.append(path)
class Tcnc(inkext.InkscapeExtension): """Inkscape plugin that converts selected SVG elements into gcode suitable for a four axis (XYZA) CNC machine with a tangential tool, such as a knife or a brush, that rotates about the Z axis. """ OPTIONSPEC = ( inkext.ExtOption('--origin-ref', default='doc', help=_('Lower left origin reference.')), inkext.ExtOption('--path-sort-method', default='none', help=_('Path sorting method.')), inkext.ExtOption('--biarc-tolerance', type='docunits', default=0.01, help=_('Biarc approximation fitting tolerance.')), inkext.ExtOption('--biarc-max-depth', type='int', default=4, help=_('Biarc approximation maximum curve ' 'splitting recursion depth.')), inkext.ExtOption('--line-flatness', type='docunits', default=0.001, help=_('Curve to line flatness.')), inkext.ExtOption('--min-arc-radius', type='degrees', default=0.01, help=_('All arcs having radius less than minimum ' 'will be considered as straight line.')), inkext.ExtOption('--tolerance', type='float', default=0.00001, help=_('Tolerance')), inkext.ExtOption('--gcode-units', default='in', help=_('G code output units (inch or mm).')), inkext.ExtOption('--xy-feed', type='float', default=10.0, help=_('XY axis feed rate in unit/m')), inkext.ExtOption('--z-feed', type='float', default=10.0, help=_('Z axis feed rate in unit/m')), inkext.ExtOption('--a-feed', type='float', default=60.0, help=_('A axis feed rate in deg/m')), inkext.ExtOption('--z-safe', type='float', default=1.0, help=_('Z axis safe height for rapid moves')), inkext.ExtOption('--z-wait', type='float', default=500, help=_('Z axis wait (milliseconds)')), inkext.ExtOption('--blend-mode', default='', help=_('Trajectory blending mode.')), inkext.ExtOption('--blend-tolerance', type='float', default='0', help=_('Trajectory blending tolerance.')), inkext.ExtOption('--disable-tangent', type='inkbool', default=False, help=_('Disable tangent rotation')), inkext.ExtOption('--z-depth', type='float', default=-0.125, help=_('Z full depth of cut')), inkext.ExtOption('--z-step', type='float', default=-0.125, help=_('Z cutting step depth')), inkext.ExtOption('--tool-width', type='docunits', default=1.0, help=_('Tool width')), inkext.ExtOption('--a-feed-match', type='inkbool', default=False, help=_('A axis feed rate match XY feed')), inkext.ExtOption('--tool-trail-offset', type='docunits', default=0.25, help=_('Tool trail offset')), inkext.ExtOption('--a-offset', type='degrees', default=0, help=_('Tool offset angle')), inkext.ExtOption('--allow-tool-reversal', type='inkbool', default=False, help=_('Allow tool reversal')), inkext.ExtOption('--tool-wait', type='float', default=0, help=_('Tool up/down wait time in seconds')), inkext.ExtOption('--spindle-mode', default='', help=_('Spindle startup mode.')), inkext.ExtOption('--spindle-speed', type='int', default=0, help=_('Spindle RPM')), inkext.ExtOption('--spindle-wait-on', type='float', default=0, help=_('Spindle warmup delay')), inkext.ExtOption('--spindle-clockwise', type='inkbool', default=True, help=_('Clockwise spindle rotation')), inkext.ExtOption('--skip-path-count', type='int', default=0, help=_('Number of paths to skip.')), inkext.ExtOption('--ignore-segment-angle', type='inkbool', default=False, help=_('Ignore segment start angle.')), inkext.ExtOption('--path-tool-fillet', type='inkbool', default=False, help=_('Fillet paths for tool width')), inkext.ExtOption('--path-tool-offset', type='inkbool', default=False, help=_('Offset paths for tool trail offset')), inkext.ExtOption('--path-preserve-g1', type='inkbool', default=False, help=_('Preserve G1 continuity for offset arcs')), inkext.ExtOption('--path-smooth-fillet', type='inkbool', default=False, help=_('Fillets at sharp corners')), inkext.ExtOption('--path-smooth-radius', type='docunits', default=0.0, help=_('Smoothing radius')), inkext.ExtOption('--path-close-polygons', type='inkbool', default=False, help=_('Close polygons with fillet')), inkext.ExtOption('--path-split-cusps', type='inkbool', default=False, help=_('Split paths at non-tangent control points')), # inkext.ExtOption('--brush-flip-stroke', type='inkbool', default=False, # help=_('Flip brush before every stroke.')), # inkext.ExtOption('--brush-flip-path', type='inkbool', default=False, # help=_('Flip after each path.')), # inkext.ExtOption('--brush-flip-reload', type='inkbool', default=False, # help=_('Flip before reload.')), inkext.ExtOption('--brush-reload-enable', type='inkbool', default=False, help=_('Enable brush reload.')), inkext.ExtOption('--brush-reload-rotate', type='inkbool', default=False, help=_('Rotate brush before reload.')), inkext.ExtOption('--brush-pause-mode', default='', help=_('Brush reload pause mode.')), inkext.ExtOption('--brush-reload-max-paths', type='int', default=1, help=_('Number of paths between reload.')), inkext.ExtOption('--brush-reload-dwell', type='float', default=0.0, help=_('Brush reload time (seconds).')), inkext.ExtOption('--brush-reload-angle', type='degrees', default=90.0, help=_('Brush reload angle (degrees).')), inkext.ExtOption('--brush-overshoot-mode', default='', help=_('Brush overshoot mode.')), inkext.ExtOption('--brush-overshoot-distance', type='docunits', default=0.0, help=_('Brush overshoot distance.')), inkext.ExtOption('--brush-soft-landing', type='inkbool', default=False, help=_('Enable soft landing.')), inkext.ExtOption('--brush-landing-strip', type='docunits', default=0.0, help=_('Landing strip distance.')), inkext.ExtOption('--brushstroke-max', type='docunits', default=0.0, help=_('Max brushstroke distance before reload.')), inkext.ExtOption('--output-path', default='~/output.ngc', help=_('Output path name')), inkext.ExtOption('--append-suffix', type='inkbool', default=False, help=_('Append auto-incremented numeric' ' suffix to filename')), inkext.ExtOption('--separate-layers', type='inkbool', default=False, help=_('Separate gcode file per layer')), inkext.ExtOption('--preview-toolmarks', type='inkbool', default=False, help=_('Show tangent tool preview.')), inkext.ExtOption('--preview-toolmarks-outline', type='inkbool', default=False, help=_('Show tangent tool preview outline.')), inkext.ExtOption('--preview-scale', default='medium', help=_('Preview scale.')), inkext.ExtOption('--write-settings', type='inkbool', default=False, help=_('Write Tcnc command line options in header.')), inkext.ExtOption('--x-subpath-render', type='inkbool', default=False, help=_('Render subpaths')), inkext.ExtOption('--x-subpath-offset', type='docunits', default=0.0, help=_('Subpath spacing')), inkext.ExtOption('--x-subpath-smoothness', type='float', default=0.0, help=_('Subpath smoothness')), inkext.ExtOption('--x-subpath-layer', default='subpaths (tcnc)', help=_('Subpath layer name')), ) # Document units that can be expressed as imperial (inches) _IMPERIAL_UNITS = ('in', 'ft', 'yd', 'pc', 'pt', 'px') # Document units that can be expressed as metric (mm) _METRIC_UNITS = ('mm', 'cm', 'm', 'km') _DEFAULT_DIR = '~' _DEFAULT_FILEROOT = 'output' _DEFAULT_FILEEXT = '.ngc' def run(self): """Main entry point for Inkscape plugins. """ # Initialize the geometry module with tolerances and debug output geom.set_epsilon(self.options.tolerance) geom.debug.set_svg_context(self.debug_svg) # Create a transform to flip the Y axis. page_height = self.svg.get_document_size()[1] flip_transform = transform2d.matrix_scale_translate( 1.0, -1.0, 0.0, page_height) timer_start = timeit.default_timer() # skip_layers = (gcodesvg.SVGPreviewPlotter.PATH_LAYER_NAME, # gcodesvg.SVGPreviewPlotter.TOOL_LAYER_NAME) skip_layers = ['tcnc .*'] # Get a list of selected SVG shape elements and their transforms svg_elements = self.svg.get_shape_elements(self.get_elements(), skip_layers=skip_layers) if not svg_elements: # Nothing selected or document is empty return # Convert SVG elements to path geometry path_list = geomsvg.svg_to_geometry(svg_elements, flip_transform) # Create the output file path name filepath = create_pathname(self.options.output_path, append_suffix=self.options.append_suffix) try: with io.open(filepath, 'w') as output: gcgen = self._init_gcode(output) cam = self._init_cam(gcgen) cam.generate_gcode(path_list) except IOError as error: self.errormsg(str(error)) timer_end = timeit.default_timer() total_time = timer_end - timer_start logger.info('Tcnc time: %s', str(timedelta(seconds=total_time))) def _init_gcode(self, output): """Create and initialize the G code generator with machine details. """ if self.options.a_feed_match: # This option sets the angular feed rate of the A axis so # that the outside edge of the brush matches the linear feed # rate of the XY axes when doing a simple rotation. # TODO: verify correctness here angular_rate = self.options.xy_feed / self.options.tool_width / 2 self.options.a_feed = math.degrees(angular_rate) # Create G-code preview plotter. preview_svg_context = inksvg.InkscapeSVGContext(self.svg.document) preview_plotter = gcodesvg.SVGPreviewPlotter( preview_svg_context, tool_width=self.options.tool_width, tool_offset=self.options.tool_trail_offset, style_scale=self.options.preview_scale, show_toolmarks=self.options.preview_toolmarks, show_tm_outline=self.options.preview_toolmarks_outline) # Experimental options preview_plotter.x_subpath_render = self.options.x_subpath_render preview_plotter.x_subpath_layer_name = self.options.x_subpath_layer preview_plotter.x_subpath_offset = self.options.x_subpath_offset preview_plotter.x_subpath_smoothness = self.options.x_subpath_smoothness # Create G-code generator. gcgen = gcode.GCodeGenerator(xyfeed=self.options.xy_feed, zsafe=self.options.z_safe, zfeed=self.options.z_feed, afeed=self.options.a_feed, plotter=preview_plotter, output=output) gcgen.add_header_comment(( 'Generated by TCNC Version %s' % __version__, '', )) # Show option settings in header if self.options.write_settings: gcgen.add_header_comment('Settings:') option_dict = vars(self.options) for option in self.OPTIONSPEC: val = option_dict.get(option.dest) if val is not None: if val == None or val == option.default: # Skip default settings... continue # valstr = '%s (default)' % str(val) else: valstr = str(val) optname = option.dest.replace('_', '-') gcgen.add_header_comment('--%s = %s' % (optname, valstr)) # This will be 'doc', 'in', or 'mm' units = self.options.gcode_units doc_units = self.svg.get_document_units() if units == 'doc': if doc_units != 'in' and doc_units != 'mm': # Determine if the units are metric or imperial. # Pica and pixel units are considered imperial for now... if doc_units in self._IMPERIAL_UNITS: units = 'in' elif doc_units in self._METRIC_UNITS: units = 'mm' else: self.errormsg( _('Document units must be imperial or metric.')) raise Exception() else: units = doc_units unit_scale = self.svg.uu2unit('1.0', to_unit=units) gcgen.set_units(units, unit_scale) # logger = logging.getLogger(__name__) # logger.debug('doc units: %s' % doc_units) # logger.debug('view_scale: %f' % self.svg.view_scale) # logger.debug('unit_scale: %f' % unit_scale) # gcgen.set_tolerance(geom.const.EPSILON) # gcgen.set_output_precision(geom.const.EPSILON_PRECISION) gcgen.set_tolerance(self.options.tolerance) precision = max(0, int(round(abs(math.log(self.options.tolerance, 10))))) gcgen.set_output_precision(precision) if self.options.blend_mode: gcgen.set_path_blending(self.options.blend_mode, self.options.blend_tolerance) gcgen.spindle_speed = self.options.spindle_speed gcgen.spindle_wait_on = self.options.spindle_wait_on * 1000 gcgen.spindle_clockwise = self.options.spindle_clockwise gcgen.spindle_auto = (self.options.spindle_mode == 'path') gcgen.tool_wait_down = self.options.tool_wait gcgen.tool_wait_up = self.options.tool_wait return gcgen def _init_cam(self, gc): """Create and initialize the tool path generator.""" enable_tangent = not self.options.disable_tangent cam = paintcam.PaintCAM(gc) cam.debug_svg = self.debug_svg cam.z_depth = self.options.z_depth cam.z_step = max(-(abs(self.options.z_step)), cam.z_depth) if self.options.path_sort_method != 'none': cam.path_sort_method = self.options.path_sort_method cam.tool_width = self.options.tool_width cam.tool_trail_offset = self.options.tool_trail_offset cam.biarc_tolerance = self.options.biarc_tolerance cam.biarc_max_depth = self.options.biarc_max_depth cam.line_flatness = self.options.line_flatness cam.skip_path_count = self.options.skip_path_count cam.enable_tangent = enable_tangent cam.path_tool_fillet = self.options.path_tool_fillet and enable_tangent cam.path_tool_offset = self.options.path_tool_offset and enable_tangent cam.path_preserve_g1 = self.options.path_preserve_g1 and enable_tangent cam.path_close_polygons = self.options.path_close_polygons and enable_tangent cam.path_smooth_fillet = self.options.path_smooth_fillet cam.path_smooth_radius = self.options.path_smooth_radius cam.path_split_cusps = self.options.path_split_cusps cam.allow_tool_reversal = self.options.allow_tool_reversal # cam.brush_landing_angle = self.options.brush_landing_angle # cam.brush_landing_end_height = self.options.brush_landing_end_height # cam.brush_landing_start_height = self.options.brush_landing_start_height # cam.brush_liftoff_angle = self.options.brush_liftoff_angle # cam.brush_liftoff_height = self.options.brush_liftoff_height # cam.brush_overshoot = self.options.brush_overshoot cam.brush_reload_enable = self.options.brush_reload_enable cam.brush_reload_rotate = self.options.brush_reload_rotate if self.options.brush_pause_mode in ('restart', 'time'): cam.brush_reload_pause = True if self.options.brush_pause_mode == 'time': cam.brush_reload_dwell = self.options.brush_reload_dwell else: cam.brush_reload_dwell = 0 cam.brush_reload_max_paths = self.options.brush_reload_max_paths cam.brush_reload_angle = self.options.brush_reload_angle # cam.brush_reload_after_interval = self.options.brushstroke_max > 0.0 cam.brush_depth = self.options.z_depth cam.brush_soft_landing = self.options.brush_soft_landing cam.brush_landing_strip = self.options.brush_landing_strip if self.options.brush_overshoot_mode == 'auto': cam.brush_overshoot_enable = True cam.brush_overshoot_auto = True cam.brush_overshoot_distance = cam.tool_width / 2 elif self.options.brush_overshoot_mode == 'manual': cam.brush_overshoot_enable = True cam.brush_overshoot_distance = self.options.brush_overshoot_distance # if self.options.brushstroke_max > 0.0: # cam.feed_interval = self.options.brushstroke_max return cam