Esempio n. 1
0
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 = ()
Esempio n. 2
0
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)
Esempio n. 3
0
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)
Esempio n. 4
0
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
Esempio n. 5
0
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)
Esempio n. 6
0
                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.'),
Esempio n. 7
0
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))
Esempio n. 8
0
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
Esempio n. 9
0
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)
Esempio n. 10
0
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
Esempio n. 11
0
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)
Esempio n. 12
0
File: tcnc.py Progetto: sod4602/tcnc
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