#!/usr/bin/env python
#-----------------------------------------------------------------------------
# Copyright 2012-2016 Claude Zervas
# email: claude@utlco.com
#-----------------------------------------------------------------------------
"""
An Inkscape extension to create paths from a collection of vertices.
====
"""
# Python 3 compatibility boilerplate
from __future__ import (absolute_import, division,
print_function, unicode_literals)
from future_builtins import *
import math
import random
import gettext
# import logging
import geom
from geom import planargraph
from geom import polygon
from geom import fillet
from svg import geomsvg
from inkscape import inkext
__version__ = "0.2"
_ = gettext.gettext
# logger = logging.getLogger(__name__)
[docs]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',
}
[docs] 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)
if __name__ == '__main__':
PolyPath().main(optionspec=PolyPath.OPTIONSPEC)