Added Delaunay Triangulation

This commit is contained in:
leyghisbb 2021-01-07 23:46:05 +01:00
parent 5b785c06a7
commit b10fc90a4c
2 changed files with 262 additions and 0 deletions

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension
xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Delaunay Triangulation</name>
<id>fablabchemnitz.de.delaunay_triangulation</id>
<param name="tab" type="notebook">
<page name="Options" gui-text="Options">
<param name="joggling" type="bool" gui-text="Support concavity and holes">false</param>
<param name="furthest" type="bool" gui-text="Use furthest-site triangulation">false</param>
<param name="elt_type" type="enum" gui-text="Object type to generate">
<item value="poly">Triangles</item>
<item value="line">Individual lines</item>
</param>
<spacer />
<separator />
<spacer />
<hbox>
<param name="fill_type" type="enum" gui-text="Fill color source">
<item value="first_sel">Same as first object selected</item>
<item value="last_sel">Same as last object selected</item>
<item value="random">Random</item>
<item value="specified">Explicitly specified</item>
</param>
<param name="fill_color" type="color" appearance="colorbutton" gui-text=" " gui-description="Specific fill color">-1</param>
</hbox>
<hbox>
<param name="stroke_type" type="enum" gui-text="Stroke color source">
<item value="first_sel">Same as first object selected</item>
<item value="last_sel">Same as last object selected</item>
<item value="random">Random</item>
<item value="specified">Explicitly specified</item>
</param>
<param name="stroke_color" type="color" appearance="colorbutton" gui-text=" " gui-description="Specific stroke color">255</param>
</hbox>
</page>
<page name="Advanced" gui-text="Advanced">
<param name="qhull" type="string" gui-text="qhull options">Qbb Qc Qz Q12</param>
<param name="name" type="description">
If "Support concavity" is enabled on the Options tab, "QJ" will be
prepended to the qhull options listed above. The default options
are "Qbb Qc Qz Q12". The following website describes the available
options.
</param>
<label appearance="url">http://www.qhull.org/html/qhull.htm#options</label>
</page>
<page name="Help" gui-text="Help">
<label>
This effect uses the Delaunay triangulation algorithm to create
triangles from all of the points found in the selected objects.
</label>
</page>
</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Shape/Pattern from existing Path(s)" />
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">delaunay.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,198 @@
#! /usr/bin/python
'''
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):
'Parse the arguments passed to us from an Inkscape dialog box.'
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()