From b10fc90a4c0f91fcfaa2e765eca9b9ad4c783070 Mon Sep 17 00:00:00 2001 From: leyghisbb Date: Thu, 7 Jan 2021 23:46:05 +0100 Subject: [PATCH] Added Delaunay Triangulation --- extensions/fablabchemnitz/delaunay.inx | 64 ++++++++ extensions/fablabchemnitz/delaunay.py | 198 +++++++++++++++++++++++++ 2 files changed, 262 insertions(+) create mode 100644 extensions/fablabchemnitz/delaunay.inx create mode 100644 extensions/fablabchemnitz/delaunay.py diff --git a/extensions/fablabchemnitz/delaunay.inx b/extensions/fablabchemnitz/delaunay.inx new file mode 100644 index 00000000..e804f647 --- /dev/null +++ b/extensions/fablabchemnitz/delaunay.inx @@ -0,0 +1,64 @@ + + + Delaunay Triangulation + fablabchemnitz.de.delaunay_triangulation + + + false + false + + Triangles + Individual lines + + + + + + + Same as first object selected + Same as last object selected + Random + Explicitly specified + + -1 + + + + Same as first object selected + Same as last object selected + Random + Explicitly specified + + 255 + + + + Qbb Qc Qz Q12 + + 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. + + + + + + + + + all + + + + + + + + \ No newline at end of file diff --git a/extensions/fablabchemnitz/delaunay.py b/extensions/fablabchemnitz/delaunay.py new file mode 100644 index 00000000..f19a54c8 --- /dev/null +++ b/extensions/fablabchemnitz/delaunay.py @@ -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()