198 lines
8.1 KiB
Python
198 lines
8.1 KiB
Python
|
#! /usr/bin/python3
|
||
|
|
||
|
'''
|
||
|
Copyright (C) 2020 Scott Pakin, scott-ink@pakin.org
|
||
|
|
||
|
This program is free software; you can redistribute it and/or modify
|
||
|
it under the terms of the GNU General Public License as published by
|
||
|
the Free Software Foundation; either version 3 of the License, or
|
||
|
(at your option) any later version.
|
||
|
|
||
|
This program is distributed in the hope that it will be useful,
|
||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
GNU General Public License for more details.
|
||
|
|
||
|
You should have received a copy of the GNU General Public License
|
||
|
along with this program; if not, write to the Free Software
|
||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||
|
02110-1301, USA.
|
||
|
|
||
|
'''
|
||
|
|
||
|
import inkex
|
||
|
import numpy as np
|
||
|
import random
|
||
|
import sys
|
||
|
from inkex import Group, Line, Polygon, TextElement
|
||
|
from inkex.styles import Style
|
||
|
from inkex.transforms import Vector2d
|
||
|
from scipy.spatial import Delaunay
|
||
|
|
||
|
class DelaunayTriangulation(inkex.EffectExtension):
|
||
|
'Overlay selected objects with triangles.'
|
||
|
|
||
|
def add_arguments(self, pars):
|
||
|
pars.add_argument('--tab', help='The selected UI tab when OK was pressed')
|
||
|
pars.add_argument('--joggling', type=inkex.Boolean, default=False, help='Use joggled input instead of merged facets')
|
||
|
pars.add_argument('--furthest', type=inkex.Boolean, default=False, help='Furthest-site Delaunay triangulation')
|
||
|
pars.add_argument('--elt_type', default='poly', help='Element type to generate ("poly" or "line")')
|
||
|
pars.add_argument('--qhull', help='Triangulation options to pass to qhull')
|
||
|
pars.add_argument('--fill_type', help='How to fill generated polygons')
|
||
|
pars.add_argument('--fill_color', type=inkex.Color, help='Fill color to use with a fill type of "specified"')
|
||
|
pars.add_argument('--stroke_type', help='How to stroke generated polygons')
|
||
|
pars.add_argument('--stroke_color', type=inkex.Color, help='Stroke color to use with a stroke type of "specified"')
|
||
|
|
||
|
def _path_points(self, elt):
|
||
|
'Return a list of all points on a path (endpoints, not control points).'
|
||
|
pts = set()
|
||
|
first = None
|
||
|
prev = Vector2d()
|
||
|
for cmd in elt.path.to_absolute():
|
||
|
if first is None:
|
||
|
first = cmd.end_point(first, prev)
|
||
|
ep = cmd.end_point(first, prev)
|
||
|
pts.add((ep.x, ep.y))
|
||
|
prev = ep
|
||
|
return pts
|
||
|
|
||
|
def _create_styles(self, n):
|
||
|
'Return a style to use for the generated objects.'
|
||
|
# Use either the first or the last element's stroke for line caps,
|
||
|
# stroke widths, etc.
|
||
|
fstyle = self.svg.selection.first().style
|
||
|
lstyle = self.svg.selection[-1].style
|
||
|
if self.options.stroke_type == 'last_sel':
|
||
|
style = Style(lstyle)
|
||
|
else:
|
||
|
style = Style(fstyle)
|
||
|
|
||
|
# Apply the specified fill color.
|
||
|
if self.options.fill_type == 'first_sel':
|
||
|
fcolor = fstyle.get_color('fill')
|
||
|
style.set_color(fcolor, 'fill')
|
||
|
elif self.options.fill_type == 'last_sel':
|
||
|
fcolor = lstyle.get_color('fill')
|
||
|
style.set_color(fcolor, 'fill')
|
||
|
elif self.options.fill_type == 'specified':
|
||
|
style.set_color(self.options.fill_color, 'fill')
|
||
|
elif self.options.fill_type == 'random':
|
||
|
pass # Handled below
|
||
|
else:
|
||
|
sys.exit(inkex.utils.errormsg(_('Internal error: Unrecognized fill type "%s".')) % self.options.fill_type)
|
||
|
|
||
|
# Apply the specified stroke color.
|
||
|
if self.options.stroke_type == 'first_sel':
|
||
|
scolor = fstyle.get_color('stroke')
|
||
|
style.set_color(scolor, 'stroke')
|
||
|
elif self.options.stroke_type == 'last_sel':
|
||
|
scolor = lstyle.get_color('stroke')
|
||
|
style.set_color(scolor, 'stroke')
|
||
|
elif self.options.stroke_type == 'specified':
|
||
|
style.set_color(self.options.stroke_color, 'stroke')
|
||
|
elif self.options.stroke_type == 'random':
|
||
|
pass # Handled below
|
||
|
else:
|
||
|
sys.exit(inkex.utils.errormsg(_('Internal error: Unrecognized stroke type "%s".')) % self.options.stroke_type)
|
||
|
|
||
|
# Produce n copies of the style.
|
||
|
styles = [Style(style) for i in range(n)]
|
||
|
if self.options.fill_type == 'random':
|
||
|
for s in styles:
|
||
|
r = random.randint(0, 255)
|
||
|
g = random.randint(0, 255)
|
||
|
b = random.randint(0, 255)
|
||
|
s.set_color('#%02x%02x%02x' % (r, g, b), 'fill')
|
||
|
s['fill-opacity'] = 255
|
||
|
if self.options.stroke_type == 'random':
|
||
|
for s in styles:
|
||
|
r = random.randint(0, 255)
|
||
|
g = random.randint(0, 255)
|
||
|
b = random.randint(0, 255)
|
||
|
s.set_color('#%02x%02x%02x' % (r, g, b), 'stroke')
|
||
|
s['stroke-opacity'] = 255
|
||
|
|
||
|
# Return the list of styles.
|
||
|
return [str(s) for s in styles]
|
||
|
|
||
|
def _create_polygons(self, triangles):
|
||
|
'Render triangles as SVG polygons.'
|
||
|
styles = self._create_styles(len(triangles))
|
||
|
group = self.svg.get_current_layer().add(Group())
|
||
|
for tri, style in zip(triangles, styles):
|
||
|
tri_str = ' '.join(['%.10g %.10g' % (pt[0], pt[1]) for pt in tri])
|
||
|
poly = Polygon()
|
||
|
poly.set('points', tri_str)
|
||
|
poly.style = style
|
||
|
group.add(poly)
|
||
|
|
||
|
def _create_lines(self, triangles):
|
||
|
'Render triangles as individual SVG lines.'
|
||
|
# First, find all unique lines.
|
||
|
lines = set()
|
||
|
for tri in triangles:
|
||
|
if len(tri) != 3:
|
||
|
sys.exit(inkex.utils.errormsg(_('Internal error: Encountered a non-triangle.')))
|
||
|
for i, j in [(0, 1), (0, 2), (1, 2)]:
|
||
|
xy1 = tuple(tri[i])
|
||
|
xy2 = tuple(tri[j])
|
||
|
if xy1 < xy2:
|
||
|
lines.update([(xy1, xy2)])
|
||
|
else:
|
||
|
lines.update([(xy2, xy1)])
|
||
|
|
||
|
# Then, create SVG line elements.
|
||
|
styles = self._create_styles(len(lines))
|
||
|
group = self.svg.get_current_layer().add(Group())
|
||
|
for ([(x1, y1), (x2, y2)], style) in zip(lines, styles):
|
||
|
line = Line()
|
||
|
line.set('x1', x1)
|
||
|
line.set('y1', y1)
|
||
|
line.set('x2', x2)
|
||
|
line.set('y2', y2)
|
||
|
line.style = style
|
||
|
group.add(line)
|
||
|
|
||
|
def effect(self):
|
||
|
'Triangulate a set of objects.'
|
||
|
|
||
|
# Complain if the selection is empty.
|
||
|
if len(self.svg.selection) == 0:
|
||
|
return inkex.utils.errormsg(_('Please select at least one object.'))
|
||
|
|
||
|
# Acquire a set of all points from all selected objects.
|
||
|
all_points = set()
|
||
|
warned_text = False
|
||
|
for obj in self.svg.selection.values():
|
||
|
if isinstance(obj, TextElement) and not warned_text:
|
||
|
sys.stderr.write('Warning: Text elements are not currently supported. Ignoring all text in the selection.\n')
|
||
|
warned_text = True
|
||
|
all_points.update(self._path_points(obj.to_path_element()))
|
||
|
|
||
|
# Use SciPy to perform the Delaunay triangulation.
|
||
|
pts = np.array(list(all_points))
|
||
|
if len(pts) == 0:
|
||
|
return inkex.utils.errormsg(_('No points were found.'))
|
||
|
qopts = self.options.qhull
|
||
|
if self.options.joggling:
|
||
|
qopts = 'QJ ' + qopts
|
||
|
simplices = Delaunay(pts, furthest_site=self.options.furthest, qhull_options=qopts).simplices
|
||
|
|
||
|
# Create either triangles or lines, as request. Either option uses the
|
||
|
# style of the first object in the selection.
|
||
|
triangles = []
|
||
|
for s in simplices:
|
||
|
try:
|
||
|
triangles.append(pts[s])
|
||
|
except IndexError:
|
||
|
pass
|
||
|
if self.options.elt_type == 'poly':
|
||
|
self._create_polygons(triangles)
|
||
|
elif self.options.elt_type == 'line':
|
||
|
self._create_lines(triangles)
|
||
|
else:
|
||
|
return inkex.utils.errormsg(_('Internal error: unexpected element type "%s".') % self.options.elt_type)
|
||
|
|
||
|
if __name__ == '__main__':
|
||
|
DelaunayTriangulation().run()
|